diff --git a/src/TwigComponent/CHANGELOG.md b/src/TwigComponent/CHANGELOG.md index 716e0c555a8..eda8d6b76cd 100644 --- a/src/TwigComponent/CHANGELOG.md +++ b/src/TwigComponent/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## Unreleased + +- Allow `string[]` for component attribute values (filter non-strings). + ## 2.13.0 - [BC BREAK] Add component metadata to `PreMountEvent` and `PostMountEvent` diff --git a/src/TwigComponent/src/ComponentAttributes.php b/src/TwigComponent/src/ComponentAttributes.php index 408b2055fa0..e3ec011c4ae 100644 --- a/src/TwigComponent/src/ComponentAttributes.php +++ b/src/TwigComponent/src/ComponentAttributes.php @@ -21,8 +21,10 @@ */ final class ComponentAttributes { + private bool $normalized = false; + /** - * @param array $attributes + * @param array $attributes */ public function __construct(private array $attributes) { @@ -30,15 +32,13 @@ public function __construct(private array $attributes) public function __toString(): string { + $this->ensureNormalized(); + return array_reduce( array_keys($this->attributes), 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; @@ -59,7 +59,7 @@ function (string $carry, string $key) { */ public function all(): array { - return $this->attributes; + return $this->ensureNormalized()->attributes; } /** @@ -79,6 +79,8 @@ public function defaults(iterable $attributes): self $attributes = iterator_to_array($attributes); } + self::normalize($attributes); + foreach ($this->attributes as $key => $value) { if (\in_array($key, ['class', 'data-controller', 'data-action'], true) && isset($attributes[$key])) { $attributes[$key] = "{$attributes[$key]} {$value}"; @@ -157,4 +159,29 @@ public function remove($key): self return new self($attributes); } + + private function ensureNormalized(): self + { + if ($this->normalized) { + return $this; + } + + self::normalize($this->attributes); + $this->normalized = true; + + return $this; + } + + private static function normalize(array &$attributes): void + { + foreach ($attributes as $key => &$value) { + if (\is_array($value) && array_is_list($value)) { + $value = implode(' ', array_filter($value, static fn ($v) => \is_string($v) && '' !== $v)); + } + + 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))); + } + } + } } diff --git a/src/TwigComponent/tests/Unit/ComponentAttributesTest.php b/src/TwigComponent/tests/Unit/ComponentAttributesTest.php index 32e7d312f81..197d81e708e 100644 --- a/src/TwigComponent/tests/Unit/ComponentAttributesTest.php +++ b/src/TwigComponent/tests/Unit/ComponentAttributesTest.php @@ -191,4 +191,30 @@ public function testNullBehaviour(): void $this->assertSame(['disabled' => null], $attributes->all()); $this->assertSame(' disabled', (string) $attributes); } + + public function testCannotPassNonScalarValues(): void + { + $this->expectException(\LogicException::class); + + (string) new ComponentAttributes(['data-foo' => new \stdClass()]); + } + + public function testListAttributeValues(): void + { + $attributes = new ComponentAttributes([ + 'style' => ['foo', null, false, true, '', new \stdClass(), 'bar'], + 'class' => ['baz', ''], + ]); + + $this->assertSame(' style="foo bar" class="baz"', (string) $attributes); + $this->assertSame(['style' => 'foo bar', 'class' => 'baz'], $attributes->all()); + + $attributes = $attributes->defaults([ + 'style' => ['baz', false], + 'class' => ['', 'qux'], + ]); + + $this->assertSame(' style="foo bar" class="qux baz"', (string) $attributes); + $this->assertSame(['style' => 'foo bar', 'class' => 'qux baz'], $attributes->all()); + } }