Skip to content

Commit 39951e0

Browse files
committed
wip
1 parent 85c2bad commit 39951e0

10 files changed

Lines changed: 345 additions & 62 deletions

config/inertia.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,19 @@
104104

105105
],
106106

107+
/*
108+
|--------------------------------------------------------------------------
109+
| Expose Shared Prop Keys
110+
|--------------------------------------------------------------------------
111+
|
112+
| When enabled, each page response includes a `sharedProps` metadata key
113+
| listing the top-level prop keys that were registered via `Inertia::share`.
114+
| The frontend can use this to carry shared props over during instant visits.
115+
|
116+
*/
117+
118+
'expose_shared_prop_keys' => true,
119+
107120
/*
108121
|--------------------------------------------------------------------------
109122
| History

src/PropsResolver.php

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ class PropsResolver
120120
*/
121121
protected $onceProps = [];
122122

123+
/**
124+
* The top-level keys of shared props.
125+
*
126+
* @var array<int, string>
127+
*/
128+
protected $sharedPropKeys = [];
129+
123130
/**
124131
* Create a new props resolver instance.
125132
*/
@@ -137,19 +144,47 @@ public function __construct(Request $request, string $component)
137144
}
138145

139146
/**
140-
* Resolve the given props and collect their metadata.
147+
* Resolve the given shared and page props, collecting their metadata.
141148
*
142-
* @param array<array-key, mixed> $props
149+
* @param array<array-key, mixed|\Inertia\ProvidesInertiaProperties> $shared
150+
* @param array<array-key, mixed|\Inertia\ProvidesInertiaProperties> $props
143151
* @return array{array<string, mixed>, array<string, mixed>}
144152
*/
145-
public function resolve(array $props): array
153+
public function resolve(array $shared, array $props): array
146154
{
155+
$props = array_merge($this->resolveSharedProps($shared), $props);
156+
147157
return [
148158
$this->resolveProps($this->unpackDotProps($props)),
149159
$this->buildMetadata(),
150160
];
151161
}
152162

163+
/**
164+
* Resolve shared property providers and collect shared prop keys.
165+
*
166+
* @param array<array-key, mixed|\Inertia\ProvidesInertiaProperties> $shared
167+
* @return array<string, mixed>
168+
*/
169+
protected function resolveSharedProps(array $shared): array
170+
{
171+
$resolved = $this->resolvePropertyProviders($shared);
172+
173+
if (! config('inertia.expose_shared_prop_keys', true)) {
174+
return $resolved;
175+
}
176+
177+
foreach (array_keys($resolved) as $key) {
178+
$this->sharedPropKeys[] = str_contains((string) $key, '.')
179+
? strstr((string) $key, '.', true)
180+
: (string) $key;
181+
}
182+
183+
$this->sharedPropKeys = array_values(array_unique($this->sharedPropKeys));
184+
185+
return $resolved;
186+
}
187+
153188
/**
154189
* Resolve ProvidesInertiaProperties instances into keyed props.
155190
*
@@ -184,6 +219,7 @@ protected function resolvePropertyProviders(array $props): array
184219
protected function buildMetadata(): array
185220
{
186221
return array_filter([
222+
'sharedProps' => $this->sharedPropKeys,
187223
'mergeProps' => $this->mergeProps,
188224
'prependProps' => $this->prependProps,
189225
'deepMergeProps' => $this->deepMergeProps,

src/Response.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,30 @@ class Response implements Responsable
8080
*/
8181
protected ?Closure $urlResolver = null;
8282

