Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Property Info Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

Install via Composer:

composer require symfony/property-info

First Use Case: Type Extraction

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');

Where to Look First

  • Official Documentation (API reference, extractors, and usage examples).
  • PropertyInfoExtractor (main class for combining multiple extractors).
  • Built-in extractors (ReflectionExtractor, PhpDocExtractor, PhpStanExtractor, DoctrineExtractor).

Implementation Patterns

1. Combining Extractors

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);

2. Dynamic Type Resolution

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');

3. Integration with Laravel

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(),
    ]);
});

Validation (e.g., Form Requests)

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.

API Serialization (e.g., Laravel Sanctum/Fractal)

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

Dynamic Forms (e.g., Laravel Nova/Backpack)

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

4. Custom Extractors

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();

Gotchas and Tips

Pitfalls

  1. Extractor Order Matters

    • Extractors are evaluated in order and the first non-null result wins. Place the most explicit/reliable sources (e.g., your custom attribute extractor) first.
    • Example: putting ReflectionExtractor before PhpDocExtractor means a typed property wins over an @var docblock.
  2. PHPStanExtractor Dependency

    • Requires phpstan/phpstan (or at least phpstan/phpdoc-parser) to be installed.
    • Version combinations matter. v8.1.8 fixes a bug where quoted string literal types (e.g. '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.
    • Guard calls that may fail on unsupported constructs:
      try {
          $types = $propertyInfo->getTypes(User::class, 'status');
      } catch (\Symfony\Component\PropertyInfo\Exception\UnsupportedTypeException $e) {
          // Fallback to other extractors / a default
      }
      
  3. Promoted Properties in PHP 8+

    • PhpDocExtractor may misread promoted constructor properties. Put ReflectionExtractor first:
      $extractors = [new ReflectionExtractor(), new PhpDocExtractor()];
      
  4. DocBlock Parsing Quirks

    • Inline/attribute-style type hints are not understood; use full DocBlock syntax:
      /**
       * @var string
       */
      public $email;
      
    • Union and literal-string types (int|'none') are only reliable with a modern phpdoc-parser; verify on your locked parser version.
  5. Performance

    • Extraction is reflection/docblock parsing on every call and caching is not built-in. Memoize per-request (or in Laravel's cache) keyed on class::property:
      $types = Cache::rememberForever("pi:".User::class.":email", fn () =>
          $propertyInfo->getTypes(User::class, 'email')
      );
      
    • Remember cache invalidation on deploy if you cache beyond a single request.

Debugging Tips

  • Inspect Extractor Outputs one at a time, not just the composed extractor:
    dump((new ReflectionExtractor())->getTypes(User::class, 'email'));
    dump((new PhpStanExtractor())->getTypes(User::class, 'email'));
    
  • Check for Conflicts:
    • If types are inconsistent, reorder extractors or insert a custom one ahead of the stack.
  • Validate with PHPStan: run phpstan analyse (with phpstan/phpdoc-parser aligned) to catch type mismatches early.
  • Reproduce parser bugs in isolation with a single PhpStanExtractor before blaming the aggregator — literal/complex type failures are almost always the underlying phpdoc-parser version.

Extension Points

  1. 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]);
    
  2. 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) ?? []);
        }
    }
    
  3. 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
        ]));
    }
    

Config Quirks

  • No Default Configuration: all extractors are opt-in; Symfony does not auto-wire a stack for you.
  • PHP version matrix: Symfony 8.x requires PHP 8.4+. Use symfony/property-info:^7.4 for PHP 8.1–8.3.
  • DoctrineExtractor: needs Doctrine ORM (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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource