From cd3c55058230b06e4b142640ab6f3dbe1ad5b029 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 22 Jan 2024 20:04:32 -0500 Subject: [PATCH 1/2] feat(twig): allow `string[]` for `ComponentAttribute` values --- src/TwigComponent/CHANGELOG.md | 4 ++ src/TwigComponent/src/ComponentAttributes.php | 49 ++++++++++++++----- .../tests/Unit/ComponentAttributesTest.php | 19 ++++++- 3 files changed, 60 insertions(+), 12 deletions(-) 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..90b247976db 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,20 +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; - } - return match ($value) { true => "{$carry} {$key}", false => $carry, @@ -59,7 +54,7 @@ function (string $carry, string $key) { */ public function all(): array { - return $this->attributes; + return $this->ensureNormalized()->attributes; } /** @@ -79,6 +74,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 +154,34 @@ 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 (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; + } + + if (\is_array($value) && array_is_list($value)) { + $value = implode(' ', array_filter($value, static fn ($v) => \is_string($v) && '' !== $v)); + } + + if (!\is_scalar($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..e8f2137c211 100644 --- a/src/TwigComponent/tests/Unit/ComponentAttributesTest.php +++ b/src/TwigComponent/tests/Unit/ComponentAttributesTest.php @@ -188,7 +188,24 @@ public function testNullBehaviour(): void { $attributes = new ComponentAttributes(['disabled' => null]); - $this->assertSame(['disabled' => null], $attributes->all()); + $this->assertSame(['disabled' => true], $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'], + ]); + + $this->assertSame(' style="foo bar"', (string) $attributes); + $this->assertSame(['style' => 'foo bar'], $attributes->all()); + } } From 4ce090b9b24ec34383a85a08c925c14ae61e1d79 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 22 Jan 2024 20:20:14 -0500 Subject: [PATCH 2/2] fix(twig): bc break --- src/TwigComponent/src/ComponentAttributes.php | 12 ++++++------ .../tests/Unit/ComponentAttributesTest.php | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/TwigComponent/src/ComponentAttributes.php b/src/TwigComponent/src/ComponentAttributes.php index 90b247976db..e3ec011c4ae 100644 --- a/src/TwigComponent/src/ComponentAttributes.php +++ b/src/TwigComponent/src/ComponentAttributes.php @@ -39,6 +39,11 @@ public function __toString(): string function (string $carry, string $key) { $value = $this->attributes[$key]; + 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; + } + return match ($value) { true => "{$carry} {$key}", false => $carry, @@ -170,16 +175,11 @@ private function ensureNormalized(): self private static function normalize(array &$attributes): void { foreach ($attributes as $key => &$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; - } - if (\is_array($value) && array_is_list($value)) { $value = implode(' ', array_filter($value, static fn ($v) => \is_string($v) && '' !== $v)); } - if (!\is_scalar($value)) { + 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 e8f2137c211..197d81e708e 100644 --- a/src/TwigComponent/tests/Unit/ComponentAttributesTest.php +++ b/src/TwigComponent/tests/Unit/ComponentAttributesTest.php @@ -188,7 +188,7 @@ public function testNullBehaviour(): void { $attributes = new ComponentAttributes(['disabled' => null]); - $this->assertSame(['disabled' => true], $attributes->all()); + $this->assertSame(['disabled' => null], $attributes->all()); $this->assertSame(' disabled', (string) $attributes); } @@ -203,9 +203,18 @@ 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"', (string) $attributes); - $this->assertSame(['style' => 'foo bar'], $attributes->all()); + $this->assertSame(' style="foo bar" class="qux baz"', (string) $attributes); + $this->assertSame(['style' => 'foo bar', 'class' => 'qux baz'], $attributes->all()); } }