Skip to content

Commit

Permalink
Add caching to navigation and snippet controller (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
Prokyonn committed Jun 10, 2022
1 parent dbb306a commit 2d55107
Show file tree
Hide file tree
Showing 12 changed files with 393 additions and 6 deletions.
52 changes: 48 additions & 4 deletions Controller/NavigationController.php
Expand Up @@ -16,7 +16,9 @@
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;
use Sulu\Bundle\HeadlessBundle\Content\Serializer\MediaSerializerInterface;
use Sulu\Bundle\HttpCacheBundle\Cache\SuluHttpCache;
use Sulu\Bundle\WebsiteBundle\Navigation\NavigationMapperInterface;
use Sulu\Bundle\WebsiteBundle\ReferenceStore\ReferenceStoreInterface;
use Sulu\Component\Rest\ListBuilder\CollectionRepresentation;
use Sulu\Component\Rest\RequestParametersTrait;
use Sulu\Component\Webspace\Analyzer\Attributes\RequestAttributes;
Expand All @@ -43,14 +45,42 @@ class NavigationController
*/
private $mediaSerializer;

/**
* @var ReferenceStoreInterface
*/
private $navigationReferenceStore;

/**
* @var int
*/
private $maxAge;

/**
* @var int
*/
private $sharedMaxAge;

/**
* @var int
*/
private $cacheLifetime;

public function __construct(
NavigationMapperInterface $navigationMapper,
SerializerInterface $serializer,
MediaSerializerInterface $mediaSerializer
MediaSerializerInterface $mediaSerializer,
ReferenceStoreInterface $pageReferenceStore,
int $maxAge,
int $sharedMaxAge,
int $cacheLifetime
) {
$this->navigationMapper = $navigationMapper;
$this->serializer = $serializer;
$this->mediaSerializer = $mediaSerializer;
$this->navigationReferenceStore = $pageReferenceStore;
$this->maxAge = $maxAge;
$this->sharedMaxAge = $sharedMaxAge;
$this->cacheLifetime = $cacheLifetime;
}

public function getAction(Request $request, string $context): Response
Expand All @@ -70,10 +100,12 @@ public function getAction(Request $request, string $context): Response

$navigation = $this->loadNavigation($webspace->getKey(), $locale, $depth, $flat, $context, $excerpt, $uuid);

$this->navigationReferenceStore->add($context);

// need to serialize the media entities inside the excerpt to keep the media serialization consistent
$navigation = $this->serializeExcerptMedia($navigation, $locale);

return new Response(
$response = new Response(
$this->serializer->serialize(
new CollectionRepresentation($navigation, 'items'),
'json',
Expand All @@ -84,6 +116,13 @@ public function getAction(Request $request, string $context): Response
'Content-Type' => 'application/json',
]
);

$response->setPublic();
$response->setMaxAge($this->maxAge);
$response->setSharedMaxAge($this->sharedMaxAge);
$response->headers->set(SuluHttpCache::HEADER_REVERSE_PROXY_TTL, (string) $this->cacheLifetime);

return $response;
}

/**
Expand All @@ -110,7 +149,7 @@ protected function loadNavigation(
);
}

return $navigation = $this->navigationMapper->getRootNavigation(
return $this->navigationMapper->getRootNavigation(
$webspaceKey,
$locale,
$depth,
Expand All @@ -128,7 +167,7 @@ protected function loadNavigation(
private function serializeExcerptMedia(array $navigation, string $locale): array
{
foreach ($navigation as $itemIndex => $navigationItem) {
if (\array_key_exists('excerpt', $navigationItem)) {
if (isset($navigationItem['excerpt'])) {
foreach ($navigationItem['excerpt']['icon'] as $iconIndex => $iconMedia) {
$navigation[$itemIndex]['excerpt']['icon'][$iconIndex] = $this->mediaSerializer->serialize(
$iconMedia->getEntity(),
Expand All @@ -143,6 +182,11 @@ private function serializeExcerptMedia(array $navigation, string $locale): array
);
}
}

// recursively serialize all excerpt medias
if (!empty($navigationItem['children'])) {
$navigation[$itemIndex]['children'] = $this->serializeExcerptMedia($navigationItem['children'], $locale);
}
}

return $navigation;
Expand Down
43 changes: 41 additions & 2 deletions Controller/SnippetAreaController.php
Expand Up @@ -16,7 +16,9 @@
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;
use Sulu\Bundle\HeadlessBundle\Content\StructureResolverInterface;
use Sulu\Bundle\HttpCacheBundle\Cache\SuluHttpCache;
use Sulu\Bundle\SnippetBundle\Snippet\DefaultSnippetManagerInterface;
use Sulu\Bundle\WebsiteBundle\ReferenceStore\ReferenceStoreInterface;
use Sulu\Component\Content\Mapper\ContentMapperInterface;
use Sulu\Component\Rest\RequestParametersTrait;
use Sulu\Component\Webspace\Analyzer\Attributes\RequestAttributes;
Expand Down Expand Up @@ -50,16 +52,44 @@ class SnippetAreaController
*/
private $serializer;

/**
* @var ReferenceStoreInterface
*/
private $snippetAreaReferenceStore;

/**
* @var int
*/
private $maxAge;

/**
* @var int
*/
private $sharedMaxAge;

/**
* @var int
*/
private $cacheLifetime;

public function __construct(
DefaultSnippetManagerInterface $defaultSnippetManager,
ContentMapperInterface $contentMapper,
StructureResolverInterface $structureResolver,
SerializerInterface $serializer
SerializerInterface $serializer,
ReferenceStoreInterface $snippetReferenceStore,
int $maxAge,
int $sharedMaxAge,
int $cacheLifetime
) {
$this->defaultSnippetManager = $defaultSnippetManager;
$this->contentMapper = $contentMapper;
$this->structureResolver = $structureResolver;
$this->serializer = $serializer;
$this->snippetAreaReferenceStore = $snippetReferenceStore;
$this->maxAge = $maxAge;
$this->sharedMaxAge = $sharedMaxAge;
$this->cacheLifetime = $cacheLifetime;
}

public function getAction(Request $request, string $area): Response
Expand Down Expand Up @@ -96,13 +126,15 @@ public function getAction(Request $request, string $area): Response
throw new NotFoundHttpException(sprintf('Snippet for snippet area "%s" does not exist in locale "%s"', $area, $locale));
}

