From aca73a54d632989ff587b9bc77e2a57d449b59a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Sep 2024 23:56:04 +0200 Subject: [PATCH] Use system cache for ComponentProperties + add CacheWarmer --- src/TwigComponent/config/cache.php | 23 ++++++++ .../CacheWarmer/TwigComponentCacheWarmer.php | 55 +++++++++++++++++++ src/TwigComponent/src/ComponentProperties.php | 41 +++++++++++--- src/TwigComponent/src/ComponentRenderer.php | 7 +-- .../Compiler/TwigComponentPass.php | 3 + .../TwigComponentExtension.php | 29 ++++++++-- 6 files changed, 140 insertions(+), 18 deletions(-) create mode 100644 src/TwigComponent/config/cache.php create mode 100644 src/TwigComponent/src/CacheWarmer/TwigComponentCacheWarmer.php diff --git a/src/TwigComponent/config/cache.php b/src/TwigComponent/config/cache.php new file mode 100644 index 00000000000..e8f62ee2027 --- /dev/null +++ b/src/TwigComponent/config/cache.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\DependencyInjection\Loader\Configurator; + +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + +return static function (ContainerConfigurator $container): void { + $container->services() + ->set('cache.ux.twig_component') + ->parent('cache.system') + ->private() + ->tag('cache.pool') + ; +}; diff --git a/src/TwigComponent/src/CacheWarmer/TwigComponentCacheWarmer.php b/src/TwigComponent/src/CacheWarmer/TwigComponentCacheWarmer.php new file mode 100644 index 00000000000..fad5c74cade --- /dev/null +++ b/src/TwigComponent/src/CacheWarmer/TwigComponentCacheWarmer.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\TwigComponent\CacheWarmer; + +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; +use Symfony\Contracts\Service\ServiceSubscriberInterface; +use Symfony\UX\TwigComponent\ComponentProperties; + +/** + * Warm the TwigComponent metadata caches. + * + * @author Simon André + * + * @internal + */ +final class TwigComponentCacheWarmer implements CacheWarmerInterface, ServiceSubscriberInterface +{ + /** + * As this cache warmer is optional, dependencies should be lazy-loaded, that's why a container should be injected. + */ + public function __construct( + private readonly ContainerInterface $container, + ) { + } + + public static function getSubscribedServices(): array + { + return [ + 'ux.twig_component.component_properties' => ComponentProperties::class, + ]; + } + + public function warmUp(string $cacheDir, ?string $buildDir = null): array + { + $properties = $this->container->get('ux.twig_component.component_properties'); + $properties->warmup(); + + return []; + } + + public function isOptional(): bool + { + return true; + } +} diff --git a/src/TwigComponent/src/ComponentProperties.php b/src/TwigComponent/src/ComponentProperties.php index eb5aa95dbf8..abb8c733a0e 100644 --- a/src/TwigComponent/src/ComponentProperties.php +++ b/src/TwigComponent/src/ComponentProperties.php @@ -11,6 +11,7 @@ namespace Symfony\UX\TwigComponent; +use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate; @@ -21,11 +22,24 @@ */ final class ComponentProperties { - private array $classMetadata = []; + private const CACHE_KEY = 'ux.twig_component.component_properties'; + + /** + * @var array, + * methods: array, + * }|null> + */ + private array $classMetadata; public function __construct( private readonly PropertyAccessorInterface $propertyAccessor, + ?array $classMetadata = [], + private readonly ?AdapterInterface $cache = null, ) { + $cacheItem = $this->cache?->getItem(self::CACHE_KEY); + + $this->classMetadata = $cacheItem?->isHit() ? [...$cacheItem->get(), ...$classMetadata] : $classMetadata; } /** @@ -36,7 +50,25 @@ public function getProperties(object $component, bool $publicProps = false): arr return iterator_to_array($this->extractProperties($component, $publicProps)); } - private function extractProperties(object $component, bool $publicProps): iterable + public function warmup(): void + { + if (!$this->cache) { + return; + } + + foreach ($this->classMetadata as $class => $metadata) { + if (null === $metadata) { + $this->classMetadata[$class] = $this->loadClassMetadata($class); + } + } + + $this->cache->save($this->cache->getItem(self::CACHE_KEY)->set($this->classMetadata)); + } + + /** + * @return \Generator + */ + private function extractProperties(object $component, bool $publicProps): \Generator { yield from $publicProps ? get_object_vars($component) : []; @@ -85,12 +117,10 @@ private function loadClassMetadata(string $class): array continue; } $attribute = $attributes[0]->newInstance(); - $properties[$property->name] = [ 'name' => $attribute->name ?? $property->name, 'getter' => $attribute->getter ? rtrim($attribute->getter, '()') : null, ]; - if ($attribute->destruct) { unset($properties[$property->name]['name']); $properties[$property->name]['destruct'] = true; @@ -105,11 +135,8 @@ private function loadClassMetadata(string $class): array if ($method->getNumberOfRequiredParameters()) { throw new \LogicException(\sprintf('Cannot use "%s" on methods with required parameters (%s::%s).', ExposeInTemplate::class, $class, $method->name)); } - $attribute = $attributes[0]->newInstance(); - $name = $attribute->name ?? (str_starts_with($method->name, 'get') ? lcfirst(substr($method->name, 3)) : $method->name); - $methods[$method->name] = $attribute->destruct ? ['destruct' => true] : ['name' => $name]; } diff --git a/src/TwigComponent/src/ComponentRenderer.php b/src/TwigComponent/src/ComponentRenderer.php index 0ebc898a77d..fb6a01ffda6 100644 --- a/src/TwigComponent/src/ComponentRenderer.php +++ b/src/TwigComponent/src/ComponentRenderer.php @@ -11,7 +11,6 @@ namespace Symfony\UX\TwigComponent; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\UX\TwigComponent\Event\PostRenderEvent; use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent; @@ -25,17 +24,13 @@ */ final class ComponentRenderer implements ComponentRendererInterface { - // TODO update DI - private readonly ComponentProperties $componentProperties; - public function __construct( private Environment $twig, private EventDispatcherInterface $dispatcher, private ComponentFactory $factory, - PropertyAccessorInterface $propertyAccessor, + private ComponentProperties $componentProperties, private ComponentStack $componentStack, ) { - $this->componentProperties = new ComponentProperties($propertyAccessor); } /** diff --git a/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php b/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php index 2a4ec1b0ba4..faf86afe925 100644 --- a/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php +++ b/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php @@ -80,6 +80,9 @@ public function process(ContainerBuilder $container): void $factoryDefinition->setArgument(4, $componentConfig); $factoryDefinition->setArgument(5, $componentClassMap); + $componentPropertiesDefinition = $container->findDefinition('ux.twig_component.component_properties'); + $componentPropertiesDefinition->setArgument(1, array_fill_keys(array_keys($componentClassMap), null)); + $debugCommandDefinition = $container->findDefinition('ux.twig_component.command.debug'); $debugCommandDefinition->setArgument(3, $componentClassMap); } diff --git a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php index 72cd6f261ab..0ee7230600a 100644 --- a/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php +++ b/src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php @@ -21,14 +21,17 @@ use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; +use Symfony\UX\TwigComponent\CacheWarmer\TwigComponentCacheWarmer; use Symfony\UX\TwigComponent\Command\TwigComponentDebugCommand; use Symfony\UX\TwigComponent\ComponentFactory; +use Symfony\UX\TwigComponent\ComponentProperties; use Symfony\UX\TwigComponent\ComponentRenderer; use Symfony\UX\TwigComponent\ComponentRendererInterface; use Symfony\UX\TwigComponent\ComponentStack; @@ -84,21 +87,29 @@ static function (ChildDefinition $definition, AsTwigComponent $attribute) { $container->register('ux.twig_component.component_factory', ComponentFactory::class) ->setArguments([ new Reference('ux.twig_component.component_template_finder'), - class_exists(AbstractArgument::class) ? new AbstractArgument(\sprintf('Added in %s.', TwigComponentPass::class)) : null, + new AbstractArgument(\sprintf('Added in %s.', TwigComponentPass::class)), new Reference('property_accessor'), new Reference('event_dispatcher'), - class_exists(AbstractArgument::class) ? new AbstractArgument(\sprintf('Added in %s.', TwigComponentPass::class)) : [], + new AbstractArgument(\sprintf('Added in %s.', TwigComponentPass::class)), ]) ; $container->register('ux.twig_component.component_stack', ComponentStack::class); + $container->register('ux.twig_component.component_properties', ComponentProperties::class) + ->setArguments([ + new Reference('property_accessor'), + new AbstractArgument(\sprintf('Added in %s.', TwigComponentPass::class)), + new Reference('cache.ux.twig_component', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), + ]) + ; + $container->register('ux.twig_component.component_renderer', ComponentRenderer::class) ->setArguments([ new Reference('twig'), new Reference('event_dispatcher'), new Reference('ux.twig_component.component_factory'), - new Reference('property_accessor'), + new Reference('ux.twig_component.component_properties'), new Reference('ux.twig_component.component_stack'), ]) ; @@ -107,7 +118,7 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(\sprintf('Added in ->addTag('twig.extension') ; - $container->register('.ux.twig_component.twig.component_runtime', ComponentRuntime::class) + $container->register('ux.twig_component.twig.component_runtime', ComponentRuntime::class) ->setArguments([ new Reference('ux.twig_component.component_renderer'), new ServiceLocatorArgument(new TaggedIteratorArgument('ux.twig_component.twig_renderer', indexAttribute: 'key', needsIndexes: true)), @@ -126,7 +137,7 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(\sprintf('Added in new Parameter('twig.default_path'), new Reference('ux.twig_component.component_factory'), new Reference('twig'), - class_exists(AbstractArgument::class) ? new AbstractArgument(\sprintf('Added in %s.', TwigComponentPass::class)) : [], + new AbstractArgument(\sprintf('Added in %s.', TwigComponentPass::class)), $config['anonymous_template_directory'], ]) ->addTag('console.command') @@ -138,6 +149,14 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(\sprintf('Added in if ($container->getParameter('kernel.debug') && $config['profiler']) { $loader->load('debug.php'); } + + $loader->load('cache.php'); + + $container->register('ux.twig_component.cache_warmer', TwigComponentCacheWarmer::class) + ->setArguments([new Reference(\Psr\Container\ContainerInterface::class)]) + ->addTag('kernel.cache_warmer') + ->addTag('container.service_subscriber', ['id' => 'ux.twig_component.component_properties']) + ; } public function getConfigTreeBuilder(): TreeBuilder