<?php
namespace App\Controller\Website;
use App\Article\ArticleListFactory;
use App\BlockManager\ContentBlockManager;
use App\BlockManager\SidebarBlockManager;
use App\DataLayer\DataLayerEvent;
use App\DataLayer\DataLayerManager;
use App\Entity\Article;
use App\Entity\ArticleCategory;
use App\Repository\ArticleCategoryRepository;
use App\Repository\ArticleRepository;
use App\Utils\SchemaManager;
use App\Utils\SnippetManager;
use Spatie\SchemaOrg\NewsArticle;
use Spatie\SchemaOrg\Schema;
use Sulu\Bundle\ContactBundle\Api\Contact;
use Sulu\Bundle\ContactBundle\Contact\ContactManagerInterface;
use Sulu\Bundle\DocumentManagerBundle\Bridge\DocumentInspector;
use Sulu\Bundle\MediaBundle\Api\Media;
use Sulu\Bundle\MediaBundle\Media\Manager\MediaManagerInterface;
use Sulu\Bundle\PageBundle\Document\HomeDocument;
use Sulu\Bundle\PageBundle\Document\PageDocument;
use Sulu\Bundle\RedirectBundle\Model\RedirectRouteRepositoryInterface;
use Sulu\Component\Content\Compat\StructureInterface;
use Sulu\Component\Content\Compat\StructureManagerInterface;
use Sulu\Component\DocumentManager\DocumentManagerInterface;
use Sulu\Component\Rest\Exception\EntityNotFoundException;
use Sulu\Component\Webspace\Webspace;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
class ArticleController extends SuluExtendController
{
private Request $request;
public function __construct(
private readonly ArticleRepository $articleRepository,
private readonly ArticleCategoryRepository $articleCategoryRepository,
private readonly ContentBlockManager $contentBlockManager,
private readonly ArticleListFactory $articleListFactory,
private readonly SchemaManager $schemaManager,
private readonly ContactManagerInterface $contactManager,
private readonly SnippetManager $snippetManager,
private readonly DocumentManagerInterface $documentManager,
private readonly SidebarBlockManager $sidebarBlockManager,
private readonly MediaManagerInterface $mediaManager,
private readonly StructureManagerInterface $structureManager,
private readonly DocumentInspector $documentInspector,
private readonly RedirectRouteRepositoryInterface $redirectRouteRepository,
private readonly DataLayerManager $dataLayerManager,
public RequestStack $requestStack,
) {
$this->request = $this->requestStack->getCurrentRequest();
}
public function viewAction(
Article $article,
$attributes = [],
$preview = false,
$partial = false,
): Response
{
$postSettingsArea = $this->snippetManager->loadByArea('post_settings');
$postSettings = $postSettingsArea['content'] ?? [];
$attributes = $this->getAttributes($attributes);
$attributes['breadcrumbs'] = $this->getBreadcrumbs('article', $article->getUuid());
$schema = $this->schemaManager->getDefaultSchemas($attributes['breadcrumbs']);
$articleSchema = Schema::newsArticle()
->headline($article->getTitle())
->if($article->getPublished(), function (NewsArticle $schema) use ($article) {
$schema->datePublished($article->getPublished());
})
->if($article->getChanged(), function (NewsArticle $schema) use ($article) {
$schema->dateModified($article->getChanged());
});
if ($article->getAuthor()) {
$articleSchema->author($this->schemaManager->getPerson($article->getAuthor()));
}
// ---------------------------------------
// Post header
// ---------------------------------------
$attributes['post'] = [
'header' => [
'heading' => [
'title' => $article->getTitle(),
],
'perex' => $article->getPerex(),
],
'spacing' => 'top-0',
];
if($categories = $article->getCategories()) {
$attributes['post']['tags'] = [];
foreach($categories as $category) {
$categorySlug = $category->hasSlug() ? $category->getSlug() : $category->getId();
$attributes['post']['tags'][] = [
'title' => $category->getTitle(),
'url' => $this->router->generate('app.article_category', ['slug' => $categorySlug])
];
}
}
if($article->getPublished()) {
$attributes['post']['date'] =
$article->getPublished()->format('d.m.Y');
}
if($article->getAuthor()) {
$attributes['post']['author']['title'] = $article->getAuthor()->getFullName();
if($article->getAuthor()->getAvatar()) {
$media = $this->mediaManager->getById($article->getAuthor()->getAvatar()->getId(), $article->getLocale());
$attributes['post']['author']['img'] = [
'src' => $media->getThumbnails()['64x64'],
'srcset' => [
$media->getThumbnails()['64x64'] . ' 1x',
$media->getThumbnails()['128x128'] . ' 2x',
],
'alt' => $media->getTitle(),
];
}
}
// ---------------------------------------
// Post static main image
// ---------------------------------------
if($article->getImage()) {
$media = $this->mediaManager->getById($article->getImage()->getId(), $article->getLocale());
$articleSchema->image($this->request->getSchemeAndHttpHost() . $media->getUrl());
}
// ---------------------------------------
// Share section
// ---------------------------------------
$articleUrl = $this->request->getSchemeAndHttpHost() . $article->getRoute()->getPath();
$attributes['post']['share'] = [
'title' => 'Sdílejte článek:',
'facebook' => 'https://www.facebook.com/sharer/sharer.php?u=' . $articleUrl,
'linkedin' => 'https://www.linkedin.com/feed/?shareActive=true&text=' . $articleUrl,
];
// ---------------------------------------
// Related posts
// ---------------------------------------
$limit = 3;
if ($postSettings && is_numeric($postSettings['similarCount'])) {
$limit = $postSettings['similarCount'];
}
$similarArticles = $this->articleRepository->findSimilarArticles($article, $limit);
if($similarArticles && $postSettings) {
$attributes['similar_posts'] = [
'header' => [
'heading' => [
'title' => $postSettings['similarName'] ?: 'Podobné články',
'level' => 3,
'style' => 'h3',
],
'variant' => 'left',
],
'grid' => [
'items' => [],
'variant' => 'inline',
],
'buttons' => [],
'spacing' => 'none',
];
$categories = $article->getCategories();
if(!$categories->isEmpty()) {
$categorySlug = $categories->first()->hasSlug() ? $categories->first()->getSlug() : $categories->first()->getId();
$attributes['similar_posts']['buttons']['primary'] = [
'title' => $postSettings['similarLinkText'] ?: 'Více článků',
'url' => $this->router->generate('app.article_category', ['slug' => $categorySlug]),
];
}
foreach($similarArticles as $similarArticle) {
$attributes['similar_posts']['grid']['items'][] = $this->articleRepository->renderArticle($similarArticle);
}
}
// ---------------------------------------
// Sidebar
// ---------------------------------------
if(isset($postSettings['sidebar']['blocks'])) {
$attributes['sidebar']['blocks'] = $this->sidebarBlockManager->mapBlocksAttributes($postSettings['sidebar']['blocks']);
}
// ---------------------------------------
// Content
// ---------------------------------------
$attributes['content_blocks'] = [];
$mappedContentBlocks = $this->contentBlockManager->mapBlocksAttributes($attributes['content']['content_blocks']);
$articleSchemaImages = [];
foreach ($mappedContentBlocks as $block) {
if($block['type'] == 'Image' || $block['type'] == 'ImageWithText') {
$imgUrl = $this->request->getSchemeAndHttpHost() . $block['content']['img']?->getUrl();
$articleSchemaImages[] = $imgUrl;
}
elseif($block['type'] == 'gallery' && isset($block['content']['images'])) {
/** @var Media $image */
foreach ($block['content']['images'] as $image) {
$imgUrl = $this->request->getSchemeAndHttpHost() . $image?->getUrl();
$articleSchemaImages[] = $imgUrl;
}
}
$attributes['content_blocks'][] = $block;
}
$articleSchema->image($articleSchemaImages);
$schema[] = $articleSchema;
// ---------------------------------------
// Schema.org
// ---------------------------------------
$schema[] = $articleSchema;
$attributes['_schema'] = implode('', $schema);
// ---------------------------------------
// DataLayer
// ---------------------------------------
$viewArticleEvent = new DataLayerEvent('view_article');
if($article->getCategories()) {
$i = 0;
foreach($article->getCategories() as $category) {
$i++;
$viewArticleEvent->setValue('article_category'.$i, $category->getTitle());
}
}
$this->dataLayerManager
->addEvent($viewArticleEvent)
->addEvent((new DataLayerEvent())->setValue('page_type', 'article'));
$this->setEntitySocialMeta($article);
$this->setEntitySeoMeta($article);
return $this->renderAll('pages/article.html.twig', $attributes, $preview, $partial);
}
public function listAction(
?StructureInterface $structure = null,
?string $pageId = null,
int|string $page = 1,
$preview = false,
$partial = false,
): Response
{
$page = (int)$page;
if (!$structure && $pageId) {
/** @var HomeDocument|PageDocument $document */
$document = $this->documentManager->find($pageId, $this->request->getLocale());
$structure = $this->getDocumentStructure($document);
}
$attributes = $this->getAttributes([], $structure);
$attributes['breadcrumbs'] = $this->getBreadcrumbs('page', $attributes['uuid']);
$schema = $this->schemaManager->getDefaultSchemas($attributes['breadcrumbs']);
$schemaItemList = Schema::itemList()
->name($attributes['content']['title'])
->description($attributes['content']['description']);
$attributes['posts'] = [
'header' => [
'heading' => [
'title' => $attributes['content']['title'],
],
'perex' => $attributes['content']['description'],
],
];
$articleList = $this->articleListFactory->create(
limit: $attributes['content']['limit'],
page: $page,
);
$attributes['posts']['grid']['items'] = $articleList->renderArticles();
$schema[] = $schemaItemList->numberOfItems(
$articleList->getPagination()->getPageResultsCount())->itemListElement($articleList->getSchemaElements()
);
$attributes['posts']['pagination'] = $articleList->getPagination()->renderPagination();
if(empty($attributes['posts']['grid']['items'])) {
$attributes['posts']['empty'] = $this->renderEmpty('Bohužel ještě žádné články nemáme...');
$attributes['posts']['pagination'] = null;
}
// Sidebar
if ($attributes['content']['sidebar'] && array_key_exists('blocks', $attributes['content']['sidebar'])) {
$attributes['sidebar']['blocks'] = $this->sidebarBlockManager->mapBlocksAttributes($attributes['content']['sidebar']['blocks']);
}
$this->dataLayerManager->addEvent(
(new DataLayerEvent())->setValue('page_type', 'blog')
);
$attributes['_schema'] = implode('', $schema);
return $this->renderAll('pages/article-list.html.twig', $attributes, $preview, $partial);
}
//#[Route('/{_locale}/category/{slug}/{page}', name: 'app.article_category', defaults: ['page' => 1])]
#[Route('/rubrika/{slug}/{page}', name: 'app.article_category', defaults: ['page' => 1])]
public function categoryAction(string|int $slug, int $page = 1): Response
{
$redirect = $this->redirectRouteRepository->findEnabledBySource($this->request->getPathInfo());
if($redirect) {
return new RedirectResponse($redirect->getTarget(), $redirect->getStatusCode());
}
$blogDocument = $this->getBlogDocument();
if(!$blogDocument) {
throw new NotFoundHttpException('Blog document not found. Add snippet "post_settings" to default snippets and set blog page.');
}
$structure = $this->getDocumentStructure($blogDocument);
$category = $this->getCategoryFromSlug($slug);
if(!$category) {
throw new NotFoundHttpException('Category with slug or id "' . $slug . '" not found.');
}
$attributes = $this->getAttributes([], $structure);
$attributes['breadcrumbs'] = $this->getBreadcrumbs('article_category', $category->getUuid());
$schema = $this->schemaManager->getDefaultSchemas($attributes['breadcrumbs']);
$schemaItemList = Schema::itemList()
->name($category->getTitle())
->description($category->getDescription());
$attributes['posts'] = [
'header' => [
'heading' => [
'title' => $category->getTitle(),
],
'perex' => $category->getDescription(),
],
];
$articleList = $this->articleListFactory->create(
category: $category,
limit: $attributes['content']['limit'],
page: $page,
);
$attributes['posts']['grid']['items'] = $articleList->renderArticles();
$schema[] = $schemaItemList->numberOfItems(
$articleList->getPagination()->getPageResultsCount())->itemListElement($articleList->getSchemaElements()
);
$attributes['posts']['pagination'] = $articleList->getPagination()->renderPagination();
if(empty($attributes['posts']['grid']['items'])) {
$attributes['posts']['empty'] = $this->renderEmpty('V této kategorii bohužel žádné články nemáme...');
$attributes['posts']['pagination'] = null;
}
// Sidebar
if ($attributes['content']['sidebar'] && array_key_exists('blocks', $attributes['content']['sidebar'])) {
$attributes['sidebar']['blocks'] = $this->sidebarBlockManager->mapBlocksAttributes($attributes['content']['sidebar']['blocks']);
}
$this->dataLayerManager->addEvent(
(new DataLayerEvent())->setValue('page_type', 'blog_category')
);
$attributes['_schema'] = implode('', $schema);
return $this->renderAll('pages/article-list.html.twig', $attributes);
}
private function renderCategories(
?ArticleCategory $currentCategory = null,
?array $categoryUuids = null,
?Contact $author = null,
): array
{
$categories = $this->articleCategoryRepository->prepareQueryBuilder(
webspace: $this->getWebspace()->getKey(),
locale: $this->getLocale(),
categoryUuids: $categoryUuids,
author: $author,
)->getQuery()->getResult();
$categoryItems[] = [
'title' => 'Vše',
'value' => '',
'checked' => !$currentCategory,
];
if($categories) {
foreach($categories as $cat) {
$categoryItems[] = [
'title' => $cat->getTitle(),
'value' => $cat->getId(),
'checked' => $cat === $currentCategory,
];
}
}
return [
'name' => 'categories',
'items' => $categoryItems,
];
}
private function renderEmpty(?string $value = null): array
{
return [
'title' => $value ?? 'Žádné výsledky',
];
}
private function getCategory(): ?ArticleCategory
{
$categoryId = $this->request->query->get('c');
$category = null;
if($categoryId) {
$category = $this->articleCategoryRepository->findOneBySlug($categoryId);
if($category === null) {
$category = $this->articleCategoryRepository->find($categoryId);
}
}
return $category;
}
private function getCategoryFromSlug(string|int $slug): ?ArticleCategory
{
$category = null;
if($slug) {
$category = $this->articleCategoryRepository->findOneBySlug($slug);
if($category === null) {
$category = $this->articleCategoryRepository->find($slug);
}
}
return $category;
}
private function getAuthor(): ?Contact
{
$authorId = $this->request->query->get('a');
$author = null;
if($authorId) {
try {
$author = $this->contactManager->getById($authorId, $this->getLocale());
} catch(EntityNotFoundException $e) {
throw new NotFoundHttpException('Author with id ' . $authorId . ' not found.');
}
}
return $author;
}
private function getWebspace(): Webspace
{
return $this->request->attributes->get('_sulu')->getAttribute('webspace');
}
private function getLocale(): string
{
return $this->request->getLocale();
}
private function getBlogDocument(): HomeDocument|PageDocument|null
{
$blogSettings = $this->snippetManager->loadByArea(
'post_settings',
$this->getWebspace()->getKey(),
$this->getLocale()
);
if(!$blogSettings || !$blogSettings['content']['tagHref']) {
return null;
}
return $this->documentManager->find($blogSettings['content']['tagHref'], $this->getLocale());
}
private function getDocumentStructure(object $document): ?StructureInterface
{
$structure = $this->structureManager->wrapStructure(
$this->documentInspector->getMetadata($document)->getAlias(),
$this->documentInspector->getStructureMetadata($document)
);
$structure->setDocument($document);
return $structure;
}
}