src/Controller/Website/ArticleController.php line 248

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Website;
  3. use App\Article\ArticleListFactory;
  4. use App\BlockManager\ContentBlockManager;
  5. use App\BlockManager\SidebarBlockManager;
  6. use App\DataLayer\DataLayerEvent;
  7. use App\DataLayer\DataLayerManager;
  8. use App\Entity\Article;
  9. use App\Entity\ArticleCategory;
  10. use App\Repository\ArticleCategoryRepository;
  11. use App\Repository\ArticleRepository;
  12. use App\Utils\SchemaManager;
  13. use App\Utils\SnippetManager;
  14. use Spatie\SchemaOrg\NewsArticle;
  15. use Spatie\SchemaOrg\Schema;
  16. use Sulu\Bundle\ContactBundle\Api\Contact;
  17. use Sulu\Bundle\ContactBundle\Contact\ContactManagerInterface;
  18. use Sulu\Bundle\DocumentManagerBundle\Bridge\DocumentInspector;
  19. use Sulu\Bundle\MediaBundle\Api\Media;
  20. use Sulu\Bundle\MediaBundle\Media\Manager\MediaManagerInterface;
  21. use Sulu\Bundle\PageBundle\Document\HomeDocument;
  22. use Sulu\Bundle\PageBundle\Document\PageDocument;
  23. use Sulu\Bundle\RedirectBundle\Model\RedirectRouteRepositoryInterface;
  24. use Sulu\Component\Content\Compat\StructureInterface;
  25. use Sulu\Component\Content\Compat\StructureManagerInterface;
  26. use Sulu\Component\DocumentManager\DocumentManagerInterface;
  27. use Sulu\Component\Rest\Exception\EntityNotFoundException;
  28. use Sulu\Component\Webspace\Webspace;
  29. use Symfony\Component\HttpFoundation\RedirectResponse;
  30. use Symfony\Component\HttpFoundation\Request;
  31. use Symfony\Component\HttpFoundation\RequestStack;
  32. use Symfony\Component\HttpFoundation\Response;
  33. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  34. use Symfony\Component\Routing\Annotation\Route;
  35. class ArticleController extends SuluExtendController
  36. {
  37.     private Request $request;
  38.     public function __construct(
  39.         private readonly ArticleRepository                              $articleRepository,
  40.         private readonly ArticleCategoryRepository                      $articleCategoryRepository,
  41.         private readonly ContentBlockManager                            $contentBlockManager,
  42.         private readonly ArticleListFactory                             $articleListFactory,
  43.         private readonly SchemaManager                                  $schemaManager,
  44.         private readonly ContactManagerInterface                        $contactManager,
  45.         private readonly SnippetManager                                 $snippetManager,
  46.         private readonly DocumentManagerInterface                       $documentManager,
  47.         private readonly SidebarBlockManager                            $sidebarBlockManager,
  48.         private readonly MediaManagerInterface                          $mediaManager,
  49.         private readonly StructureManagerInterface                      $structureManager,
  50.         private readonly DocumentInspector                              $documentInspector,
  51.         private readonly RedirectRouteRepositoryInterface               $redirectRouteRepository,
  52.         private readonly DataLayerManager                               $dataLayerManager,
  53.         public           RequestStack                                   $requestStack,
  54.     ) {
  55.         $this->request $this->requestStack->getCurrentRequest();
  56.     }
  57.     public function viewAction(
  58.         Article            $article,
  59.                            $attributes = [],
  60.                            $preview false,
  61.                            $partial false,
  62.     ): Response
  63.     {
  64.         $postSettingsArea $this->snippetManager->loadByArea('post_settings');
  65.         $postSettings $postSettingsArea['content'] ?? [];
  66.         $attributes $this->getAttributes($attributes);
  67.         $attributes['breadcrumbs'] = $this->getBreadcrumbs('article'$article->getUuid());
  68.         $schema $this->schemaManager->getDefaultSchemas($attributes['breadcrumbs']);
  69.         $articleSchema Schema::newsArticle()
  70.             ->headline($article->getTitle())
  71.             ->if($article->getPublished(), function (NewsArticle $schema) use ($article) {
  72.                 $schema->datePublished($article->getPublished());
  73.             })
  74.             ->if($article->getChanged(), function (NewsArticle $schema) use ($article) {
  75.                 $schema->dateModified($article->getChanged());
  76.             });
  77.         if ($article->getAuthor()) {
  78.             $articleSchema->author($this->schemaManager->getPerson($article->getAuthor()));
  79.         }
  80.         // ---------------------------------------
  81.         // Post header
  82.         // ---------------------------------------
  83.         $attributes['post'] = [
  84.             'header' => [
  85.                 'heading' => [
  86.                     'title' => $article->getTitle(),
  87.                 ],
  88.                 'perex' => $article->getPerex(),
  89.             ],
  90.             'spacing' => 'top-0',
  91.         ];
  92.         if($categories $article->getCategories()) {
  93.             $attributes['post']['tags'] = [];
  94.             foreach($categories as $category) {
  95.                 $categorySlug $category->hasSlug() ? $category->getSlug() : $category->getId();
  96.                 $attributes['post']['tags'][] = [
  97.                     'title' => $category->getTitle(),
  98.                     'url' => $this->router->generate('app.article_category', ['slug' => $categorySlug])
  99.                 ];
  100.             }
  101.         }
  102.         if($article->getPublished()) {
  103.             $attributes['post']['date'] =
  104.               $article->getPublished()->format('d.m.Y');
  105.         }
  106.         if($article->getAuthor()) {
  107.             $attributes['post']['author']['title'] = $article->getAuthor()->getFullName();
  108.             if($article->getAuthor()->getAvatar()) {
  109.                 $media $this->mediaManager->getById($article->getAuthor()->getAvatar()->getId(), $article->getLocale());
  110.                 $attributes['post']['author']['img'] = [
  111.                     'src' => $media->getThumbnails()['64x64'],
  112.                     'srcset' => [
  113.                         $media->getThumbnails()['64x64'] . ' 1x',
  114.                         $media->getThumbnails()['128x128'] . ' 2x',
  115.                     ],
  116.                     'alt' => $media->getTitle(),
  117.                 ];
  118.             }
  119.         }
  120.         // ---------------------------------------
  121.         // Post static main image
  122.         // ---------------------------------------
  123.         if($article->getImage()) {
  124.             $media $this->mediaManager->getById($article->getImage()->getId(), $article->getLocale());
  125.             $articleSchema->image($this->request->getSchemeAndHttpHost() . $media->getUrl());
  126.         }
  127.         // ---------------------------------------
  128.         // Share section
  129.         // ---------------------------------------
  130.         $articleUrl $this->request->getSchemeAndHttpHost() . $article->getRoute()->getPath();
  131.         $attributes['post']['share'] = [
  132.             'title' => 'Sdílejte článek:',
  133.             'facebook' => 'https://www.facebook.com/sharer/sharer.php?u=' $articleUrl,
  134.             'linkedin' => 'https://www.linkedin.com/feed/?shareActive=true&text=' $articleUrl,
  135.         ];
  136.         // ---------------------------------------
  137.         // Related posts
  138.         // ---------------------------------------
  139.         $limit 3;
  140.         if ($postSettings && is_numeric($postSettings['similarCount'])) {
  141.             $limit $postSettings['similarCount'];
  142.         }
  143.         $similarArticles $this->articleRepository->findSimilarArticles($article$limit);
  144.         if($similarArticles && $postSettings) {
  145.             $attributes['similar_posts'] = [
  146.                 'header' => [
  147.                     'heading' => [
  148.                         'title' => $postSettings['similarName'] ?: 'Podobné články',
  149.                         'level' => 3,
  150.                         'style' => 'h3',
  151.                     ],
  152.                     'variant' => 'left',
  153.                 ],
  154.                 'grid' => [
  155.                     'items' => [],
  156.                     'variant' => 'inline',
  157.                 ],
  158.                 'buttons' => [],
  159.                 'spacing' => 'none',
  160.             ];
  161.             $categories $article->getCategories();
  162.             if(!$categories->isEmpty()) {
  163.                 $categorySlug $categories->first()->hasSlug() ? $categories->first()->getSlug() : $categories->first()->getId();
  164.                 $attributes['similar_posts']['buttons']['primary'] = [
  165.                    'title' => $postSettings['similarLinkText'] ?: 'Více článků',
  166.                    'url' => $this->router->generate('app.article_category', ['slug' => $categorySlug]),
  167.                 ];
  168.             }
  169.             foreach($similarArticles as $similarArticle) {
  170.                 $attributes['similar_posts']['grid']['items'][] = $this->articleRepository->renderArticle($similarArticle);
  171.             }
  172.         }
  173.         // ---------------------------------------
  174.         // Sidebar
  175.         // ---------------------------------------
  176.         if(isset($postSettings['sidebar']['blocks'])) {
  177.             $attributes['sidebar']['blocks'] = $this->sidebarBlockManager->mapBlocksAttributes($postSettings['sidebar']['blocks']);
  178.         }
  179.         // ---------------------------------------
  180.         // Content
  181.         // ---------------------------------------
  182.         $attributes['content_blocks'] = [];
  183.         $mappedContentBlocks $this->contentBlockManager->mapBlocksAttributes($attributes['content']['content_blocks']);
  184.         $articleSchemaImages = [];
  185.         foreach ($mappedContentBlocks as $block) {
  186.             if($block['type'] == 'Image' || $block['type'] == 'ImageWithText') {
  187.                 $imgUrl $this->request->getSchemeAndHttpHost() . $block['content']['img']?->getUrl();
  188.                 $articleSchemaImages[] = $imgUrl;
  189.             }
  190.             elseif($block['type'] == 'gallery' && isset($block['content']['images'])) {
  191.                 /** @var Media $image */
  192.                 foreach ($block['content']['images'] as $image) {
  193.                     $imgUrl $this->request->getSchemeAndHttpHost() . $image?->getUrl();
  194.                     $articleSchemaImages[] = $imgUrl;
  195.                 }
  196.             }
  197.             $attributes['content_blocks'][] = $block;
  198.         }
  199.         $articleSchema->image($articleSchemaImages);
  200.         $schema[] = $articleSchema;
  201.         // ---------------------------------------
  202.         // Schema.org
  203.         // ---------------------------------------
  204.         $schema[] = $articleSchema;
  205.         $attributes['_schema'] = implode(''$schema);
  206.         // ---------------------------------------
  207.         // DataLayer
  208.         // ---------------------------------------
  209.         $viewArticleEvent = new DataLayerEvent('view_article');
  210.         if($article->getCategories()) {
  211.             $i 0;
  212.             foreach($article->getCategories() as $category) {
  213.                 $i++;
  214.                 $viewArticleEvent->setValue('article_category'.$i$category->getTitle());
  215.             }
  216.         }
  217.         $this->dataLayerManager
  218.             ->addEvent($viewArticleEvent)
  219.             ->addEvent((new DataLayerEvent())->setValue('page_type''article'));
  220.         $this->setEntitySocialMeta($article);
  221.         $this->setEntitySeoMeta($article);
  222.         return $this->renderAll('pages/article.html.twig'$attributes$preview$partial);
  223.     }
  224.     public function listAction(
  225.         ?StructureInterface $structure null,
  226.         ?string             $pageId null,
  227.         int|string          $page 1,
  228.                             $preview false,
  229.                             $partial false,
  230.     ): Response
  231.     {
  232.         $page = (int)$page;
  233.         if (!$structure && $pageId) {
  234.             /** @var HomeDocument|PageDocument $document */
  235.             $document $this->documentManager->find($pageId$this->request->getLocale());
  236.             $structure $this->getDocumentStructure($document);
  237.         }
  238.         $attributes $this->getAttributes([], $structure);
  239.         $attributes['breadcrumbs'] = $this->getBreadcrumbs('page'$attributes['uuid']);
  240.         $schema $this->schemaManager->getDefaultSchemas($attributes['breadcrumbs']);
  241.         $schemaItemList Schema::itemList()
  242.             ->name($attributes['content']['title'])
  243.             ->description($attributes['content']['description']);
  244.         $attributes['posts'] = [
  245.             'header' => [
  246.                 'heading' => [
  247.                     'title' => $attributes['content']['title'],
  248.                 ],
  249.                 'perex' => $attributes['content']['description'],
  250.             ],
  251.         ];
  252.         $articleList $this->articleListFactory->create(
  253.             limit$attributes['content']['limit'],
  254.             page$page,
  255.         );
  256.         $attributes['posts']['grid']['items'] = $articleList->renderArticles();
  257.         $schema[] = $schemaItemList->numberOfItems(
  258.             $articleList->getPagination()->getPageResultsCount())->itemListElement($articleList->getSchemaElements()
  259.         );
  260.         $attributes['posts']['pagination'] = $articleList->getPagination()->renderPagination();
  261.         if(empty($attributes['posts']['grid']['items'])) {
  262.             $attributes['posts']['empty'] = $this->renderEmpty('Bohužel ještě žádné články nemáme...');
  263.             $attributes['posts']['pagination'] = null;
  264.         }
  265.         // Sidebar
  266.         if ($attributes['content']['sidebar'] && array_key_exists('blocks'$attributes['content']['sidebar'])) {
  267.             $attributes['sidebar']['blocks'] = $this->sidebarBlockManager->mapBlocksAttributes($attributes['content']['sidebar']['blocks']);
  268.         }
  269.         $this->dataLayerManager->addEvent(
  270.             (new DataLayerEvent())->setValue('page_type''blog')
  271.         );
  272.         $attributes['_schema'] = implode(''$schema);
  273.         return $this->renderAll('pages/article-list.html.twig'$attributes$preview$partial);
  274.     }
  275.     //#[Route('/{_locale}/category/{slug}/{page}', name: 'app.article_category', defaults: ['page' => 1])]
  276.     #[Route('/rubrika/{slug}/{page}'name'app.article_category'defaults: ['page' => 1])]
  277.     public function categoryAction(string|int $slugint $page 1): Response
  278.     {
  279.         $redirect $this->redirectRouteRepository->findEnabledBySource($this->request->getPathInfo());
  280.         if($redirect) {
  281.             return new RedirectResponse($redirect->getTarget(), $redirect->getStatusCode());
  282.         }
  283.         $blogDocument $this->getBlogDocument();
  284.         if(!$blogDocument) {
  285.             throw new NotFoundHttpException('Blog document not found. Add snippet "post_settings" to default snippets and set blog page.');
  286.         }
  287.         $structure $this->getDocumentStructure($blogDocument);
  288.         $category $this->getCategoryFromSlug($slug);
  289.         if(!$category) {
  290.             throw new NotFoundHttpException('Category with slug or id "' $slug '" not found.');
  291.         }
  292.         $attributes $this->getAttributes([], $structure);
  293.         $attributes['breadcrumbs'] = $this->getBreadcrumbs('article_category'$category->getUuid());
  294.         $schema $this->schemaManager->getDefaultSchemas($attributes['breadcrumbs']);
  295.         $schemaItemList Schema::itemList()
  296.             ->name($category->getTitle())
  297.             ->description($category->getDescription());
  298.         $attributes['posts'] = [
  299.             'header' => [
  300.                 'heading' => [
  301.                     'title' => $category->getTitle(),
  302.                 ],
  303.                 'perex' => $category->getDescription(),
  304.             ],
  305.         ];
  306.         $articleList $this->articleListFactory->create(
  307.             category$category,
  308.             limit$attributes['content']['limit'],
  309.             page$page,
  310.         );
  311.         $attributes['posts']['grid']['items'] = $articleList->renderArticles();
  312.         $schema[] = $schemaItemList->numberOfItems(
  313.             $articleList->getPagination()->getPageResultsCount())->itemListElement($articleList->getSchemaElements()
  314.         );
  315.         $attributes['posts']['pagination'] = $articleList->getPagination()->renderPagination();
  316.         if(empty($attributes['posts']['grid']['items'])) {
  317.             $attributes['posts']['empty'] = $this->renderEmpty('V této kategorii bohužel žádné články nemáme...');
  318.             $attributes['posts']['pagination'] = null;
  319.         }
  320.         // Sidebar
  321.         if ($attributes['content']['sidebar'] && array_key_exists('blocks'$attributes['content']['sidebar'])) {
  322.             $attributes['sidebar']['blocks'] = $this->sidebarBlockManager->mapBlocksAttributes($attributes['content']['sidebar']['blocks']);
  323.         }
  324.         $this->dataLayerManager->addEvent(
  325.             (new DataLayerEvent())->setValue('page_type''blog_category')
  326.         );
  327.         $attributes['_schema'] = implode(''$schema);
  328.         return $this->renderAll('pages/article-list.html.twig'$attributes);
  329.     }
  330.     private function renderCategories(
  331.         ?ArticleCategory $currentCategory null,
  332.         ?array $categoryUuids null,
  333.         ?Contact $author null,
  334.     ): array
  335.     {
  336.         $categories $this->articleCategoryRepository->prepareQueryBuilder(
  337.             webspace$this->getWebspace()->getKey(),
  338.             locale$this->getLocale(),
  339.             categoryUuids$categoryUuids,
  340.             author$author,
  341.         )->getQuery()->getResult();
  342.         $categoryItems[] = [
  343.             'title' => 'Vše',
  344.             'value' => '',
  345.             'checked' => !$currentCategory,
  346.         ];
  347.         if($categories) {
  348.             foreach($categories as $cat) {
  349.                 $categoryItems[] = [
  350.                     'title' => $cat->getTitle(),
  351.                     'value' => $cat->getId(),
  352.                     'checked' => $cat === $currentCategory,
  353.                 ];
  354.             }
  355.         }
  356.         return [
  357.             'name' => 'categories',
  358.             'items' => $categoryItems,
  359.         ];
  360.     }
  361.     private function renderEmpty(?string $value null): array
  362.     {
  363.         return [
  364.             'title' => $value ?? 'Žádné výsledky',
  365.         ];
  366.     }
  367.     private function getCategory(): ?ArticleCategory
  368.     {
  369.         $categoryId $this->request->query->get('c');
  370.         $category null;
  371.         if($categoryId) {
  372.             $category $this->articleCategoryRepository->findOneBySlug($categoryId);
  373.             if($category === null) {
  374.                 $category $this->articleCategoryRepository->find($categoryId);
  375.             }
  376.         }
  377.         return $category;
  378.     }
  379.     private function getCategoryFromSlug(string|int $slug): ?ArticleCategory
  380.     {
  381.         $category null;
  382.         if($slug) {
  383.             $category $this->articleCategoryRepository->findOneBySlug($slug);
  384.             if($category === null) {
  385.                 $category $this->articleCategoryRepository->find($slug);
  386.             }
  387.         }
  388.         return $category;
  389.     }
  390.     private function getAuthor(): ?Contact
  391.     {
  392.         $authorId $this->request->query->get('a');
  393.         $author null;
  394.         if($authorId) {
  395.             try {
  396.                 $author $this->contactManager->getById($authorId$this->getLocale());
  397.             } catch(EntityNotFoundException $e) {
  398.                 throw new NotFoundHttpException('Author with id ' $authorId ' not found.');
  399.             }
  400.         }
  401.         return $author;
  402.     }
  403.     private function getWebspace(): Webspace
  404.     {
  405.         return $this->request->attributes->get('_sulu')->getAttribute('webspace');
  406.     }
  407.     private function getLocale(): string
  408.     {
  409.         return $this->request->getLocale();
  410.     }
  411.     private function getBlogDocument(): HomeDocument|PageDocument|null
  412.     {
  413.         $blogSettings $this->snippetManager->loadByArea(
  414.             'post_settings',
  415.             $this->getWebspace()->getKey(),
  416.             $this->getLocale()
  417.         );
  418.         if(!$blogSettings || !$blogSettings['content']['tagHref']) {
  419.             return null;
  420.         }
  421.         return $this->documentManager->find($blogSettings['content']['tagHref'], $this->getLocale());
  422.     }
  423.     private function getDocumentStructure(object $document): ?StructureInterface
  424.     {
  425.         $structure $this->structureManager->wrapStructure(
  426.             $this->documentInspector->getMetadata($document)->getAlias(),
  427.             $this->documentInspector->getStructureMetadata($document)
  428.         );
  429.         $structure->setDocument($document);
  430.         return $structure;
  431.     }
  432. }