From 991315170e2f223869472e46dedcbc17074613b2 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Fri, 3 Dec 2021 16:27:13 -0500 Subject: [PATCH] [TwigComponent] add PreMount attribute/hook (closes #142) --- .../src/Attribute/AsLiveComponent.php | 14 -------- src/TwigComponent/README.md | 34 +++++++++++++++++++ .../src/Attribute/AsTwigComponent.php | 24 +++++++++++++ src/TwigComponent/src/Attribute/PreMount.php | 22 ++++++++++++ src/TwigComponent/src/ComponentFactory.php | 11 ++++++ .../tests/Fixture/Component/ComponentB.php | 12 +++++++ .../templates/components/custom1.html.twig | 1 + .../Fixture/templates/template_b.html.twig | 2 +- .../Integration/ComponentExtensionTest.php | 1 + 9 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 src/TwigComponent/src/Attribute/PreMount.php diff --git a/src/LiveComponent/src/Attribute/AsLiveComponent.php b/src/LiveComponent/src/Attribute/AsLiveComponent.php index 7519f3bd00e..dcf8b6212cc 100644 --- a/src/LiveComponent/src/Attribute/AsLiveComponent.php +++ b/src/LiveComponent/src/Attribute/AsLiveComponent.php @@ -99,20 +99,6 @@ public static function preDehydrateMethods(object $component): \Traversable yield from self::attributeMethodsFor(PreDehydrate::class, $component); } - /** - * @param string|object $classOrObject - * - * @return \ReflectionMethod[] - */ - private static function attributeMethodsFor(string $attribute, object $component): \Traversable - { - foreach ((new \ReflectionClass($component))->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { - if ($method->getAttributes($attribute)[0] ?? null) { - yield $method; - } - } - } - /** * @return \ReflectionProperty[] */ diff --git a/src/TwigComponent/README.md b/src/TwigComponent/README.md index c510aca5abe..ae722ee625a 100644 --- a/src/TwigComponent/README.md +++ b/src/TwigComponent/README.md @@ -217,6 +217,40 @@ If an option name matches an argument name in `mount()`, the option is passed as that argument and the component system will _not_ try to set it directly on a property. +### PreMount Hook + +If you need to modify/validate data before it's _mounted_ on the +component use a `PreMount` hook: + +```php +// src/Components/AlertComponent.php + +use Symfony\UX\TwigComponent\Attribute\PreMount; +// ... + +#[AsTwigComponent('alert')] +class AlertComponent +{ + public string $message; + public string $type = 'success'; + + #[PreMount] + public function preMount(array $data): array + { + // validate data + $resolver = new OptionsResolver(); + $resolver->setDefaults(['type' => 'success']); + $resolver->setAllowedValues('type', ['success', 'danger']); + $resolver->setRequired('message'); + $resolver->setAllowedTypes('message', 'string'); + + return $resolver->resolve($data) + } + + // ... +} +``` + ## Fetching Services Let's create a more complex example: a "featured products" component. diff --git a/src/TwigComponent/src/Attribute/AsTwigComponent.php b/src/TwigComponent/src/Attribute/AsTwigComponent.php index 7d5d58a1254..711a19e20da 100644 --- a/src/TwigComponent/src/Attribute/AsTwigComponent.php +++ b/src/TwigComponent/src/Attribute/AsTwigComponent.php @@ -27,4 +27,28 @@ public function __construct(string $name, ?string $template = null) $this->name = $name; $this->template = $template; } + + /** + * @internal + * + * @return \ReflectionMethod[] + */ + public static function preMountMethods(object $component): \Traversable + { + yield from self::attributeMethodsFor(PreMount::class, $component); + } + + /** + * @internal + * + * @return \ReflectionMethod[] + */ + protected static function attributeMethodsFor(string $attribute, object $component): \Traversable + { + foreach ((new \ReflectionClass($component))->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if ($method->getAttributes($attribute)[0] ?? null) { + yield $method; + } + } + } } diff --git a/src/TwigComponent/src/Attribute/PreMount.php b/src/TwigComponent/src/Attribute/PreMount.php new file mode 100644 index 00000000000..a7964c0ae02 --- /dev/null +++ b/src/TwigComponent/src/Attribute/PreMount.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * @author Kevin Bond + * + * @experimental + */ +#[\Attribute(\Attribute::TARGET_METHOD)] +final class PreMount +{ +} diff --git a/src/TwigComponent/src/ComponentFactory.php b/src/TwigComponent/src/ComponentFactory.php index 864d90fc171..0678cb05a49 100644 --- a/src/TwigComponent/src/ComponentFactory.php +++ b/src/TwigComponent/src/ComponentFactory.php @@ -13,6 +13,7 @@ use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; /** * @author Kevin Bond @@ -80,6 +81,7 @@ public function configFor($component, string $name = null): array public function create(string $name, array $data = []): object { $component = $this->getComponent($name); + $data = $this->preMount($component, $data); $this->mount($component, $data); @@ -140,4 +142,13 @@ private function getComponent(string $name): object return $this->components->get($name); } + + private function preMount(object $component, array $data): array + { + foreach (AsTwigComponent::preMountMethods($component) as $method) { + $data = $component->{$method->name}($data); + } + + return $data; + } } diff --git a/src/TwigComponent/tests/Fixture/Component/ComponentB.php b/src/TwigComponent/tests/Fixture/Component/ComponentB.php index 478b1c70344..b19a9df6e79 100644 --- a/src/TwigComponent/tests/Fixture/Component/ComponentB.php +++ b/src/TwigComponent/tests/Fixture/Component/ComponentB.php @@ -12,6 +12,7 @@ namespace Symfony\UX\TwigComponent\Tests\Fixture\Component; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; +use Symfony\UX\TwigComponent\Attribute\PreMount; /** * @author Kevin Bond @@ -19,4 +20,15 @@ #[AsTwigComponent('component_b', template: 'components/custom1.html.twig')] final class ComponentB { + public string $value; + + #[PreMount] + public function preMount(array $data): array + { + if (isset($data['value'])) { + $data['value'] = 'pre-mount '.$data['value']; + } + + return $data; + } } diff --git a/src/TwigComponent/tests/Fixture/templates/components/custom1.html.twig b/src/TwigComponent/tests/Fixture/templates/components/custom1.html.twig index 11ebe6a9ee6..1806be7f4ed 100644 --- a/src/TwigComponent/tests/Fixture/templates/components/custom1.html.twig +++ b/src/TwigComponent/tests/Fixture/templates/components/custom1.html.twig @@ -1 +1,2 @@ Custom template 1 +b value: {{ this.value }} diff --git a/src/TwigComponent/tests/Fixture/templates/template_b.html.twig b/src/TwigComponent/tests/Fixture/templates/template_b.html.twig index 22c4c05fdae..b4c4439efe6 100644 --- a/src/TwigComponent/tests/Fixture/templates/template_b.html.twig +++ b/src/TwigComponent/tests/Fixture/templates/template_b.html.twig @@ -1,3 +1,3 @@ {{ component('component_a', { propA: 'prop a value 1', propB: 'prop b value 1' }) }} {{ component('component_a', { propA: 'prop a value 2', propB: 'prop b value 2' }) }} -{{ component('component_b') }} +{{ component('component_b', { value: 'b value 1' }) }} diff --git a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php index 22ce7ca6a4f..65181be1a7d 100644 --- a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php +++ b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php @@ -38,6 +38,7 @@ public function testCanRenderTheSameComponentMultipleTimes(): void $this->assertStringContainsString('propB: prop b value 1', $output); $this->assertStringContainsString('propA: prop a value 2', $output); $this->assertStringContainsString('propB: prop b value 2', $output); + $this->assertStringContainsString('b value: pre-mount b value 1', $output); $this->assertStringContainsString('service: service a value', $output); }