Description
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