diff --git a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php index 5fedbf522a0..c0a832f964d 100644 --- a/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php +++ b/src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php @@ -191,6 +191,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) { ->setArguments([ new Reference('ux.twig_component.component_factory'), new Reference('property_info'), + new Reference('type_info.resolver', ContainerInterface::NULL_ON_INVALID_REFERENCE), ]) ->addTag('kernel.reset', ['method' => 'reset']) ; diff --git a/src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php b/src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php index fddff53a9ed..63109093377 100644 --- a/src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php +++ b/src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php @@ -13,9 +13,9 @@ use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type as LegacyType; -use Symfony\Component\TypeInfo\Type\IntersectionType; -use Symfony\Component\TypeInfo\Type\NullableType; -use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; use Symfony\Contracts\Service\ResetInterface; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\TwigComponent\ComponentFactory; @@ -33,7 +33,11 @@ class LiveComponentMetadataFactory implements ResetInterface public function __construct( private ComponentFactory $componentFactory, private PropertyTypeExtractorInterface $propertyTypeExtractor, + private ?TypeResolver $typeResolver = null, ) { + if (method_exists($this->propertyTypeExtractor, 'getType') && !$this->typeResolver) { + throw new \LogicException('Symfony TypeInfo is required to use LiveProps. Try running "composer require symfony/type-info".'); + } } public function getMetadata(string $name): LiveComponentMetadata @@ -77,13 +81,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')) { - $type = $property->getType(); - if ($type instanceof \ReflectionUnionType || $type 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())); - } - $infoTypes = $this->propertyTypeExtractor->getTypes($className, $propertyName) ?? []; $collectionValueType = null; @@ -96,14 +100,16 @@ public function createLivePropMetadata(string $className, string $propertyName, } } - if (null === $type && null === $collectionValueType && isset($infoTypes[0])) { + if (null === $reflectionType && null === $collectionValueType && isset($infoTypes[0])) { + // If it's an "advanced" type (like a Collection), let's use the PropertyTypeExtractor to get the Type $infoType = LegacyType::BUILTIN_TYPE_OBJECT === $infoTypes[0]->getBuiltinType() ? $infoTypes[0]->getClassName() : $infoTypes[0]->getBuiltinType(); $isTypeBuiltIn = null === $infoTypes[0]->getClassName(); $isTypeNullable = $infoTypes[0]->isNullable(); } else { - $infoType = $type?->getName(); - $isTypeBuiltIn = $type?->isBuiltin() ?? false; - $isTypeNullable = $type?->allowsNull() ?? true; + // Otherwise, we can use the ReflectionType to get the Type + $infoType = $reflectionType?->getName(); + $isTypeBuiltIn = $reflectionType?->isBuiltin() ?? false; + $isTypeNullable = $reflectionType?->allowsNull() ?? true; } return new LegacyLivePropMetadata( @@ -115,10 +121,17 @@ public function createLivePropMetadata(string $className, string $propertyName, $collectionValueType ); } else { - $type = $this->propertyTypeExtractor->getType($className, $property->getName()); - - if ($type instanceof UnionType && !$type instanceof NullableType || $type instanceof IntersectionType) { - 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".', $propertyName, $className)); + $infoType = $this->propertyTypeExtractor->getType($className, $property->getName()); + + if ($infoType instanceof CollectionType) { + // If it's an "advanced" type (like CollectionType), let's use the PropertyTypeExtractor to get the Type + $type = $infoType; + } elseif (null !== $reflectionType) { + // Otherwise, we can use the TypeResolver to convert the ReflectionType to a Type + $type = $this->typeResolver->resolve($reflectionType); + } else { + // If no type is available, we default to mixed + $type = Type::mixed(); } return new LivePropMetadata($property->getName(), $liveProp, $type); diff --git a/src/LiveComponent/tests/Fixtures/Component/FormWithUserInterfaceComponent.php b/src/LiveComponent/tests/Fixtures/Component/FormWithUserInterfaceComponent.php new file mode 100644 index 00000000000..0e7d53b23dc --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Component/FormWithUserInterfaceComponent.php @@ -0,0 +1,36 @@ + + * + * 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\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\FormInterface; +use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; +use Symfony\UX\LiveComponent\Attribute\LiveProp; +use Symfony\UX\LiveComponent\ComponentWithFormTrait; +use Symfony\UX\LiveComponent\DefaultActionTrait; +use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\User; +use Symfony\UX\LiveComponent\Tests\Fixtures\Form\UserFormType; + +#[AsLiveComponent('form_with_user_interface', template: 'components/form_with_user_interface.html.twig')] +class FormWithUserInterfaceComponent extends AbstractController +{ + use ComponentWithFormTrait; + use DefaultActionTrait; + + #[LiveProp] + public User $user; + + protected function instantiateForm(): FormInterface + { + return $this->createForm(UserFormType::class, $this->user); + } +} diff --git a/src/LiveComponent/tests/Fixtures/Entity/User.php b/src/LiveComponent/tests/Fixtures/Entity/User.php new file mode 100644 index 00000000000..499559a35da --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Entity/User.php @@ -0,0 +1,57 @@ + + * + * 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\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Security\Core\User\UserInterface; + +#[ORM\Entity] +class User implements UserInterface +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + public $id; + + #[ORM\Column(type: 'string', length: 180, unique: true)] + public $username; + + public function getRoles(): array + { + return ['ROLE_USER']; + } + + public function eraseCredentials(): void + { + // no-op + } + + public function getUsername(): string + { + return $this->getUserIdentifier(); + } + + public function getUserIdentifier(): string + { + return $this->username; + } + + public function getPassword(): ?string + { + return null; + } + + public function getSalt(): ?string + { + return null; + } +} diff --git a/src/LiveComponent/tests/Fixtures/Form/UserFormType.php b/src/LiveComponent/tests/Fixtures/Form/UserFormType.php new file mode 100644 index 00000000000..99bd456d38c --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Form/UserFormType.php @@ -0,0 +1,35 @@ + + * + * 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\Form; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\User; + +class UserFormType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('username', TextType::class) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/LiveComponent/tests/Fixtures/templates/components/form_with_user_interface.html.twig b/src/LiveComponent/tests/Fixtures/templates/components/form_with_user_interface.html.twig new file mode 100644 index 00000000000..32dce1b7b1e --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/templates/components/form_with_user_interface.html.twig @@ -0,0 +1,3 @@ +