first commit
This commit is contained in:
277
bundles/KupShop/ContentBundle/Util/ArticleList.php
Normal file
277
bundles/KupShop/ContentBundle/Util/ArticleList.php
Normal file
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use KupShop\I18nBundle\Translations\ArticlesAuthorsTranslation;
|
||||
use KupShop\I18nBundle\Translations\ArticlesSectionsTranslation;
|
||||
use KupShop\I18nBundle\Translations\ArticlesTagsTranslation;
|
||||
use KupShop\I18nBundle\Translations\ArticlesTranslation;
|
||||
use KupShop\I18nBundle\Translations\PhotosTranslation;
|
||||
use KupShop\KupShopBundle\Util\StringUtil;
|
||||
use Query\Operator;
|
||||
use Query\QueryBuilder;
|
||||
use Query\Translation;
|
||||
|
||||
class ArticleList
|
||||
{
|
||||
protected $divide;
|
||||
|
||||
protected $total_count;
|
||||
|
||||
private $image;
|
||||
|
||||
protected $tags;
|
||||
|
||||
// returns section by defined spec
|
||||
public function getSection($spec = null)
|
||||
{
|
||||
return $this->getSections($spec)[0] ?? false;
|
||||
}
|
||||
|
||||
// return sections by specs
|
||||
public function getSections($spec = null)
|
||||
{
|
||||
$qb = sqlQueryBuilder()->select('ab.*')
|
||||
->from('articles_branches', 'ab')
|
||||
->andWhere($spec)
|
||||
->andWhere(Translation::coalesceTranslatedFields(ArticlesSectionsTranslation::class))
|
||||
->orderBy('ab.position');
|
||||
|
||||
$qb->andWhere(Translation::joinTranslatedFields(ArticlesSectionsTranslation::class,
|
||||
function (QueryBuilder $qb, $columnName, $translatedField) {
|
||||
$qb->andWhere(Operator::coalesce($translatedField, 'ab.figure').' = "Y" ');
|
||||
|
||||
return false;
|
||||
}, ['figure']));
|
||||
|
||||
return $qb->execute()->fetchAll();
|
||||
}
|
||||
|
||||
public function getSectionById(int $id): ?array
|
||||
{
|
||||
if ($section = $this->getSection(Operator::equals(['ab.id' => $id]))) {
|
||||
return $this->prepareArticleSection($section);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getSectionsTree($topSection = null)
|
||||
{
|
||||
$tree = [];
|
||||
|
||||
$spec = function (QueryBuilder $qb) use ($topSection) {
|
||||
$qb->andWhere(Operator::equalsNullable(['ab.top_branch' => $topSection]));
|
||||
};
|
||||
|
||||
foreach ($this->getSections($spec) as $section) {
|
||||
$tree[$section['id']] = $this->prepareArticleSection($section);
|
||||
}
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
public function getArticlesCount($spec = null)
|
||||
{
|
||||
$qb = sqlQueryBuilder()->select('COUNT(DISTINCT a.id) as count')
|
||||
->from('articles_relation', 'ar')
|
||||
->leftJoin('ar', 'articles', 'a', 'ar.id_art=a.id')
|
||||
->andWhere('a.date <= NOW()')
|
||||
->andWhere($spec);
|
||||
|
||||
$qb->andWhere(Translation::joinTranslatedFields(ArticlesTranslation::class,
|
||||
function (QueryBuilder $qb, $columnName, $translatedField) {
|
||||
$qb->andWhere(Operator::coalesce($translatedField, 'a.figure').' = "Y" ');
|
||||
}, ['figure']));
|
||||
|
||||
return $qb->execute()->fetch()['count'];
|
||||
}
|
||||
|
||||
public function getArticles($spec = null)
|
||||
{
|
||||
$data = [];
|
||||
$dbcfg = \Settings::getDefault();
|
||||
|
||||
$qb = sqlQueryBuilder()->select('SQL_CALC_FOUND_ROWS DISTINCT(a.id) as id, ar.id_branch, a.figure,
|
||||
a.title, a.type, a.link, a.source, DATE_FORMAT(a.date, "'.$dbcfg->date_format.' '.$dbcfg->time_format.'") AS datef,
|
||||
a.lead_in, a.date, a.data, a.id_block, a.url, a.seen, GROUP_CONCAT(t.id) AS tags')
|
||||
->from('articles_relation', 'ar')
|
||||
->leftJoin('ar', 'articles', 'a', 'ar.id_art=a.id')
|
||||
->leftJoin('a', 'articles_tags_relation', 'atr', 'a.id = atr.id_article')
|
||||
->leftJoin('atr', 'articles_tags', 't', 'atr.id_tag = t.id')
|
||||
->leftJoin('ar', 'articles_branches', 'ab', 'ab.id = ar.id_branch')
|
||||
->andWhere('a.date <= NOW()')
|
||||
->having('figure = "Y"')
|
||||
->groupBy('a.id')
|
||||
->andWhere($spec)
|
||||
->andWhere(Translation::coalesceTranslatedFields(ArticlesTranslation::class));
|
||||
|
||||
$qb->andWhere(Translation::joinTranslatedFields(ArticlesSectionsTranslation::class,
|
||||
function (QueryBuilder $qb, $columnName, $translatedField) {
|
||||
$qb->andWhere(Operator::coalesce($translatedField, 'ab.figure').' = "Y" ');
|
||||
|
||||
return false;
|
||||
}, ['figure']));
|
||||
|
||||
$qb = $qb->execute();
|
||||
|
||||
$this->total_count = (int) sqlFetchAssoc(sqlQuery('SELECT FOUND_ROWS() as total_count'))['total_count'];
|
||||
|
||||
foreach ($qb as $row) {
|
||||
$row['lead'] = $row['lead_in'];
|
||||
$row['seo_url'] = !empty($row['url']) ? $row['url'] : StringUtil::slugify($row['title']);
|
||||
|
||||
// if isset section
|
||||
if ($id_branch = $row['id_branch']) {
|
||||
$spec = function (QueryBuilder $qb) use ($id_branch) {
|
||||
$qb->andWhere(Operator::equals(['ab.id' => $id_branch]));
|
||||
};
|
||||
} else {
|
||||
$spec = null;
|
||||
}
|
||||
$row['section'] = $this->getSection($spec);
|
||||
|
||||
$row['date'] = new \DateTime($row['date']);
|
||||
|
||||
$row['data'] = json_decode($row['data'], true);
|
||||
|
||||
if (!empty($row['tags'])) {
|
||||
$tag_ids = array_flip(explode(',', $row['tags']));
|
||||
$row['tags'] = array_intersect_key($this->getTags(), $tag_ids);
|
||||
}
|
||||
|
||||
$data[$row['id']] = $row;
|
||||
}
|
||||
|
||||
// multi fetches
|
||||
$this->fetchArticlesPhotos($data);
|
||||
$this->fetchArticlesAuthors($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function fetchArticlesPhotos(array &$articles): void
|
||||
{
|
||||
if (!$this->image) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($articles)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$qb = sqlQueryBuilder()
|
||||
->select('pa.id_art as id_article, ph.id, ph.descr, ph.source, ph.image_2 AS image_big, ph.date_update')
|
||||
->from('photos_articles_relation', 'pa')
|
||||
->leftJoin('pa', 'photos', 'ph', 'pa.id_photo = ph.id')
|
||||
->andWhere(Translation::coalesceTranslatedFields(PhotosTranslation::class))
|
||||
->andWhere(Operator::inIntArray(array_keys($articles), 'pa.id_art'))
|
||||
->andWhere(Operator::equals(['pa.show_in_lead' => 'Y', 'pa.active' => 'Y']));
|
||||
|
||||
foreach ($qb->execute() as $item) {
|
||||
if (empty($articles[$item['id_article']])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->image == 'photo') {
|
||||
$articles[$item['id_article']]['photoId'] = $item['id'];
|
||||
$articles[$item['id_article']]['photoDescr'] = $item['descr'];
|
||||
$articles[$item['id_article']]['photoDateUpdate'] = strtotime($item['date_update']);
|
||||
} else {
|
||||
$articles[$item['id_article']]['image'] = getImage($item['id'], $item['image_big'], $item['source'], $this->image, $item['descr'], strtotime($item['date_update']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function fetchArticlesAuthors(array &$articles): void
|
||||
{
|
||||
if (!findModule(\Modules::ARTICLES_AUTHORS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($articles)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$qb = sqlQueryBuilder()
|
||||
->select('aar.id_art as id_article, au.*')
|
||||
->from('articles_authors', 'au')
|
||||
->join('au', 'articles_authors_relation', 'aar', 'au.id = aar.id_auth')
|
||||
->andWhere(Translation::coalesceTranslatedFields(ArticlesAuthorsTranslation::class))
|
||||
->andWhere(Operator::inIntArray(array_keys($articles), 'aar.id_art'));
|
||||
|
||||
foreach ($qb->execute() as $item) {
|
||||
if (empty($articles[$item['id_article']])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$articles[$item['id_article']]['authors'][$item['id']] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
public function setImage($image): self
|
||||
{
|
||||
$this->image = $image;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function createPager(int $pageNumber)
|
||||
{
|
||||
$pager = new \Pager();
|
||||
$pager->setOnPage($this->getDivide());
|
||||
$pager->setPageNumber($pageNumber);
|
||||
|
||||
return $pager;
|
||||
}
|
||||
|
||||
public function getDivide(): int
|
||||
{
|
||||
if (!$this->divide) {
|
||||
$this->divide = findModule(\Modules::ARTICLES, 'limit') ?: 25;
|
||||
}
|
||||
|
||||
return $this->divide;
|
||||
}
|
||||
|
||||
public function setDivide(int $divide)
|
||||
{
|
||||
$this->divide = $divide;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalCount()
|
||||
{
|
||||
return $this->total_count;
|
||||
}
|
||||
|
||||
public function getTags()
|
||||
{
|
||||
if (!isset($this->tags)) {
|
||||
$this->tags = [];
|
||||
$qb = sqlQueryBuilder()
|
||||
->select('at.*')
|
||||
->from('articles_tags', 'at')
|
||||
->andWhere(Translation::coalesceTranslatedFields(ArticlesTagsTranslation::class))
|
||||
->execute();
|
||||
|
||||
$tags = $qb->fetchAll();
|
||||
if (!empty($tags)) {
|
||||
$this->tags = array_combine(array_column($tags, 'id'), $tags);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
protected function prepareArticleSection(array $section): array
|
||||
{
|
||||
$section['url'] = path('kupshop_content_articles_articles_2', ['IDb' => $section['id'], 'slug' => StringUtil::slugify($section['name'])]);
|
||||
$section['children'] = $this->getSectionsTree($section['id']);
|
||||
$section['articles_count'] = $this->getArticlesCount(Operator::equals(['ar.id_branch' => $section['id']]));
|
||||
|
||||
return $section;
|
||||
}
|
||||
}
|
||||
253
bundles/KupShop/ContentBundle/Util/ArticlesUtil.php
Normal file
253
bundles/KupShop/ContentBundle/Util/ArticlesUtil.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use KupShop\I18nBundle\Translations\ArticlesTagsTranslation;
|
||||
use KupShop\I18nBundle\Translations\ArticlesTranslation;
|
||||
use KupShop\KupShopBundle\Context\LanguageContext;
|
||||
use Query\Operator;
|
||||
use Query\QueryBuilder;
|
||||
use Query\Translation;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
|
||||
class ArticlesUtil
|
||||
{
|
||||
#[Required]
|
||||
public LanguageContext $languageContext;
|
||||
|
||||
#[Required]
|
||||
public ArticleList $articleList;
|
||||
|
||||
public function getArticleSections($topSection = null)
|
||||
{
|
||||
$sectionsCacheName = "article-sections-{$this->languageContext->getActiveId()}";
|
||||
if (isset($topSection)) {
|
||||
$sectionsCacheName .= '-'.$topSection;
|
||||
}
|
||||
if ($cache = getCache($sectionsCacheName)) {
|
||||
$sections = $cache;
|
||||
} else {
|
||||
$sections = $this->articleList->getSectionsTree($topSection);
|
||||
setCache($sectionsCacheName, $sections);
|
||||
}
|
||||
|
||||
return $sections;
|
||||
}
|
||||
|
||||
public function getArticleTags($idArticle, $onlyActive = null)
|
||||
{
|
||||
if (!findModule(\Modules::ARTICLES)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$qb = sqlQueryBuilder()
|
||||
->select('at.*, COUNT(atr.id_article) as count')
|
||||
->from('articles_tags', 'at')
|
||||
->leftJoin('at', 'articles_tags_relation', 'atr', 'atr.id_tag = at.id')
|
||||
->andWhere(Translation::coalesceTranslatedFields(ArticlesTagsTranslation::class))
|
||||
->groupBy('at.id')
|
||||
->orderBy('at.tag');
|
||||
|
||||
if ($idArticle) {
|
||||
$qb->andWhere(Operator::equals(['atr.id_article' => $idArticle]));
|
||||
}
|
||||
|
||||
if ($onlyActive) {
|
||||
$qb->leftJoin('atr', 'articles', 'a', 'atr.id_article = a.id')
|
||||
->andWhere('a.date <= NOW()');
|
||||
$qb->andWhere(Translation::joinTranslatedFields(ArticlesTranslation::class,
|
||||
function (QueryBuilder $qb, $columnName, $translatedField) {
|
||||
$qb->andWhere(Operator::coalesce($translatedField, 'a.figure').' = "Y" ');
|
||||
},
|
||||
['figure']));
|
||||
$qb->having('count > 0');
|
||||
}
|
||||
|
||||
return $qb->execute()->fetchAll();
|
||||
}
|
||||
|
||||
public function getArticles($params)
|
||||
{
|
||||
$defaults = [
|
||||
'count' => null,
|
||||
'section' => null,
|
||||
'section_id' => null,
|
||||
'product_id' => null,
|
||||
'id' => null,
|
||||
'section_id_exclude' => [],
|
||||
'related' => [],
|
||||
'related_symmetric' => false,
|
||||
'tag' => null,
|
||||
'assign' => null,
|
||||
'order_by' => 'a.date',
|
||||
'order' => 'DESC',
|
||||
'require_tag' => null,
|
||||
'image' => 1,
|
||||
];
|
||||
|
||||
$params = array_merge($defaults, $params);
|
||||
|
||||
if (empty($params['section']) && empty($params['section_id']) && empty($params['product_id']) && empty($params['tag']) && empty($params['id']) && empty($params['related'])) {
|
||||
echo "Použij parametr 'section' nebo 'section_id' nebo 'product_id' nebo 'tag' nebo 'id' nebo 'related'";
|
||||
}
|
||||
|
||||
if ($params['tag'] && !is_array($params['tag'])) {
|
||||
$params['tag'] = [$params['tag']];
|
||||
}
|
||||
|
||||
$spec_section = null;
|
||||
if ($params['section'] != 'all') {
|
||||
$spec_section = function (QueryBuilder $qb) use ($params) {
|
||||
$qb->andWhere('(ab.name=:section OR ab.id=:section_id)')
|
||||
->setParameters(['section' => $params['section'], 'section_id' => $params['section_id']]);
|
||||
};
|
||||
}
|
||||
|
||||
return [
|
||||
'section' => $this->articleList->getSection($spec_section),
|
||||
'articles' => $this->getArticlesWithFallback($params, $defaults),
|
||||
];
|
||||
}
|
||||
|
||||
private function getArticlesWithFallback(array $params, array $defaultParams): array
|
||||
{
|
||||
if (!empty($params['section_id_exclude'])) {
|
||||
$specs[] = function (QueryBuilder $qb) use ($params) {
|
||||
$qb->andWhere(
|
||||
Operator::not(
|
||||
Operator::inIntArray($params['section_id_exclude'], 'ar.id_branch')
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
$specs[] = function (QueryBuilder $qb) use ($params) {
|
||||
if (is_array($params['order_by'])) {
|
||||
$qb->orderBy('FIELD(a.id, :orderByArticlesId)')
|
||||
->setParameter('orderByArticlesId', $params['order_by'], \Doctrine\DBAL\Connection::PARAM_INT_ARRAY);
|
||||
} else {
|
||||
$qb->orderBy($params['order_by'], $params['order']);
|
||||
}
|
||||
};
|
||||
|
||||
if (isset($params['section_id']) && $params['section'] != 'all') {
|
||||
$sectionIds = [$params['section_id']];
|
||||
|
||||
if ($filterSection = $this->articleList->getSectionById((int) $params['section_id'])) {
|
||||
$this->getFilterSectionsIdsRecursively($filterSection, $sectionIds);
|
||||
}
|
||||
|
||||
$specs[] = Operator::inIntArray($sectionIds, 'ar.id_branch');
|
||||
}
|
||||
|
||||
if ($params['count'] > 0) {
|
||||
$specs[] = function (QueryBuilder $qb) use ($params) {
|
||||
$qb->setMaxResults($params['count']);
|
||||
};
|
||||
}
|
||||
|
||||
if ($params['product_id']) {
|
||||
$specs[] = function (QueryBuilder $qb) use ($params) {
|
||||
$qb->join('a', 'products_in_articles', 'pia', 'pia.id_article = a.id')
|
||||
->andWhere('pia.id_product=:id_product')->setParameter('id_product', $params['product_id']);
|
||||
};
|
||||
}
|
||||
|
||||
if ($params['id']) {
|
||||
$specs[] = function (QueryBuilder $qb) use ($params) {
|
||||
$qb->andWhere(Operator::inIntArray((array) $params['id'], 'a.id'));
|
||||
};
|
||||
}
|
||||
|
||||
if ($params['tag']) {
|
||||
$specs[] = function (QueryBuilder $qb) use ($params) {
|
||||
$qb->leftJoin('a', 'articles_tags_relation', 'atr', 'a.id = atr.id_article')
|
||||
->andWhere(Operator::inIntArray($params['tag'], 'atr.id_tag'));
|
||||
};
|
||||
}
|
||||
|
||||
if ($params['require_tag']) {
|
||||
$specs[] = function (QueryBuilder $qb) use ($params) {
|
||||
$qb->join('a', 'articles_tags_relation', 'atr2', 'a.id = atr2.id_article')
|
||||
->andWhere(Operator::equals(['atr2.id_tag' => $params['require_tag']]));
|
||||
};
|
||||
}
|
||||
|
||||
if (!empty($params['exclude_articles'])) {
|
||||
if (!is_array($params['exclude_articles'])) {
|
||||
$params['exclude_articles'] = [$params['exclude_articles']];
|
||||
}
|
||||
$specs[] = function (QueryBuilder $qb) use ($params) {
|
||||
$qb->andWhere(Operator::not(Operator::inIntArray($params['exclude_articles'], 'a.id')));
|
||||
};
|
||||
}
|
||||
|
||||
if (!empty($params['related'])) {
|
||||
$specs[] = function (QueryBuilder $qb) use ($params) {
|
||||
$qb->leftJoin('a', 'articles_related', 'arr', 'a.id = arr.id_article_related')
|
||||
->orderBy('arr.position');
|
||||
|
||||
$orX = [Operator::inIntArray((array) $params['related'], 'arr.id_article')];
|
||||
|
||||
if ($params['related_symmetric'] ?? false) {
|
||||
$qb->leftJoin('a', 'articles_related', 'arr2', 'a.id = arr2.id_article');
|
||||
$orX[] = Operator::inIntArray((array) $params['related'], 'arr2.id_article_related');
|
||||
}
|
||||
|
||||
$andX = [Operator::orX($orX)];
|
||||
|
||||
if (findModule(\Modules::ARTICLES_RELATED_TYPES) && !empty($params['related_type'])) {
|
||||
$andX[] = Operator::inIntArray((array) $params['related_type'], 'arr.type');
|
||||
if (!empty($params['related_symmetric'])) {
|
||||
$andX[] = Operator::inIntArray((array) $params['related_type'], 'arr2.type');
|
||||
}
|
||||
}
|
||||
|
||||
$qb->andWhere(Operator::andX($andX));
|
||||
};
|
||||
}
|
||||
|
||||
if ($params['image']) {
|
||||
$this->articleList->setImage($params['image']);
|
||||
}
|
||||
|
||||
$articles = $this->articleList->getArticles(Operator::andX($specs));
|
||||
$count = count($articles);
|
||||
|
||||
if (!empty($params['count']) && $count < $params['count'] && !empty($params['fallback'])) {
|
||||
$fallback = $params['fallback'];
|
||||
unset($params['fallback']);
|
||||
|
||||
foreach ($fallback as $fallbackParams) {
|
||||
if ($count >= $params['count']) {
|
||||
break;
|
||||
}
|
||||
|
||||
$fallbackParams = array_merge($defaultParams, $fallbackParams, [
|
||||
'count' => $params['count'] - $count,
|
||||
]);
|
||||
|
||||
$fallbackArticles = $this->getArticlesWithFallback($fallbackParams, $defaultParams);
|
||||
$count += count($fallbackArticles);
|
||||
|
||||
$articles = array_merge($articles, $fallbackArticles);
|
||||
}
|
||||
}
|
||||
|
||||
return $articles;
|
||||
}
|
||||
|
||||
private function getFilterSectionsIdsRecursively(array $section, array &$filterIds): void
|
||||
{
|
||||
// pokud je nastaveno "Zobrazovat jen články v sekci", tak nenacitam IDcka podsekci
|
||||
if (($section['behaviour'] ?? null) != 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// pokud je u sekce zaksrnuto "Zobrazovat články v této sekci a jejích podsekcích"
|
||||
foreach ($section['children'] ?? [] as $child) {
|
||||
$filterIds[] = $child['id'];
|
||||
$this->getFilterSectionsIdsRecursively($child, $filterIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
235
bundles/KupShop/ContentBundle/Util/BlocekSettings.php
Normal file
235
bundles/KupShop/ContentBundle/Util/BlocekSettings.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use KupShop\ComponentsBundle\Utils\ComponentsLocator;
|
||||
use KupShop\KupShopBundle\Config;
|
||||
use KupShop\KupShopBundle\Context\DomainContext;
|
||||
use KupShop\KupShopBundle\Context\LanguageContext;
|
||||
use KupShop\KupShopBundle\Util\Logging\SentryLogger;
|
||||
use KupShop\LLMBundle\TextObjects\BlocekTextBlock;
|
||||
use KupShop\LLMBundle\TextObjects\ProductBlocekTextBlock;
|
||||
use KupShop\LLMBundle\Util\TextObjectUtil;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
|
||||
class BlocekSettings
|
||||
{
|
||||
#[Required]
|
||||
public DomainContext $domainContext;
|
||||
#[Required]
|
||||
public LanguageContext $languageContext;
|
||||
|
||||
public ?ComponentsLocator $componentsLocator;
|
||||
|
||||
public ?TextObjectUtil $textObjectUtil;
|
||||
|
||||
#[Required]
|
||||
public SentryLogger $sentryLogger;
|
||||
|
||||
private bool $isDevelopment;
|
||||
private bool $isWpjAdmin;
|
||||
private bool $allowHtml;
|
||||
private string $urlPrefix;
|
||||
private string $adminPath;
|
||||
private string $language;
|
||||
private string $feLanguage;
|
||||
private array $modules;
|
||||
private array $components;
|
||||
private array $llmPrompts;
|
||||
|
||||
public function fetchSettings(): void
|
||||
{
|
||||
$config = Config::get()->getContainer();
|
||||
$settings = \Settings::getDefault();
|
||||
$user = getAdminUser();
|
||||
|
||||
$isLocalDev = ($this->domainContext->getActiveId() == 'www.kupshop.local' || $this->domainContext->getActiveId() == 'kupshop.local');
|
||||
$originalAddr = $config['Addr']['full_original'] ?? '/';
|
||||
$adminPath = $config['Path']['admin'];
|
||||
|
||||
$this->isDevelopment = (isDevelopment() && $isLocalDev);
|
||||
$this->urlPrefix = "{$originalAddr}{$adminPath}";
|
||||
$this->adminPath = '/'.ltrim($adminPath, '/');
|
||||
$this->isWpjAdmin = $user['superuser'] ?? false;
|
||||
$this->allowHtml = $settings['blocek_allow_html'] == 'Y';
|
||||
$this->language = $config['Lang']['language'] == 'en' ? 'en' : 'cs';
|
||||
$this->feLanguage = $this->languageContext->getActiveId();
|
||||
$this->modules = $this->loadModules();
|
||||
|
||||
if (findModule(\Modules::COMPONENTS)) {
|
||||
$this->components = $this->loadComponents();
|
||||
}
|
||||
|
||||
if (findModule(\Modules::LLM)) {
|
||||
$this->llmPrompts = $this->loadLlmPrompts([BlocekTextBlock::getLabel(), ProductBlocekTextBlock::getLabel()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function loadModules(): array
|
||||
{
|
||||
return [
|
||||
\Modules::COMPONENTS => findModule(\Modules::COMPONENTS),
|
||||
\Modules::VIDEOS => findModule(\Modules::CDN) && findModule(\Modules::VIDEOS),
|
||||
];
|
||||
}
|
||||
|
||||
private function loadComponents(): array
|
||||
{
|
||||
$loadedComponents = [];
|
||||
try {
|
||||
foreach ($this->componentsLocator->getComponents() as $component) {
|
||||
$reflectionClass = new \ReflectionClass($component['class']);
|
||||
if ($attributes = $reflectionClass->getAttributes('KupShop\ComponentsBundle\Attributes\Blocek', \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
|
||||
$loadedComponents[] = [
|
||||
'name' => $component['name'],
|
||||
'title' => $attributes->getArguments()['title'] ?? null,
|
||||
'descr' => $attributes->getArguments()['descr'] ?? null,
|
||||
'icon' => $attributes->getArguments()['icon'] ?? null,
|
||||
'attributes' => $this->loadAttributes($reflectionClass),
|
||||
'lazy_component' => $attributes->getArguments()['lazy'] ?? false,
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\ReflectionException $e) {
|
||||
$this->sentryLogger->captureException($e);
|
||||
}
|
||||
|
||||
return $loadedComponents;
|
||||
}
|
||||
|
||||
private function loadLlmPrompts(array $textObjects): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($textObjects as $textObject) {
|
||||
$result[$textObject] = array_values(array_map(fn ($prompt) => [
|
||||
'id' => $prompt->getId(),
|
||||
'title' => $prompt->getTitle(),
|
||||
], $this->textObjectUtil->getObjectLabelPrompts($textObject)));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @throws \Exception */
|
||||
private function loadAttributes(\ReflectionClass $reflectionClass): array
|
||||
{
|
||||
$loadedAttributes = [];
|
||||
foreach ($reflectionClass->getProperties() as $property) {
|
||||
if ($attributes = $property->getAttributes('KupShop\ComponentsBundle\Attributes\BlocekAttribute', \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
|
||||
$loadedAttributes[$property->getName()] = [
|
||||
'name' => $property->getName(),
|
||||
'default' => !empty($attributes->getArguments()['default']) ? $attributes->getArguments()['default'] : $property->getDefaultValue(),
|
||||
'type' => !empty($attributes->getArguments()['type']) ? $attributes->getArguments()['type'] : $this->processBlocekTypeConversion($property),
|
||||
'title' => !empty($attributes->getArguments()['title']) ? $attributes->getArguments()['title'] : $property->getName(),
|
||||
'options' => $this->getAttributeOptionValues($attributes, $reflectionClass),
|
||||
];
|
||||
if (!empty($attributes->getArguments()['autocompleteType'])) {
|
||||
$loadedAttributes[$property->getName()]['autocompleteType'] = $attributes->getArguments()['autocompleteType'];
|
||||
$loadedAttributes[$property->getName()]['autocompleteMulti'] = !empty($attributes->getArguments()['autocompleteMulti']) ? $attributes->getArguments()['autocompleteMulti'] : false;
|
||||
$loadedAttributes[$property->getName()]['autocompletePreload'] = !empty($attributes->getArguments()['autocompletePreload']) ? $attributes->getArguments()['autocompletePreload'] : null;
|
||||
$loadedAttributes[$property->getName()]['autocompleteInvert'] = isset($attributes->getArguments()['autocompleteInvert']) ? $attributes->getArguments()['autocompleteInvert'] : null;
|
||||
$loadedAttributes[$property->getName()]['autocompleteSortable'] = isset($attributes->getArguments()['autocompleteSortable']) ? $attributes->getArguments()['autocompleteSortable'] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $loadedAttributes;
|
||||
}
|
||||
|
||||
private function getAttributeOptionValues(\ReflectionAttribute $attributes, \ReflectionClass $class): array
|
||||
{
|
||||
if (!empty($attributes->getArguments()['options'])) {
|
||||
$options = $attributes->getArguments()['options'];
|
||||
if (is_string($options) && $class->hasMethod($options)) {
|
||||
return $class->getMethod($options)->invoke(null);
|
||||
}
|
||||
|
||||
return $attributes->getArguments()['options'];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function processBlocekTypeConversion(\ReflectionProperty $property): BlocekTypes
|
||||
{
|
||||
return match ($property->getType()->getName()) {
|
||||
'int' => BlocekTypes::NUMBER,
|
||||
'string' => BlocekTypes::TEXT,
|
||||
'bool' => BlocekTypes::TOGGLE,
|
||||
default => throw new \Exception('Add new conversion type or specify trough parameter'),
|
||||
};
|
||||
}
|
||||
|
||||
public function isDevelopment(): bool
|
||||
{
|
||||
return $this->isDevelopment;
|
||||
}
|
||||
|
||||
public function getUrlPrefix(): string
|
||||
{
|
||||
return $this->urlPrefix;
|
||||
}
|
||||
|
||||
public function isWpjAdmin(): bool
|
||||
{
|
||||
return $this->isWpjAdmin;
|
||||
}
|
||||
|
||||
public function isAllowHtml(): bool
|
||||
{
|
||||
return $this->allowHtml;
|
||||
}
|
||||
|
||||
public function getLanguage(): string
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
public function getFeLanguage(): string
|
||||
{
|
||||
return $this->feLanguage;
|
||||
}
|
||||
|
||||
public function getModules(): array
|
||||
{
|
||||
return $this->modules;
|
||||
}
|
||||
|
||||
public function getComponents(): array
|
||||
{
|
||||
return $this->components;
|
||||
}
|
||||
|
||||
public function asArray(): array
|
||||
{
|
||||
$array = [
|
||||
'isDevelopment' => $this->isDevelopment,
|
||||
'isWpjAdmin' => $this->isWpjAdmin,
|
||||
'allowHtml' => $this->allowHtml,
|
||||
'urlPrefix' => $this->urlPrefix,
|
||||
'adminPath' => $this->adminPath,
|
||||
'language' => $this->language,
|
||||
'feLanguage' => $this->feLanguage,
|
||||
'modules' => $this->modules,
|
||||
'llmPrompts' => $this->llmPrompts,
|
||||
];
|
||||
|
||||
if (findModule(\Modules::COMPONENTS)) {
|
||||
$array['components'] = $this->components;
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
#[Required]
|
||||
public function setComponentsLocator(?ComponentsLocator $componentsLocator = null): void
|
||||
{
|
||||
$this->componentsLocator = $componentsLocator;
|
||||
}
|
||||
|
||||
#[Required]
|
||||
public function setTextObjectUtil(?TextObjectUtil $textObjectUtil): void
|
||||
{
|
||||
$this->textObjectUtil = $textObjectUtil;
|
||||
}
|
||||
}
|
||||
18
bundles/KupShop/ContentBundle/Util/BlocekTypes.php
Normal file
18
bundles/KupShop/ContentBundle/Util/BlocekTypes.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
enum BlocekTypes: string
|
||||
{
|
||||
case URL = 'url';
|
||||
case TOGGLE = 'toggle';
|
||||
case CHECKBOX = 'checkbox';
|
||||
case SELECT = 'select';
|
||||
case NUMBER = 'number';
|
||||
case TEXT = 'text';
|
||||
case TEXT_LIST = 'text_list';
|
||||
case HTML = 'html';
|
||||
case SECTION = 'section';
|
||||
case RANGE = 'range';
|
||||
case AUTOCOMPLETE = 'autocomplete';
|
||||
}
|
||||
574
bundles/KupShop/ContentBundle/Util/Block.php
Normal file
574
bundles/KupShop/ContentBundle/Util/Block.php
Normal file
@@ -0,0 +1,574 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use KupShop\CatalogBundle\Util\ProductsFilterSpecs;
|
||||
use KupShop\ComponentsBundle\Exception\TemplateRecursionException;
|
||||
use KupShop\ComponentsBundle\Utils\ComponentRenderer;
|
||||
use KupShop\I18nBundle\Translations\BlocksTranslation;
|
||||
use KupShop\KupShopBundle\Util\StringUtil;
|
||||
use KupShop\KupShopBundle\Util\System\UrlFinder;
|
||||
use Query\Operator;
|
||||
use Query\QueryBuilder;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
|
||||
use function Sentry\captureException;
|
||||
|
||||
class Block
|
||||
{
|
||||
use BlocksTrait;
|
||||
use \DatabaseCommunication;
|
||||
|
||||
private ?ComponentRenderer $componentRenderer;
|
||||
|
||||
private static int $NESTED_COMPONENTS_RENDER_LEVEL = 0;
|
||||
|
||||
public function __construct(
|
||||
private readonly UrlFinder $urlFinder,
|
||||
private ProductsFilterSpecs $productsFilterSpecs,
|
||||
protected readonly ?BlocksTranslation $blocksTranslation = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $tableName string : table name of object
|
||||
* @param $ID int : ID of object in table
|
||||
* @param $text string : text of block
|
||||
*
|
||||
* @return bool : false if table row already has first block, true when successfully added
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function insertFirstBlock(string $tableName, int $ID, ?string $text, bool $returnRootId = false): int|bool
|
||||
{
|
||||
if ($id_init_block = sqlQueryBuilder()->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\s+data-block-component="(({|&#123;).*?(}|&#125;))"(?:\s+data-block-lazy="true")?\s*><\/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;
|
||||
}
|
||||
}
|
||||
51
bundles/KupShop/ContentBundle/Util/BlockCollection.php
Normal file
51
bundles/KupShop/ContentBundle/Util/BlockCollection.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use KupShop\ContentBundle\Entity\Block;
|
||||
|
||||
class BlockCollection extends ArrayCollection
|
||||
{
|
||||
public $identifierToId;
|
||||
|
||||
/**
|
||||
* @param $offset int Either block ID or block identification
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function translateOffset($offset)
|
||||
{
|
||||
if (is_numeric($offset)) {
|
||||
return $offset;
|
||||
}
|
||||
|
||||
// Load identification cache
|
||||
$this->identifierToId = [];
|
||||
|
||||
/** @var Block $block */
|
||||
foreach ($this as $index => $block) {
|
||||
$this->identifierToId[$block->getIdentifier()] = $index;
|
||||
}
|
||||
|
||||
if (isset($this->identifierToId[$offset])) {
|
||||
return $this->identifierToId[$offset];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function get($offset)
|
||||
{
|
||||
$offset = $this->translateOffset($offset);
|
||||
|
||||
return parent::get($offset);
|
||||
}
|
||||
|
||||
public function containsKey($offset)
|
||||
{
|
||||
$offset = $this->translateOffset($offset);
|
||||
|
||||
return parent::containsKey($offset);
|
||||
}
|
||||
}
|
||||
359
bundles/KupShop/ContentBundle/Util/BlocksTrait.php
Normal file
359
bundles/KupShop/ContentBundle/Util/BlocksTrait.php
Normal file
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Query\Operator;
|
||||
|
||||
trait BlocksTrait
|
||||
{
|
||||
/**
|
||||
* @return array hierarchy of blocks (parental blocks has children - nested set of blocks)
|
||||
*/
|
||||
public function getBlocks(int $blockID, bool $forceTranslations = false)
|
||||
{
|
||||
$allBlocks = sqlQueryBuilder()->select(
|
||||
'b.*',
|
||||
"JSON_ARRAYAGG(
|
||||
IF(pb.id_photo IS NULL,
|
||||
NULL,
|
||||
JSON_OBJECT('date_update', p.date_update, 'id_photo', pb.id_photo)
|
||||
)
|
||||
ORDER BY pb.position
|
||||
) AS photos"
|
||||
)
|
||||
->from('blocks', 'b')
|
||||
->leftJoin('b', 'photos_blocks_relation', 'pb', 'pb.id_block = b.id')
|
||||
->where('id_root=:id_root')->setParameter('id_root', $blockID)
|
||||
->leftJoin('pb', 'photos', 'p', 'pb.id_photo = p.id')
|
||||
->groupBy('b.id')
|
||||
->orderBy('position');
|
||||
|
||||
if (!isAdministration() || $forceTranslations) {
|
||||
$allBlocks->andWhere(
|
||||
\Query\Translation::coalesceTranslatedFields(
|
||||
\KupShop\I18nBundle\Translations\BlocksTranslation::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$allBlocks = $allBlocks->execute()->fetchAll();
|
||||
|
||||
return $this->buildBlockHierarchy($allBlocks, $blockID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $pool of blocks to process into hierarchy
|
||||
* @param int|null $parentID to begin with - can be null
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function buildBlockHierarchy(array $pool, ?int $parentID = null)
|
||||
{
|
||||
$finalArray = [];
|
||||
foreach ($pool as $index => $item) {
|
||||
if ((int) $item['id_parent'] === $parentID) {
|
||||
unset($pool[$index]);
|
||||
$children = $this->buildBlockHierarchy($pool, $item['id']);
|
||||
if (count($children) > 0) {
|
||||
$item['children'] = $children;
|
||||
}
|
||||
$item['photos'] = array_map(function (array $photoData) {
|
||||
return getImage($photoData['id_photo'], '', '', 'product_catalog', '', strtotime($photoData['date_update']));
|
||||
}, array_filter(json_decode($item['photos'] ?: '[]', true)));
|
||||
|
||||
$finalArray[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $finalArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $entityBlockIDFieldName = id_block
|
||||
*/
|
||||
public function saveBlocks(
|
||||
array $data,
|
||||
int $entityID,
|
||||
string $entityTableName,
|
||||
string $entityBlockIDFieldName = 'id_block')
|
||||
{
|
||||
if (!isset($_POST['relations'])) {
|
||||
$this->returnError('Chyba ve formuláři');
|
||||
}
|
||||
// decode hierarchy relations
|
||||
$relations = json_decode_strict($_POST['relations'], true);
|
||||
|
||||
// remove block IDs from $data and $relations when duplicating parent entity
|
||||
if (getVal('Duplicate', $_REQUEST, false)) {
|
||||
foreach ($data['blocks'] as $index => $block) {
|
||||
if (!empty($block['id'])) {
|
||||
$content = $this->selectSQL('blocks', ['id' => $block['id']], ['content', 'json_content'])->fetch();
|
||||
$data['blocks'][$index] = array_merge($block, $content);
|
||||
}
|
||||
unset($data['blocks'][$index]['id']);
|
||||
}
|
||||
foreach ($relations as $index => $relation) {
|
||||
$relations[$index]['parentID'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
$conn = sqlGetConnection();
|
||||
$conn->transactional(function (Connection $conn) use ($data, $relations, $entityID, $entityTableName, $entityBlockIDFieldName) {
|
||||
$rootID = sqlQueryBuilder()->select($entityBlockIDFieldName)->from($entityTableName)
|
||||
->where('id=:id')->setParameter('id', $entityID)->setMaxResults(1)->execute()->fetch();
|
||||
if (isset($rootID[$entityBlockIDFieldName])) {
|
||||
$rootID = $rootID[$entityBlockIDFieldName];
|
||||
} elseif (count($data['blocks']) > 1) { // Create new root block only if any blocks present (0 block always present)
|
||||
$this->insertSQL('blocks', []);
|
||||
$rootID = sqlInsertId();
|
||||
$this->updateSQL($entityTableName, [$entityBlockIDFieldName => $rootID], ['id' => $entityID]);
|
||||
}
|
||||
$blocksToRemove = [];
|
||||
$newPosition = 1;
|
||||
$counter = 0;
|
||||
foreach ($data['blocks'] as $index => $block) {
|
||||
$counter++;
|
||||
|
||||
// ignore block with index 0 (it is the default one)
|
||||
if ($index == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if relation is not present return error (maybe save it as a root item instead?)
|
||||
if (isset($relations[$index])) {
|
||||
$block['id_parent'] = $relations[$index]['parentID'];
|
||||
if (is_null($block['id_parent'])) {
|
||||
$block['id_parent'] = $rootID;
|
||||
}
|
||||
} else {
|
||||
$this->returnError('Chyba ve formuláři');
|
||||
}
|
||||
|
||||
if (!isset($block['id'])) {
|
||||
// save new items
|
||||
if (!isset($block['delete'])) {
|
||||
$valuesToSave = [
|
||||
'id_root' => $rootID,
|
||||
'id_parent' => $block['id_parent'],
|
||||
'position' => $newPosition,
|
||||
'name' => $block['name'],
|
||||
'json_content' => $block['json_content'] ?? '',
|
||||
];
|
||||
if (getVal('Duplicate', $_REQUEST, false)) {
|
||||
$valuesToSave['content'] = $block['content'] ?? '';
|
||||
}
|
||||
if (isSuperuser() || getVal('Duplicate', $_REQUEST, false)) {
|
||||
$valuesToSave['identifier'] = $block['identifier'];
|
||||
}
|
||||
$this->insertSQL('blocks', $valuesToSave);
|
||||
$block['id'] = $newID = sqlInsertId();
|
||||
$newPosition++;
|
||||
foreach ($relations as $index2 => $arr2) {
|
||||
if (!empty($arr2['parentIndex'])
|
||||
&& $arr2['parentIndex'] == $index
|
||||
) {
|
||||
$relations[$index2]['parentID'] = (int) $newID;
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif (isset($block['delete'])) {
|
||||
$blocksToRemove[] = $block['id'];
|
||||
} else {
|
||||
// update block
|
||||
$valuesToSave = [
|
||||
'id_root' => $rootID,
|
||||
'id_parent' => $block['id_parent'],
|
||||
'position' => $newPosition,
|
||||
'name' => $block['name'],
|
||||
];
|
||||
if (isset($block['identifier'])) {
|
||||
$valuesToSave['identifier'] = $block['identifier'];
|
||||
}
|
||||
$this->updateSQL('blocks', $valuesToSave, ['id' => $block['id']]);
|
||||
$newPosition++;
|
||||
}
|
||||
|
||||
// update photos - blocks relations
|
||||
if (!empty($block['id'])) {
|
||||
$this->deleteSQL('photos_blocks_relation', ['id_block' => $block['id']]);
|
||||
foreach (getVal('photos', $block, []) as $position => $photo) {
|
||||
$this->insertSQL('photos_blocks_relation', [
|
||||
'id_photo' => $photo,
|
||||
'id_block' => $block['id'],
|
||||
'position' => $position,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($blocksToRemove) > 0) {
|
||||
$conn->createQueryBuilder()->delete('blocks')->where('id IN (:ids)')
|
||||
->setParameter('ids', $blocksToRemove, Connection::PARAM_INT_ARRAY)->execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $relationTableName
|
||||
*/
|
||||
public function updateBlocksPhotosPositions(int $entityID, string $entityTableName, string $tableField, $relationTableName = null)
|
||||
{
|
||||
if (!$relationTableName) {
|
||||
$relationTableName = 'photos_'.$entityTableName.'_relation';
|
||||
}
|
||||
|
||||
$rootBlockId = $this->selectSQL($entityTableName, ['id' => $entityID], ['id_block'])->fetchColumn();
|
||||
if ($rootBlockId) {
|
||||
$blocks = $this->getBlocks((int) $rootBlockId);
|
||||
$blockIds = [];
|
||||
foreach ($blocks as $block) {
|
||||
$blockIds[] = $block['id'];
|
||||
}
|
||||
if (!empty($blockIds)) {
|
||||
sqlQuery(
|
||||
'UPDATE photos_blocks_relation pbr
|
||||
LEFT JOIN '.$relationTableName.' rt ON rt.'.$tableField.' = :id AND rt.id_photo = pbr.id_photo
|
||||
SET pbr.position = rt.position WHERE pbr.id_photo = rt.id_photo AND pbr.id_block IN (:block_ids);',
|
||||
['id' => $entityID, 'block_ids' => $blockIds],
|
||||
['block_ids' => Connection::PARAM_INT_ARRAY]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate blocks by root ID.
|
||||
*
|
||||
* @return int|false
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function duplicateBlock(int $rootBlockID)
|
||||
{
|
||||
return sqlGetConnection()->transactional(function () use ($rootBlockID) {
|
||||
$mapping = [];
|
||||
|
||||
$blocks = sqlQueryBuilder()->select('*')
|
||||
->from('blocks')
|
||||
->where(Operator::equals(['id_root' => $rootBlockID]))
|
||||
->execute();
|
||||
|
||||
if ($blocks) {
|
||||
$this->insertSQL('blocks', []);
|
||||
$rootID = sqlInsertId();
|
||||
|
||||
$mapping[$rootBlockID] = $rootID;
|
||||
|
||||
foreach ($blocks as $block) {
|
||||
$originalBlockID = $block['id'];
|
||||
|
||||
unset($block['id']);
|
||||
|
||||
$block['id_parent'] = $mapping[$block['id_parent']] ?? null;
|
||||
$block['id_root'] = $rootID;
|
||||
|
||||
sqlQueryBuilder()->insert('blocks')
|
||||
->directValues($block)
|
||||
->execute();
|
||||
$blockID = sqlInsertId();
|
||||
|
||||
// copy photos relations
|
||||
$photoRelations = sqlQueryBuilder()->select('*')
|
||||
->from('photos_blocks_relation')
|
||||
->where(Operator::equals(['id_block' => $originalBlockID]))
|
||||
->execute();
|
||||
|
||||
foreach ($photoRelations as $photoRelation) {
|
||||
$photoRelation['id_block'] = $blockID;
|
||||
sqlQueryBuilder()->insert('photos_blocks_relation')
|
||||
->directValues($photoRelation)
|
||||
->execute();
|
||||
}
|
||||
|
||||
// copy translations
|
||||
if (findModule(\Modules::TRANSLATIONS)) {
|
||||
sqlQuery('INSERT INTO blocks_translations (id_block, id_language, created, name, content, json_content)
|
||||
SELECT
|
||||
'.$blockID.' as id_block,
|
||||
id_language,
|
||||
NOW(),
|
||||
name,
|
||||
content,
|
||||
json_content
|
||||
FROM blocks_translations
|
||||
WHERE id_block=:originalBlockId', ['originalBlockId' => $originalBlockID]);
|
||||
}
|
||||
|
||||
$mapping[$originalBlockID] = $blockID;
|
||||
}
|
||||
|
||||
return $rootID;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove blocks tree by root block ID.
|
||||
*
|
||||
* @param int|null $rootBlockID - if null, do nothing
|
||||
*
|
||||
* @return int number of affected rows
|
||||
*/
|
||||
public function removeBlocks(?int $rootBlockID = null)
|
||||
{
|
||||
// removing root block is enough, because fk_id_parent has ON DELETE CASCADE
|
||||
if (isset($rootBlockID)) {
|
||||
return $this->deleteSQL('blocks', ['id' => $rootBlockID]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function renderBlock($json_content, $block = '')
|
||||
{
|
||||
$post_data = json_encode(['data' => $json_content]);
|
||||
|
||||
$curl = curl_init();
|
||||
|
||||
curl_setopt_array(
|
||||
$curl,
|
||||
[
|
||||
CURLOPT_URL => isRunningOnCluster() ? 'blocek.services' : 'http://blocek.wpj.cz/',
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_MAXREDIRS => 10,
|
||||
CURLOPT_TIMEOUT => 50,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
CURLOPT_CUSTOMREQUEST => 'POST',
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'content-type: application/json',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $post_data);
|
||||
|
||||
$response = curl_exec($curl);
|
||||
$err = curl_error($curl);
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
if ($err) {
|
||||
$response = ['success' => false, 'error' => 'cURL Error: '.$err];
|
||||
} elseif ($decode = json_decode($response, true)) {
|
||||
$response = $decode;
|
||||
} else {
|
||||
$response = ['success' => false, 'response' => $response];
|
||||
}
|
||||
|
||||
if (!($response['success'] ?? false)) {
|
||||
$raven = getRaven();
|
||||
$raven->captureException(new \Exception("Server-side rendering failed for {$block}."), ['extra' => $response]);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
100
bundles/KupShop/ContentBundle/Util/Captcha.php
Normal file
100
bundles/KupShop/ContentBundle/Util/Captcha.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use KupShop\ContentBundle\View\Exception\ValidationException;
|
||||
use KupShop\KupShopBundle\Context\DomainContext;
|
||||
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
|
||||
use KupShop\KupShopBundle\Util\Contexts;
|
||||
use KupShop\RecaptchaBundle\Util\RecaptchaGCloudUtil;
|
||||
use ReCaptcha\ReCaptcha;
|
||||
use ReCaptcha\RequestMethod\Post;
|
||||
|
||||
class Captcha
|
||||
{
|
||||
/**
|
||||
* @param null $data
|
||||
* @param null $type One of null, 'shared', 'invisible', 'default'
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public static function checkCaptcha($data = null, $type = null)
|
||||
{
|
||||
if (isFunctionalTests()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($captcha = getVal('captcha', $data)) {
|
||||
if ($captcha != generateCaptcha(getVal('captcha_id', $data))) {
|
||||
throw new ValidationException(translate('errorCaptcha', 'form'));
|
||||
}
|
||||
} elseif ($captcha = getVal('g-recaptcha-response', $data)) {
|
||||
if (findModule(\Modules::RECAPTCHA, \Modules::SUB_GCLOUD)) {
|
||||
$util = ServiceContainer::getService(RecaptchaGCloudUtil::class);
|
||||
if (!$util->validate(token: $captcha, type: $type ?: getVal('recaptcha', $data))) {
|
||||
throw new ValidationException(translate('errorCaptcha', 'form'));
|
||||
}
|
||||
} else {
|
||||
$key = self::getCaptchaType($type ?: getVal('recaptcha', $data));
|
||||
$recaptcha = new ReCaptcha(findModule('recaptcha', $key));
|
||||
$resp = $recaptcha->verify($captcha, $_SERVER['REMOTE_ADDR']);
|
||||
if (!$resp->isSuccess()) {
|
||||
throw new ValidationException(translate('errorCaptcha', 'form'));
|
||||
}
|
||||
if ($type == 'shared' && isProduction()) {
|
||||
$domainContext = ServiceContainer::getService(DomainContext::class);
|
||||
if (array_search($resp->getHostname(), $domainContext->getSupported()) === false) {
|
||||
throw new ValidationException('Invalid CAPTCHA domain');
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($captcha = getVal('wpj-captcha-response', $data)) {
|
||||
$requestMethod = new Post('https://challenges.cloudflare.com/turnstile/v0/siteverify');
|
||||
$recaptcha = new ReCaptcha(findModule('recaptcha', 'secret_cloudflare'), $requestMethod);
|
||||
|
||||
$resp = $recaptcha->verify($captcha, $_SERVER['REMOTE_ADDR']);
|
||||
|
||||
if (!$resp->isSuccess()) {
|
||||
throw new ValidationException(translate('errorCaptcha', 'form'));
|
||||
}
|
||||
|
||||
if (isProduction()) {
|
||||
$domains = Contexts::get(DomainContext::class)->getSupported();
|
||||
if (!in_array($resp->getHostname(), $domains)) {
|
||||
throw new ValidationException('Invalid CAPTCHA domain');
|
||||
}
|
||||
}
|
||||
} elseif ($captcha = getVal('wpj-captcha-invisible-response', $data)) {
|
||||
$requestMethod = new Post('https://challenges.cloudflare.com/turnstile/v0/siteverify');
|
||||
$recaptcha = new ReCaptcha(findModule('recaptcha', 'secret_cloudflare_invisible'), $requestMethod);
|
||||
|
||||
$resp = $recaptcha->verify($captcha, $_SERVER['REMOTE_ADDR']);
|
||||
|
||||
if (!$resp->isSuccess()) {
|
||||
throw new ValidationException(translate('errorCaptcha', 'form'));
|
||||
}
|
||||
|
||||
if (isProduction()) {
|
||||
$domains = Contexts::get(DomainContext::class)->getSupported();
|
||||
if (!in_array($resp->getHostname(), $domains)) {
|
||||
throw new ValidationException('Invalid CAPTCHA domain');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new ValidationException(translate('errorCaptcha', 'form'));
|
||||
}
|
||||
}
|
||||
|
||||
protected static function getCaptchaType($type)
|
||||
{
|
||||
switch ($type) {
|
||||
case 'invisible':
|
||||
return 'secret_invisible';
|
||||
case 'shared':
|
||||
return 'secret_shared';
|
||||
|
||||
default:
|
||||
return 'secret';
|
||||
}
|
||||
}
|
||||
}
|
||||
69
bundles/KupShop/ContentBundle/Util/CartMerge.php
Normal file
69
bundles/KupShop/ContentBundle/Util/CartMerge.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use KupShop\ContentBundle\Exception\InvalidCartItemException;
|
||||
use Query\Operator;
|
||||
|
||||
class CartMerge
|
||||
{
|
||||
/** @required */
|
||||
public \Cart $cart;
|
||||
|
||||
public function merge($mergeIdCart)
|
||||
{
|
||||
$mergedCount = 0;
|
||||
|
||||
foreach ($this->getCartItems($mergeIdCart) as $item) {
|
||||
$actualItem = sqlQueryBuilder()->select('id', '`date`')
|
||||
->from('cart')
|
||||
->where(Operator::equals($this->cart->getSelectParams() + ['id_product' => $item['id_product']]))
|
||||
->andWhere(Operator::equalsNullable(['id_variation' => $item['id_variation'], 'note' => $item['note']]))
|
||||
->execute()->fetch();
|
||||
|
||||
if (!$actualItem || (new \DateTime($actualItem['date'])) < (new \DateTime($item['date']))) {
|
||||
if ($actualItem) {
|
||||
sqlQueryBuilder()->delete('cart')->where(Operator::equals(['id' => $actualItem['id']]))->execute();
|
||||
}
|
||||
unset($item['date']);
|
||||
$item['note'] = json_decode($item['note'], true) ?: '';
|
||||
try {
|
||||
if ($this->cart->addItem($item)) {
|
||||
$mergedCount++;
|
||||
}
|
||||
// kdyz merguju produkty, ktery jsou nedostupny
|
||||
} catch (InvalidCartItemException) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->cart->invalidatePurchaseState();
|
||||
|
||||
return $mergedCount;
|
||||
}
|
||||
|
||||
public function isUserCartEmpty(): bool
|
||||
{
|
||||
$item = sqlQueryBuilder()
|
||||
->select('id')
|
||||
->from('cart')
|
||||
->where(Operator::equals($this->cart->getSelectParams()))
|
||||
->execute()->fetchOne();
|
||||
|
||||
if ($item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCartItems($mergeIdCart)
|
||||
{
|
||||
return sqlQueryBuilder()
|
||||
->select('id_product', 'id_variation', 'pieces', 'note', '`date`')
|
||||
->from('cart')
|
||||
->where(Operator::equals(['user_key' => $mergeIdCart]))
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
246
bundles/KupShop/ContentBundle/Util/ImageLocator.php
Normal file
246
bundles/KupShop/ContentBundle/Util/ImageLocator.php
Normal file
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use KupShop\ComponentsBundle\Entity\Thumbnail;
|
||||
use KupShop\ContentBundle\Entity\Image;
|
||||
use KupShop\KupShopBundle\Context\LanguageContext;
|
||||
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
|
||||
use KupShop\KupShopBundle\Util\Contexts;
|
||||
use KupShop\KupShopBundle\Util\StringUtil;
|
||||
use KupShop\KupShopBundle\Util\System\PathFinder;
|
||||
use KupShop\KupShopBundle\Util\System\UrlFinder;
|
||||
use Query\Operator;
|
||||
use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext;
|
||||
|
||||
class ImageLocator
|
||||
{
|
||||
/**
|
||||
* @var PathFinder
|
||||
*/
|
||||
private $pathFinder;
|
||||
|
||||
/** @var UrlFinder */
|
||||
protected $urlFinder;
|
||||
|
||||
// Default settings
|
||||
private $default = [
|
||||
'crop' => true,
|
||||
'background' => false,
|
||||
'watermark' => false,
|
||||
'png' => false,
|
||||
'svg' => false,
|
||||
'upscale' => false,
|
||||
'quality' => null,
|
||||
'sharpen' => null,
|
||||
'contrast' => null,
|
||||
];
|
||||
|
||||
public function __construct(PathFinder $pathFinder)
|
||||
{
|
||||
$this->pathFinder = $pathFinder;
|
||||
}
|
||||
|
||||
public function getConfig()
|
||||
{
|
||||
global $cfg;
|
||||
|
||||
return $cfg['Photo'];
|
||||
}
|
||||
|
||||
public function getThumbnailPath(Thumbnail $thumbnail, string $type, string $size): string
|
||||
{
|
||||
// TODO: Tady začni generovat cestu bez extension a asi teda i kvůli SEO šovinistům i s názevm souboru :-(
|
||||
// TODO: A ideálně nějakou custom URL, co se bude zpracovávat v PHP, ale bude na CDN cachovaná.
|
||||
$config = $this->getTypeConfig($type);
|
||||
|
||||
$lang = null;
|
||||
if (!empty($config['lang'])) {
|
||||
$lang = Contexts::get(LanguageContext::class)->getActiveId();
|
||||
}
|
||||
|
||||
return $this->getNewPath($thumbnail, $type, $size, $lang);
|
||||
}
|
||||
|
||||
public function getThumbnailUrl(Thumbnail $thumbnail, string $type, string $size)
|
||||
{
|
||||
$path = $this->getThumbnailPath($thumbnail, $type, $size);
|
||||
|
||||
if (isFunctionalTests()) {
|
||||
$version = '';
|
||||
} else {
|
||||
$version = is_numeric($thumbnail->version) ? $thumbnail->version : strtotime($thumbnail->version);
|
||||
}
|
||||
|
||||
return $this->urlFinder->staticUrl("{$path}?{$version}");
|
||||
}
|
||||
|
||||
public function getNewPath(Thumbnail $thumbnail, string $type, string $version, ?string $lang = null): string
|
||||
{
|
||||
$path = "photo/{$type}/{$thumbnail->id}/";
|
||||
$path .= empty($thumbnail->title) ? 'photo' : StringUtil::slugify($thumbnail->title);
|
||||
$path .= empty($version) ? '' : ".{$version}";
|
||||
$path .= empty($lang) ? '' : ".{$lang}";
|
||||
|
||||
return $this->pathFinder->shopPath($path);
|
||||
}
|
||||
|
||||
public function getPath($id, $type, $ext = 'jpg', $lang = null, ?string $version = null)
|
||||
{
|
||||
$config = $this->getConfig();
|
||||
|
||||
if (!is_numeric($type)) {
|
||||
if (!isset($config['type_to_id'])) {
|
||||
$config['type_to_id'] = array_flip($config['id_to_type']);
|
||||
}
|
||||
|
||||
$type = $config['type_to_id'][$type];
|
||||
}
|
||||
|
||||
$subFolder = $id % 10;
|
||||
|
||||
if ($lang) {
|
||||
$ext = "{$lang}.{$ext}";
|
||||
}
|
||||
|
||||
$versionString = '';
|
||||
if ($version && $version !== \Photos::PHOTO_VERSION_DESKTOP && $version !== 'desktop') {
|
||||
$versionString = "_{$version}";
|
||||
}
|
||||
|
||||
return $this->pathFinder->tmpPath("{$type}/{$subFolder}/{$id}_{$type}{$versionString}.{$ext}");
|
||||
}
|
||||
|
||||
public function getTypeName($type)
|
||||
{
|
||||
$config = $this->getConfig();
|
||||
if (is_numeric($type)) {
|
||||
$type = getVal($type, $config['id_to_type'], $type);
|
||||
}
|
||||
|
||||
if (!isset($config['types'][$type])) {
|
||||
throw new \UnexpectedValueException('Neznámý typ obrázku: '.$type);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
public function getTypeConfig(&$type)
|
||||
{
|
||||
$config = $this->getConfig();
|
||||
|
||||
$type = $this->getTypeName($type);
|
||||
|
||||
return array_merge($this->default, $config['default'], $config['types'][$type]);
|
||||
}
|
||||
|
||||
public function getImageById(int $id): ?Image
|
||||
{
|
||||
$photo = sqlQueryBuilder()
|
||||
->select('*')
|
||||
->from('photos')
|
||||
->where(Operator::equals(['id' => $id]))
|
||||
->execute()->fetchAssociative();
|
||||
|
||||
if (!$photo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getImage($photo);
|
||||
}
|
||||
|
||||
public function getImage(array $photo): Image
|
||||
{
|
||||
return (new Image())
|
||||
->setId((int) $photo['id'])
|
||||
->setVideoId($photo['id_video'] ?? null)
|
||||
->setDescription($photo['descr'])
|
||||
->setSource($photo['source'])
|
||||
->setImageDesktop($photo['image_2'])
|
||||
->setImageTablet($photo['image_tablet'])
|
||||
->setImageMobile($photo['image_mobile'])
|
||||
->setDate(!empty($photo['date']) ? new \DateTime($photo['date_update']) : null)
|
||||
->setDateUpdated(new \DateTime($photo['date_update']))
|
||||
->setData(json_decode($photo['data'] ?? '', true) ?: []);
|
||||
}
|
||||
|
||||
public function getOldImageArray($id, $file, $folder, $type, $desc = '', ?int $dateUpdate = null, ?string $data = null): ?array
|
||||
{
|
||||
if (empty($id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$settings = $this->getTypeConfig($type);
|
||||
|
||||
// All thumbnails now use `jpg` extension
|
||||
$ext = 'jpg';
|
||||
|
||||
$lang = null;
|
||||
if (!empty($settings['lang'])) {
|
||||
$lang = Contexts::get(LanguageContext::class)->getActiveId();
|
||||
}
|
||||
|
||||
$path_large = $this->getPath($id, 0, $ext, $lang);
|
||||
$path_thumbnail = $this->getPath($id, $type, $ext, $lang);
|
||||
|
||||
$ret = [
|
||||
'id' => $id,
|
||||
'src' => &$path_thumbnail,
|
||||
'source' => &$path_thumbnail,
|
||||
'descr' => $desc,
|
||||
'width' => $settings['size'][0] ?? 0, // TODO: Revert, responsive types should not be accessed using getOldImageArray
|
||||
'height' => $settings['size'][1] ?? 0,
|
||||
'src_big' => &$path_large,
|
||||
'source_big' => &$path_large,
|
||||
'date_update' => $dateUpdate,
|
||||
'type' => $this->getTypeName($type),
|
||||
];
|
||||
|
||||
if ($data) {
|
||||
$data = json_decode($data, true);
|
||||
$ret = array_merge($data, $ret);
|
||||
}
|
||||
|
||||
if (($dateUpdate ?? -1) > 0) {
|
||||
$time_thumbnail = $time_large = (isFunctionalTests()) ? '' : $dateUpdate;
|
||||
} else {
|
||||
if (isLocalDevelopment()) {
|
||||
$logger = ServiceContainer::getService('logger');
|
||||
$logger->error('getOldImageArray bez data poslední změny '.$type, array_merge($ret, ['exception' => new SilencedErrorContext(1, '', 0, debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 5), 1)]));
|
||||
}
|
||||
$time_thumbnail = $time_large = '';
|
||||
}
|
||||
|
||||
$imageVersion = '';
|
||||
if (!isFunctionalTests()) {
|
||||
$imageVersion = '_'.(\Settings::getDefault()->image_version ?? '1');
|
||||
}
|
||||
|
||||
$path_thumbnail .= "?{$time_thumbnail}{$imageVersion}";
|
||||
$path_large .= "?{$time_large}{$imageVersion}";
|
||||
|
||||
$path_thumbnail = $this->urlFinder->staticUrl($path_thumbnail);
|
||||
$path_large = $this->urlFinder->staticUrl($path_large);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function clearThumbnails($image_id, $version = null)
|
||||
{
|
||||
foreach ($this->getConfig()['id_to_type'] as $id => $type) {
|
||||
foreach (glob($this->getPath($image_id, $id, '', null, $version).'*') as $filename) {
|
||||
@unlink($filename);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @required
|
||||
*/
|
||||
public function setUrlFinder(UrlFinder $urlFinder): void
|
||||
{
|
||||
$this->urlFinder = $urlFinder;
|
||||
}
|
||||
}
|
||||
47
bundles/KupShop/ContentBundle/Util/InfoPanelLoader.php
Normal file
47
bundles/KupShop/ContentBundle/Util/InfoPanelLoader.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use KupShop\I18nBundle\Translations\InfoPanelTranslation;
|
||||
use Query\Operator;
|
||||
use Query\QueryBuilder;
|
||||
use Query\Translation;
|
||||
|
||||
class InfoPanelLoader
|
||||
{
|
||||
public function loadInfoPanel(?string $type = 'default')
|
||||
{
|
||||
$infoPanel = sqlQueryBuilder()
|
||||
->select('*, ip.id AS page_id')
|
||||
->from('info_panels', 'ip')
|
||||
->andWhere(
|
||||
Operator::andX(
|
||||
Operator::equals(['type' => !empty($type) ? $type : 'default']),
|
||||
'(date_from IS NULL OR date_from <= NOW()) AND (date_to IS NULL OR date_to >= NOW())'
|
||||
)
|
||||
);
|
||||
|
||||
$infoPanel->andWhere(
|
||||
Translation::joinTranslatedFields(
|
||||
InfoPanelTranslation::class,
|
||||
function (QueryBuilder $qb, $columnName, $translatedField) {
|
||||
if ($columnName === 'active') {
|
||||
$qb->andWhere(Operator::coalesce($translatedField, 'ip.active')." = 'Y'");
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [
|
||||
'body' => 'body',
|
||||
'active' => 'is_panel_active',
|
||||
'data' => 'data',
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return $infoPanel
|
||||
->orderBy('date_from', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->execute()
|
||||
->fetchAssociative();
|
||||
}
|
||||
}
|
||||
106
bundles/KupShop/ContentBundle/Util/InlineEdit.php
Normal file
106
bundles/KupShop/ContentBundle/Util/InlineEdit.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use KupShop\ContentBundle\Entity\Wrapper\BlockWrapper;
|
||||
use KupShop\KupShopBundle\Context\LanguageContext;
|
||||
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
|
||||
|
||||
class InlineEdit
|
||||
{
|
||||
private $block;
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(Block $block, EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->block = $block;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $block BlockWrapper|array
|
||||
*
|
||||
* @return BlockWrapper|array
|
||||
*/
|
||||
public function wrapBlock($block)
|
||||
{
|
||||
static $counter = 0;
|
||||
|
||||
if (!getAdminUser()) {
|
||||
return $block;
|
||||
}
|
||||
|
||||
if (($block instanceof BlockWrapper && $block['isWrapped']) || ($block['isWrapped'] ?? false)) {
|
||||
return $block;
|
||||
}
|
||||
|
||||
if (isset($block['id_object']) && isset($block['type'])) {
|
||||
$block = $this->createBlock($block['id_object'], $block['type']);
|
||||
}
|
||||
|
||||
$jsonContent = $block['json_content'];
|
||||
if (empty($jsonContent)) {
|
||||
$jsonContent = json_encode([]);
|
||||
}
|
||||
|
||||
$id_language = '';
|
||||
$languageContext = ServiceContainer::getService(LanguageContext::class);
|
||||
if ($languageContext->translationActive()) {
|
||||
$id_language = ' data-kupshop-language-id=\''.$languageContext->getActiveId().'\'';
|
||||
}
|
||||
|
||||
$block['isWrapped'] = true;
|
||||
$block['unwrapped_content'] = $block['content'];
|
||||
$object_info = $block['object_info'] ?? '';
|
||||
$block['content'] = '<div data-kupshop-block-id=\''.$block['id'].'\''.$id_language.' data-blocek-editable data-block-title=\''.htmlspecialchars($block['name']).'\' data-blocek-id=\''.$counter++.'\' data-blocek-object-info="'.htmlspecialchars($object_info).'" data-blocek-source="'.htmlspecialchars($jsonContent).'">'.$block['content'].'</div>';
|
||||
|
||||
if (!empty($block['children'])) {
|
||||
foreach ($block['children'] as &$child) {
|
||||
$child['object_info'] = $object_info;
|
||||
$child = $this->wrapBlock($child);
|
||||
}
|
||||
}
|
||||
|
||||
return $block;
|
||||
}
|
||||
|
||||
public function createBlock($objectID, $type)
|
||||
{
|
||||
$blocks = sqlGetConnection()->transactional(function () use ($objectID, $type) {
|
||||
$rootId = $this->block->insertFirstBlock($type, $objectID, '', true);
|
||||
|
||||
return $this->block->getBlocks($rootId);
|
||||
});
|
||||
|
||||
return reset($blocks);
|
||||
}
|
||||
|
||||
public function createBlockForEntity($objectID, $type)
|
||||
{
|
||||
$block = $this->createBlock($objectID, $type);
|
||||
if (!$block) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = $this->entityManager->getRepository(\KupShop\ContentBundle\Entity\Block::class);
|
||||
|
||||
return $entity->find($block['id_root']);
|
||||
}
|
||||
|
||||
public function insertBlockForEntity($rootID)
|
||||
{
|
||||
sqlQueryBuilder()
|
||||
->insert('blocks')
|
||||
->directValues(['id_parent' => $rootID, 'id_root' => $rootID, 'content' => ''])
|
||||
->execute();
|
||||
|
||||
$entity = $this->entityManager->getRepository(\KupShop\ContentBundle\Entity\Block::class);
|
||||
|
||||
$block = $entity->find($rootID);
|
||||
// ten root block je cached, takze to ho potrebuju refreshnout, aby se mi vratil v aktualizovane podobe
|
||||
$this->entityManager->refresh($block);
|
||||
|
||||
return $entity->find($rootID);
|
||||
}
|
||||
}
|
||||
205
bundles/KupShop/ContentBundle/Util/InstagramUtil.php
Normal file
205
bundles/KupShop/ContentBundle/Util/InstagramUtil.php
Normal file
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use EspressoDev\Instagram\Instagram;
|
||||
use EspressoDev\Instagram\InstagramException;
|
||||
use KupShop\KupShopBundle\Context\ContextManager;
|
||||
use KupShop\KupShopBundle\Util\SettingsUtil;
|
||||
|
||||
class InstagramUtil
|
||||
{
|
||||
public const ITEM_TEMPLATE = '{
|
||||
"id": null,
|
||||
"user": null,
|
||||
"images": {
|
||||
"thumbnail": {
|
||||
"width": 150,
|
||||
"height": 150,
|
||||
"url": null
|
||||
},
|
||||
"low_resolution": {
|
||||
"width": 320,
|
||||
"height": 320,
|
||||
"url": null
|
||||
},
|
||||
"standard_resolution": {
|
||||
"width": 640,
|
||||
"height": 640,
|
||||
"url": null
|
||||
}
|
||||
},
|
||||
"created_time": "1554361307",
|
||||
"caption": {
|
||||
"id": "17886256120312260",
|
||||
"text": null,
|
||||
"created_time": "1554361307",
|
||||
"from": null
|
||||
},
|
||||
"user_has_liked": false,
|
||||
"likes": {
|
||||
"count": null
|
||||
},
|
||||
"tags": [
|
||||
],
|
||||
"filter": "Clarendon",
|
||||
"comments": {
|
||||
"count": 0
|
||||
},
|
||||
"type": "image",
|
||||
"link": "",
|
||||
"location": null,
|
||||
"attribution": null,
|
||||
"users_in_photo": []
|
||||
}';
|
||||
|
||||
public const FEED_TEMPLATE = '{
|
||||
"pagination": {
|
||||
"next_max_id": "",
|
||||
"next_url": ""
|
||||
},
|
||||
"data": null,
|
||||
"meta": {
|
||||
"code": 200
|
||||
}
|
||||
}';
|
||||
|
||||
/** @required */
|
||||
public SettingsUtil $settingsUtil;
|
||||
|
||||
/** @required */
|
||||
public ContextManager $contextManager;
|
||||
|
||||
public function updateFeed(string $lang, ?string $key = null)
|
||||
{
|
||||
$languageSettings = \Settings::getFromCache($lang);
|
||||
|
||||
$cfgKey = $this->getSettingsKey('instagram', $key);
|
||||
|
||||
if (!isset($languageSettings->{$cfgKey}['api'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$settings = $languageSettings->{$cfgKey};
|
||||
|
||||
$count = $settings['count'] ?? 10;
|
||||
|
||||
$instagram = new Instagram($settings['api']);
|
||||
|
||||
if (($settings['refresh_time'] ?? time()) <= time()) {
|
||||
try {
|
||||
$refresh = $instagram->refreshLongLivedToken();
|
||||
} catch (InstagramException $e) {
|
||||
throw new \InvalidArgumentException('Chyba komunikace. Nepodařilo se obnovit token.');
|
||||
}
|
||||
if (isset($refresh) && $refresh->access_token) {
|
||||
$userProfile = $instagram->getUserProfile();
|
||||
if (empty($userProfile->error)) {
|
||||
$settings['api'] = $refresh->access_token;
|
||||
$settings['refresh_time'] = time() + ($refresh->expires_in / 2);
|
||||
$settings['username'] = $userProfile->username;
|
||||
$this->settingsUtil->saveValueInLanguage($lang, $cfgKey, $settings);
|
||||
\Settings::clearCache(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$user = $instagram->getUserProfile();
|
||||
|
||||
if (!$user) {
|
||||
throw new \InvalidArgumentException('Chyba komunikace. Instagram nevrátil číslo chyby. Zkuste aktualizovat svůj token na nový.');
|
||||
}
|
||||
|
||||
if (isset($user->error)) {
|
||||
if (in_array($user->error->code, [10, 100])) {
|
||||
throw new \InvalidArgumentException('Chyba komunikace. Aktualizujte svůj token na nový.');
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException($user->error->message);
|
||||
}
|
||||
|
||||
$medias = $instagram->getUserMedia('me', $count);
|
||||
|
||||
$item = json_decode(static::ITEM_TEMPLATE, true);
|
||||
|
||||
$result = [];
|
||||
foreach (array_slice($medias->data, 0, $count) as $index => $media) {
|
||||
$new_item = $item;
|
||||
|
||||
$new_item['id'] = $media->id;
|
||||
$new_item['user'] = [
|
||||
'pk' => $user->id,
|
||||
'username' => $user->username,
|
||||
];
|
||||
$thumbnail = $this->getThumbnailFromMedia($media);
|
||||
$new_item['images']['thumbnail']['url'] = $thumbnail;
|
||||
$new_item['images']['low_resolution']['url'] = $thumbnail;
|
||||
$new_item['images']['standard_resolution']['url'] = $thumbnail;
|
||||
$new_item['likes']['count'] = null;
|
||||
$new_item['caption']['from'] = $media->username;
|
||||
$new_item['caption']['text'] = $media->caption ?? '';
|
||||
$new_item['link'] = $media->permalink;
|
||||
$new_item['created_time'] = \DateTime::createFromFormat(DATE_W3C, $media->timestamp)->getTimestamp();
|
||||
|
||||
$result[] = $new_item;
|
||||
}
|
||||
|
||||
$data = json_decode(static::FEED_TEMPLATE, true);
|
||||
$data['data'] = $result;
|
||||
|
||||
$this->settingsUtil->saveValueInLanguage($lang, $this->getSettingsKey('instagram_feed', $key), $data, false);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getFeed(string $callback, ?int $count, ?string $key = null)
|
||||
{
|
||||
$data = $this->getFeedData($count, $key);
|
||||
|
||||
if (!$data) {
|
||||
return 'empty';
|
||||
}
|
||||
|
||||
$feed = "/**/{$callback}(";
|
||||
|
||||
$feed .= json_encode($data);
|
||||
$feed .= ');';
|
||||
|
||||
return $feed;
|
||||
}
|
||||
|
||||
public function getFeedData(?int $count, ?string $key = null)
|
||||
{
|
||||
$data = \Settings::getDefault()->loadValue($this->getSettingsKey('instagram_feed', $key));
|
||||
|
||||
if ($count) {
|
||||
$data['data'] = array_slice($data['data'], 0, $count);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getSettingsKey(string $name, ?string $additionalKey = null)
|
||||
{
|
||||
if ($additionalKey) {
|
||||
$name .= '_'.$additionalKey;
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
protected function getThumbnailFromMedia(\stdClass $media): string
|
||||
{
|
||||
switch ($media->media_type) {
|
||||
case 'CAROUSEL_ALBUM':
|
||||
return $this->getThumbnailFromMedia(reset($media->children->data) ?? new \stdClass());
|
||||
case 'VIDEO':
|
||||
return $media->thumbnail_url ?? '';
|
||||
default:
|
||||
case 'IMAGE':
|
||||
return $media->media_url ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
169
bundles/KupShop/ContentBundle/Util/MenuLinksPage.php
Normal file
169
bundles/KupShop/ContentBundle/Util/MenuLinksPage.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use KupShop\ContentBundle\Entity\Page;
|
||||
use KupShop\I18nBundle\Translations\MenuLinksTranslation;
|
||||
use Query\Operator;
|
||||
use Query\QueryBuilder;
|
||||
use Query\Translation;
|
||||
|
||||
class MenuLinksPage
|
||||
{
|
||||
private const MAX_RECURSIVE_DEPTH = 50;
|
||||
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function getPages(?int $parentId = null): array
|
||||
{
|
||||
$qb = $this->getBasePageQueryBuilder()
|
||||
->andWhere(Operator::equalsNullable(['ml.parent' => $parentId]))
|
||||
->orderBy('list_order', 'ASC');
|
||||
|
||||
$pages = [];
|
||||
foreach ($qb->execute() as $item) {
|
||||
$pages[$item['id']] = $this->createPageEntity($item);
|
||||
$pages[$item['id']]->setChildren(
|
||||
$this->getPages($item['id'])
|
||||
);
|
||||
}
|
||||
|
||||
return $pages;
|
||||
}
|
||||
|
||||
public function getPage(int $menuLinkId): ?Page
|
||||
{
|
||||
if (!($page = $this->getBasePageQueryBuilder()
|
||||
->andWhere(Operator::equals(['ml.id' => $menuLinkId]))
|
||||
->execute()->fetchAssociative())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->createPageEntity($page);
|
||||
}
|
||||
|
||||
public function createPageEntity(array $page, bool $withBlock = true): Page
|
||||
{
|
||||
$pageEntity = new Page();
|
||||
$pageData = array_replace_recursive(json_decode($page['data'], true) ?: [], json_decode($page['data_translated'], true) ?: []);
|
||||
$pageEntity->setId($page['id'])
|
||||
->setType((int) $page['type'])
|
||||
->setParentId($page['parent'] ? (int) $page['parent'] : null)
|
||||
->setName($page['name'])
|
||||
->setNameShort($page['name_short'])
|
||||
->setTemplate($page['template'])
|
||||
->setMetaTitle($page['meta_title'] ?: '')
|
||||
->setMetaKeywords($page['meta_keywords'] ?: '')
|
||||
->setMetaDescription($page['meta_description'] ?: '')
|
||||
->setOldIdPage($page['old_id_page'] ?: '')
|
||||
->setFigure($page['figure'])
|
||||
->setShowInSearch($page['show_in_search'] ?? 'Y')
|
||||
->setUrl($page['url'])
|
||||
->setLink($page['link'])
|
||||
->setTarget($page['target'])
|
||||
->setData($pageData);
|
||||
|
||||
if ($withBlock && !empty($page['id_block'])) {
|
||||
$block = $this->entityManager->getRepository(\KupShop\ContentBundle\Entity\Block::class);
|
||||
$pageEntity->setBlock($block->find($page['id_block']));
|
||||
}
|
||||
|
||||
return $pageEntity;
|
||||
}
|
||||
|
||||
protected function getBasePageQueryBuilder(): QueryBuilder
|
||||
{
|
||||
return sqlQueryBuilder()->select('ml.*')
|
||||
->from('menu_links', 'ml')
|
||||
->andWhere(Translation::coalesceTranslatedFields(MenuLinksTranslation::class,
|
||||
function ($columns) {
|
||||
$columns['data'] = 'data_translated';
|
||||
|
||||
return $columns;
|
||||
}));
|
||||
}
|
||||
|
||||
public function getPageWithChildren(string $label, bool $withBlocks = true): ?Page
|
||||
{
|
||||
$pagesResult = $this->createRecursiveQueryBuilder($label)->execute();
|
||||
|
||||
if (!$rawRootPage = $pagesResult->fetchAssociative()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rootPage = $this->createPageEntity($rawRootPage, $withBlocks);
|
||||
$pagesIterator = $pagesResult->fetchAllAssociative();
|
||||
|
||||
$this->buildTree([$rootPage->getId() => $rootPage], $pagesIterator, withBlocks: $withBlocks);
|
||||
|
||||
return $rootPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Page> $parentPages
|
||||
*/
|
||||
protected function buildTree(array $parentPages, array $restPages, int $depth = 1, bool $withBlocks = true): void
|
||||
{
|
||||
if (empty($parentPages)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentLevelPages = [];
|
||||
|
||||
$i = 0;
|
||||
foreach ($restPages as $pageRow) {
|
||||
if ($pageRow['level'] > $depth) {
|
||||
$this->buildTree($currentLevelPages, array_slice($restPages, $i), $depth + 1, $withBlocks);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$i++;
|
||||
|
||||
$page = $this->createPageEntity($pageRow, $withBlocks);
|
||||
|
||||
$currentLevelPages[$page->getId()] = $page;
|
||||
$parentPages[$page->getParentId()]->appendChild($page);
|
||||
}
|
||||
}
|
||||
|
||||
protected function createRecursiveQueryBuilder(string $label, ?int $depth = null): QueryBuilder
|
||||
{
|
||||
// language=MariaDB
|
||||
$recursiveQuery = <<<__SQL__
|
||||
WITH RECURSIVE cte AS (
|
||||
SELECT *,
|
||||
0 as level
|
||||
FROM menu_links
|
||||
WHERE code = :label
|
||||
UNION ALL
|
||||
SELECT ml.*,
|
||||
cte.level + 1 AS level
|
||||
FROM menu_links ml
|
||||
INNER JOIN cte ON ml.parent = cte.id WHERE level < :depth
|
||||
) SELECT * FROM cte
|
||||
__SQL__;
|
||||
|
||||
$qb = sqlQueryBuilder()
|
||||
->select('ml.*')
|
||||
->from("({$recursiveQuery})", 'ml')
|
||||
->setParameter('depth', $depth ?? self::MAX_RECURSIVE_DEPTH)
|
||||
->setParameter('label', $label)
|
||||
->andWhere(Translation::coalesceTranslatedFields(MenuLinksTranslation::class,
|
||||
static function ($columns) {
|
||||
$columns['data'] = 'data_translated';
|
||||
|
||||
return $columns;
|
||||
}))
|
||||
->addOrderBy('ml.level')
|
||||
->addOrderBy('ml.list_order');
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
94
bundles/KupShop/ContentBundle/Util/MenuUtil.php
Normal file
94
bundles/KupShop/ContentBundle/Util/MenuUtil.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use KupShop\I18nBundle\Translations\MenuLinksTranslation;
|
||||
use Query\Operator;
|
||||
use Query\QueryBuilder;
|
||||
use Query\Translation;
|
||||
|
||||
class MenuUtil
|
||||
{
|
||||
/** @var array menu item types */
|
||||
public static $TYPES = [
|
||||
self::TYPE_PAGE => 'Page',
|
||||
self::TYPE_LINK => 'Link',
|
||||
self::TYPE_GROUP => 'Group',
|
||||
];
|
||||
|
||||
public const TYPE_PAGE = 1;
|
||||
public const TYPE_LINK = 2;
|
||||
public const TYPE_GROUP = 3;
|
||||
|
||||
/** @var array menu item type classes */
|
||||
public static $TYPES_CLASSES = [
|
||||
self::TYPE_PAGE => 'bi bi-pencil-square',
|
||||
self::TYPE_LINK => 'bi bi-link-45deg',
|
||||
self::TYPE_GROUP => 'bi bi-folder',
|
||||
];
|
||||
|
||||
/** @var array menu item type titles */
|
||||
public static $TYPES_TITLES = [
|
||||
self::TYPE_PAGE => 'Obsahová stránka',
|
||||
self::TYPE_LINK => 'Odkaz',
|
||||
self::TYPE_GROUP => 'Skupina',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array [menuID => menuRow]
|
||||
*/
|
||||
public static function getMenuRoots(): array
|
||||
{
|
||||
$result = sqlQueryBuilder()->select('*')->from('menu_links')
|
||||
->where('parent IS NULL AND id=id_menu')
|
||||
->orderBy('list_order')->execute();
|
||||
$roots = [];
|
||||
foreach ($result as $row) {
|
||||
$roots[$row['id']] = $row;
|
||||
}
|
||||
|
||||
return $roots;
|
||||
}
|
||||
|
||||
public static function fixOrderTreeLevelPositions($parentID, $menuID, bool $sortMenus = false)
|
||||
{
|
||||
if ($sortMenus) {
|
||||
$data = ['parent' => $parentID];
|
||||
$menuID = null;
|
||||
} else {
|
||||
$data = ['parent' => $parentID, 'id_menu' => $menuID];
|
||||
}
|
||||
|
||||
if (is_null($parentID)) {
|
||||
if (is_null($menuID)) {
|
||||
// ignore unclassified items when sortMenus enabled (root menu items has id_menu value)
|
||||
// ignore root menu items when sortMenu disabled -> only sort unclassified root items
|
||||
$where = $sortMenus ? ' id_menu IS NOT NULL' : ' id_menu IS NULL';
|
||||
} else {
|
||||
$where = ' id_menu = :id_menu';
|
||||
}
|
||||
$where .= ' AND parent IS NULL';
|
||||
} else {
|
||||
$where = ' parent = :parent';
|
||||
}
|
||||
|
||||
sqlQuery("SELECT @i := -1; UPDATE menu_links SET list_order = (select @i := @i + 1) WHERE {$where} ORDER BY list_order, id", $data);
|
||||
}
|
||||
|
||||
public function resolve($uri)
|
||||
{
|
||||
return sqlQueryBuilder()->select('ml.id')
|
||||
->from('menu_links', 'ml')
|
||||
->andWhere(
|
||||
Translation::joinTranslatedFields(
|
||||
MenuLinksTranslation::class,
|
||||
function (QueryBuilder $qb, $columnName, $translatedField, $langID) use ($uri) {
|
||||
$qb->andWhere(Operator::equals([Operator::coalesce($translatedField, 'ml.url') => $uri]));
|
||||
|
||||
return false;
|
||||
},
|
||||
['url']
|
||||
))
|
||||
->execute()->fetchColumn();
|
||||
}
|
||||
}
|
||||
41
bundles/KupShop/ContentBundle/Util/PageLocator.php
Normal file
41
bundles/KupShop/ContentBundle/Util/PageLocator.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use KupShop\ContentBundle\Exception\UnknownPageTypeException;
|
||||
use KupShop\ContentBundle\Page\PageInterface;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
|
||||
class PageLocator
|
||||
{
|
||||
private $locator;
|
||||
private $pages;
|
||||
|
||||
public function __construct(ServiceLocator $locator, array $pages)
|
||||
{
|
||||
$this->locator = $locator;
|
||||
$this->pages = $pages;
|
||||
}
|
||||
|
||||
public function getTypes()
|
||||
{
|
||||
return array_keys($this->pages);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PageInterface
|
||||
*
|
||||
* @throws UnknownPageTypeException
|
||||
*/
|
||||
public function getPageService(string $type)
|
||||
{
|
||||
if (isset($this->pages[$type])) {
|
||||
$class = $this->pages[$type]['class'];
|
||||
if ($this->locator->has($class)) {
|
||||
return $this->locator->get($class);
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnknownPageTypeException($type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
trait RecursiveTemplateFinderTrait
|
||||
{
|
||||
public function getTemplatesFromPath(string $path, string $templateName, string $folderName): iterable
|
||||
{
|
||||
// List all shop templates in target path
|
||||
if (is_dir($path)) {
|
||||
yield from $this->yieldTemplates($path, $templateName, $folderName);
|
||||
}
|
||||
}
|
||||
|
||||
private function yieldTemplates(string $path, string $templateName, string $folderName): iterable
|
||||
{
|
||||
foreach (new \FilesystemIterator($path) as $file) {
|
||||
if (is_dir($file->getPathname())) {
|
||||
yield from $this->yieldTemplates($file->getPathname(), $templateName, $folderName);
|
||||
} else {
|
||||
if ($file->getFilename() == $templateName) {
|
||||
preg_match("#{$folderName}/[^/]+#", $file->getPathname(), $matches);
|
||||
yield $matches[0].'/'.$file->getFilename();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
bundles/KupShop/ContentBundle/Util/SitemapLocator.php
Normal file
40
bundles/KupShop/ContentBundle/Util/SitemapLocator.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use KupShop\ContentBundle\Exception\UnknownSitemapTypeException;
|
||||
use KupShop\ContentBundle\Sitemap\SitemapInterface;
|
||||
|
||||
class SitemapLocator
|
||||
{
|
||||
private $handlers;
|
||||
|
||||
public function __construct(iterable $handlers)
|
||||
{
|
||||
$this->handlers = $handlers;
|
||||
}
|
||||
|
||||
public function getTypes(): array
|
||||
{
|
||||
$types = [];
|
||||
|
||||
foreach ($this->handlers as $handler) {
|
||||
if ($handler::isAllowed()) {
|
||||
$types[] = $handler::getType();
|
||||
}
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
public function getSitemap(string $type): SitemapInterface
|
||||
{
|
||||
foreach ($this->handlers as $handler) {
|
||||
if ($handler::getType() === $type && $handler::isAllowed()) {
|
||||
return $handler;
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnknownSitemapTypeException($type);
|
||||
}
|
||||
}
|
||||
348
bundles/KupShop/ContentBundle/Util/SliderUtil.php
Normal file
348
bundles/KupShop/ContentBundle/Util/SliderUtil.php
Normal file
@@ -0,0 +1,348 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\ContentBundle\Util;
|
||||
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use KupShop\ContentBundle\Entity\Slider;
|
||||
use KupShop\I18nBundle\Translations\PhotosTranslation;
|
||||
use KupShop\I18nBundle\Translations\SlidersImagesTranslation;
|
||||
use KupShop\I18nBundle\Translations\SlidersTranslation;
|
||||
use KupShop\KupShopBundle\Util\Entity\EntityUtil;
|
||||
use KupShop\KupShopBundle\Util\StringUtil;
|
||||
use KupShop\KupShopBundle\Util\System\PathFinder;
|
||||
use Query\Operator;
|
||||
use Query\QueryBuilder;
|
||||
use Query\Translation;
|
||||
use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerException;
|
||||
|
||||
class SliderUtil
|
||||
{
|
||||
private EntityUtil $entityUtil;
|
||||
private ImageLocator $imageLocator;
|
||||
private PathFinder $pathFinder;
|
||||
|
||||
public function __construct(EntityUtil $entityUtil, ImageLocator $imageLocator, PathFinder $pathFinder)
|
||||
{
|
||||
$this->entityUtil = $entityUtil;
|
||||
$this->imageLocator = $imageLocator;
|
||||
$this->pathFinder = $pathFinder;
|
||||
}
|
||||
|
||||
public function get(int $id, bool $random = false): ?Slider
|
||||
{
|
||||
$sliders = $this->fetchSliders(
|
||||
Operator::andX(
|
||||
Operator::equals(['sl.id' => $id]),
|
||||
static::specRandomSlider($random),
|
||||
),
|
||||
);
|
||||
|
||||
return count($sliders) > 0 ? array_values($sliders)[0] : null;
|
||||
}
|
||||
|
||||
public function getByName(string $name, bool $random = false): ?Slider
|
||||
{
|
||||
$sliders = $this->fetchSliders(
|
||||
Operator::andX(
|
||||
Operator::like(['sl.name' => $name]),
|
||||
static::specRandomSlider($random),
|
||||
),
|
||||
);
|
||||
|
||||
return count($sliders) > 0 ? array_values($sliders)[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Slider[]
|
||||
*
|
||||
* @throws DBALException|SerializerException
|
||||
*/
|
||||
public function fetchSliders(?callable $spec = null, string $indexBy = 'id'): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder();
|
||||
if ($spec) {
|
||||
$qb->andWhere($spec);
|
||||
}
|
||||
|
||||
$slidersData = [];
|
||||
foreach ($qb->execute() as $item) {
|
||||
$index = $item[$indexBy];
|
||||
|
||||
if (!isset($slidersData[$index])) {
|
||||
$slidersData[$index] = [
|
||||
'id' => (int) $item['id'],
|
||||
'name' => $item['name'],
|
||||
'custom_data' => array_replace_recursive(
|
||||
json_decode($item['slider_data'] ?: '', true) ?: [],
|
||||
json_decode($item['slider_data2'] ?: '', true) ?: [],
|
||||
),
|
||||
'images' => [],
|
||||
'_index' => $index,
|
||||
];
|
||||
|
||||
if (!empty($item['id_section'])) {
|
||||
$slidersData[$index]['id_section'] = $item['id_section'];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($item['id_photo'])) {
|
||||
if ($item['figure'] === 'N') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$photo = $this->imageLocator->getImage(
|
||||
$this->getImageArrayFromItem($item),
|
||||
);
|
||||
|
||||
$slidersData[$index]['images'][] = [
|
||||
'id' => $item['id_slider_image'],
|
||||
'link' => $item['link'],
|
||||
'description' => $item['description'],
|
||||
'image' => '/'.$this->pathFinder->dataPath('photos/'.$photo->getSource().$photo->getImageDesktop()),
|
||||
'image_data' => json_decode($item['custom_data'] ?? '', true),
|
||||
'photo' => $photo,
|
||||
'custom_data' => array_replace_recursive(
|
||||
json_decode($item['image_data'] ?: '', true) ?: [],
|
||||
json_decode($item['image_data2'] ?: '', true) ?: [],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$sliders = [];
|
||||
foreach ($slidersData as $index => $sliderData) {
|
||||
$sliders[$index] = $slider = $this->entityUtil->createEntity($sliderData, Slider::class);
|
||||
|
||||
foreach ($slider->images as &$image) {
|
||||
$image['slider'] = $slider;
|
||||
}
|
||||
}
|
||||
|
||||
return $sliders;
|
||||
}
|
||||
|
||||
public function createQueryBuilder(): QueryBuilder
|
||||
{
|
||||
$qb = sqlQueryBuilder()
|
||||
->from('sliders', 'sl');
|
||||
|
||||
foreach ($this->getSelectFields() as $alias => $column) {
|
||||
$qb->addSelect("{$column} as {$alias}");
|
||||
}
|
||||
|
||||
$qb->leftJoin('sl', 'sliders_images', 'si', 'si.id_slider=sl.id AND ((si.date_from < NOW() OR si.date_from IS NULL) AND (si.date_to > NOW() OR si.date_to IS NULL))')
|
||||
->join('si', 'photos', 'ph', 'ph.id = si.id_photo')
|
||||
->andWhere(Translation::coalesceTranslatedFields(SlidersTranslation::class, ['name' => 'name', 'data' => 'slider_data2']))
|
||||
->andWhere(
|
||||
Translation::coalesceTranslatedFields(
|
||||
SlidersImagesTranslation::class,
|
||||
[
|
||||
'description' => 'description',
|
||||
'link' => 'link',
|
||||
'data' => 'image_data2',
|
||||
'figure' => 'figure',
|
||||
]
|
||||
)
|
||||
)
|
||||
->andWhere(
|
||||
Translation::coalesceTranslatedFields(
|
||||
PhotosTranslation::class,
|
||||
[
|
||||
'descr' => 'photo_descr',
|
||||
'image_2' => 'photo_image_2',
|
||||
'image_tablet' => 'photo_image_tablet',
|
||||
'image_mobile' => 'photo_image_mobile',
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
if (findModule(\Modules::VIDEOS)) {
|
||||
$qb->leftJoin('ph', 'videos', 'v', 'v.id_photo = ph.id');
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
protected function getSelectFields(): array
|
||||
{
|
||||
$fields = [
|
||||
'id' => 'sl.id',
|
||||
'name' => 'sl.name',
|
||||
'slider_data' => 'sl.data',
|
||||
'link' => 'si.link',
|
||||
'description' => 'si.description',
|
||||
'id_slider_image' => 'si.id',
|
||||
'id_photo' => 'si.id_photo',
|
||||
'image_data' => 'si.data',
|
||||
|
||||
'photo_id' => 'ph.id',
|
||||
'photo_filename' => 'ph.filename',
|
||||
'photo_descr' => 'ph.descr',
|
||||
'photo_source' => 'ph.source',
|
||||
'photo_image_2' => 'ph.image_2',
|
||||
'photo_image_tablet' => 'ph.image_tablet',
|
||||
'photo_image_mobile' => 'ph.image_mobile',
|
||||
'photo_date' => 'ph.date',
|
||||
'photo_sync_id' => 'ph.sync_id',
|
||||
'photo_date_update' => 'ph.date_update',
|
||||
'custom_data' => 'ph.data',
|
||||
];
|
||||
|
||||
if (findModule(\Modules::VIDEOS)) {
|
||||
$fields['id_video'] = 'v.id_cdn';
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
private function getImageArrayFromItem(array $item): array
|
||||
{
|
||||
$image = [];
|
||||
foreach ($item as $key => $value) {
|
||||
if (StringUtil::startsWith($key, 'photo_') || $key == 'id_video') {
|
||||
$image[ltrim($key, 'photo_')] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{position: string, id_section: int, id_slider: int} $positionsInSections
|
||||
*
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function saveSliderPositions(array $positionsInSections, ?array $onDuplicateKeyUpdate = null): void
|
||||
{
|
||||
if (empty($positionsInSections)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$validPositions = static::getPositions();
|
||||
$types = [
|
||||
'id_section' => Type::INTEGER,
|
||||
'id_slider' => Type::INTEGER,
|
||||
'position' => Type::STRING,
|
||||
];
|
||||
|
||||
$qb = sqlQueryBuilder()->insert('sliders_in_sections');
|
||||
foreach ($positionsInSections as $mapping) {
|
||||
ksort($mapping);
|
||||
|
||||
if (!in_array($mapping['position'], array_keys($validPositions))) {
|
||||
throw new \InvalidArgumentException("Invalid position '{$mapping['position']}'");
|
||||
}
|
||||
|
||||
$qb->multiDirectValues($mapping, $types);
|
||||
}
|
||||
|
||||
if ($onDuplicateKeyUpdate) {
|
||||
$qb->onDuplicateKeyUpdate($onDuplicateKeyUpdate);
|
||||
}
|
||||
|
||||
$qb->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DBALException|\InvalidArgumentException
|
||||
*/
|
||||
public function clearSliderSections(?int $idSlider = null, ?int $idSection = null): void
|
||||
{
|
||||
if (!isset($idSlider) && !isset($idSection)) {
|
||||
throw new \InvalidArgumentException('Both $idSlider and $idSection cannot be empty');
|
||||
}
|
||||
|
||||
$qb = sqlQueryBuilder()->delete('sliders_in_sections');
|
||||
if (isset($idSlider)) {
|
||||
$qb->andWhere(Operator::equals(['id_slider' => $idSlider]));
|
||||
}
|
||||
|
||||
if (isset($idSection)) {
|
||||
$qb->andWhere(Operator::equals(['id_section' => $idSection]));
|
||||
}
|
||||
|
||||
$qb->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Slider[]
|
||||
*
|
||||
* @throws DBALException|SerializerException
|
||||
*/
|
||||
public function getSectionSliders(int $sectionID): array
|
||||
{
|
||||
return $this->fetchSliders(
|
||||
function (QueryBuilder $qb) use ($sectionID) {
|
||||
$sectionPositions = sqlQueryBuilder()
|
||||
->select('DISTINCT sis.id_slider', 'sis.position')
|
||||
->from('sliders_in_sections', 'sis')
|
||||
->andWhere(Operator::equals(['sis.id_section' => $sectionID]));
|
||||
|
||||
$qb->addSelect('sis.position AS section_position')
|
||||
->joinSubQuery('sl', $sectionPositions, 'sis', 'sl.id = sis.id_slider');
|
||||
},
|
||||
indexBy: 'section_position',
|
||||
);
|
||||
}
|
||||
|
||||
public function getSliderPositions(?callable $spec = null): array
|
||||
{
|
||||
$qb = sqlQueryBuilder()
|
||||
->select('sis.position AS section_position', 'sis.id_section', 'sis.id_slider', 'sl.name')
|
||||
->from('sliders_in_sections', 'sis')
|
||||
->leftJoin('sis', 'sliders', 'sl', 'sl.id = sis.id_slider')
|
||||
->addGroupBy('sis.id_section', 'sis.position');
|
||||
|
||||
if ($spec !== null) {
|
||||
$qb->andWhere($spec);
|
||||
}
|
||||
|
||||
$mapped = [];
|
||||
foreach ($qb->execute() as $positionInSection) {
|
||||
$sectionID = $positionInSection['id_section'];
|
||||
$position = $positionInSection['section_position'];
|
||||
|
||||
if (!isset($mapped[$sectionID])) {
|
||||
$mapped[$sectionID] = [];
|
||||
}
|
||||
|
||||
$mapped[$sectionID][$position] = $positionInSection;
|
||||
}
|
||||
|
||||
return $mapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array Slider positions from config <code>($cfg['Modules']['sliders']['positions'])</code>
|
||||
*/
|
||||
public static function getPositions(): array
|
||||
{
|
||||
$positions = findModule(\Modules::SLIDERS, 'positions', default: []);
|
||||
|
||||
if (!array_key_exists('main', $positions)) {
|
||||
$positions = array_merge(['main' => 'Hlavní'], $positions);
|
||||
}
|
||||
|
||||
if (findModule(\Modules::SLIDERS, 'additional_section_slider')
|
||||
&& !array_key_exists('additional', $positions)) {
|
||||
$positions['additional'] = 'Vedlejší';
|
||||
}
|
||||
|
||||
return $positions;
|
||||
}
|
||||
|
||||
private static function specRandomSlider(bool $random): callable
|
||||
{
|
||||
return function (QueryBuilder $qb) use ($random) {
|
||||
if ($random) {
|
||||
$qb->orderBy('RAND()')
|
||||
->setMaxResults(1);
|
||||
} else {
|
||||
$qb->orderBy('si.position');
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user