diff --git a/src/TwigComponent/src/ComponentAttributes.php b/src/TwigComponent/src/ComponentAttributes.php index ecd8f97918a..7ac148bb509 100644 --- a/src/TwigComponent/src/ComponentAttributes.php +++ b/src/TwigComponent/src/ComponentAttributes.php @@ -35,6 +35,10 @@ public function __toString(): string function (string $carry, string $key) { $value = $this->attributes[$key]; + if (!\is_scalar($value) && null !== $value) { + throw new \LogicException(sprintf('A "%s" prop was passed when creating the component. No matching "%s" property or mount() argument was found, so we attempted to use this as an HTML attribute. But, the value is not a scalar (it\'s a %s). Did you mean to pass this to your component or is there a typo on its name?', $key, $key, get_debug_type($value))); + } + if (null === $value) { trigger_deprecation('symfony/ux-twig-component', '2.8.0', 'Passing "null" as an attribute value is deprecated and will throw an exception in 3.0.'); $value = true; @@ -144,4 +148,13 @@ public function add($stimulusDto): self // add the remaining attributes for values/classes return $clone->defaults($controllersAttributes); } + + public function remove($key): self + { + $attributes = $this->attributes; + + unset($attributes[$key]); + + return new self($attributes); + } } diff --git a/src/TwigComponent/src/ComponentFactory.php b/src/TwigComponent/src/ComponentFactory.php index 3877889f888..b75fc32f19f 100644 --- a/src/TwigComponent/src/ComponentFactory.php +++ b/src/TwigComponent/src/ComponentFactory.php @@ -102,9 +102,7 @@ public function mountFromObject(object $component, array $data, ComponentMetadat continue; } - if (!\is_scalar($value) && null !== $value) { - throw new \LogicException(sprintf('A "%s" prop was passed when creating the "%s" component. No matching %s property or mount() argument was found, so we attempted to use this as an HTML attribute. But, the value is not a scalar (it\'s a %s). Did you mean to pass this to your component or is there a typo on its name?', $key, $componentMetadata->getName(), $key, get_debug_type($value))); - } + $data[$key] = $value; } return new MountedComponent( diff --git a/src/TwigComponent/src/Twig/PropsNode.php b/src/TwigComponent/src/Twig/PropsNode.php index 91208c5195e..92932a5d53f 100644 --- a/src/TwigComponent/src/Twig/PropsNode.php +++ b/src/TwigComponent/src/Twig/PropsNode.php @@ -28,17 +28,21 @@ public function __construct(array $propsNames, array $values, $lineno = 0, strin public function compile(Compiler $compiler): void { + $compiler + ->addDebugInfo($this) + ->write('$propsNames = [];') + ; + foreach ($this->getAttribute('names') as $name) { $compiler - ->addDebugInfo($this) - ->write('if (!isset($context[\''.$name.'\'])) {') - ; + ->write('$propsNames[] = \''.$name.'\';') + ->write('$context[\'attributes\'] = $context[\'attributes\']->remove(\''.$name.'\');') + ->write('if (!isset($context[\''.$name.'\'])) {'); if (!$this->hasNode($name)) { $compiler ->write('throw new \Twig\Error\RuntimeError("'.$name.' should be defined for component '.$this->getTemplateName().'");') - ->write('}') - ; + ->write('}'); continue; } @@ -47,8 +51,20 @@ public function compile(Compiler $compiler): void ->write('$context[\''.$name.'\'] = ') ->subcompile($this->getNode($name)) ->raw(";\n") - ->write('}') - ; + ->write('}'); } + + $compiler + ->write('$attributesKeys = array_keys($context[\'attributes\']->all());') + ->raw("\n") + ->write('foreach ($context as $key => $value) {') + ->raw("\n") + ->write('if (in_array($key, $attributesKeys) && !in_array($key, $propsNames)) {') + ->raw("\n") + ->raw('unset($context[$key]);') + ->raw("\n") + ->write('}') + ->write('}') + ; } } diff --git a/src/TwigComponent/tests/Fixtures/User.php b/src/TwigComponent/tests/Fixtures/User.php new file mode 100644 index 00000000000..eb25026072a --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/User.php @@ -0,0 +1,21 @@ +name; + } + + public function getEmail(): string + { + return $this->email; + } +} \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/anonymous_component_none_scalar_prop.html.twig b/src/TwigComponent/tests/Fixtures/templates/anonymous_component_none_scalar_prop.html.twig new file mode 100644 index 00000000000..4bbccfb3fb7 --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/anonymous_component_none_scalar_prop.html.twig @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/TwigComponent/tests/Fixtures/templates/components/UserCard.html.twig b/src/TwigComponent/tests/Fixtures/templates/components/UserCard.html.twig new file mode 100644 index 00000000000..7acaf4afd51 --- /dev/null +++ b/src/TwigComponent/tests/Fixtures/templates/components/UserCard.html.twig @@ -0,0 +1,7 @@ +{% props user %} + +
+

{{ user.name }}

+

{{ user.email }}

+

class variable defined? {{ class is defined ? 'yes': 'no' }}

+
\ No newline at end of file diff --git a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php index 5c4c71148db..1e238f8d694 100644 --- a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php +++ b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php @@ -12,6 +12,7 @@ namespace Symfony\UX\TwigComponent\Tests\Integration; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\UX\TwigComponent\Tests\Fixtures\User; use Twig\Environment; /** @@ -182,6 +183,18 @@ public function testRenderAnonymousComponentInNestedDirectory(): void $this->assertStringContainsString('class="primary"', $output); } + public function testRenderAnonymousComponentWithNonScalarProps(): void + { + $user = new User('Fabien', 'test@test.com'); + + $output = self::getContainer()->get(Environment::class)->render('anonymous_component_none_scalar_prop.html.twig', ['user' => $user]); + + $this->assertStringContainsString('class="foo"', $output); + $this->assertStringContainsString('Fabien', $output); + $this->assertStringContainsString('test@test.com', $output); + $this->assertStringContainsString('class variable defined? no', $output); + } + private function renderComponent(string $name, array $data = []): string { return self::getContainer()->get(Environment::class)->render('render_component.html.twig', [ diff --git a/src/TwigComponent/tests/Integration/ComponentFactoryTest.php b/src/TwigComponent/tests/Integration/ComponentFactoryTest.php index 19e6f889418..61426a650bc 100644 --- a/src/TwigComponent/tests/Integration/ComponentFactoryTest.php +++ b/src/TwigComponent/tests/Integration/ComponentFactoryTest.php @@ -91,14 +91,6 @@ public function testExceptionThrownIfRequiredMountParameterIsMissingFromPassedDa $this->createComponent('component_c'); } - public function testExceptionThrownIfUnableToWritePassedDataToPropertyAndIsNotScalar(): void - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('But, the value is not a scalar (it\'s a stdClass)'); - - $this->createComponent('component_a', ['propB' => 'B', 'service' => new \stdClass()]); - } - public function testStringableObjectCanBePassedToComponent(): void { $attributes = $this->factory()->create('component_a', ['propB' => 'B', 'data-item-id-param' => new class() {