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('/<\/div>/', function ($matches) { if (str_contains($matches[0], 'data-block-lazy')) { return $matches[0]; } if (!isset($matches[1])) { throw new \Exception('Empty JSON inside component placeholder'); } // html_entity_decode twice, because the json is escaped twice (because of brackets {}). To avoid collision with placeholders $options = json_decode(html_entity_decode(html_entity_decode($matches[1])), true); if (empty($options['name'])) { throw new \Exception('Required parameter "name" is missing'); } return $this->componentRenderer->renderToString($options['name'], array_merge($options['params'] ?? [])); }, $content); } public function createBlockDataFromPortableData(string $portableData): string { $portableData = json_decode($portableData, true) ?: []; $data = $this->recursivelyIterateDataBlocks($portableData, function ($item) { if ($item['type'] === 'image' && !empty($item['settings']['photo']['url'])) { $item['settings']['photo']['id'] = (int) $this->getDownloader()->importProductImage( $item['settings']['photo']['url'], 'full' ); unset($item['settings']['photo']['url']); } if ($item['type'] === 'gallery') { foreach ($item['settings']['photos'] ?? [] as &$photo) { $photo['photo']['id'] = (int) $this->getDownloader()->importProductImage( $photo['photo']['url'], 'full' ); unset($photo['photo']['url']); } } return $item; }, ['image', 'gallery']); return json_encode($data); } /** * Vraci JSON data bloku, ve kterem jsou IDcka fotek nahrazeny za URL adresy. */ public function getBlockPortableData(array $block): string { $data = json_decode($block['json_content'] ?: '', true) ?: []; $photos = sqlQueryBuilder() ->select('ph.id, CONCAT("data/photos/", ph.source, ph.image_2) as file_path') ->from('photos_blocks_new_relation', 'pbnr') ->join('pbnr', 'photos', 'ph', 'ph.id = pbnr.id_photo') ->where(Operator::equals(['pbnr.id_block' => $block['id']])) ->execute()->fetchAllKeyValue(); // k bloku nejsou prirazene zadne fotky, takze nemam co hledat a nahrazovat if (empty($photos)) { return $block['json_content'] ?? ''; } $dataPortable = $this->recursivelyIterateDataBlocks($data, function ($item) use ($photos) { if ($item['type'] === 'image') { $photoId = $item['settings']['photo']['id']; $item['settings']['photo']['url'] = $this->urlFinder->staticUrlAbsolute($photos[$photoId] ?? ''); unset($item['settings']['photo']['id']); } if ($item['type'] === 'gallery') { foreach ($item['settings']['photos'] ?? [] as &$photo) { $photoId = $photo['photo']['id']; $photo['photo']['url'] = $this->urlFinder->staticUrlAbsolute($photos[$photoId] ?? ''); unset($photo['photo']['id']); } } return $item; }, ['image', 'gallery']); return json_encode($dataPortable); } public function getProductsBlockSpecs(&$blocekData) { $orderBy = getVal('order_by', $blocekData); $orderDir = getVal('order_dir', $blocekData); $products_filter = getVal('products_filter', $blocekData); $filter = json_decode_strict($products_filter, true); $specs = []; if ($orderBy == 'customOrder') { $specs[] = function (QueryBuilder $qb) use ($filter) { $qb->setParameter('order_id_products', $filter['products'] ?? [], Connection::PARAM_INT_ARRAY); }; $blocekData['orderBy'] = 'FIELD(p.id, :order_id_products)'; } else { $blocekData['orderBy'] = $this->getProductsBlockOrderFields($orderBy); } $blocekData['orderBy'] .= ' '.$this->getProductsBlockOrderDir($orderDir); return $this->productsFilterSpecs->getSpecs($filter, $specs); } public function getBlockContent(int $rootId, bool $strip = false): string { $blocks = $this->getBlocks($rootId); $content = ''; foreach ($blocks as $block) { $content .= $block['content']; } if (!$strip) { return $content; } // replace \n with space $content = strip_tags($content); $content = preg_replace("/[\n]/i", ' ', $content); return StringUtil::normalizeWhitespace($content); } private function recursivelyIterateDataBlocks(array $data, callable $fn, ?array $types = null): array { foreach ($data as &$item) { $type = $item['type'] ?? null; if ($types === null || in_array($type, $types)) { $item = $fn($item); } if (!empty($item['children'])) { $item['children'] = $this->recursivelyIterateDataBlocks($item['children'], $fn, $types); } } return $data; } protected function getProductsBlockOrderDir($orderDir) { $order_dir = trim($orderDir); if (($order_dir != 'ASC') && ($order_dir != 'DESC')) { $order_dir = 'ASC'; } return $order_dir; } protected function getProductsBlockOrderFields($orderBy) { switch ($orderBy) { case 'code': $order = 'p.code'; break; case 'title': $order = 'p.title'; break; case 'price': $order = 'p.price'; break; case 'date': $order = 'p.date_added'; break; case 'sell': $order = 'p.pieces_sold'; break; case 'discount': $order = 'p.discount'; break; case 'store': $order = 'p.in_store'; break; case 'random': $order = 'RAND()'; break; default: $order = 'p.title'; break; } return $order; } public function isCurrentTemplateNested(): bool { return (bool) self::$NESTED_COMPONENTS_RENDER_LEVEL; } private function getDownloader(): \Downloader { static $downloader; if (!$downloader) { $downloader = new \Downloader(); $downloader->setMethod('curl'); } return $downloader; } #[Required] public function setComponentRenderer(?ComponentRenderer $componentRenderer = null): void { $this->componentRenderer = $componentRenderer; } }