symfony/property-info
Symfony PropertyInfo extracts metadata about PHP class properties from sources like reflection, phpDoc, and Doctrine annotations. It reveals types, readability, and writability, making it useful for serializers, validators, and other tools that need to inspect objects dynamically.
Install via Composer:
composer require symfony/property-info
Extract property types from a class (e.g., for validation or serialization):
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
$extractors = [
new ReflectionExtractor(),
new PhpDocExtractor(),
];
$propertyInfoExtractor = new PropertyInfoExtractor($extractors);
// Get type for a property
$type = $propertyInfoExtractor->getTypes(User::class, 'email');
PropertyInfoExtractor (main class for combining multiple extractors).ReflectionExtractor, PhpDocExtractor, PhpStanExtractor, DoctrineExtractor).Use multiple extractors to handle different metadata sources (e.g., PHPDoc, reflection, PHPStan):
$extractors = [
new ReflectionExtractor(), // PHP 8 attributes + reflection
new PhpDocExtractor(), // DocBlock annotations
new PhpStanExtractor(), // PHPStan type hints (if installed)
new DoctrineExtractor(), // Doctrine ORM metadata
];
$propertyInfo = new PropertyInfoExtractor($extractors);
Resolve types at runtime (e.g., for form validation or API responses):
// Check if a property is writable
$isWritable = $propertyInfo->isWritable(User::class, 'email');
// Get read/write info (e.g., for serialization)
$readInfo = $propertyInfo->getReadInfo(User::class, 'email');
$writeInfo = $propertyInfo->getWriteInfo(User::class, 'email');
Bind a shared instance in a service provider so the extractor stack is built once per request:
// AppServiceProvider::register()
$this->app->singleton(PropertyInfoExtractor::class, function () {
return new PropertyInfoExtractor([
new ReflectionExtractor(),
new PhpDocExtractor(),
]);
});
Use PropertyInfo to dynamically infer validation rules:
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
class StoreUserRequest extends FormRequest
{
public function rules(PropertyInfoExtractor $propertyInfo)
{
$types = $propertyInfo->getTypes(User::class, 'email') ?? [];
return [
'email' => ['required', 'email', ...array_map(
fn ($type) => (string) $type,
$types
)],
];
}
}
Prefer mapping Symfony Type objects to a small whitelist of validation rules rather than passing type strings straight into the rules array.
Generate JSON schema/OpenAPI definitions dynamically:
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
class UserSchema
{
public function __construct(private PropertyInfoExtractor $propertyInfo) {}
public function toArray(): array
{
$attributes = collect($this->propertyInfo->getProperties(User::class))
->map(function (string $property) {
$types = $this->propertyInfo->getTypes(User::class, $property) ?? [];
return [
'name' => $property,
'type' => $types ? (string) $types[0] : 'string',
'nullable' => $types && $types[0]->isNullable(),
];
});
return ['attributes' => $attributes->values()->all()];
}
}
Generate form fields based on property types:
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
class UserCrudController extends Controller
{
public function edit(Request $request, PropertyInfoExtractor $propertyInfo)
{
$fields = collect($propertyInfo->getProperties(User::class) ?? [])
->mapWithKeys(function (string $property) use ($propertyInfo) {
$types = $propertyInfo->getTypes(User::class, $property) ?? [];
$builtin = $types ? $types[0]->getBuiltinType() : 'string';
return [$property => match ($builtin) {
'string' => 'text',
'int' => 'number',
'bool' => 'checkbox',
'float' => 'number',
default => 'text',
}];
});
return view('user.edit', compact('fields'));
}
}
Extend for domain-specific metadata (e.g., custom attributes). Implement the abstract PropertyTypeExtractorInterface (and PropertyAccessExtractorInterface / PropertyListExtractorInterface if you need those capabilities):
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
class CustomAttributeExtractor implements PropertyTypeExtractorInterface
{
public function getTypes(string $class, string $property, array $context = []): ?array
{
$reflectionProperty = new \ReflectionProperty($class, $property);
$attribute = $reflectionProperty->getAttributes(CustomType::class)[0] ?? null;
return $attribute
? [new Type($attribute->newInstance()->phpType)]
: null;
}
}
Register the extractor:
$extractors[] = new CustomAttributeExtractor();
Extractor Order Matters
ReflectionExtractor before PhpDocExtractor means a typed property wins over an @var docblock.PHPStanExtractor Dependency
phpstan/phpstan (or at least phpstan/phpdoc-parser) to be installed.'draft'|'published') were resolved incorrectly in PhpStanExtractor when running against phpstan/phpdoc-parser 1.x. If you deliberately pin the 1.x parser (common when other tooling has not moved to 2.x yet), require symfony/property-info:>=8.1.8.try {
$types = $propertyInfo->getTypes(User::class, 'status');
} catch (\Symfony\Component\PropertyInfo\Exception\UnsupportedTypeException $e) {
// Fallback to other extractors / a default
}
Promoted Properties in PHP 8+
PhpDocExtractor may misread promoted constructor properties. Put ReflectionExtractor first:
$extractors = [new ReflectionExtractor(), new PhpDocExtractor()];
DocBlock Parsing Quirks
/**
* @var string
*/
public $email;
int|'none') are only reliable with a modern phpdoc-parser; verify on your locked parser version.Performance
class::property:
$types = Cache::rememberForever("pi:".User::class.":email", fn () =>
$propertyInfo->getTypes(User::class, 'email')
);
dump((new ReflectionExtractor())->getTypes(User::class, 'email'));
dump((new PhpStanExtractor())->getTypes(User::class, 'email'));
phpstan analyse (with phpstan/phpdoc-parser aligned) to catch type mismatches early.PhpStanExtractor before blaming the aggregator — literal/complex type failures are almost always the underlying phpdoc-parser version.Custom Extractors for Special Cases Wrap or replace resolution for known classes by prepending your own extractor:
class UserEmailExtractor implements PropertyTypeExtractorInterface
{
public function getTypes(string $class, string $property, array $context = []): ?array
{
return $class === User::class && $property === 'email'
? [new Type('string', true)]
: null;
}
}
new PropertyInfoExtractor([new UserEmailExtractor(), ...$extractors]);
Decorating the Aggregator
Create a thin PropertyInfo wrapper in your app to add logging, caching, and fallbacks without touching the Symfony classes:
class CachedPropertyInfo
{
public function __construct(private PropertyInfoExtractor $inner) {}
public function types(string $class, string $property): array
{
return once(fn () => $this->inner->getTypes($class, $property) ?? []);
}
}
Laravel Service Provider
public function register()
{
$this->app->singleton(PropertyInfoExtractor::class, fn () => new PropertyInfoExtractor([
new ReflectionExtractor(),
new PhpDocExtractor(),
// new PhpStanExtractor(), // only if phpstan/phpdoc-parser is installed
]));
}
symfony/property-info:^7.4 for PHP 8.1–8.3.composer require doctrine/orm).phpstan/phpdoc-parser major version: 1.x and 2.x have different behavior; the quoted-string-literal fix in v8.1.8 only applies to the 1.x path. Pin and test consciously, and keep symfony/property-info current when you pin the older parser.How can I help you explore Laravel packages today?