83+
/**
84+
* The shared properties (before merge with page props).
85+
*
86+
* @var array<array-key, mixed|\Inertia\ProvidesInertiaProperties>
87+
*/
88+
protected array $sharedProps = [];
89+
8390
/**
8491
* Create a new Inertia response instance.
8592
*
93+
* @param array<array-key, mixed|\Inertia\ProvidesInertiaProperties> $sharedProps
8694
* @param array<array-key, mixed|\Inertia\ProvidesInertiaProperties> $props
8795
*/
8896
public function __construct(
8997
string $component,
98+
array $sharedProps,
9099
array $props,
91100
string $rootView = 'app',
92101
string $version = '',
93102
bool $encryptHistory = false,
94-
?Closure $urlResolver = null
103+
?Closure $urlResolver = null,
95104
) {
96105
$this->component = $component;
106+
$this->sharedProps = $sharedProps;
97107
$this->props = $props;
98108
$this->rootView = $rootView;
99109
$this->version = $version;
@@ -175,7 +185,7 @@ public function flash(BackedEnum|UnitEnum|string|array $key, mixed $value = null
175185
public function toResponse($request)
176186
{
177187
$resolver = new PropsResolver($request, $this->component);
178-
[$resolvedProps, $resolvedMetadata] = $resolver->resolve($this->props);
188+
[$resolvedProps, $resolvedMetadata] = $resolver->resolve($this->sharedProps, $this->props);
179189

180190
$page = array_merge(
181191
[

src/ResponseFactory.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,8 @@ public function render($component, $props = []): Response
324324

325325
return new Response(
326326
$component,
327-
array_merge($this->sharedProps, $props),
327+
$this->sharedProps,
328+
$props,
328329
$this->rootView,
329330
$this->getVersion(),
330331
$this->encryptHistory ?? config('inertia.history.encrypt', false),

tests/ControllerTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public function test_controller_returns_an_inertia_response(): void
3030
'version' => '',
3131
'encryptHistory' => false,
3232
'clearHistory' => false,
33+
'sharedProps' => ['errors'],
3334
]);
3435
}
3536
}

tests/HistoryTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public function test_the_history_can_be_cleared_when_redirecting(): void
160160
]);
161161

162162
$response->assertSuccessful();
163-
$response->assertContent('<script data-page="app" type="application/json">{"component":"User\/Edit","props":{"errors":{}},"url":"\/users","version":"","clearHistory":true,"encryptHistory":false}</script><div id="app"></div>');
163+
$response->assertContent('<script data-page="app" type="application/json">{"component":"User\/Edit","props":{"errors":{}},"url":"\/users","version":"","clearHistory":true,"encryptHistory":false,"sharedProps":["errors"]}</script><div id="app"></div>');
164164
}
165165

166166
public function test_the_fragment_is_not_preserved_by_default(): void

tests/PropsResolverTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -935,7 +935,7 @@ public function test_deferred_props_inside_closure_are_resolved_on_partial_reque
935935
*/
936936
protected function makePage(Request $request, array $props): array
937937
{
938-
$response = new Response('TestComponent', $props, 'app', '123');
938+
$response = new Response('TestComponent', [], $props, 'app', '123');
939939
$response = $response->toResponse($request);
940940

941941
if ($response instanceof JsonResponse) {

tests/ResponseFactoryTest.php

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,211 @@ public function test_multiple_flash_calls_are_merged(): void
735735
]);
736736
}
737737

