<?php
namespace Boldr\Cms\CmsBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\{ Request, Response };
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Cache\{ TagAwareCacheInterface, ItemInterface };
use Psr\Cache\CacheItemPoolInterface;
use Boldr\Cms\CmsBundle\Permalink\{ PermalinkHandlerInterface, PermalinkResolverInterface, PermalinkGeneratorInterface, PermalinkableInterface };
use Symfony\Contracts\Translation\TranslatorInterface;
use Psr\Container\ContainerInterface;
class PermalinkController extends AbstractController
{
/** @var TagAwareCacheInterface&CacheItemPoolInterface */
private TagAwareCacheInterface $cache;
private ContainerInterface $permalinkHandlers;
/** @var iterable<PermalinkResolverInterface> */
private iterable $permalinkResolvers;
private ContainerInterface $permalinkGenerators;
private ContainerInterface $permalinkableSerializers;
/**
* @param TagAwareCacheInterface&CacheItemPoolInterface $cache
* @param iterable<PermalinkResolverInterface> $permalinkResolvers
* @phpstan-ignore-next-line
*/
public function __construct(TagAwareCacheInterface $cache, ContainerInterface $permalinkHandlers,
ContainerInterface $permalinkGenerators, iterable $permalinkResolvers, ContainerInterface $permalinkableSerializers)
{
$this->cache = $cache;
$this->permalinkHandlers = $permalinkHandlers;
$this->permalinkGenerators = $permalinkGenerators;
$this->permalinkResolvers = $permalinkResolvers;
$this->permalinkableSerializers = $permalinkableSerializers;
}
private function getPermalinkableClass(PermalinkableInterface $permalinkable): string
{
$permalinkableClass = get_class($permalinkable);
if (substr($permalinkableClass, 0, 15) == 'Proxies\__CG__\\')
{
$permalinkableClass = substr($permalinkableClass, 15);
}
return $permalinkableClass;
}
/**
* Returns the permalinkable name and a serialize()able representation of a permalinkable.
*
* @see self::unserialize()
* @return null|array{0: string, 1: mixed} Array of
*/
public function serialize(PermalinkableInterface $permalinkable)
{
$permalinkableClass = $this->getPermalinkableClass($permalinkable);
if (!$this->permalinkableSerializers->has($permalinkableClass))
{
return null;
}
$serializer = $this->permalinkableSerializers->get($permalinkableClass);
$serializable = $serializer->serialize($permalinkable) ?? null;
return [$permalinkableClass, $serializable];
}
/**
* Unserialized a serialized Permalinkable into a
*/
public function unserialize(string $permalinkableClass, $serialized): ?PermalinkableInterface
{
if (!$this->permalinkableSerializers->has($permalinkableClass))
{
return null;
}
// Convert the cached data into a Permalinkable object which can be handled by a PermalinkHandlerInterface.
$serializer = $this->permalinkableSerializers->get($permalinkableClass);
$permalinkable = $serializer->unserialize($permalinkableClass, $serialized);
return $permalinkable;
}
private function _generate(PermalinkableInterface $permalinkable, string $locale)
{
// Find the permalink generator
$permalinkableClass = $this->getPermalinkableClass($permalinkable);
$permalink = $this->permalinkGenerators->has($permalinkableClass)
? $this->permalinkGenerators->get($permalinkableClass)->generatePermalink($permalinkable, $locale)
: null;
// If there is no permalink generator, or the permalink generator returns an empty link, return null
if ($permalink === '' || $permalink === null)
{
return null;
}
// If permalinks must be prefixed with the locale
if ($this->getParameter('boldr_cms.prefix_permalinks_with_locale'))
{
return $this->generateUrl('cms_permalink_localized', [
'permalink' => $permalink,
'_locale' => $locale
]);
}
return $this->generateUrl('cms_permalink', [
'permalink' => $permalink
]);
}
/**
* Generates a permalink URL for a permalinkable
*
* @param PermalinkableInterface $permalinkable
* @param string $locale
*/
public function generate(PermalinkableInterface $permalinkable, string $locale): ?string
{
[$permalinkableClass, $serializable] = $this->serialize($permalinkable);
if ($serializable === null)
return $this->_generate($permalinkable, $locale);
$cacheKeyPrefix = $this->getCacheKeyPrefix($permalinkableClass, $serializable);
return $this->cache->get($cacheKeyPrefix.'.'.$locale, function($item) use ($permalinkable, $locale, $cacheKeyPrefix) {
$item->tag($cacheKeyPrefix);
return $this->_generate($permalinkable, $locale);
});
}
/**
* Flush permainks
*/
public function regeneratePermalink(PermalinkableInterface $permalinkable): void
{
[$permalinkableClass, $serializable] = $this->serialize($permalinkable);
if ($serializable !== null)
{
$cacheKeyPrefix = $this->getCacheKeyPrefix($permalinkableClass, $serializable);
$this->cache->invalidateTags([$cacheKeyPrefix]);
}
}
private function getCacheKeyPrefix(string $permalinkableClass, $representation)
{
$cacheKeyPrefix = 'boldr_cms.permalinkable_permalinks.'. $permalinkableClass .'.'. serialize($representation);
$cacheKeyPrefix = str_replace(['{', '}', '(', ')', '/', '\\', '@', ':', '"'], '-', $cacheKeyPrefix);
return $cacheKeyPrefix;
}
/**
* @Route("/{permalink<.+>}", name="cms_permalink", priority=-1)
* @Route("/{_locale}/{permalink<.+>}", name="cms_permalink_localized", priority=-2)
*/
public function resolve(Request $request, TranslatorInterface $translator, string $permalink, bool $refresh = false): Response
{
// Strip trailing slash
$permalink = trim($permalink, '/');
// If permalinks are prefixed with the locale, extract the locale and permalink identifier, and update the Request.
$prefixPermalinks = $this->getParameter('boldr_cms.prefix_permalinks_with_locale');
if ($prefixPermalinks)
{
$permalinkParts = explode('/', $permalink, 2);
if (count($permalinkParts) == 2)
{
$locale = $permalinkParts[0];
$enabledLocales = $this->getParameter('boldr_cms.enabled_locales');
if (in_array($locale, $enabledLocales))
{
$request->setLocale($locale);
$translator->setLocale($locale);
$this->container->get('router')->getContext()->setParameter('_locale', $locale);
$permalink = $permalinkParts[1];
}
}
}
// Permalink resolution and caching
$permalinkable = null;
// Search for information about this permalink in the cache, unless $refresh is true.
$cacheItem = $this->cache->getItem($this->getPermalinkCacheKey($permalink, $request->getLocale()));
if (!$refresh && $cacheItem->isHit())
{
[$permalinkableClass, $serialized] = $cacheItem->get();
// If no cache manager exists for this permalinkable class, the cache must be out of date.
// Re-resolve the permalink.
if (!$this->permalinkableSerializers->has($permalinkableClass))
{
return $this->resolve($request, $translator, $permalink, true);
}
// Convert the cached data into a Permalinkable object which can be handled by a PermalinkHandlerInterface.
$serializer = $this->permalinkableSerializers->get($permalinkableClass);
$permalinkable = $serializer->unserialize($permalinkableClass, $serialized);
}
else
{
// When no information about the permalink is found, query all permalink resolvers for a Permalinkable object that answers to this permalink.
$locale = $request->getLocale();
foreach ($this->permalinkResolvers as $resolver)
{
$permalinkable = $resolver->resolvePermalink($permalink, $locale);
if ($permalinkable !== null)
{
// When a permalink was found, attempt to store the result in the permalink cache.
$permalinkableClass = $this->getPermalinkableClass($permalinkable);
if ($this->permalinkableSerializers->has($permalinkableClass))
{
// If a cache manager exists for this type of Permalinkable, create a cache representation and store it in the cache.
$serializer = $this->permalinkableSerializers->get($permalinkableClass);
$representation = $serializer->serialize($permalinkable);
$cacheItem->set([$permalinkableClass, $representation]);
$cacheKeyPrefix = $this->getCacheKeyPrefix($permalinkableClass, $representation);
$cacheItem->tag($cacheKeyPrefix);
$this->cache->save($cacheItem);
}
break;
}
}
}
// If no Permalinkable object could be found for the permalink, show a not found page.
if ($permalinkable === null)
throw $this->createNotFoundException();
// Search for the appropriate PermalinkHandlerInterface for this Permalinkable.
$permalinkableClass = $this->getPermalinkableClass($permalinkable);
if (!$this->permalinkHandlers->has($permalinkableClass))
{
if (!$refresh)
{
// If no handler could be found, the cache is probably out of date. Re-resolve the permalink.
return $this->resolve($request, $translator, $permalink, true);
}
else
{
// If ::resolve was already called with $refresh = true and still no correct handler was found, this means the permalink handler is misconfigured.
throw new \Exception('No permalink handler for permalinkable of type '. $permalinkableClass);
}
}
// Save the permalinkable in a request attribute
$request->attributes->set('_permalinkable', $permalinkable);
// Delegate handling the permalink to the responsible PermalinkHandlerInterface
$handler = $this->permalinkHandlers->get($permalinkableClass);
return $handler->handlePermalink($permalinkable, $request);
}
/**
* Gets a cache key for the permalink
*/
private function getPermalinkCacheKey(string $permalink, string $locale): string
{
return 'boldr_cms.permalink_permalinkables.'. $locale .'.'.
str_replace(['{', '}', '(', ')','/','\\','@', ':'], '_', str_replace('_', '__', $permalink))
;
}
}