From 698bdd7701cd5c73c345f8849b86d97983b58747 Mon Sep 17 00:00:00 2001 From: Tim Sterker Date: Tue, 24 Feb 2026 08:53:19 +0100 Subject: [PATCH] fix: only use PhpDocExtractor phpdocumentor/reflection-docblock is installed The feature was originally introduced in https://github.com/hirethunk/verbs/pull/125 to get array-of-class deserialization working properly. Ref: https://github.com/hirethunk/verbs/pull/210 (related PR) Ref: https://github.com/hirethunk/verbs/pull/125 (original introduction of PhpDocExtractor) --- composer.json | 3 +++ docs/serialization.md | 6 +++++ src/VerbsServiceProvider.php | 19 ++++++++++++---- tests/Feature/SerializationTest.php | 34 +++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 3b5fca96..0a90cf82 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,9 @@ "phpunit/phpunit": "^10.5|^11.5.3", "projektgopher/whisky": "^0.5.1|^0.7" }, + "suggest": { + "phpdocumentor/reflection-docblock": "Enables PhpDoc-based serializer type extraction (for example, `/** @var DTO[] */` collection element inference)." + }, "autoload": { "psr-4": { "Thunk\\Verbs\\": "src/", diff --git a/docs/serialization.md b/docs/serialization.md index 3b971e23..855cf7d3 100644 --- a/docs/serialization.md +++ b/docs/serialization.md @@ -8,3 +8,9 @@ application. If you need to store more complex data, you may need to add your ow which you can do in `config/verbs.php` file. You may also change the [default serializer context](https://symfony.com/doc/current/components/serializer.html#context) there as well. + +For PhpDoc-based type inference (for example `/** @var MyDto[] */` on array properties), +install `phpdocumentor/reflection-docblock`. Verbs will still work without it, but serializer +type extraction falls back to reflection-only behavior and PhpDoc collection element types are +not inferred during deserialization. See also [this PR](https://github.com/hirethunk/verbs/pull/125#issuecomment-2359638427), +that introduced the PhpDoc-based type inference feature. diff --git a/src/VerbsServiceProvider.php b/src/VerbsServiceProvider.php index d3f537eb..1d993d62 100644 --- a/src/VerbsServiceProvider.php +++ b/src/VerbsServiceProvider.php @@ -110,10 +110,8 @@ public function packageRegistered() return new PropertyNormalizer( propertyTypeExtractor: new PropertyInfoExtractor( - typeExtractors: [ - new PhpDocExtractor, - new ReflectionExtractor, - ]), + typeExtractors: $this->getPropertyTypeExtractors(), + ), classDiscriminatorResolver: new ClassDiscriminatorFromClassMetadata(new ClassMetadataFactory($loader)), ); }); @@ -181,4 +179,17 @@ protected function handleEvent($event = null) app(AutoCommitManager::class)->commitIfAutoCommitting(); } } + + protected function getPropertyTypeExtractors(): array + { + return [ + ...$this->hasPhpDocExtractorDependency() ? [new PhpDocExtractor] : [], + new ReflectionExtractor, + ]; + } + + protected function hasPhpDocExtractorDependency(): bool + { + return class_exists(PhpDocExtractor::class); + } } diff --git a/tests/Feature/SerializationTest.php b/tests/Feature/SerializationTest.php index c130ddf9..b064ba79 100644 --- a/tests/Feature/SerializationTest.php +++ b/tests/Feature/SerializationTest.php @@ -12,6 +12,7 @@ use Thunk\Verbs\State; use Thunk\Verbs\Support\Normalization\NormalizeToPropertiesAndClassName; use Thunk\Verbs\Support\Serializer; +use Thunk\Verbs\VerbsServiceProvider; it('supports instantiation via an associative array', function () { $snowflake = Snowflake::make(); @@ -167,6 +168,39 @@ public function __construct() ->and($deserialized_event->dtos[0])->toBeInstanceOf(DTO::class); }); +it('falls back to reflection type extraction when PhpDoc extraction is unavailable', function () { + $provider = new class(app()) extends VerbsServiceProvider + { + protected function hasPhpDocExtractorDependency(): bool + { + return false; + } + }; + + $provider->packageRegistered(); + app()->forgetInstance(Serializer::class); + app()->forgetInstance(PropertyNormalizer::class); + + $original_event = new EventWithPhpDocArray; + $serialized_data = app(Serializer::class)->serialize($original_event); + + $deserialized_event = app(Serializer::class)->deserialize(EventWithPhpDocArray::class, $serialized_data); + + expect($deserialized_event->dto) + ->toBeInstanceOf(DTO::class); + + // NOTE: Type inference via PhpDoc is not available, which breaks the collection type inference. + // See also https://github.com/hirethunk/verbs/pull/125#issuecomment-2359638427 + expect($deserialized_event->dtos) + ->toBeArray() + ->toBe([ + [ + 'fqcn' => DTO::class, + 'foo' => 1, + ], + ]); +}); + class EventWithConstructorPromotion extends Event { public function __construct(