$this->snippetAreaReferenceStore->add($area);

$resolvedSnippet = $this->structureResolver->resolve(
$snippet,
$locale,
$includeExtension
);

return new Response(
$response = new Response(
$this->serializer->serialize(
$resolvedSnippet,
'json',
Expand All @@ -113,5 +145,12 @@ public function getAction(Request $request, string $area): Response
'Content-Type' => 'application/json',
]
);

$response->setPublic();
$response->setMaxAge($this->maxAge);
$response->setSharedMaxAge($this->sharedMaxAge);
$response->headers->set(SuluHttpCache::HEADER_REVERSE_PROXY_TTL, (string) $this->cacheLifetime);

return $response;
}
}
16 changes: 16 additions & 0 deletions DependencyInjection/Configuration.php
Expand Up @@ -24,6 +24,22 @@ class Configuration implements ConfigurationInterface
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('sulu_headless');
$rootNode = $treeBuilder->getRootNode();

$rootNode->children()
->arrayNode('navigation')
->addDefaultsIfNotSet()
->children()
->scalarNode('cache_lifetime')->defaultValue(86400)->end()
->end()
->end()
->arrayNode('snippet_area')
->addDefaultsIfNotSet()
->children()
->scalarNode('cache_lifetime')->defaultValue(86400)->end()
->end()
->end()
->end();

return $treeBuilder;
}
Expand Down
10 changes: 10 additions & 0 deletions DependencyInjection/SuluHeadlessExtension.php
Expand Up @@ -25,11 +25,21 @@ public function load(array $configs, ContainerBuilder $container): void
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);

$container->setParameter(
'sulu_headless.navigation.cache_lifetime',
$config['navigation']['cache_lifetime']
);
$container->setParameter(
'sulu_headless.snippet_area.cache_lifetime',
$config['snippet_area']['cache_lifetime']
);

$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.xml');
$loader->load('controllers.xml');
$loader->load('content-type-resolvers.xml');
$loader->load('data-provider-resolvers.xml');
$loader->load('serializers.xml');
$loader->load('event-subscribers.xml');
}
}
156 changes: 156 additions & 0 deletions EventSubscriber/NavigationInvalidationSubscriber.php
@@ -0,0 +1,156 @@
<?php

declare(strict_types=1);

