Cache reflection lookups in ExtendableTrait magic method handling#234
Cache reflection lookups in ExtendableTrait magic method handling#234austinderrick wants to merge 1 commit into
Conversation
WalkthroughThis pull request enhances Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Every __get/__set/__call on a class using ExtendableTrait built a fresh ReflectionClass, recursively walked the parent chain to find the first non-extendable ancestor, and resolved ReflectionMethod handles for the parent magic methods twice per call (once in extensionMethodExists and again in extensionCallMethod). For classes with behaviors, every property read also constructed a new ReflectionClass per extension to check property visibility. All of these resolutions depend only on the class, never the instance, so they are now memoized in per-class static caches: - extensionGetParentClass() caches the resolved parent reflection per class (intermediate steps of the recursion are not cached) - extensionMethodExists()/extensionCallMethod() share a cached ReflectionMethod handle per class and method - extendableIsAccessible() caches the visibility check per class and property A microbenchmark of the __get path shows roughly a 10x reduction in overhead per attribute access; the win multiplies across the thousands of magic method calls a typical request makes through Database models. Follows the same pattern as the existing extendableCallStatic() cache.
591dd47 to
bf25e93
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/Extension/ExtendableTrait.php (2)
596-596: ⚡ Quick winStrengthen type hint to
ReflectionClass.The method calls
$reflector->getParentClass()which requires aReflectionClassinstance. Usingobjectallows any object to be passed, which would cause a runtime error.- protected function extensionResolveParentClass(object $reflector) + protected function extensionResolveParentClass(ReflectionClass $reflector)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Extension/ExtendableTrait.php` at line 596, The method extensionResolveParentClass currently types its parameter as object but calls $reflector->getParentClass(), so strengthen the type hint to ReflectionClass (use the ReflectionClass type for the $reflector parameter in extensionResolveParentClass), and add/import the ReflectionClass symbol (or fully qualify it) to ensure static analysis and runtime correctness when invoking getParentClass().
573-576: Remove dead$instancebranch inextensionGetParentClass
src/Extension/ExtendableTrait.phpalways callsextensionGetParentClass()with no arguments (call sites at lines 393, 420, 464), so theif (!is_null($instance)) { return $this->extensionResolveParentClass($instance); }block at lines 573-576 is unreachable. Consider removing the$instanceparameter and that dead branch for clarity.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Extension/ExtendableTrait.php` around lines 573 - 576, The branch handling a non-null $instance in extensionGetParentClass is dead because callers always invoke extensionGetParentClass() with no arguments; remove the unused $instance parameter and the if (!is_null($instance)) { return $this->extensionResolveParentClass($instance); } block from ExtendableTrait::extensionGetParentClass, update the method signature and its docblock to drop $instance, and adjust any callers and related tests/docblocks to match the new zero-arg signature while keeping extensionResolveParentClass intact for actual resolution logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/Extension/ExtendableTraitTest.php`:
- Around line 66-71: The test currently only asserts that repeated calls on
Level3NonExtendable reuse the same ReflectionClass instance; update the test to
also assert that Level3NonExtendable's cached ReflectionClass is a distinct
instance from the one cached by Level2NonExtendable: call the protected method
extensionGetParentClass on a Level2NonExtendable instance to get $first (the
parent-resolving ReflectionClass), then add an identity assertion that
$level3First !== $first to guarantee per-caller-class memoization (reference the
methods/variables extensionGetParentClass, Level2NonExtendable,
Level3NonExtendable, $first, $level3First).
---
Nitpick comments:
In `@src/Extension/ExtendableTrait.php`:
- Line 596: The method extensionResolveParentClass currently types its parameter
as object but calls $reflector->getParentClass(), so strengthen the type hint to
ReflectionClass (use the ReflectionClass type for the $reflector parameter in
extensionResolveParentClass), and add/import the ReflectionClass symbol (or
fully qualify it) to ensure static analysis and runtime correctness when
invoking getParentClass().
- Around line 573-576: The branch handling a non-null $instance in
extensionGetParentClass is dead because callers always invoke
extensionGetParentClass() with no arguments; remove the unused $instance
parameter and the if (!is_null($instance)) { return
$this->extensionResolveParentClass($instance); } block from
ExtendableTrait::extensionGetParentClass, update the method signature and its
docblock to drop $instance, and adjust any callers and related tests/docblocks
to match the new zero-arg signature while keeping extensionResolveParentClass
intact for actual resolution logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 59305219-3747-4f90-95ad-50526f2edfd4
📒 Files selected for processing (2)
src/Extension/ExtendableTrait.phptests/Extension/ExtendableTraitTest.php
Problem
Every
__get/__set/__callon a class usingExtendableTrait(i.e. every attribute access on every Database model) currently:ReflectionClassand recursively walks the parent chain inextensionGetParentClass()to find the first non-extendable ancestor,__get/__set/__callReflectionMethodtwice per call — once inextensionMethodExists()and again inextensionCallMethod(),ReflectionClassper extension per property read inextendableIsAccessible().None of this was memoized, although every one of these resolutions depends only on the class, never the instance. A microbenchmark of the
__getpath shows roughly a 10x overhead per attribute access (~30ns direct vs ~340ns through the reflection path) — and a typical request makes thousands of magic-method calls through models, components and widgets, plus GC pressure from the throwaway reflection objects.Fix
Three per-class static caches, following the same pattern as the existing
extendableCallStatic()cache in the same file:extensionGetParentClass()caches the resolved parent reflection (orfalse) per class; the recursion's intermediate steps are split intoextensionResolveParentClass()and are not cached.extensionMethodExists()/extensionCallMethod()share a cachedReflectionMethodhandle per class and method.extensionCallMethod()keeps a fallback to an uncachedgetMethod()for non-public methods so its behaviour for direct, unguarded calls is unchanged (including theReflectionExceptionfor unknown methods).extendableIsAccessible()caches the visibility check per class and property.All cached values are class-structure facts that cannot change at runtime, so there is no invalidation concern; dynamic methods and properties continue to live in the per-instance
$extensionDataand are checked before any of these paths.Testing
New tests assert the parent reflection is reused across calls and instances (and that subclasses memoize independently), and that repeated magic access behaves consistently across instances. The full test suite passes — the Database model tests exercise the cached
__get/__set/__callpaths heavily.Summary by CodeRabbit
Refactor
Tests