(feat) Router: compile controller actions into cached "action plans" instead of using Reflection on every request - #341
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request Checklist
Description
The Router already caches route definitions, but every dispatch still read the action with the Reflection API:
#[Resolver](class and method),#[Transaction], and per parameter#[Model],#[BindPayload]and#[Bind], plus the constructor and every parameter type. None of that changes between requests.This PR reads an action once and keeps the result as plain data (an action plan). The Router resolves arguments from the plan without Reflection or attribute instantiation. Everything that depends on the request still runs per request: container
bind()/singleton(),make(), model lookup, payload validation, and the transaction.Design
Three tiers, cheapest first:
WeakMapfor closures). In worker mode this is once per action per worker.route:cachewritesstorage/framework/cache/actions.php(avar_exportarray, written to a temp file and renamed, thenopcache_invalidated). opcache serves it from shared memory.ActionPlannerbuilds a plan with Reflection. It is the fallback, and the only thing that ever produced a plan, so tiers 1 and 2 cannot change behavior.A compiled file with a different
FORMAT, or one that is missing, unreadable or corrupt, is ignored and the plan is rebuilt. It can never fail a request.route:clearremoves it.New classes:
Phaseolies\Support\Router\Plan\ActionPlannerandActionPlanStore.RoutergainsuseActionPlans()/actionPlans()andresolveAction()now works from a plan. Plans contain no objects and no closures, only scalars and arrays. A parameter whose default value cannot be stored as data (an enum case,newin an initializer, an unresolvable constant expression) is flaggedlazyDefaultand read from Reflection only if the argument is actually needed, exactly as before.Measured performance
resolveAction()for a controller with class + method#[Resolver], a constructor dependency, and an action with aRequest, a#[Bind]parameter, a class dependency, a route parameter and an optional default.php -Swith shared-memory opcache, a fresh PHP request per sample, 400 samples, optimized classmap:route:cachecompiledWarm worker process (plan already memoized): 19.2 µs before, 11.5 µs after (-40%).
Honest reading:
route:cache(the production setup) and in worker mode this is a clear win.route:cacheis used, which the docs already recommend for production.Tests
tests/Router/ActionPlan/(263 router tests in total, full suite 3140 green, PHPStan clean):Behavior changes (please read)
Routermethods are removed:processAttributesClassDependencies,processAttributesMethodDependencies,resolveConstructorDependencies,resolveActionDependencies,resolveParameters,handleBindPayloadAttribute,handleBindAttribute,handleModelAttribute,getTransactionConfig. Subclasses overriding them need to move to the plan-basedresolvePlannedParameters()family.ReflectionExceptionfromgetName().[class, method]array nor aClass@methodstring now throwsInvalidArgumentException.#[Resolver]of an action is no longer applied when the method does not exist (the request fails with "Method ... does not exist" first).route:cache, re-runroute:cache(orroute:clear), as with any route change.Docs
Routing → Route Caching → "Action Plan Cache" and the Deployment page. Marked "Available from v4.1.0".
Checklist