Conversation
jasonvarga
left a comment
There was a problem hiding this comment.
Thanks for digging into this — the approach is right. I went through Inertia's PropsResolver to check the premise rather than take it on faith, and it does recurse the whole prop tree resolving callables, so wrapping these three in closures really is transparent for page responses. Partial reloads come out identical too. I also confirmed the regression test is load-bearing: reverting just the two src/ files makes it_doesnt_build_the_nav_for_responses_that_arent_inertia_pages fail.
One thing I'd like changed before this goes in — see the inline comment on resolveProps().
Not blocking, but worth being aware of: RendersControlPanelExceptions sets Statamic::$isRenderingCpException = true before calling ->toResponse(), so the guard in nav() now evaluates at render time rather than middleware time, and CP error pages will get an empty nav prop where previously they got the full one. Nothing breaks visually — errors/Error.vue and errors/404.vue both use the Blank layout — but the identical guard in protectedProps() is still evaluated eagerly, so licensing and sessionExpiry do still resolve on those pages. Just flagging the asymmetry.
| private static function resolveProps(array $props) | ||
| { | ||
| return collect($props)->map(function ($value) { | ||
| if ($value instanceof Closure) { | ||
| $value = $value(); | ||
| } | ||
|
|
||
| return is_array($value) ? static::resolveProps($value) : $value; | ||
| })->all(); |
There was a problem hiding this comment.
This doesn't quite match how Inertia itself resolves props. ResolvesCallables::resolveCallable() does:
return is_object($value) && is_callable($value) ? App::call($value) : $value;Two differences:
- No container injection. A shared closure with a type-hinted dependency resolves fine on an Inertia page, but fatals on a blade page:
ArgumentCountError: Too few arguments to function {closure}(), 0 passed in src/Statamic.php on line 527 and exactly 1 expected - Invokable objects aren't resolved.
Inertia::share(['foo' => new SomeInvokable])resolves on an Inertia page; here the object passes straight through tojson_encode()and comes out as{}.
Our own three closures take no arguments, so core is unaffected either way. But Inertia::share() is reachable by addons, and this PR is what first puts callables into the shared props at all, so this seems like the moment to make the two paths agree:
private static function resolveProps(array $props)
{
return collect($props)->map(function ($value) {
if (is_object($value) && is_callable($value)) {
$value = App::call($value);
}
return is_array($value) ? static::resolveProps($value) : $value;
})->all();
}Illuminate\Support\Facades\App is already imported in this file, and the Closure import stays since it's used elsewhere.
There was a problem hiding this comment.
Fixed in ea4546a. I reproduced both cases first. The type-hinted closure threw the same ArgumentCountError you quoted, at Statamic.php:527. The invokable came back as [] in the JSON. resolveProps() now does the is_object() && is_callable() check and calls App::call(), so both paths behave the same.
I also added a test that shares a closure with a Request dependency and an invokable object, and asserts both resolve outside Inertia.
|
You're right about the error page asymmetry, and I left it that way on purpose. Wrapping |
The
HandleAuthenticatedInertiaRequestsmiddleware sits at the end of thestatamic.cp.authenticatedgroup, so it computes its shared props on every authenticated CP request. Building the nav is the expensive part of that, since it walks every navigation tree once per site and gate-checks every creatable content type. On CP routes that never render an Inertia page (thumbnails, SVG and PDF asset responses, endpoints that return JSON) all of that work is thrown away. The reporter measured 140 queries and 277 gate checks on a request that returns a PNG.Inertia::share()evaluates nothing, and Inertia resolves closures anywhere in the shared props tree when it builds a page response. Wrappingnav,licensingandsessionExpiryin closures therefore leaves page responses identical and skips the work everywhere else.Statamic::nonInertiaPageData()is the one place that reads those props outside of an Inertia response, for blade pages rendered through the root view, so it now resolves closures itself.One side effect worth mentioning: the
Statamic::$isRenderingCpExceptioncheck insidenav()was unreachable before, because the middleware always ran ahead of the exception handler. It applies now, so production CP error pages skip the nav, which they never render anyway since they use the blank layout.Fixes #15475