Skip to content

[LiveComponent] Fix PropertyTypeExtractorInterface::getTypes() deprecation, use TypeInfo ^7.2 Type #2607

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

Merged
merged 1 commit into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/.utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,19 @@ _run_task() {
exit $ok
}
export -f _run_task

install_property_info_for_version() {
local php_version="$1"
local min_stability="$2"

if [ "$php_version" = "8.2" ]; then
composer require symfony/property-info:7.1.* symfony/type-info:7.2.*
elif [ "$php_version" = "8.3" ]; then
composer require symfony/property-info:7.2.* symfony/type-info:7.2.*
elif [ "$php_version" = "8.4" ] && [ "$min_stability" = "stable" ]; then
composer require symfony/property-info:7.3.* symfony/type-info:7.3.*
elif [ "$php_version" = "8.4" ] && [ "$min_stability" = "dev" ]; then
composer require symfony/property-info:>=7.3 symfony/type-info:>=7.3
fi
}
export -f install_property_info_for_version
7 changes: 6 additions & 1 deletion .github/workflows/unit-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ jobs:
run: |
source .github/workflows/.utils.sh

echo "$PACKAGES" | xargs -n1 | parallel -j +3 "_run_task {} '(cd src/{} && $COMPOSER_MIN_STAB && $COMPOSER_UP && $PHPUNIT)'"
echo "$PACKAGES" | xargs -n1 | parallel -j +3 "_run_task {} \
'(cd src/{} \
&& $COMPOSER_MIN_STAB \
&& $COMPOSER_UP \
&& if [ {} = LiveComponent ]; then install_property_info_for_version \"${{ matrix.php-version }}\" \"${{ matrix.minimum-stability }}\"; fi \
&& $PHPUNIT)'"

js:
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions src/LiveComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## 2.26.0

- `LiveProp`: Pass the property name as second parameter of the `modifier` callable
- Add compatibility layer to fix deprecation with `Symfony\Component\PropertyInfo\PropertyInfoExtractor::getTypes()`.
If you use PHP 8.2 or higher, we recommend you to update dependency `symfony/property-info` to at least 7.1.0

## 2.25.0

