Conversation
There was a problem hiding this comment.
Pull request overview
This PR upgrades the Tempest Inertia adapter toward Inertia v3 by changing the HTML bootstrap format (JSON-in-<script type="application/json">), and introducing a dedicated prop-processing pipeline that supports v3 features like scroll props, merge props, and once props.
Changes:
- Switch initial page bootstrap from a
data-page="...json..."attribute to a<script type="application/json" data-page="...">...payload plus a plain root<div>. - Add a new
PropPipeline(filter → evaluate → deferred/merge/scroll/once metadata) and wire it intoInertiaResponse. - Implement v3 scroll props and once props (including related headers and page payload fields).
Reviewed changes
Copilot reviewed 32 out of 34 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Integration/ViewTest.php | Updates view rendering expectations for the new <script type="application/json"> bootstrap format. |
| tests/Integration/ScrollPropsTest.php | Adds integration coverage for scroll props output (scrollProps, mergeProps). |
| tests/Integration/ResponseTest.php | Updates server-rendered HTML expectations and adds extensive once-props behavioral tests. |
| tests/Integration/InertiaTest.php | Adjusts JSON payload expectations to match updated PageData serialization. |
| tests/Integration/HistoryTest.php | Updates tests around conditional history flags serialization. |
| src/Views/Components/x-inertia.view.php | Implements the new v3 bootstrap template output (script tag + root div). |
| src/Support/Header.php | Adds x-inertia-except-once-props header constant. |
| src/Props/ScrollProp.php | Introduces ScrollProp to support scroll pagination payload + merge behavior. |
| src/Props/OptionalProp.php | Refactors prop invocation via shared callable/once traits and marks it Onceable. |
| src/Props/DeferProp.php | Refactors prop invocation via shared callable/once traits and marks it Onceable. |
| src/Props/AlwaysProp.php | Refactors prop invocation via shared callable/once traits and marks it Onceable. |
| src/Pipeline/Stages/FilterProps.php | Centralizes partial/always/once filtering into pipeline stage. |
| src/Pipeline/Stages/EvaluateProps.php | Centralizes prop evaluation (closures/callable props/arrays/dot-unpacking) into pipeline stage. |
| src/Pipeline/Stages/ResolveDeferredProps.php | Computes deferredProps, with special handling for loaded once-props. |
| src/Pipeline/Stages/ResolveMergeProps.php | Computes mergeProps, including scroll-prop merge key behavior and reset support. |
| src/Pipeline/Stages/ResolveScrollProps.php | Computes scrollProps metadata for scroll props. |
| src/Pipeline/Stages/ResolveOnceProps.php | Computes onceProps metadata payload for onceable props. |
| src/Pipeline/PropStage.php | Defines pipeline stage interface. |
| src/Pipeline/PropPipelineContext.php | Introduces immutable-ish context container for staged prop processing. |
| src/Pipeline/PropPipeline.php | Orchestrates the stages and returns ProcessedProps. |
| src/Pipeline/ProcessedProps.php | DTO for processed props + metadata (deferredProps, mergeProps, etc.). |
| src/PageData.php | Adds scrollProps/onceProps fields and omits history flags unless true. |
| src/Inertia.php | Adds Inertia::once() helper and refactors request/version resolution approach. |
| src/Http/Middleware.php | Adds redirect reflashing and fragment redirect conflict behavior (v3-related behavior). |
| src/Http/InertiaResponse.php | Switches response prop processing to use the new PropPipeline. |
| src/Contracts/ProvidesScrollMetadata.php | Adds contract for scroll metadata providers. |
| src/Contracts/Onceable.php | Adds contract for once-prop behavior configuration and metadata. |
| src/Concerns/IsOnceProp.php | Adds reusable implementation of Onceable configuration. |
| src/Concerns/IsMergeableProp.php | Changes merge behavior to return a modified clone. |
| src/Concerns/IsCallableProp.php | Adds shared callable-resolution helper for prop types. |
| mago.yaml | Bumps configured PHP version for Mago analysis/linting. |
| composer.json | Adds mago:analyze script. |
| composer.lock | Updates dependency versions (Tempest + related tooling/libs). |
| .gitignore | Ignores .vscode/settings.json. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static::assertNotContains($body->toArray(), [ | ||
| 'encryptHistory', | ||
| 'clearHistory', | ||
| ]); |
There was a problem hiding this comment.
assertNotContains() arguments are reversed here: PHPUnit expects (needle, haystack), but this passes the entire $body->toArray() as the needle and a list of strings as the haystack, so it won’t actually assert that the keys are absent. Use assertArrayNotHasKey() for each key (or assert that array_keys($body->toArray()) doesn’t contain them).
| static::assertNotContains($body->toArray(), [ | |
| 'encryptHistory', | |
| 'clearHistory', | |
| ]); | |
| static::assertArrayNotHasKey('encryptHistory', $body->toArray()); | |
| static::assertArrayNotHasKey('clearHistory', $body->toArray()); |
| public static function once(mixed $value): AlwaysProp | ||
| { | ||
| return new AlwaysProp($value)->once(); | ||
| } |
There was a problem hiding this comment.
Inertia::once() returns an AlwaysProp, which makes once-props part of the “always” set in FilterProps and therefore they bypass X-Inertia-Partial-Data/X-Inertia-Partial-Except filtering on partial reloads. Once-props should still respect partial reload inclusion/exclusion (they’re only “once” in terms of resolution/caching), so consider introducing a dedicated OnceProp type (not AlwaysProp) or adjusting FilterProps so once-props aren’t treated as always-props.
| protected function resolveValue(): mixed | ||
| { | ||
| if ($this->resolved !== null) { | ||
| return $this->resolved; | ||
| } | ||
|
|
||
| $this->resolved = $this->resolveCallablePropValue($this->value); | ||
|
|
||
| return $this->resolved; | ||
| } |
There was a problem hiding this comment.
resolveValue() treats null as “not resolved yet”, so a callable that legitimately resolves to null will be invoked repeatedly. Consider tracking resolution with a separate boolean/sentinel so null results are cached too.
| if ($value instanceof PaginatedData) { | ||
| return [ | ||
| $this->wrapper => $value->data, | ||
| ]; | ||
| } |
There was a problem hiding this comment.
When wrapper is set to an empty string, mergeKey() already avoids dot-wrapping, but __invoke() will still return an array with an empty-string key (['' => $value->data]). If empty wrapper is meant to mean “no wrapper”, __invoke() should return $value->data directly in that case.
| private function prefetch(Request $request) | ||
| { | ||
| return ( | ||
| strcasecmp($request->headers->get('HTTP_X_MOZ') ?? '', 'prefetch') === 0 |
There was a problem hiding this comment.
The prefetch detection checks header name HTTP_X_MOZ, which looks like a server/env var rather than an actual HTTP header; with Request->headers->get() this will likely never match real requests. Use the actual header name (e.g. X-Moz) and consider adding a : bool return type for prefetch() to make intent explicit.
| private function prefetch(Request $request) | |
| { | |
| return ( | |
| strcasecmp($request->headers->get('HTTP_X_MOZ') ?? '', 'prefetch') === 0 | |
| private function prefetch(Request $request): bool | |
| { | |
| return ( | |
| strcasecmp($request->headers->get('X-Moz') ?? '', 'prefetch') === 0 |
| public function test_scroll_props_renders_correctly() | ||
| { |
There was a problem hiding this comment.
For consistency with the rest of the integration tests in this repo, this test method should declare a : void return type.
This PR implements everything that inertia v3 has:
Other things to explore before releasing this is #19 and see if we might be able to use discovery for sharing props and remove the share props from anywhere, personally ive never used it, but perhaps others do.