/*
* This file is part of Sulu.
*
* (c) Sulu GmbH
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Sulu\Bundle\HeadlessBundle\EventSubscriber;

use PHPCR\SessionInterface;
use Sulu\Bundle\DocumentManagerBundle\Bridge\DocumentInspector;
use Sulu\Bundle\DocumentManagerBundle\Bridge\PropertyEncoder;
use Sulu\Bundle\HttpCacheBundle\Cache\CacheManager;
use Sulu\Component\DocumentManager\Event\PublishEvent;
use Sulu\Component\DocumentManager\Event\RemoveEvent;
use Sulu\Component\DocumentManager\Event\RemoveLocaleEvent;
use Sulu\Component\DocumentManager\Event\UnpublishEvent;
use Sulu\Component\DocumentManager\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
* @final
*
* @internal
*/
class NavigationInvalidationSubscriber implements EventSubscriberInterface, ResetInterface
{
/**
* @var CacheManager|null
*/
private $cacheManager;

/**
* @var DocumentInspector
*/
private $documentInspector;

/**
* @var PropertyEncoder
*/
private $propertyEncoder;

/**
* @var SessionInterface
*/
private $defaultSession;

/**
* @var SessionInterface
*/
private $liveSession;

/**
* @var string[]
*/
private $navigationContexts;

public function __construct(
?CacheManager $cacheManager,
PropertyEncoder $propertyEncoder,
DocumentInspector $documentInspector,
SessionInterface $defaultSession,
SessionInterface $liveSession
) {
$this->cacheManager = $cacheManager;
$this->propertyEncoder = $propertyEncoder;
$this->documentInspector = $documentInspector;
$this->defaultSession = $defaultSession;
$this->liveSession = $liveSession;

$this->navigationContexts = [];
}

public static function getSubscribedEvents()
{
return [
Events::PUBLISH => ['collectNavigationContextBeforePublishing', 8192],
Events::UNPUBLISH => ['collectNavigationContextBeforeUnpublishing', 8192],
Events::REMOVE => ['collectNavigationContextBeforeRemoving', 8192],
Events::REMOVE_LOCALE => ['collectNavigationContextBeforeRemovingLocale', 8192],
Events::FLUSH => ['invalidateNavigationContexts', -256],
];
}

public function collectNavigationContextBeforePublishing(PublishEvent $event): void
{
$path = $this->documentInspector->getPath($event->getDocument());
$this->collectNavigationContexts($path, $event->getLocale());
}

public function collectNavigationContextBeforeUnpublishing(UnpublishEvent $event): void
{
$path = $this->documentInspector->getPath($event->getDocument());
$this->collectNavigationContexts($path, $event->getLocale());
}

public function collectNavigationContextBeforeRemoving(RemoveEvent $event): void
{
$document = $event->getDocument();
$path = $this->documentInspector->getPath($event->getDocument());
foreach ($this->documentInspector->getLocales($document) as $locale) {
$this->collectNavigationContexts($path, $locale);
}
}

public function collectNavigationContextBeforeRemovingLocale(RemoveLocaleEvent $event): void
{
$path = $this->documentInspector->getPath($event->getDocument());
$this->collectNavigationContexts($path, $event->getLocale());
}

public function collectNavigationContexts(string $path, string $locale): void
{
$defaultNode = $this->defaultSession->getNode($path);
$liveNode = $this->liveSession->getNode($path);

$propertyName = $this->propertyEncoder->localizedContentName('navContexts', $locale);
$liveNavigationContexts = [];
$defaultNavigationContexts = [];
if ($liveNode->hasProperty($propertyName)) {
$liveNavigationContexts = $liveNode->getProperty($propertyName)->getValue();
}
if ($defaultNode->hasProperty($propertyName)) {
$defaultNavigationContexts = $defaultNode->getProperty($propertyName)->getValue();
}

$this->navigationContexts = array_merge(
$this->navigationContexts,
$liveNavigationContexts,
$defaultNavigationContexts
);
}

public function invalidateNavigationContexts(): void
{
if (!$this->cacheManager) {
return;
}

foreach ($this->navigationContexts as $navigationContext) {
$this->cacheManager->invalidateReference('navigation', $navigationContext);
}
}

public function reset(): void
{
$this->navigationContexts = [];
}
}

0 comments on commit 2d55107

Please sign in to comment.