Expand Down
4 changes: 3 additions & 1 deletion src/LiveComponent/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@
"zenstruck/foundry": "^2.0"
},
"conflict": {
"symfony/config": "<5.4.0"
"symfony/config": "<5.4.0",
"symfony/type-info": "<7.1",
"symfony/property-info": "~7.0.0"
},
"config": {
"sort-packages": true
Expand Down
13 changes: 13 additions & 0 deletions src/LiveComponent/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,17 @@ library. Make sure it is installed in you application:

$ composer require phpdocumentor/reflection-docblock

.. versionadded:: 2.26

Support for `Symfony TypeInfo`_ component was added in LiveComponents 2.26.

To get rid of deprecations about ``PropertyInfoExtractor::getTypes()`` from the `Symfony PropertyInfo`_ component,
ensure to upgrade ``symfony/property-info`` to at least 7.1, which requires **PHP 8.2**::

.. code-block:: terminal

$ composer require symfony/property-info:^7.1

Writable Object Properties or Array Keys
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -3900,3 +3911,5 @@ promise. However, any internal implementation in the JavaScript files
.. _`setting the locale in the request`: https://symfony.com/doc/current/translation.html#translation-locale
.. _`Stimulus action parameter`: https://stimulus.hotwired.dev/reference/actions#action-parameters
.. _`@symfony/ux-live-component npm package`: https://www.npmjs.com/package/@symfony/ux-live-component
.. _`Symfony TypeInfo`: https://symfony.com/doc/current/components/type_info.html
.. _`Symfony PropertyInfo`: https://symfony.com/doc/current/components/property_info.html
224 changes: 175 additions & 49 deletions src/LiveComponent/src/LiveComponentHydrator.php

Large diffs are not rendered by default.

148 changes: 148 additions & 0 deletions src/LiveComponent/src/Metadata/LegacyLivePropMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?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\Metadata;

use Symfony\Component\PropertyInfo\Type;
use Symfony\UX\LiveComponent\Attribute\LiveProp;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @internal
*/
final class LegacyLivePropMetadata
{
public function __construct(
private string $name,
private LiveProp $liveProp,
private ?string $typeName,
private bool $isBuiltIn,
private bool $allowsNull,
private ?Type $collectionValueType,
) {
}

public function getName(): string
{
return $this->name;
}

public function getType(): ?string
{
return $this->typeName;
}

public function isBuiltIn(): bool
{
return $this->isBuiltIn;
}

public function allowsNull(): bool
{
return $this->allowsNull;
}

public function urlMapping(): ?UrlMapping
{
return $this->liveProp->url() ?: null;
}

public function calculateFieldName(object $component, string $fallback): string
{
return $this->liveProp->calculateFieldName($component, $fallback);
}

/**
* @return array<string>
*/
public function writablePaths(): array
{
return $this->liveProp->writablePaths();
}

public function hydrateMethod(): ?string
{
return $this->liveProp->hydrateMethod();
}

public function dehydrateMethod(): ?string
{
return $this->liveProp->dehydrateMethod();
}

public function isIdentityWritable(): bool
{
return $this->liveProp->isIdentityWritable();
}

public function acceptUpdatesFromParent(): bool
{
return $this->liveProp->acceptUpdatesFromParent();
}

public function useSerializerForHydration(): bool
{
return $this->liveProp->useSerializerForHydration();
}

public function serializationContext(): array
{
return $this->liveProp->serializationContext();
}

public function collectionValueType(): ?Type
{
return $this->collectionValueType;
}

public function getFormat(): ?string
{
return $this->liveProp->format();
}

public function onUpdated(): string|array|null
{
return $this->liveProp->onUpdated();
}

public function hasModifier(): bool
{
return null !== $this->liveProp->modifier();
}

/**
* Applies a modifier specified in LiveProp attribute.
*
* If a modifier is specified, a modified clone is returned.
* Otherwise, the metadata is returned as it is.
*/
public function withModifier(object $component): self
{
if (null === ($modifier = $this->liveProp->modifier())) {
return $this;
}

if (!method_exists($component, $modifier)) {
throw new \LogicException(\sprintf('Method "%s::%s()" given in LiveProp "modifier" does not exist.', $component::class, $modifier));
}

$modifiedLiveProp = $component->{$modifier}($this->liveProp, $this->getName());
if (!$modifiedLiveProp instanceof LiveProp) {
throw new \LogicException(\sprintf('Method "%s::%s()" should return an instance of "%s" (given: "%s").', $component::class, $modifier, LiveProp::class, get_debug_type($modifiedLiveProp)));
}

$clone = clone $this;
$clone->liveProp = $modifiedLiveProp;

return $clone;
}
}
8 changes: 4 additions & 4 deletions src/LiveComponent/src/Metadata/LiveComponentMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ class LiveComponentMetadata
{
public function __construct(
private ComponentMetadata $componentMetadata,
/** @var LivePropMetadata[] */
/** @var list<LivePropMetadata|LegacyLivePropMetadata> */
private array $livePropsMetadata,
) {
uasort(
$this->livePropsMetadata,
static fn (LivePropMetadata $a, LivePropMetadata $b) => $a->hasModifier() <=> $b->hasModifier()
static fn (LivePropMetadata|LegacyLivePropMetadata $a, LivePropMetadata|LegacyLivePropMetadata $b) => $a->hasModifier() <=> $b->hasModifier()
);
}

Expand All @@ -37,7 +37,7 @@ public function getComponentMetadata(): ComponentMetadata
}

/**
* @return LivePropMetadata[]
* @return list<LivePropMetadata|LegacyLivePropMetadata>
*/
public function getAllLivePropsMetadata(object $component): iterable
{
Expand All @@ -55,7 +55,7 @@ public function getAllLivePropsMetadata(object $component): iterable
*/
public function getOnlyPropsThatAcceptUpdatesFromParent(array $inputProps): array
{
$writableProps = array_filter($this->livePropsMetadata, function (LivePropMetadata $livePropMetadata) {
$writableProps = array_filter($this->livePropsMetadata, function (LivePropMetadata|LegacyLivePropMetadata $livePropMetadata) {
return $livePropMetadata->acceptUpdatesFromParent();
});

Expand Down
76 changes: 45 additions & 31 deletions src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
namespace Symfony\UX\LiveComponent\Metadata;

use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
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\Contracts\Service\ResetInterface;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\TwigComponent\ComponentFactory;
Expand Down Expand Up @@ -48,7 +51,7 @@ public function getMetadata(string $name): LiveComponentMetadata
}

/**
* @return LivePropMetadata[]
* @return list<LivePropMetadata|LegacyLivePropMetadata>
*
* @internal
*/
Expand All @@ -72,43 +75,54 @@ public function createPropMetadatas(\ReflectionClass $class): array
return array_values($metadatas);
}

public function createLivePropMetadata(string $className, string $propertyName, \ReflectionProperty $property, LiveProp $liveProp): LivePropMetadata
public function createLivePropMetadata(string $className, string $propertyName, \ReflectionProperty $property, LiveProp $liveProp): LivePropMetadata|LegacyLivePropMetadata
{
$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()));
}
// BC layer when "symfony/type-info" is not available
if (!method_exists($this->propertyTypeExtractor, 'getType')) {
Copy link
Member

Choose a reason for hiding this comment

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

Method $this->propertyTypeExtractor->getTypes() triggers a deprecation explaining to use getType(), which is a bit useless for the end-user since getTypes() is internally called by us.

At this point, can we add a deprecation for PHP 8.2+ to tell the user to install symfony/property-info:^7.1 (which implements getType() and ships with symfony/type-info)?

Copy link
Member

Choose a reason for hiding this comment

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

Hum, I don't see how we can do it correctly, the deprecation on getTypes() has been added in symfony/property-info:^7.3.

I guess the current deprecation and a changelog entry could be enough

Copy link
Member

Choose a reason for hiding this comment

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

changelog entry added

$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) ?? [];
$infoTypes = $this->propertyTypeExtractor->getTypes($className, $propertyName) ?? [];

$collectionValueType = null;
foreach ($infoTypes as $infoType) {
if ($infoType->isCollection()) {
foreach ($infoType->getCollectionValueTypes() as $valueType) {
$collectionValueType = $valueType;
break;
$collectionValueType = null;
foreach ($infoTypes as $infoType) {
if ($infoType->isCollection()) {
foreach ($infoType->getCollectionValueTypes() as $valueType) {
$collectionValueType = $valueType;
break;
}
}
}
}

if (null === $type && null === $collectionValueType && isset($infoTypes[0])) {
$infoType = Type::BUILTIN_TYPE_OBJECT === $infoTypes[0]->getBuiltinType() ? $infoTypes[0]->getClassName() : $infoTypes[0]->getBuiltinType();
$isTypeBuiltIn = null === $infoTypes[0]->getClassName();
$isTypeNullable = $infoTypes[0]->isNullable();
if (null === $type && null === $collectionValueType && isset($infoTypes[0])) {
$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;
}

return new LegacyLivePropMetadata(
$property->getName(),
$liveProp,
$infoType,
$isTypeBuiltIn,
$isTypeNullable,
$collectionValueType
);
} else {
$infoType = $type?->getName();
$isTypeBuiltIn = $type?->isBuiltin() ?? false;
$isTypeNullable = $type?->allowsNull() ?? true;
}
$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));
}

return new LivePropMetadata(
$property->getName(),
$liveProp,
$infoType,
$isTypeBuiltIn,
$isTypeNullable,
$collectionValueType
);
return new LivePropMetadata($property->getName(), $liveProp, $type);
}
}

/**
Expand Down
Loading
Loading