From c67028fc530adcb94918c5a5f310c39e94383c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Sun, 10 Nov 2024 04:43:57 +0100 Subject: [PATCH] [TwigComponent] Store mount methods in compiler pass --- .../Integration/LiveComponentHydratorTest.php | 5 +- .../src/Attribute/AsTwigComponent.php | 40 ---------- .../src/Command/TwigComponentDebugCommand.php | 18 ++--- src/TwigComponent/src/ComponentFactory.php | 42 ++++------ src/TwigComponent/src/ComponentMetadata.php | 30 ++++++++ .../Compiler/TwigComponentPass.php | 35 ++++++++- .../AcmeComponent/AcmeRootComponent.php | 1 - .../AcmeSubDir/AcmeOtherComponent.php | 1 - .../tests/Fixtures/Component/Conflict.php | 2 +- .../SubDirectory/ComponentInSubDirectory.php | 1 - .../Integration/ComponentExtensionTest.php | 12 ++- .../Unit/Attribute/AsTwigComponentTest.php | 77 ------------------- .../tests/Unit/ComponentFactoryTest.php | 7 +- 13 files changed, 105 insertions(+), 166 deletions(-) delete mode 100644 src/TwigComponent/tests/Unit/Attribute/AsTwigComponentTest.php diff --git a/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php b/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php index 511a5580f72..66ea8cd24d6 100644 --- a/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php +++ b/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php @@ -1872,7 +1872,10 @@ public function getTest(LiveComponentMetadataFactory $metadataFactory): Hydratio return new HydrationTestCase( $this->component, new LiveComponentMetadata( - new ComponentMetadata(['key' => '__testing']), + new ComponentMetadata([ + 'key' => '__testing', + 'mount' => $reflectionClass->hasMethod('mount') ? ['mount'] : [], + ]), $metadataFactory->createPropMetadatas($reflectionClass), ), $this->inputProps, diff --git a/src/TwigComponent/src/Attribute/AsTwigComponent.php b/src/TwigComponent/src/Attribute/AsTwigComponent.php index d58e5647ae8..829fd5bfbdb 100644 --- a/src/TwigComponent/src/Attribute/AsTwigComponent.php +++ b/src/TwigComponent/src/Attribute/AsTwigComponent.php @@ -75,46 +75,6 @@ public function serviceConfig(): array ]; } - /** - * @param object|class-string $component - * - * @internal - */ - public static function mountMethod(object|string $component): ?\ReflectionMethod - { - foreach ((new \ReflectionClass($component))->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { - if ('mount' === $method->getName()) { - return $method; - } - } - - return null; - } - - /** - * @param object|class-string $component - * - * @return \ReflectionMethod[] - * - * @internal - */ - public static function postMountMethods(object|string $component): array - { - return self::attributeMethodsByPriorityFor($component, PostMount::class); - } - - /** - * @param object|class-string $component - * - * @return \ReflectionMethod[] - * - * @internal - */ - public static function preMountMethods(object|string $component): array - { - return self::attributeMethodsByPriorityFor($component, PreMount::class); - } - /** * @param object|class-string $component * @param class-string $attributeClass diff --git a/src/TwigComponent/src/Command/TwigComponentDebugCommand.php b/src/TwigComponent/src/Command/TwigComponentDebugCommand.php index 843138026c6..f883f59ef96 100644 --- a/src/TwigComponent/src/Command/TwigComponentDebugCommand.php +++ b/src/TwigComponent/src/Command/TwigComponentDebugCommand.php @@ -21,7 +21,6 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Finder\Finder; -use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate; use Symfony\UX\TwigComponent\ComponentFactory; use Symfony\UX\TwigComponent\ComponentMetadata; @@ -214,7 +213,7 @@ private function displayComponentDetails(SymfonyStyle $io, string $name): void ]); // Anonymous Component - if (null === $metadata->get('class')) { + if ($metadata->isAnonymous()) { $table->addRows([ ['Type', 'Anonymous'], new TableSeparator(), @@ -229,7 +228,7 @@ private function displayComponentDetails(SymfonyStyle $io, string $name): void ['Type', $metadata->get('live') ? 'Live' : ''], new TableSeparator(), // ['Attributes Var', $metadata->get('attributes_var')], - ['Public Props', $metadata->get('expose_public_props') ? 'Yes' : 'No'], + ['Public Props', $metadata->isPublicPropsExposed() ? 'Yes' : 'No'], ['Properties', implode("\n", $this->getComponentProperties($metadata))], ]); @@ -242,14 +241,15 @@ private function displayComponentDetails(SymfonyStyle $io, string $name): void return \sprintf('%s(%s)', $m->getName(), implode(', ', $params)); }; $hooks = []; - if ($method = AsTwigComponent::mountMethod($metadata->getClass())) { - $hooks[] = ['Mount', $logMethod($method)]; + $reflector = new \ReflectionClass($metadata->getClass()); + foreach ($metadata->getPreMounts() as $method) { + $hooks[] = ['PreMount', $logMethod($reflector->getMethod($method))]; } - foreach (AsTwigComponent::preMountMethods($metadata->getClass()) as $method) { - $hooks[] = ['PreMount', $logMethod($method)]; + foreach ($metadata->getMounts() as $method) { + $hooks[] = ['Mount', $logMethod($reflector->getMethod($method))]; } - foreach (AsTwigComponent::postMountMethods($metadata->getClass()) as $method) { - $hooks[] = ['PostMount', $logMethod($method)]; + foreach ($metadata->getPostMounts() as $method) { + $hooks[] = ['PostMount', $logMethod($reflector->getMethod($method))]; } if ($hooks) { $table->addRows([ diff --git a/src/TwigComponent/src/ComponentFactory.php b/src/TwigComponent/src/ComponentFactory.php index c191bc1bb4a..8699fb10a32 100644 --- a/src/TwigComponent/src/ComponentFactory.php +++ b/src/TwigComponent/src/ComponentFactory.php @@ -15,7 +15,6 @@ use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Contracts\Service\ResetInterface; -use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; use Symfony\UX\TwigComponent\Event\PostMountEvent; use Symfony\UX\TwigComponent\Event\PreMountEvent; @@ -26,13 +25,12 @@ */ final class ComponentFactory implements ResetInterface { - private static $mountMethods = []; - private static $preMountMethods = []; - private static $postMountMethods = []; + private static array $mountMethods = []; /** * @param array $config * @param array $classMap + * @param array $classMounts */ public function __construct( private ComponentTemplateFinderInterface $componentTemplateFinder, @@ -92,7 +90,7 @@ public function mountFromObject(object $component, array $data, ComponentMetadat $originalData = $data; $data = $this->preMount($component, $data, $componentMetadata); - $this->mount($component, $data); + $this->mount($component, $data, $componentMetadata); // set data that wasn't set in mount on the component directly foreach ($data as $property => $value) { @@ -144,7 +142,7 @@ public function get(string $name): object return $this->components->get($metadata->getName()); } - private function mount(object $component, array &$data): void + private function mount(object $component, array &$data, ComponentMetadata $componentMetadata): void { if ($component instanceof AnonymousComponent) { $component->mount($data); @@ -152,22 +150,14 @@ private function mount(object $component, array &$data): void return; } - if (null === (self::$mountMethods[$component::class] ?? null)) { - try { - $mountMethod = self::$mountMethods[$component::class] = (new \ReflectionClass($component))->getMethod('mount'); - } catch (\ReflectionException) { - self::$mountMethods[$component::class] = false; - - return; - } - } - - if (false === $mountMethod ??= self::$mountMethods[$component::class]) { + if (!$componentMetadata->getMounts()) { return; } + $mount = self::$mountMethods[$component::class] ??= (new \ReflectionClass($component))->getMethod('mount'); + $parameters = []; - foreach ($mountMethod->getParameters() as $refParameter) { + foreach ($mount->getParameters() as $refParameter) { if (\array_key_exists($name = $refParameter->getName(), $data)) { $parameters[] = $data[$name]; // remove the data element so it isn't used to set the property directly. @@ -175,11 +165,11 @@ private function mount(object $component, array &$data): void } elseif ($refParameter->isDefaultValueAvailable()) { $parameters[] = $refParameter->getDefaultValue(); } else { - throw new \LogicException(\sprintf('%s::mount() has a required $%s parameter. Make sure to pass it or give it a default value.', $component::class, $name)); + throw new \LogicException(\sprintf('%s has a required $%s parameter. Make sure to pass it or give it a default value.', $component::class.'::mount()', $name)); } } - $mountMethod->invoke($component, ...$parameters); + $mount->invoke($component, ...$parameters); } private function preMount(object $component, array $data, ComponentMetadata $componentMetadata): array @@ -188,9 +178,8 @@ private function preMount(object $component, array $data, ComponentMetadata $com $this->eventDispatcher->dispatch($event); $data = $event->getData(); - $methods = self::$preMountMethods[$component::class] ??= AsTwigComponent::preMountMethods($component::class); - foreach ($methods as $method) { - if (null !== $newData = $method->invoke($component, $data)) { + foreach ($componentMetadata->getPreMounts() as $preMount) { + if (null !== $newData = $component->$preMount($data)) { $data = $newData; } } @@ -207,9 +196,8 @@ private function postMount(object $component, array $data, ComponentMetadata $co $this->eventDispatcher->dispatch($event); $data = $event->getData(); - $methods = self::$postMountMethods[$component::class] ??= AsTwigComponent::postMountMethods($component::class); - foreach ($methods as $method) { - if (null !== $newData = $method->invoke($component, $data)) { + foreach ($componentMetadata->getPostMounts() as $postMount) { + if (null !== $newData = $component->$postMount($data)) { $data = $newData; } } @@ -257,7 +245,5 @@ private function throwUnknownComponentException(string $name): void public function reset(): void { self::$mountMethods = []; - self::$preMountMethods = []; - self::$postMountMethods = []; } } diff --git a/src/TwigComponent/src/ComponentMetadata.php b/src/TwigComponent/src/ComponentMetadata.php index b71e349161c..4eb30327eca 100644 --- a/src/TwigComponent/src/ComponentMetadata.php +++ b/src/TwigComponent/src/ComponentMetadata.php @@ -67,6 +67,36 @@ public function getAttributesVar(): string return $this->get('attributes_var', 'attributes'); } + /** + * @return list + * + * @internal + */ + public function getPreMounts(): array + { + return $this->get('pre_mount', []); + } + + /** + * @return list + * + * @internal + */ + public function getMounts(): array + { + return $this->get('mount', []); + } + + /** + * @return list + * + * @internal + */ + public function getPostMounts(): array + { + return $this->get('post_mount', []); + } + public function get(string $key, mixed $default = null): mixed { return $this->config[$key] ?? $default; diff --git a/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php b/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php index faf86afe925..b39ae4535dc 100644 --- a/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php +++ b/src/TwigComponent/src/DependencyInjection/Compiler/TwigComponentPass.php @@ -16,6 +16,8 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Reference; +use Symfony\UX\TwigComponent\Attribute\PostMount; +use Symfony\UX\TwigComponent\Attribute\PreMount; /** * @author Kevin Bond @@ -68,7 +70,7 @@ public function process(ContainerBuilder $container): void $tag['service_id'] = $id; $tag['class'] = $definition->getClass(); $tag['template'] = $tag['template'] ?? $this->calculateTemplate($tag['key'], $defaults); - $componentConfig[$tag['key']] = $tag; + $componentConfig[$tag['key']] = [...$tag, ...$this->getMountMethods($tag['class'])]; $componentReferences[$tag['key']] = new Reference($id); $componentNames[] = $tag['key']; $componentClassMap[$tag['class']] = $tag['key']; @@ -109,4 +111,35 @@ private function calculateTemplate(string $componentName, ?array $defaults): str return \sprintf('%s/%s.html.twig', rtrim($directory, '/'), str_replace(':', '/', $componentName)); } + + /** + * @param class-string $component + * + * @return array{preMount: string[], mount: string[], postMount: string[]} + */ + private function getMountMethods(string $component): array + { + $preMount = $mount = $postMount = []; + foreach ((new \ReflectionClass($component))->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + foreach ($method->getAttributes(PreMount::class) as $attribute) { + $preMount[$method->getName()] = $attribute->newInstance()->priority; + } + foreach ($method->getAttributes(PostMount::class) as $attribute) { + $postMount[$method->getName()] = $attribute->newInstance()->priority; + } + if ('mount' === $method->getName()) { + $mount['mount'] = 0; + } + } + + arsort($preMount, \SORT_NUMERIC); + arsort($mount, \SORT_NUMERIC); + arsort($postMount, \SORT_NUMERIC); + + return [ + 'pre_mount' => array_keys($preMount), + 'mount' => array_keys($mount), + 'post_mount' => array_keys($postMount), + ]; + } } diff --git a/src/TwigComponent/tests/Fixtures/AcmeComponent/AcmeRootComponent.php b/src/TwigComponent/tests/Fixtures/AcmeComponent/AcmeRootComponent.php index 47fba424103..5783818d3c6 100644 --- a/src/TwigComponent/tests/Fixtures/AcmeComponent/AcmeRootComponent.php +++ b/src/TwigComponent/tests/Fixtures/AcmeComponent/AcmeRootComponent.php @@ -7,5 +7,4 @@ #[AsTwigComponent] class AcmeRootComponent { - } diff --git a/src/TwigComponent/tests/Fixtures/AcmeComponent/AcmeSubDir/AcmeOtherComponent.php b/src/TwigComponent/tests/Fixtures/AcmeComponent/AcmeSubDir/AcmeOtherComponent.php index d42ad45b77c..fa31f3eae84 100644 --- a/src/TwigComponent/tests/Fixtures/AcmeComponent/AcmeSubDir/AcmeOtherComponent.php +++ b/src/TwigComponent/tests/Fixtures/AcmeComponent/AcmeSubDir/AcmeOtherComponent.php @@ -7,5 +7,4 @@ #[AsTwigComponent] class AcmeOtherComponent { - } diff --git a/src/TwigComponent/tests/Fixtures/Component/Conflict.php b/src/TwigComponent/tests/Fixtures/Component/Conflict.php index 0ffd16a80ee..c3b7960b175 100644 --- a/src/TwigComponent/tests/Fixtures/Component/Conflict.php +++ b/src/TwigComponent/tests/Fixtures/Component/Conflict.php @@ -8,4 +8,4 @@ class Conflict { public string $name; -} \ No newline at end of file +} diff --git a/src/TwigComponent/tests/Fixtures/Component/SubDirectory/ComponentInSubDirectory.php b/src/TwigComponent/tests/Fixtures/Component/SubDirectory/ComponentInSubDirectory.php index 723eafbc1a3..a7a53df45fe 100644 --- a/src/TwigComponent/tests/Fixtures/Component/SubDirectory/ComponentInSubDirectory.php +++ b/src/TwigComponent/tests/Fixtures/Component/SubDirectory/ComponentInSubDirectory.php @@ -7,5 +7,4 @@ #[AsTwigComponent] class ComponentInSubDirectory { - } diff --git a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php index 8b2b0e0987a..4f113262f88 100644 --- a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php +++ b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php @@ -283,7 +283,8 @@ public function testRenderingComponentWithNestedAttributes(): void { $output = $this->renderComponent('NestedAttributes'); - $this->assertSame(<<assertSame( + <<
@@ -302,7 +303,8 @@ public function testRenderingComponentWithNestedAttributes(): void 'title:span:class' => 'baz', ]); - $this->assertSame(<<assertSame( + <<
@@ -324,7 +326,8 @@ public function testRenderingHtmlSyntaxComponentWithNestedAttributes(): void ->render() ; - $this->assertSame(<<assertSame( + <<
@@ -343,7 +346,8 @@ public function testRenderingHtmlSyntaxComponentWithNestedAttributes(): void ->render() ; - $this->assertSame(<<assertSame( + <<
diff --git a/src/TwigComponent/tests/Unit/Attribute/AsTwigComponentTest.php b/src/TwigComponent/tests/Unit/Attribute/AsTwigComponentTest.php deleted file mode 100644 index 1606792ca99..00000000000 --- a/src/TwigComponent/tests/Unit/Attribute/AsTwigComponentTest.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\TwigComponent\Tests\Unit; - -use PHPUnit\Framework\TestCase; -use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; -use Symfony\UX\TwigComponent\Attribute\PostMount; -use Symfony\UX\TwigComponent\Attribute\PreMount; - -/** - * @author Kevin Bond - */ -final class AsTwigComponentTest extends TestCase -{ - public function testPreMountHooksAreOrderedByPriority(): void - { - $hooks = AsTwigComponent::preMountMethods( - new class { - #[PreMount(priority: -10)] - public function hook1() - { - } - - #[PreMount(priority: 10)] - public function hook2() - { - } - - #[PreMount] - public function hook3() - { - } - } - ); - - $this->assertCount(3, $hooks); - $this->assertSame('hook2', $hooks[0]->name); - $this->assertSame('hook3', $hooks[1]->name); - $this->assertSame('hook1', $hooks[2]->name); - } - - public function testPostMountHooksAreOrderedByPriority(): void - { - $hooks = AsTwigComponent::postMountMethods( - new class { - #[PostMount(priority: -10)] - public function hook1() - { - } - - #[PostMount(priority: 10)] - public function hook2() - { - } - - #[PostMount] - public function hook3() - { - } - } - ); - - $this->assertCount(3, $hooks); - $this->assertSame('hook2', $hooks[0]->name); - $this->assertSame('hook3', $hooks[1]->name); - $this->assertSame('hook1', $hooks[2]->name); - } -} diff --git a/src/TwigComponent/tests/Unit/ComponentFactoryTest.php b/src/TwigComponent/tests/Unit/ComponentFactoryTest.php index fd1fd337c89..ccbf85f5cbc 100644 --- a/src/TwigComponent/tests/Unit/ComponentFactoryTest.php +++ b/src/TwigComponent/tests/Unit/ComponentFactoryTest.php @@ -31,7 +31,8 @@ public function testMetadataForConfig(): void $this->createMock(PropertyAccessorInterface::class), $this->createMock(EventDispatcherInterface::class), ['foo' => ['key' => 'foo', 'template' => 'bar.html.twig']], - [] + [], + [], ); $metadata = $factory->metadataFor('foo'); @@ -52,6 +53,7 @@ public function testMetadataForResolveAlias(): void 'foo' => ['key' => 'foo', 'template' => 'foo.html.twig'], ], ['Foo\\Bar' => 'bar'], + [], ); $metadata = $factory->metadataFor('Foo\\Bar'); @@ -74,7 +76,8 @@ public function testMetadataForReuseAnonymousConfig(): void $this->createMock(PropertyAccessorInterface::class), $this->createMock(EventDispatcherInterface::class), [], - [] + [], + [], ); $metadata = $factory->metadataFor('foo');