Repository navigation
fix(reflection): resolve short docblock names in getIterableType - #2305
radoslav-grencik wants to merge 9 commits into
Conversation
Benchmark ResultsComparison of Open to see the benchmark results
Generated by phpbench against commit 59a7d5c |
|
I'm on the fence about this. Yes, it's more convenient, but it's also a lot more parsing to do. One thing that's crucial is that these results are memoized — I don't think this method already is? If memoization is in place, I feel ok-ish about merging it. @xHeaven @innocenzi how do you feel about it? |
Actually it's memoized. The expensive part - the But the common cases don't touch the parser at all: no docblock returns |
|
I would be interested in a good benchmark here, that's the most important part. Code-wise the complexity looks fine to me |
I'll try to come up with something. |
@innocenzi Benchmark added - It measures four scenarios, all through
Two ratios worth highlighting:
Fixtures intentionally have only 3 properties per class — variance is handled by iterations (20×), not property count, and rstdev stays ≤ ±2.5%. |
|
Can we try add a short-circuit trick where, before we call If that regex fails then it might be worth falling back to |
I’ll try this for the cold path and benchmark it. I have some local fixes for namespace/trait resolution and memoization of the resolved type names. I also corrected the benchmark to measure one property per call, with class/file preparation outside the timed section. I’ll push those first as a baseline, then compare the regex fast path against it. |
I've pushed the new baseline for the regex experiment. Compared with the current PR:
The benchmark needed correcting too. My earlier warm numbers measured three properties, and the builtin batch also included a property without a docblock. The cold benchmark timed preparation of the next class as well. It now measures one lookup per call, with setup and correctness checks outside timing and the same method/parameter passing for compared scenarios. Cold sources are real temporary files, prepared and loaded before timing, then cleaned up afterwards. Latest local results, PHP 8.5.12 CLI, OPcache disabled:
Warm short names, FQCNs and builtins are roughly on par. These replace the earlier figures, not a direct before/after speedup comparison: I haven't rerun the original implementation with this benchmark. Cold means uncached resolver metadata, not an empty filesystem cache; source reading and tokenization are included. The regex fast path isn't implemented yet. That's the next comparison against this baseline. |
I've added the regex fast path and benchmarked it against the corrected baseline. It matches a restricted header from the start of the file through the reflected class declaration, rather than searching for Tests cover fake imports in comments and strings, namespace isolation, and looking up another class after caching the first class's header. I ran baseline → regex → baseline → regex with the same benchmark. Latest pair on PHP 8.5.12 CLI, OPcache disabled, in µs per lookup:
The simple cold path is about 54% faster on the small file and 93% faster with 100 extra properties. The tradeoff is the grouped-import fallback: about 3.2 µs slower (~12.5%). Warm and cached-source lookups are roughly unchanged. The 20-property case includes one cold lookup followed by 19 lookups with cached source metadata, not 20 cold parses. Cold here means uncached resolver metadata, not an empty filesystem cache. Setup and result checks are outside timing. |
Summary
PropertyReflector::getIterableType()regex-captured the bare short name from iterable docblocks (Filter[],Book[],Chapter[]) and returned aTypeReflectorfor a class literally namedFilter/Book/Chapter— ignoring the declaring file's namespace andusestatements. Everything downstream that consumes the iterable type resolved against a class that does not exist:/** @var Book[] */onAuthor::$books)/** @var Chapter[] */onBookRequest::$chapters)getIterableType()now resolves docblock names through PHP namespace andusesemantics.Change
packages/reflection/src/PropertyReflector.php:int[],array, …) pass through unchanged.\App\Foo[]) resolve as before — the leading\is dropped, the FQCN is used verbatim.DocblockRelativeTarget).use App\Foo;) and aliases (use App\Foo as Bar;) resolve both the exact class and alias-prefixed paths (use A\B as C;→C\D→A\B\D).use App\{Foo, Bar as Baz};) resolve first-class and aliased members, including a shared group prefix.list<Type>andarray<Type>docblock forms go through the same resolution.The parser is dependency-free (
token_get_all) and deliberately skips things that are not imports:useblocks (brace-depth tracking),use function ...anduse const ...statements,Parsed imports are cached per declaring file in a method-local
static, so repeated property reflection per process re-parses once.End-to-end coverage
Two existing FQCN fixture docblocks were intentionally converted to short names:
Author::$books→/** @var Book[] */— relation hydration resolves and loadsBookmodels.BookRequest::$chapters→/** @var Chapter[] */— request-body array mapping producesChapterobjects viaArrayToObjectCollectionCaster, which is selected automatically from the resolved iterable type.New tests:
packages/reflection/tests/PropertyReflectorTest.php— unit coverage for imports, aliases, grouped imports (plain + aliased + prefixed), relative names, FQCN,list<…>/array<…>forms, and the no-docblocknullcase with a dedicated fixture model + bucket targets.ArrayToObjectMapperTest::map_collection_of_objects_from_short_docblock_name— maps['items' => [['name' => 'a'], …]]intoLibraryIteminstances from a shortLibraryItem[]docblock (Library/Children\LibraryItemfixtures).