<?php declare(strict_types=1);
namespace H1web\Blog\Seo\SeoUrlRoute;
use Cocur\Slugify\SlugifyInterface;
use Doctrine\DBAL\Connection;
use Shopware\Core\Content\Seo\SeoUrl\SeoUrlCollection;
use Shopware\Core\Content\Seo\SeoUrlUpdater;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use H1web\Blog\Blog\Events\BlogIndexerEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\Tag\TagCollection;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SeoUrlUpdateListener implements EventSubscriberInterface
{
/**
* @var SeoUrlUpdater
*/
private $seoUrlUpdater;
/**
* @var SlugifyInterface
*/
private $slugify;
/**
* @var EntityRepository
*/
private $seoUrlRepository;
/**
* @var EntityRepository
*/
private $tagRepository;
/**
* @var Connection
*/
private $connection;
/**
* @var string
*/
public const TAG_OVERVIEW_PREFIX = 'blog/tag/';
/**
* SeoUrlUpdateListener constructor.
* @param SeoUrlUpdater $seoUrlUpdater
* @param SlugifyInterface $slugify
* @param EntityRepository $seoUrlRepository
* @param EntityRepository $tagRepository
* @param Connection $connection
*/
public function __construct(
SeoUrlUpdater $seoUrlUpdater,
SlugifyInterface $slugify,
EntityRepository $seoUrlRepository,
EntityRepository $tagRepository,
Connection $connection
)
{
$this->seoUrlUpdater = $seoUrlUpdater;
$this->slugify = $slugify;
$this->seoUrlRepository = $seoUrlRepository;
$this->tagRepository = $tagRepository;
$this->connection = $connection;
}
/**
* @return string[]
*/
public static function getSubscribedEvents()
{
return [
BlogIndexerEvent::class => 'updateBlogUrls',
'tag.written' => 'updateBlogTagUrl',
'tag.deleted' => 'deleteBlogTagUrl',
'sales_channel.written' => 'onSalesChannelWritten'
];
}
/**
* @param EntityWrittenEvent $event
*/
public function onSalesChannelWritten(EntityWrittenEvent $event): void
{
$this->updateTagOverviewSeoUrls($event->getWriteResults()[0]->getPrimaryKey(), $event->getContext());
}
/**
* @param EntityWrittenEvent $event
*/
public function updateBlogTagUrl(EntityWrittenEvent $event)
{
$ids = $event->getIds();
/* @var TagCollection $tags */
$tags = $this->tagRepository->search(
new Criteria($ids),
$event->getContext()
);
$this->createTagOverviewUrls($tags);
}
/**
* @param EntitySearchResult $tags
*/
public function createTagOverviewUrls(EntitySearchResult $tags)
{
foreach ($tags as $tag) {
$slug = $this->slugify->slugify($tag->getName());
$url = $this::TAG_OVERVIEW_PREFIX . $slug;
// Find existing urls for this tag
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('foreignKey', $tag->getId()));
$existingUrls = $this->seoUrlRepository->search(
$criteria,
$tags->getContext()
);
$createNew = true;
$updates = [];
foreach ($existingUrls as $existingUrl) {
$updates[] = [
'id' => $existingUrl->getId(),
'isCanonical' => false,
];
if ($existingUrl->getSeoPathInfo() == $url) {
// Do not create a new url if it is already present
$createNew = false;
}
}
if ($createNew) {
foreach ($this->connection->fetchAll("SELECT DISTINCT HEX(language_id) FROM sales_channel_language") as $language) {
$language = strtolower(current($language));
$this->seoUrlRepository->create(
[$this->prepareSeoUrlData($language, $tag->getId(), $url)],
$tags->getContext()
);
}
if (sizeof($updates) > 0) {
// Set all old urls to non canonical
$this->seoUrlRepository->update(
$updates,
$tags->getContext()
);
}
}
}
}
/**
* We have to go through the seo_urls since SW
* Doesn't autoremove associated tags/entities in on a delete
* Tag urls are created by the blog module so go through them on delete
*
* @param EntityWrittenEvent $event
*/
public function deleteBlogTagUrl(EntityWrittenEvent $event)
{
$criteria = new Criteria();
// Search By fk field since no real FK is assigned for tags in the seo_url table
// Name should be more in line of foreign_id
$criteria->addFilter(new EqualsAnyFilter('foreignKey', $event->getIds()));
$existingUrls = $this->seoUrlRepository->searchIds(
$criteria,
$event->getContext()
);
if ($existingUrls->getTotal() > 0) {
// Remap because getIds throws errors and SW doesn't expect an id list on delete
$this->seoUrlRepository->delete(
array_map(function ($id) {
return ['id' => $id];
}, $existingUrls->getIds()),
$event->getContext()
);
}
}
/**
* @param BlogIndexerEvent $event
*/
public function updateBlogUrls(BlogIndexerEvent $event): void
{
$this->seoUrlUpdater->update(BlogPageSeoUrlRoute::ROUTE_NAME, $event->getIds());
}
private function updateTagOverviewSeoUrls(string $salesChannelId, Context $context): void
{
/** @var TagCollection $tags */
$tags = $this->tagRepository->search((new Criteria())->addAssociation('blogs'), $context)
->getEntities()
->filter(static function ($tag) {
return $tag->getExtension('blogs')->count();
});
$languageIds = $this->getSalesChannelLanguageIds($salesChannelId);
$seoUrls = $this->getExistingSeoUrls($languageIds, $tags, $context);
$blogTagSeoUrls = [];
foreach ($tags as $tag) {
foreach ($languageIds as $languageId) {
$seoUrl = $seoUrls->filter(static function ($seoUrl) use ($tag, $languageId) {
return $seoUrl->getForeignKey() === $tag->getId() && $seoUrl->getLanguageId() === $languageId;
});
if ($seoUrl->count()) {
continue;
}
$slug = $this->slugify->slugify($tag->getName());
$url = $this::TAG_OVERVIEW_PREFIX . $slug;
$blogTagSeoUrls[] = $this->prepareSeoUrlData($languageId, $tag->getId(), $url);
}
}
if (!empty($blogTagSeoUrls)) {
$this->seoUrlRepository->create($blogTagSeoUrls, $context);
}
}
private function getSalesChannelLanguageIds(string $salesChannelId): array
{
$languageIds = $this->connection->fetchAllNumeric(
'SELECT lower(HEX(language_id)) as languageId from sales_channel_language
where sales_channel_id=:salesChannelId',
['salesChannelId' => Uuid::fromHexToBytes($salesChannelId)],
);
return array_map(static function ($languageIdArray) {
return array_shift($languageIdArray);
}, $languageIds);
}
private function getExistingSeoUrls(
array $languageIds,
TagCollection $tags,
Context $context
): SeoUrlCollection {
/** @var SeoUrlCollection $seoUrls */
$seoUrls = $this->seoUrlRepository->search(
(new Criteria())
->addFilter(new EqualsFilter('routeName', 'frontend.h1webblog.tag_overview'))
->addFilter(new EqualsAnyFilter('foreignKey', $tags->getIds()))
->addFilter(new EqualsAnyFilter('languageId', $languageIds)),
$context,
)->getEntities();
return $seoUrls;
}
private function prepareSeoUrlData(string $languageId, string $tagId, string $url): array
{
return [
'languageId' => $languageId,
'foreignKey' => $tagId,
'routeName' => 'frontend.h1webblog.tag_overview',
'pathInfo' => '/h1webblog/tag/' . $tagId,
'seoPathInfo' => $url,
'isCanonical' => true,
'isModified' => false,
'isDeleted' => false,
];
}
}