Skip to content

[LiveComponent] Fix BC break when using PropertyTypeExtractorInterface::getType() on a #[LiveProp] property x when getter getX exists #2922

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: 2.x
Choose a base branch
from

Conversation

Kocal
Copy link
Member

@Kocal Kocal commented Jul 13, 2025

Q A
Bug fix? yes
New feature? no
Docs? no
Issues Fix #2888
License MIT

Prevent:

1) Symfony\UX\LiveComponent\Tests\Functional\Form\ComponentWithFormTest::testFormWithLivePropContainingAnEntityImplementingAnInterface
LogicException: Cannot dehydrate value typed as interface "Symfony\UX\LiveComponent\Tests\Fixtures\Entity\User" on component "Symfony\UX\LiveComponent\Tests\Fixtures\Component\FormWithUserInterfaceComponent". Change this to a concrete type that can be dehydrated. Or set the hydrateWith/dehydrateWith options in LiveProp or set "useSerializerForHydration: true" on the LiveProp to use the serializer.

Given the LiveComponent:

<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <[email protected]>
 *
 * 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);
    }
}

Dumping $propMetadata in \Symfony\UX\LiveComponent\LiveComponentHydrator::dehydrateValue gives:

Symfony\UX\LiveComponent\Metadata\LivePropMetadata {#240
  -name: "user"
  -liveProp: Symfony\UX\LiveComponent\Attribute\LiveProp {#230
    -writable: false
    -hydrateWith: null
    -dehydrateWith: null
    -useSerializerForHydration: false
    -serializationContext: []
    -fieldName: null
    -format: null
    -updateFromParent: false
    -onUpdated: null
    -url: false
    -modifier: null
  }
  -type: Symfony\Component\TypeInfo\Type\NullableType {#4197
    -types: array:2 [
      0 => Symfony\Component\TypeInfo\Type\ObjectType {#5811
        -className: "Symfony\Component\Security\Core\User\UserInterface"
      }
      1 => Symfony\Component\TypeInfo\Type\BuiltinType {#4208
        -typeIdentifier: Symfony\Component\TypeInfo\TypeIdentifier {#5983
          +name: "NULL"
          +value: "null"
        }
      }
    ]
    -type: Symfony\Component\TypeInfo\Type\ObjectType {#5811}
  }
}

The class name is the UserInterface and not User 🤔

To tests it

  • With PropertyInfo only: sfcp req symfony/property-info:^6.4 -W && sfp vendor/bin/simple-phpunit tests/Functional/Form/ComponentWithFormTest.php --filter "testFormWithLivePropContainingAnEntityImplementingAnInterface"
  • With PropertyInfo & Type: sfcp req 'symfony/property-info:7.2.*' 'symfony/type-info:7.2.*' && sfp vendor/bin/simple-phpunit tests/Functional/Form/ComponentWithFormTest.php --filter "testFormWithLivePropContainingAnEntityImplementingAnInterface"

@carsonbot carsonbot added Bug Bug Fix LiveComponent Status: Needs Review Needs to be reviewed labels Jul 13, 2025
@Kocal Kocal marked this pull request as draft July 13, 2025 13:05
@Kocal Kocal force-pushed the 2888-live-hydrate-interface branch from 0f4b08d to ee49115 Compare July 13, 2025 13:10
@Kocal Kocal force-pushed the 2888-live-hydrate-interface branch from ee49115 to d071359 Compare July 13, 2025 13:32
@Kocal
Copy link
Member Author

Kocal commented Jul 13, 2025

After some investigations, it happens because of PhpStanExtractor from Symfony PropertyInfo.

When trying to resolve the type of FormWithUserInterfaceComponent::$user, it will instead resolve the type of AbstractController::getUser() which returns ?UserInterface.

@Kocal

This comment was marked as outdated.

@Kocal
Copy link
Member Author

Kocal commented Jul 14, 2025

After more investigations, I don't think it's related to PropertyInfo itself (PropertyInfo 6.4 also returns type for AbstractController::getUser()) but because of the extra logic that use the native ReflectionType if possible:
image

@Kocal Kocal changed the title [LiveComponent] Fix BC break when dealing with entities (implementing an interface) on LiveProp [LiveComponent] Fix BC break when using PropertyTypeExtractorInterface::getType() on a #[LiveProp] property x when getter getX exists Jul 14, 2025
@Kocal Kocal marked this pull request as ready for review July 14, 2025 08:19
Comment on lines +38 to +40
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".');
}
Copy link
Member Author

@Kocal Kocal Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not happens since PropertyTypeInfoExtractor::getType() exists since 7.1, which also ship TypeInfo, but just in case..

Comment on lines 122 to 132
$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()));
}

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());

$collectionValueType = $infoType instanceof CollectionType ? $infoType->getCollectionValueType() : null;

if (null !== $collectionValueType && null !== $infoType) {
$type = $infoType;
} elseif (null !== $reflectionType) {
$type = $this->typeResolver->resolve($reflectionType);
} else {
$type = Type::mixed();
}
Copy link
Member Author

@Kocal Kocal Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really not sure about this code, it feels fragile.

I tried to re-use the legacy logic and use ReflectionType or the result of PropertyTypeExtractorInterface::getType(), and now all tests pass, even the one added in this PR (which was previously breaking)

@Kocal Kocal requested review from kbond, smnandre and mtarld July 14, 2025 08:26
…ce::getType()` on a `#[LiveProp]` property `x` when getter `getX` exists
@Kocal Kocal force-pushed the 2888-live-hydrate-interface branch from 9cd665f to 30dca26 Compare July 14, 2025 19:43
@carsonbot carsonbot added Status: Reviewed Has been reviewed by a maintainer and removed Status: Needs Review Needs to be reviewed labels Jul 15, 2025

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));
if (null !== $collectionValueType && null !== $infoType) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (null !== $collectionValueType && null !== $infoType) {
if ($infoType instance of CollectionType) {

Seems to be enough no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think yes, it would ease the readability too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Bug Fix LiveComponent Status: Reviewed Has been reviewed by a maintainer
Projects
None yet
Development

Successfully merging this pull request may close these issues.

BC break in LiveComponentHydrator
4 participants