diff --git a/config/services_dev.yaml b/config/services_dev.yaml index 8c840df23..5fd084ec4 100644 --- a/config/services_dev.yaml +++ b/config/services_dev.yaml @@ -1,12 +1,12 @@ services: # menus - Bolt\Menu\StopwatchBackendMenuBuilder: + Bolt\Menu\CachedBackendMenuBuilder: decorates: Bolt\Menu\BackendMenuBuilder autowire: true - Bolt\Menu\BackendMenuBuilderInterface: '@Bolt\Menu\StopwatchBackendMenuBuilder' + Bolt\Menu\BackendMenuBuilderInterface: '@Bolt\Menu\CachedBackendMenuBuilder' - Bolt\Menu\StopwatchFrontendMenuBuilder: + Bolt\Menu\CachedFrontendMenuBuilder: decorates: Bolt\Menu\FrontendMenuBuilder autowire: true - Bolt\Menu\FrontendMenuBuilderInterface: '@Bolt\Menu\StopwatchFrontendMenuBuilder' + Bolt\Menu\FrontendMenuBuilderInterface: '@Bolt\Menu\CachedFrontendMenuBuilder' diff --git a/public/index.php b/public/index.php index 92c8a63d4..acada7e68 100644 --- a/public/index.php +++ b/public/index.php @@ -8,6 +8,9 @@ use Symfony\Component\ErrorHandler\Debug; use Symfony\Component\HttpFoundation\Request; +set_time_limit(0); +@ini_set('memory_limit', '1024M'); + require dirname(__DIR__).'/vendor/autoload.php'; (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); diff --git a/src/Canonical.php b/src/Canonical.php index 38496a95f..5482aa8f4 100644 --- a/src/Canonical.php +++ b/src/Canonical.php @@ -14,6 +14,9 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; class Canonical { @@ -43,14 +46,20 @@ class Canonical /** @var RouterInterface */ private $router; + /** @var Stopwatch */ + private $stopwatch; + /** @var TagAwareCacheInterface */ + private $cache; - public function __construct(Config $config, UrlGeneratorInterface $urlGenerator, RequestStack $requestStack, RouterInterface $router, string $defaultLocale) + public function __construct(Config $config, UrlGeneratorInterface $urlGenerator, RequestStack $requestStack, RouterInterface $router, string $defaultLocale, Stopwatch $stopwatch, TagAwareCacheInterface $cache) { $this->config = $config; $this->urlGenerator = $urlGenerator; $this->request = $requestStack->getCurrentRequest() ?? Request::createFromGlobals(); $this->defaultLocale = $defaultLocale; $this->router = $router; + $this->stopwatch = $stopwatch; + $this->cache = $cache; $this->init(); } @@ -173,7 +182,24 @@ public function setPath(?string $route = null, array $params = []): void $this->path = $this->generateLink($route, $params, false); } - public function generateLink(?string $route, ?array $params, $canonical = false): ?string + public function generateLink(?string $route, ?array $params, $canonical = false): string + { + $cacheKey = 'generateLink_' . md5($route . implode('-', $params) . (string) $canonical); + + $this->stopwatch->start('bolt.GenerateLink'); + + $link = $this->cache->get($cacheKey, function (ItemInterface $item) use ($route, $params, $canonical) { + $item->tag('routes'); + + return $this->generateLinkHelper($route, $params, $canonical); + }); + + $this->stopwatch->stop('bolt.GenerateLink'); + + return $link; + } + + private function generateLinkHelper(?string $route, ?array $params, $canonical = false): string { $removeDefaultLocaleOnCanonical = $this->config->get('general/localization/remove_default_locale_on_canonical', true); $hasDefaultLocale = isset($params['_locale']) && $params['_locale'] === $this->defaultLocale; @@ -197,15 +223,17 @@ public function generateLink(?string $route, ?array $params, $canonical = false) } try { - return $this->urlGenerator->generate( + $link = $this->urlGenerator->generate( $route, $params, $canonical ? UrlGeneratorInterface::ABSOLUTE_URL : UrlGeneratorInterface::ABSOLUTE_PATH ); } catch (InvalidParameterException | MissingMandatoryParametersException | RouteNotFoundException | \TypeError $e) { // Just use the current URL /shrug - return $canonical ? $this->request->getUri() : $this->request->getPathInfo(); + $link = $canonical ? $this->request->getUri() : $this->request->getPathInfo(); } + + return $link; } private function routeRequiresParam(string $route, string $param): bool diff --git a/src/Entity/Content.php b/src/Entity/Content.php index 71182c94c..f663548d6 100644 --- a/src/Entity/Content.php +++ b/src/Entity/Content.php @@ -186,6 +186,16 @@ public function getId(): ?int return $this->id; } + public function getCacheKey(?string $locale = null): string + { + $key = sprintf('record-%05d', $this->getId()); + if ($locale !== null) { + $key .= '-' . $locale; + } + + return $key; + } + /** * @see \Bolt\Event\Listener\ContentFillListener */ diff --git a/src/Event/Subscriber/ContentSaveSubscriber.php b/src/Event/Subscriber/ContentSaveSubscriber.php index c85e84df2..b8cebbed7 100644 --- a/src/Event/Subscriber/ContentSaveSubscriber.php +++ b/src/Event/Subscriber/ContentSaveSubscriber.php @@ -25,8 +25,8 @@ public function __construct(TagAwareCacheInterface $cache) public function postSave(ContentEvent $event): ContentEvent { - // Make sure we flush the cache for the menus - $this->cache->invalidateTags(['backendmenu', 'frontendmenu']); + // Make sure we flush the cache + $this->cache->invalidateTags(['backendmenu', 'frontendmenu', $event->getContent()->getCacheKey(), $event->getContent()->getContentTypeSlug()]); // Saving an entry in the log. $context = [ diff --git a/src/Menu/CachedBackendMenuBuilder.php b/src/Menu/CachedBackendMenuBuilder.php index c2515d523..6e6963c65 100644 --- a/src/Menu/CachedBackendMenuBuilder.php +++ b/src/Menu/CachedBackendMenuBuilder.php @@ -5,6 +5,7 @@ namespace Bolt\Menu; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; @@ -20,22 +21,32 @@ final class CachedBackendMenuBuilder implements BackendMenuBuilderInterface /** @var RequestStack */ private $requestStack; - public function __construct(BackendMenuBuilderInterface $menuBuilder, TagAwareCacheInterface $cache, RequestStack $requestStack) + /** @var Stopwatch */ + private $stopwatch; + + public function __construct(BackendMenuBuilderInterface $menuBuilder, TagAwareCacheInterface $cache, RequestStack $requestStack, Stopwatch $stopwatch) { $this->cache = $cache; $this->menuBuilder = $menuBuilder; $this->requestStack = $requestStack; + $this->stopwatch = $stopwatch; } public function buildAdminMenu(): array { + $this->stopwatch->start('bolt.backendMenu'); + $locale = $this->requestStack->getCurrentRequest()->getLocale(); $cacheKey = 'backendmenu_' . $locale; - return $this->cache->get($cacheKey, function (ItemInterface $item) { + $menu = $this->cache->get($cacheKey, function (ItemInterface $item) { $item->tag('backendmenu'); return $this->menuBuilder->buildAdminMenu(); }); + + $this->stopwatch->stop('bolt.backendMenu'); + + return $menu; } } diff --git a/src/Menu/CachedFrontendMenuBuilder.php b/src/Menu/CachedFrontendMenuBuilder.php index 3863dc295..76ccbfc0a 100644 --- a/src/Menu/CachedFrontendMenuBuilder.php +++ b/src/Menu/CachedFrontendMenuBuilder.php @@ -6,6 +6,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; use Twig\Environment; @@ -21,21 +22,31 @@ final class CachedFrontendMenuBuilder implements FrontendMenuBuilderInterface /** @var Request */ private $request; - public function __construct(FrontendMenuBuilderInterface $menuBuilder, TagAwareCacheInterface $cache, RequestStack $requestStack) + /** @var Stopwatch */ + private $stopwatch; + + public function __construct(FrontendMenuBuilderInterface $menuBuilder, TagAwareCacheInterface $cache, RequestStack $requestStack, Stopwatch $stopwatch) { $this->cache = $cache; $this->menuBuilder = $menuBuilder; $this->request = $requestStack->getCurrentRequest(); + $this->stopwatch = $stopwatch; } public function buildMenu(Environment $twig, ?string $name = null): array { + $this->stopwatch->start('bolt.frontendMenu'); + $key = 'frontendmenu_' . ($name ?: 'main') . '_' . $this->request->getLocale(); - return $this->cache->get($key, function (ItemInterface $item) use ($name, $twig) { + $menu = $this->cache->get($key, function (ItemInterface $item) use ($name, $twig) { $item->tag('frontendmenu'); return $this->menuBuilder->buildMenu($twig, $name); }); + + $this->stopwatch->stop('bolt.frontendMenu'); + + return $menu; } } diff --git a/src/Menu/StopwatchBackendMenuBuilder.php b/src/Menu/StopwatchBackendMenuBuilder.php deleted file mode 100644 index 7834dafb0..000000000 --- a/src/Menu/StopwatchBackendMenuBuilder.php +++ /dev/null @@ -1,31 +0,0 @@ -menuBuilder = $menuBuilder; - $this->stopwatch = $stopwatch; - } - - public function buildAdminMenu(): array - { - $this->stopwatch->start('bolt.backendMenu'); - $menu = $this->menuBuilder->buildAdminMenu(); - $this->stopwatch->stop('bolt.backendMenu'); - - return $menu; - } -} diff --git a/src/Menu/StopwatchFrontendMenuBuilder.php b/src/Menu/StopwatchFrontendMenuBuilder.php deleted file mode 100644 index 4b86a5e3f..000000000 --- a/src/Menu/StopwatchFrontendMenuBuilder.php +++ /dev/null @@ -1,32 +0,0 @@ -menuBuilder = $menuBuilder; - $this->stopwatch = $stopwatch; - } - - public function buildMenu(Environment $twig, ?string $name = null): array - { - $this->stopwatch->start('bolt.frontendMenu'); - $menu = $this->menuBuilder->buildMenu($twig, $name); - $this->stopwatch->stop('bolt.frontendMenu'); - - return $menu; - } -} diff --git a/src/Twig/FieldExtension.php b/src/Twig/FieldExtension.php index c65b31c18..b1e036d32 100644 --- a/src/Twig/FieldExtension.php +++ b/src/Twig/FieldExtension.php @@ -16,6 +16,9 @@ use Bolt\Utils\ContentHelper; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; use Tightenco\Collect\Support\Collection; use Twig\Environment; use Twig\Extension\AbstractExtension; @@ -38,19 +41,26 @@ class FieldExtension extends AbstractExtension /** @var Query */ private $query; + /** @var Stopwatch */ + private $stopwatch; + + /** @var TagAwareCacheInterface */ + private $cache; public function __construct( Notifications $notifications, ContentRepository $contentRepository, Config $config, ContentHelper $contentHelper, - Query $query) + Query $query, Stopwatch $stopwatch, TagAwareCacheInterface $cache) { $this->notifications = $notifications; $this->contentRepository = $contentRepository; $this->config = $config; $this->contentHelper = $contentHelper; $this->query = $query; + $this->stopwatch = $stopwatch; + $this->cache = $cache; } /** @@ -275,19 +285,40 @@ private function selectOptionsContentType(Field $field): Collection 'order' => $order, ]; - /** @var Content[] $records */ - $records = iterator_to_array($this->query->getContent($contentTypeSlug, $params)->getCurrentPageResults()); + $options = $this->selectOptionsContentTypeCache($contentTypeSlug, $params, $field, $format); + + return new Collection($options); + } + + private function selectOptionsContentTypeCache(string $contentTypeSlug, array $params, Field $field, string $format) + { + $cacheKey = 'selectOptions_' . md5($contentTypeSlug . implode('-', $params)); + + $this->stopwatch->start('selectOptions'); + + $options = $this->cache->get($cacheKey, function (ItemInterface $item) use ($contentTypeSlug, $params, $field, $format) { + $item->tag($contentTypeSlug); - foreach ($records as $record) { - if ($field->getDefinition()->get('mode') === 'format') { - $formattedKey = $this->contentHelper->get($record, $field->getDefinition()->get('format')); + /** @var Content[] $records */ + $records = iterator_to_array($this->query->getContent($contentTypeSlug, $params)->getCurrentPageResults()); + + $options = []; + + foreach ($records as $record) { + if ($field->getDefinition()->get('mode') === 'format') { + $formattedKey = $this->contentHelper->get($record, $field->getDefinition()->get('format')); + } + $options[] = [ + 'key' => $formattedKey ?? $record->getId(), + 'value' => $this->contentHelper->get($record, $format), + ]; } - $options[] = [ - 'key' => $formattedKey ?? $record->getId(), - 'value' => $this->contentHelper->get($record, $format), - ]; - } - return new Collection($options); + return $options; + }); + + $this->stopwatch->stop('selectOptions'); + + return $options; } } diff --git a/src/Twig/JsonExtension.php b/src/Twig/JsonExtension.php index da38a5425..7d64854e2 100644 --- a/src/Twig/JsonExtension.php +++ b/src/Twig/JsonExtension.php @@ -8,6 +8,9 @@ use Bolt\Entity\Content; use Bolt\Entity\Field; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigTest; @@ -24,9 +27,17 @@ class JsonExtension extends AbstractExtension /** @var NormalizerInterface */ private $normalizer; - public function __construct(NormalizerInterface $normalizer) + /** @var Stopwatch */ + private $stopwatch; + + /** @var TagAwareCacheInterface */ + private $cache; + + public function __construct(NormalizerInterface $normalizer, Stopwatch $stopwatch, TagAwareCacheInterface $cache) { $this->normalizer = $normalizer; + $this->stopwatch = $stopwatch; + $this->cache = $cache; } /** @@ -55,7 +66,13 @@ public function jsonRecords($records, ?bool $includeDefinition = true, int $opti { $this->includeDefinition = $includeDefinition; - return Json::json_encode($this->normalizeRecords($records, $locale), $options); + $this->stopwatch->start('bolt.jsonRecords'); + + $json = Json::json_encode($this->normalizeRecords($records, $locale), $options); + + $this->stopwatch->stop('bolt.jsonRecords'); + + return $json; } /** @@ -79,6 +96,23 @@ public function normalizeRecords($records, string $locale = ''): array } private function contentToArray(Content $content, string $locale = ''): array + { + $cacheKey = 'bolt.contentToArray_' . $content->getCacheKey($locale); + + $this->stopwatch->start($cacheKey); + + $result = $this->cache->get($cacheKey, function (ItemInterface $item) use ($content, $locale) { + $item->tag($content->getCacheKey()); + + return $this->contentToArrayHelper($content, $locale); + }); + + $this->stopwatch->stop($cacheKey); + + return $result; + } + + private function contentToArrayHelper(Content $content, string $locale = ''): array { $group = [self::SERIALIZE_GROUP]; diff --git a/src/Twig/RelatedExtension.php b/src/Twig/RelatedExtension.php index 301b8d3ce..7b83b390b 100644 --- a/src/Twig/RelatedExtension.php +++ b/src/Twig/RelatedExtension.php @@ -10,6 +10,8 @@ use Bolt\Repository\RelationRepository; use Bolt\Storage\Query; use Bolt\Utils\ContentHelper; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; use Tightenco\Collect\Support\Collection; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; @@ -32,13 +34,17 @@ class RelatedExtension extends AbstractExtension /** @var Notifications */ private $notifications; - public function __construct(RelationRepository $relationRepository, Config $config, Query $query, ContentHelper $contentHelper, Notifications $notifications) + /** @var TagAwareCacheInterface */ + private $cache; + + public function __construct(RelationRepository $relationRepository, Config $config, Query $query, ContentHelper $contentHelper, Notifications $notifications, TagAwareCacheInterface $cache) { $this->relationRepository = $relationRepository; $this->config = $config; $this->query = $query; $this->contentHelper = $contentHelper; $this->notifications = $notifications; + $this->cache = $cache; } /** @@ -149,6 +155,19 @@ public function getRelatedOptions(string $contentTypeSlug, ?string $order = null $order = $contentType->get('order'); } + $cacheKey = 'relatedOptions_' . md5($contentTypeSlug . $order . $format . (string) $required . $maxAmount); + + $options = $this->cache->get($cacheKey, function (ItemInterface $item) use ($contentTypeSlug, $order, $format, $required, $maxAmount) { + $item->tag($contentTypeSlug); + + return $this->getRelatedOptionsCache($contentTypeSlug, $order, $format, $required, $maxAmount); + }); + + return new Collection($options); + } + + public function getRelatedOptionsCache(string $contentTypeSlug, string $order, string $format, bool $required, int $maxAmount): array + { $pager = $this->query->getContent($contentTypeSlug, ['order' => $order]) ->setMaxPerPage($maxAmount) ->setCurrentPage(1); @@ -175,7 +194,7 @@ public function getRelatedOptions(string $contentTypeSlug, ?string $order = null ]; } - return new Collection($options); + return $options; } public function getRelatedValues(Content $source, string $contentType): Collection