select('id_block')->from($tableName)->where('id = :id')->setParameter('id', $ID)->execute()->fetchColumn()) { $id_first_block = sqlQueryBuilder() ->select('id') ->from('blocks') ->where('id_parent = :id') ->setParameter('id', $id_init_block) ->orderBy('id, position') ->setMaxResults(1) ->execute() ->fetchColumn(); if ($id_first_block) { $json_content = ($text == null) ? null : $this->createTextBlockJsonContent($text); sqlQueryBuilder() ->update('blocks') ->directValues([ 'content' => $text, 'json_content' => $json_content, ]) ->where('id = :id') ->setParameter('id', $id_first_block) ->execute(); } if ($returnRootId) { return $id_init_block; } if ($id_first_block) { return $id_first_block; } return false; } $blockId = true; $rootID = null; $conn = sqlGetConnection(); $conn->transactional(function (Connection $conn) use ($tableName, $ID, $text, &$blockId, &$rootID) { $conn->createQueryBuilder() ->insert('blocks') ->values(['position' => 0]) ->execute(); $rootID = $conn->lastInsertId(); sqlQueryBuilder() ->update($tableName) ->set('id_block', $rootID) ->where('id=:id') ->setParameter('id', $ID) ->execute(); $json_content = ($text == null) ? null : $this->createTextBlockJsonContent($text); $conn->createQueryBuilder() ->insert('blocks') ->values(['id_parent' => $rootID, 'id_root' => $rootID, 'content' => ':text', 'json_content' => ':json_content']) ->setParameter('text', $text) ->setParameter('json_content', $json_content) ->execute(); $blockId = $conn->lastInsertId(); }); if ($returnRootId) { return $rootID; } return $blockId; } /** * Aktualizuje bloky u objektu podle $blocks. * * Provede nejprve delete na vsechny bloky u toho objektu, ktere potom znovu vytvori podle $blocks. Neni potreba posilat `content`, protoze * staci poslat jen `json_content`, podle ktereho se pripadne chybejici `content` vyrenderuje server-side. */ public function updateBlocks(string $tableName, int $objectId, array $blocks): void { $rootId = $this->getRootBlockId($tableName, $objectId); sqlGetConnection()->transactional(function () use ($rootId, $blocks) { // Smazu vsechny bloky, ktere jsou pod aktualnim root blokem sqlQueryBuilder() ->delete('blocks') ->where(Operator::equals(['id_root' => $rootId])) ->execute(); // Zacnu prochazet bloky, ktere chci vytvorit foreach ($blocks as $block) { // Insertu block $this->insertBlock($rootId, $rootId, $block); } }); } public function getRootBlockId(string $tableName, int $objectId): int { $rootId = sqlQueryBuilder() ->select('id_block') ->from($tableName) ->where(Operator::equals(['id' => $objectId])) ->execute()->fetchOne(); if (!$rootId) { $rootId = sqlGetConnection()->transactional(function () use ($tableName, $objectId) { sqlQueryBuilder() ->insert('blocks') ->directValues(['position' => 0]) ->execute(); $rootId = (int) sqlInsertId(); sqlQueryBuilder() ->update($tableName) ->directValues(['id_block' => $rootId]) ->where(Operator::equals(['id' => $objectId])) ->execute(); return $rootId; }); } return $rootId; } /** @deprecated use createTextBlockJsonContent */ public function createLegacyBlockJsonContent(string $content): string { $blockObj = new \stdClass(); $blockObj->type = 'legacy'; $blockObj->id = \FilipSedivy\EET\Utils\UUID::v4(); $settings = new \stdClass(); $settings->html = $content; $blockObj->settings = $settings; return json_encode([$blockObj]); } public function createTextBlockJsonContent(string $content): string { $blockObj = new \stdClass(); $blockObj->type = 'text'; $blockObj->id = \FilipSedivy\EET\Utils\UUID::v4(); $settings = new \stdClass(); $settings->html = $content; $blockObj->settings = $settings; return json_encode([$blockObj]); } public function insertBlock(int $rootId, int $parentId, array $block): void { $this->validateBlockStructure($block); if (empty($block['json_content'])) { throw new \InvalidArgumentException('Block "json_content" is required!'); } if (empty($block['content'])) { $response = $this->renderBlock($block['json_content']); if (!($response['success'] ?? false)) { throw new \RuntimeException('Some error during block render!'); } $block['content'] = $response['html']; } $blockId = sqlGetConnection()->transactional(function () use ($rootId, $parentId, $block) { sqlQueryBuilder() ->insert('blocks') ->directValues( [ 'id_root' => $rootId, 'id_parent' => $parentId, 'position' => $block['position'] ?? 0, 'identifier' => $block['identifier'] ?? '', 'name' => $block['name'] ?? null, 'content' => $block['content'], 'json_content' => $block['json_content'], ] )->execute(); return (int) sqlInsertId(); }); $this->updateBlockPhotosRelationsByData( $blockId, json_decode($block['json_content'], true) ?: [] ); // Pokud ma block sub bloky, tak insertnu i ty foreach ($block['children'] ?? [] as $childBlock) { $this->insertBlock($rootId, $blockId, $childBlock); } } public function translateBlock(string $language, int $blockId, array $block, bool $withRender = true): void { if (!$this->blocksTranslation) { return; } $this->validateBlockStructure($block); $this->blocksTranslation->saveSingleObjectForce( $language, $blockId, [ 'name' => $block['name'] ?? null, 'content' => $block['content'] ?? '', 'json_content' => json_decode($block['json_content'] ?: '', true) ?: [], ], $withRender ); } public function updateBlockPhotosRelationsByData(int $blockId, array $data): void { $blockPhotos = $this->getBlockPhotosIds($data); sqlGetConnection()->transactional(function () use ($blockId, $blockPhotos) { sqlQueryBuilder() ->delete('photos_blocks_new_relation') ->where(Operator::equals(['id_block' => $blockId])) ->execute(); foreach ($blockPhotos as $key => $blockPhotoId) { try { sqlQueryBuilder() ->insert('photos_blocks_new_relation') ->directValues( [ 'id_photo' => $blockPhotoId, 'id_block' => $blockId, 'position' => $key, ] )->execute(); } catch (\Exception) { } } }); } public function getBlockPhotosIds(array $jsonData): array { $photoIds = []; foreach ($jsonData as $item) { if (($item['type'] ?? null) === 'image' && !empty($item['settings']['photo']['id'])) { $photoIds[] = $item['settings']['photo']['id']; } if (($item['type'] ?? null) === 'gallery') { foreach ($item['settings']['photos'] ?? [] as $photo) { $photoIds[] = $photo['photo']['id']; } } if (!empty($item['children'])) { $photoIds = array_merge($photoIds, $this->getBlockPhotosIds($item['children'])); } } return array_unique(array_filter($photoIds)); } private function validateBlockStructure(array $block): void { if (($block['position'] ?? null) === null) { throw new \InvalidArgumentException('Block "position" is required!'); } } public function replacePlaceholders(array &$blocks, $placeholders, $objectPlaceholders = []) { foreach ($blocks as &$block) { $block['content'] = replacePlaceholders($block['content'], $placeholders, placeholders: $objectPlaceholders); if (!empty($block['children'])) { $this->replacePlaceholders($block['children'], $placeholders, $objectPlaceholders); } } } public function replaceComponentPlaceholders(array &$blocks): void { if (self::$NESTED_COMPONENTS_RENDER_LEVEL > 4) { throw new TemplateRecursionException('Recursively nested blocks'); } foreach ($blocks as &$block) { try { if (!empty($block['content'])) { self::$NESTED_COMPONENTS_RENDER_LEVEL++; $block['content'] = $this->renderComponentToPlaceholder($block['content']); self::$NESTED_COMPONENTS_RENDER_LEVEL--; } if (!empty($block['children'])) { $this->replaceComponentPlaceholders($block['children']); } } catch (\Exception $e) { if (isLocalDevelopment()) { throw new \Exception("Failed to render component into the placeholder [Block ID: {$block['id']}], exception: {$e->getMessage()}"); } else { captureException($e); } } } } /** @throws \Exception */ private function renderComponentToPlaceholder(string $content): string { return preg_replace_callback('/