Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Serialization] with_constructor_extractor: true - @var docs on promoted properties ignored #60795

Open
@florianhofsaessC24

Description

@florianhofsaessC24

Symfony version(s) affected

7.3.0

Description

Symfony PropertyInfo ConstructorExtractor Bug Report

The Problem

Symfony 7.3+ enables with_constructor_extractor: true by default, but ConstructorExtractor loses generic type information for promoted properties without @param tags.

Broken Pattern

final readonly class Model
{
    public function __construct(
        /** @var string[]|null */
        public array|null $items,
    ) {}
}

Result: ConstructorExtractor->getType() returns array|null and losses information about iterable types ❌

Working Pattern (Workaround)

final readonly class Model
{
    /**
     * @param string[]|null $items
     */
    public function __construct(
        /** @var string[]|null */
        public array|null $items,
    ) {}
}

Result: ConstructorExtractor->getType() returns array<int,string>|null

Impact

  • Serialization fails silently (!!): returns raw arrays instead of proper objects
  • Breaking change for existing codebases upgrading to Symfony 7.3+

Root Cause

PhpStanExtractor::getTypeFromConstructor() only checks constructor @param tags, while getType() has fallback logic to check property @var tags for promoted properties.


Priority: High (affects serialization in production applications)
Backward Compatibility: The fix would be backward compatible and only improve existing functionality.

How to reproduce

<?php

require_once __DIR__ . '/vendor/autoload.php';

use Symfony\Component\PropertyInfo\Extractor\ConstructorExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;

// Test class WITHOUT @param tag on constructor (current common pattern)
final readonly class TestModelWithoutParam
{
    public function __construct(
        /**
         * @var string[]|null $items
         */
        public array|null $items,
    ) {}
}

// Test class WITH @param tag on constructor (workaround)
final readonly class TestModelWithParam
{
    /**
     * @param string[]|null $items
     */
    public function __construct(
        /**
         * @var string[]|null $items
         */
        public array|null $items,
    ) {}
}

echo "=== Symfony PropertyInfo ConstructorExtractor Bug Report ===\n\n";

$phpStanExtractor = new PhpStanExtractor();
$constructorExtractor = new ConstructorExtractor([$phpStanExtractor]);

echo "1. Testing WITHOUT @param tag on constructor (common pattern):\n";
echo "   Class: TestModelWithoutParam\n";
echo "   Property: \$items with type array|null and @var string[]|null\n\n";

$typeWithoutParam = $phpStanExtractor->getType(TestModelWithoutParam::class, 'items');
$constructorTypeWithoutParam = $phpStanExtractor->getTypeFromConstructor(TestModelWithoutParam::class, 'items');
$constructorExtractorTypeWithoutParam = $constructorExtractor->getType(TestModelWithoutParam::class, 'items');

echo "   PhpStanExtractor->getType(): " . ($typeWithoutParam ? (string) $typeWithoutParam : 'null') . "\n";
echo "   PhpStanExtractor->getTypeFromConstructor(): " . ($constructorTypeWithoutParam ? (string) $constructorTypeWithoutParam : 'null') . "\n";
echo "   ConstructorExtractor->getType(): " . ($constructorExtractorTypeWithoutParam ? (string) $constructorExtractorTypeWithoutParam : 'null') . "\n\n";

echo "2. Testing WITH @param tag on constructor (workaround):\n";
echo "   Class: TestModelWithParam\n";
echo "   Property: \$items with type array|null, @var string[]|null, and @param string[]|null\n\n";

$typeWithParam = $phpStanExtractor->getType(TestModelWithParam::class, 'items');
$constructorTypeWithParam = $phpStanExtractor->getTypeFromConstructor(TestModelWithParam::class, 'items');
$constructorExtractorTypeWithParam = $constructorExtractor->getType(TestModelWithParam::class, 'items');

echo "   PhpStanExtractor->getType(): " . ($typeWithParam ? (string) $typeWithParam : 'null') . "\n";
echo "   PhpStanExtractor->getTypeFromConstructor(): " . ($constructorTypeWithParam ? (string) $constructorTypeWithParam : 'null') . "\n";
echo "   ConstructorExtractor->getType(): " . ($constructorExtractorTypeWithParam ? (string) $constructorExtractorTypeWithParam : 'null') . "\n\n";

This clearly demonstrates the inconsistency between getType() and getTypeFromConstructor() methods.

Possible Solution

Reconsider @var annotations from PHPStan.

class Testclass {

/** 
 * @var string[] $propertyName
 * private array $propertyName
 */
}
class Testclass {

public function __construct(
/** 
 * @var string[] $propertyName
 * /
 private array $propertyName
) {
}

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions