Skip to content

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

Merged
Kocal merged 1 commit into
symfony:2.xfrom
Kocal:2888-live-hydrate-interface
Jul 15, 2025
Merged

[LiveComponent] Fix BC break when using PropertyTypeExtractorInterface::getType() on a #[LiveProp] property x when getter getX exists#2922
Kocal merged 1 commit into
symfony:2.xfrom
Kocal:2888-live-hydrate-interface

Conversation

@Kocal

@Kocal Kocal commented Jul 13, 2025

Copy link
Copy Markdown
Member
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 <fabien@symfony.com>
 *
 * 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 2 times, most recently from ee49115 to d071359 Compare July 13, 2025 13:32
@Kocal

Kocal commented Jul 13, 2025

Copy link
Copy Markdown
Member Author

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

Kocal commented Jul 14, 2025

Copy link
Copy Markdown
Member Author

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".');
}

@Kocal Kocal Jul 14, 2025

Copy link
Copy Markdown
Member Author

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 135
$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();
}

@Kocal Kocal Jul 14, 2025

Copy link
Copy Markdown
Member Author

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, mtarld and smnandre July 14, 2025 08:26
@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
Comment thread src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php Outdated
@Kocal
Kocal force-pushed the 2888-live-hydrate-interface branch 2 times, most recently from ccadf9a to 33e1c4c Compare July 15, 2025 20:14
@Kocal

Kocal commented Jul 15, 2025

Copy link
Copy Markdown
Member Author

I've added some comments to improve comprehensibility.

Since the PR fixed the issue from issue's author (#2888 (comment)), and no tests are failing (either when using PropertyTypeExtractorInterface::getTypes() or PropertyTypeExtractorInterface::getType()), I'm merging.

Thanks both of you :)

…ce::getType()` on a `#[LiveProp]` property `x` when getter `getX` exists
@Kocal
Kocal force-pushed the 2888-live-hydrate-interface branch from 33e1c4c to de302ef Compare July 15, 2025 20:37
@Kocal
Kocal merged commit 42f5180 into symfony:2.x Jul 15, 2025
@Kocal
Kocal deleted the 2888-live-hydrate-interface branch July 24, 2025 21:20
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