738+
public function test_shared_props_tracking_can_be_disabled(): void
739+
{
740+
config()->set('inertia.expose_shared_prop_keys', false);
741+
742+
Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () {
743+
Inertia::share('app_name', 'My App');
744+
745+
return Inertia::render('User/Edit');
746+
});
747+
748+
$response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']);
749+
750+
$response->assertSuccessful();
751+
$data = $response->json();
752+
$this->assertArrayNotHasKey('sharedProps', $data);
753+
$this->assertSame('My App', $data['props']['app_name']);
754+
}
755+
756+
public function test_shared_props_metadata_includes_keys_from_middleware_share(): void
757+
{
758+
759+
Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () {
760+
return Inertia::render('User/Edit', [
761+
'user' => ['name' => 'Jonathan'],
762+
]);
763+
});
764+
765+
$response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']);
766+
767+
$response->assertSuccessful();
768+
$response->assertJson([
769+
'component' => 'User/Edit',
770+
'props' => [
771+
'user' => ['name' => 'Jonathan'],
772+
],
773+
'sharedProps' => ['errors'],
774+
]);
775+
}
776+
777+
public function test_shared_props_metadata_includes_keys_from_inertia_share(): void
778+
{
779+
780+
Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () {
781+
Inertia::share('app_name', 'My App');
782+
783+
return Inertia::render('User/Edit');
784+
});
785+
786+
$response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']);
787+
788+
$response->assertSuccessful();
789+
$response->assertJson([
790+
'sharedProps' => ['errors', 'app_name'],
791+
]);
792+
}
793+
794+
public function test_shared_props_metadata_includes_dot_notation_keys_as_top_level(): void
795+
{
796+
797+
Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () {
798+
Inertia::share('auth.user', ['name' => 'Jonathan']);
799+
800+
return Inertia::render('User/Edit');
801+
});
802+
803+
$response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']);
804+
805+
$response->assertSuccessful();
806+
$response->assertJson([
807+
'sharedProps' => ['errors', 'auth'],
808+
]);
809+
}
810+
811+
public function test_shared_props_metadata_includes_keys_from_share_once(): void
812+
{
813+
814+
Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () {
815+
Inertia::shareOnce('permissions', fn () => ['admin' => true]);
816+
817+
return Inertia::render('User/Edit');
818+
});
819+
820+
$response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']);
821+
822+
$response->assertSuccessful();
823+
$response->assertJson([
824+
'props' => [
825+
'permissions' => ['admin' => true],
826+
],
827+
'sharedProps' => ['errors', 'permissions'],
828+
]);
829+
}
830+
831+
public function test_shared_props_metadata_includes_keys_from_provides_inertia_properties(): void
832+
{
833+
834+
Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () {
835+
Inertia::share(new ExampleInertiaPropsProvider([
836+
'app_name' => 'My App',
837+
'locale' => 'en',
838+
]));
839+
840+
return Inertia::render('User/Edit');
841+
});
842+
843+
$response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']);
844+
845+
$response->assertSuccessful();
846+
$response->assertJson([
847+
'props' => [
848+
'app_name' => 'My App',
849+
'locale' => 'en',
850+
],
851+
'sharedProps' => ['errors', 'app_name', 'locale'],
852+
]);
853+
}
854+
855+
public function test_shared_props_metadata_includes_page_specific_override_keys(): void
856+
{
857+
858+
Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () {
859+
Inertia::share('auth', ['user' => null]);
860+
861+
return Inertia::render('User/Edit', [
862+
'auth' => ['user' => ['name' => 'Jonathan']],
863+
]);
864+
});
865+
866+
$response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']);
867+
868+
$response->assertSuccessful();
869+
$response->assertJson([
870+
'props' => [
871+
'auth' => ['user' => ['name' => 'Jonathan']],
872+
],
873+
'sharedProps' => ['errors', 'auth'],
874+
]);
875+
}
876+
877+
public function test_shared_props_metadata_with_multiple_share_calls(): void
878+
{
879+
880+
Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () {
881+
Inertia::share('app_name', 'My App');
882+
Inertia::share('locale', 'en');
883+
Inertia::shareOnce('permissions', fn () => ['admin' => true]);
884+
885+
return Inertia::render('User/Edit');
886+
});
887+
888+
$response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']);
889+
890+
$response->assertSuccessful();
891+
$response->assertJson([
892+
'sharedProps' => ['errors', 'app_name', 'locale', 'permissions'],
893+
]);
894+
}
895+
896+
public function test_shared_props_metadata_with_array_share(): void
897+
{
898+
899+
Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () {
900+
Inertia::share([
901+
'flash' => fn () => ['message' => 'Hello'],
902+
'auth' => fn () => ['user' => ['name' => 'Jonathan']],
903+
]);
904+
905+
return Inertia::render('User/Edit');
906+
});
907+
908+
$response = $this->withoutExceptionHandling()->get('/', ['X-Inertia' => 'true']);
909+
910+
$response->assertSuccessful();
911+
$response->assertJson([
912+
'sharedProps' => ['errors', 'flash', 'auth'],
913+
]);
914+
}
915+
916+
public function test_shared_props_metadata_includes_already_loaded_once_props(): void
917+
{
918+
919+
Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () {
920+
Inertia::shareOnce('permissions', fn () => ['admin' => true]);
921+
922+
return Inertia::render('User/Edit');
923+
});
924+
925+
$response = $this->withoutExceptionHandling()->get('/', [
926+
'X-Inertia' => 'true',
927+
'X-Inertia-Except-Once-Props' => 'permissions',
928+
]);
929+
930+
$response->assertSuccessful();
931+
$data = $response->json();
932+
933+
// The once-prop value should be excluded from props since the client already has it
934+
$this->assertArrayNotHasKey('permissions', $data['props']);
935+
936+
// But its key should still appear in the sharedProps metadata
937+
$this->assertContains('permissions', $data['sharedProps']);
938+
939+
// And its onceProps metadata should also be preserved
940+
$this->assertArrayHasKey('permissions', $data['onceProps']);
941+
}
942+
738943
public function test_without_ssr_registers_paths_with_gateway(): void
739944
{
740945
Inertia::withoutSsr(['admin/*', 'nova/*']);

0 commit comments

Comments
 (0)