diff --git a/src/LiveComponent/CHANGELOG.md b/src/LiveComponent/CHANGELOG.md index 50ba92558b3..0da7e033c53 100644 --- a/src/LiveComponent/CHANGELOG.md +++ b/src/LiveComponent/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## 2.32 + +- Add support for `LiveProp` union types in Live Components. + `LiveComponentMetadataFactory` can now create `liveProp` metadata for union types. + Note: This feature only works when using a custom `hydrate` and `dehydrate` implementation. + ## 2.31 - Add browser events assertions in `InteractsWithLiveComponents`: diff --git a/src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php b/src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php index 916963f924f..6d111dcae34 100644 --- a/src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php +++ b/src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php @@ -83,12 +83,13 @@ public function createPropMetadatas(\ReflectionClass $class): array public function createLivePropMetadata(string $className, string $propertyName, \ReflectionProperty $property, LiveProp $liveProp): LivePropMetadata|LegacyLivePropMetadata { $reflectionType = $property->getType(); - if ($reflectionType instanceof \ReflectionUnionType || $reflectionType instanceof \ReflectionIntersectionType) { - throw new \LogicException(\sprintf('Union or intersection types are not supported for LiveProps. You may want to change the type of property "%s" in "%s".', $property->getName(), $property->getDeclaringClass()->getName())); - } // BC layer when "symfony/type-info" is not available if (!method_exists($this->propertyTypeExtractor, 'getType')) { + if ($reflectionType instanceof \ReflectionUnionType || $reflectionType instanceof \ReflectionIntersectionType) { + return new LegacyLivePropMetadata($property->getName(), $liveProp, 'mixed', true, $reflectionType->allowsNull(), null); + } + $infoTypes = $this->propertyTypeExtractor->getTypes($className, $propertyName) ?? []; $collectionValueType = null; @@ -122,6 +123,10 @@ public function createLivePropMetadata(string $className, string $propertyName, $collectionValueType ); } else { + if ($reflectionType instanceof \ReflectionUnionType || $reflectionType instanceof \ReflectionIntersectionType) { + return new LivePropMetadata($property->getName(), $liveProp, Type::mixed()); + } + $infoType = $this->propertyTypeExtractor->getType($className, $property->getName()); if ($infoType instanceof CollectionType) { diff --git a/src/LiveComponent/tests/Fixtures/Component/WithUnionIntersectionType.php b/src/LiveComponent/tests/Fixtures/Component/WithUnionIntersectionType.php new file mode 100644 index 00000000000..e2377c51f38 --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Component/WithUnionIntersectionType.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component; + +use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; +use Symfony\UX\LiveComponent\Attribute\LiveProp; +use Symfony\UX\LiveComponent\DefaultActionTrait; +use Symfony\UX\LiveComponent\Tests\Fixtures\Enum\IntEnum; +use Symfony\UX\LiveComponent\Tests\Fixtures\Enum\ZeroIntEnum; + +#[AsLiveComponent('with_union_intersection_type')] +final class WithUnionIntersectionType +{ + use DefaultActionTrait; + + #[LiveProp] + public int|float|null $unionProp = null; + + #[LiveProp] + public IntEnum&ZeroIntEnum $intersectionProp; +} diff --git a/src/LiveComponent/tests/Functional/Metadata/LiveComponentMetadataFactoryTest.php b/src/LiveComponent/tests/Functional/Metadata/LiveComponentMetadataFactoryTest.php index 3529804443d..8becdf4bb03 100644 --- a/src/LiveComponent/tests/Functional/Metadata/LiveComponentMetadataFactoryTest.php +++ b/src/LiveComponent/tests/Functional/Metadata/LiveComponentMetadataFactoryTest.php @@ -15,6 +15,7 @@ use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadataFactory; use Symfony\UX\LiveComponent\Metadata\UrlMapping; use Symfony\UX\LiveComponent\Tests\Fixtures\Component\ComponentWithUrlBoundProps; +use Symfony\UX\LiveComponent\Tests\Fixtures\Component\WithUnionIntersectionType; class LiveComponentMetadataFactoryTest extends KernelTestCase { @@ -42,4 +43,21 @@ public function testQueryStringMapping() $this->assertEquals(new UrlMapping(as: 'q'), $propsMetadataByName['boundPropWithAlias']->urlMapping()); $this->assertNotNull($propsMetadataByName['boundPropWithCustomAlias']); } + + public function testLivePropUnionType() + { + /** @var LiveComponentMetadataFactory $metadataFactory */ + $metadataFactory = self::getContainer()->get('ux.live_component.metadata_factory'); + + $class = new \ReflectionClass(WithUnionIntersectionType::class); + $propsMetadata = $metadataFactory->createPropMetadatas($class); + + $propsMetadataByName = []; + foreach ($propsMetadata as $propMetadata) { + $propsMetadataByName[$propMetadata->getName()] = $propMetadata; + } + + $this->assertEquals('mixed', (string) $propsMetadataByName['unionProp']->getType()); + $this->assertEquals('mixed', (string) $propsMetadataByName['intersectionProp']->getType()); + } }