Skip to content

Commit de6b005

Browse files
Support dot notation in partial requests (#620)
* add dot notation support for `only` * add tests * add more thorough tests * fix setting partial props * extract headers into `Header` enum * refactor logic * add comments * extra test steps * Update Response.php * formatting --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 56a3c41 commit de6b005

File tree

6 files changed

+131
-31
lines changed

6 files changed

+131
-31
lines changed

src/Middleware.php

+6-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Closure;
66
use Illuminate\Http\Request;
77
use Illuminate\Support\Facades\Redirect;
8+
use Inertia\Support\Header;
89
use Symfony\Component\HttpFoundation\Response;
910

1011
class Middleware
@@ -85,13 +86,13 @@ public function handle(Request $request, Closure $next)
8586
Inertia::setRootView($this->rootView($request));
8687

8788
$response = $next($request);
88-
$response->headers->set('Vary', 'X-Inertia');
89+
$response->headers->set('Vary', Header::INERTIA);
8990

90-
if (! $request->header('X-Inertia')) {
91+
if (! $request->header(Header::INERTIA)) {
9192
return $response;
9293
}
9394

94-
if ($request->method() === 'GET' && $request->header('X-Inertia-Version', '') !== Inertia::getVersion()) {
95+
if ($request->method() === 'GET' && $request->header(Header::VERSION, '') !== Inertia::getVersion()) {
9596
$response = $this->onVersionChange($request, $response);
9697
}
9798

@@ -145,8 +146,8 @@ public function resolveValidationErrors(Request $request)
145146
return $errors[0];
146147
})->toArray();
147148
})->pipe(function ($bags) use ($request) {
148-
if ($bags->has('default') && $request->header('x-inertia-error-bag')) {
149-
return [$request->header('x-inertia-error-bag') => $bags->get('default')];
149+
if ($bags->has('default') && $request->header(Header::ERROR_BAG)) {
150+
return [$request->header(Header::ERROR_BAG) => $bags->get('default')];
150151
}
151152

152153
if ($bags->has('default')) {

src/Response.php

+73-24
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace Inertia;
44

55
use Closure;
6-
use Illuminate\Support\Arr;
76
use Illuminate\Http\Request;
87
use Illuminate\Http\JsonResponse;
98
use Illuminate\Support\Facades\App;
@@ -14,7 +13,9 @@
1413
use Illuminate\Contracts\Support\Responsable;
1514
use Illuminate\Http\Resources\Json\JsonResource;
1615
use Illuminate\Http\Resources\Json\ResourceResponse;
16+
use Illuminate\Support\Arr;
1717
use Illuminate\Support\Facades\Response as ResponseFactory;
18+
use Inertia\Support\Header;
1819

1920
class Response implements Responsable
2021
{
@@ -87,15 +88,7 @@ public function rootView(string $rootView): self
8788
*/
8889
public function toResponse($request)
8990
{
90-
$only = array_filter(explode(',', $request->header('X-Inertia-Partial-Data', '')));
91-
92-
$props = ($only && $request->header('X-Inertia-Partial-Component') === $this->component)
93-
? Arr::only($this->props, $only)
94-
: array_filter($this->props, static function ($prop) {
95-
return ! ($prop instanceof LazyProp);
96-
});
97-
98-
$props = $this->resolvePropertyInstances($props, $request);
91+
$props = $this->resolveProperties($request, $this->props);
9992

10093
$page = [
10194
'component' => $this->component,
@@ -104,17 +97,82 @@ public function toResponse($request)
10497
'version' => $this->version,
10598
];
10699

107-
if ($request->header('X-Inertia')) {
108-
return new JsonResponse($page, 200, ['X-Inertia' => 'true']);
100+
if ($request->header(Header::INERTIA)) {
101+
return new JsonResponse($page, 200, [Header::INERTIA => 'true']);
109102
}
110103

111104
return ResponseFactory::view($this->rootView, $this->viewData + ['page' => $page]);
112105
}
113106

107+
/**
108+
* Resolve the properites for the response.
109+
*/
110+
public function resolveProperties(Request $request, array $props): array
111+
{
112+
$isPartial = $request->header(Header::PARTIAL_COMPONENT) === $this->component;
113+
114+
if(!$isPartial) {
115+
$props = array_filter($this->props, static function ($prop) {
116+
return ! ($prop instanceof LazyProp);
117+
});
118+
}
119+
120+
$props = $this->resolveArrayableProperties($props, $request);
121+
122+
if($isPartial && $request->hasHeader(Header::PARTIAL_ONLY)) {
123+
$props = $this->resolveOnly($request, $props);
124+
}
125+
126+
$props = $this->resolvePropertyInstances($props, $request);
127+
128+
return $props;
129+
}
130+
131+
/**
132+
* Resolve all arrayables properties into an array.
133+
*/
134+
public function resolveArrayableProperties(array $props, Request $request, bool $unpackDotProps = true): array
135+
{
136+
foreach ($props as $key => $value) {
137+
if ($value instanceof Arrayable) {
138+
$value = $value->toArray();
139+
}
140+
141+
if (is_array($value)) {
142+
$value = $this->resolveArrayableProperties($value, $request, false);
143+
}
144+
145+
if ($unpackDotProps && str_contains($key, '.')) {
146+
Arr::set($props, $key, $value);
147+
unset($props[$key]);
148+
} else {
149+
$props[$key] = $value;
150+
}
151+
}
152+
153+
return $props;
154+
}
155+
156+
/**
157+
* Resolve the `only` partial request props.
158+
*/
159+
public function resolveOnly(Request $request, array $props): array
160+
{
161+
$only = array_filter(explode(',', $request->header(Header::PARTIAL_ONLY, '')));
162+
163+
$value = [];
164+
165+
foreach($only as $key) {
166+
Arr::set($value, $key, data_get($props, $key));
167+
}
168+
169+
return $value;
170+
}
171+
114172
/**
115173
* Resolve all necessary class instances in the given props.
116174
*/
117-
public function resolvePropertyInstances(array $props, Request $request, bool $unpackDotProps = true): array
175+
public function resolvePropertyInstances(array $props, Request $request): array
118176
{
119177
foreach ($props as $key => $value) {
120178
if ($value instanceof Closure) {
@@ -133,20 +191,11 @@ public function resolvePropertyInstances(array $props, Request $request, bool $u
133191
$value = $value->toResponse($request)->getData(true);
134192
}
135193

136-
if ($value instanceof Arrayable) {
137-
$value = $value->toArray();
138-
}
139-
140194
if (is_array($value)) {
141-
$value = $this->resolvePropertyInstances($value, $request, false);
195+
$value = $this->resolvePropertyInstances($value, $request);
142196
}
143197

144-
if ($unpackDotProps && str_contains($key, '.')) {
145-
Arr::set($props, $key, $value);
146-
unset($props[$key]);
147-
} else {
148-
$props[$key] = $value;
149-
}
198+
$props[$key] = $value;
150199
}
151200

152201
return $props;

src/ResponseFactory.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Illuminate\Contracts\Support\Arrayable;
1111
use Illuminate\Support\Facades\Redirect;
1212
use Illuminate\Support\Facades\Response as BaseResponse;
13+
use Inertia\Support\Header;
1314
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
1415
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirect;
1516

@@ -110,7 +111,7 @@ public function render(string $component, $props = []): Response
110111
public function location($url): SymfonyResponse
111112
{
112113
if (Request::inertia()) {
113-
return BaseResponse::make('', 409, ['X-Inertia-Location' => $url instanceof SymfonyRedirect ? $url->getTargetUrl() : $url]);
114+
return BaseResponse::make('', 409, [Header::LOCATION => $url instanceof SymfonyRedirect ? $url->getTargetUrl() : $url]);
114115
}
115116

116117
return $url instanceof SymfonyRedirect ? $url : Redirect::away($url);

src/ServiceProvider.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Inertia\Testing\TestResponseMacros;
1414
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
1515
use Illuminate\Foundation\Testing\TestResponse as LegacyTestResponse;
16+
use Inertia\Support\Header;
1617

1718
class ServiceProvider extends BaseServiceProvider
1819
{
@@ -73,7 +74,7 @@ protected function registerConsoleCommands(): void
7374
protected function registerRequestMacro(): void
7475
{
7576
Request::macro('inertia', function () {
76-
return (bool) $this->header('X-Inertia');
77+
return (bool) $this->header(Header::INERTIA);
7778
});
7879
}
7980

src/Support/Header.php

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Inertia\Support;
4+
5+
class Header
6+
{
7+
public const INERTIA = 'X-Inertia';
8+
public const ERROR_BAG = 'X-Inertia-Error-Bag';
9+
public const LOCATION = 'X-Inertia-Location';
10+
public const VERSION = 'X-Inertia-Version';
11+
public const PARTIAL_COMPONENT = 'X-Inertia-Partial-Component';
12+
public const PARTIAL_ONLY = 'X-Inertia-Partial-Data';
13+
}

tests/ResponseTest.php

+35
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Inertia\Tests;
44

5+
use Exception;
56
use Mockery;
67
use Inertia\LazyProp;
78
use Inertia\Response;
@@ -255,6 +256,40 @@ public function test_xhr_partial_response(): void
255256
$this->assertSame('123', $page->version);
256257
}
257258

259+
public function test_nested_partial_props(): void
260+
{
261+
$request = Request::create('/user/123', 'GET');
262+
$request->headers->add(['X-Inertia' => 'true']);
263+
$request->headers->add(['X-Inertia-Partial-Component' => 'User/Edit']);
264+
$request->headers->add(['X-Inertia-Partial-Data' => 'auth.user,auth.refresh_token']);
265+
266+
$props = [
267+
'auth' => [
268+
'user' => new LazyProp(function () {
269+
return [
270+
'name' => 'Jonathan Reinink',
271+
'email' => '[email protected]',
272+
];
273+
}),
274+
'refresh_token' => 'value',
275+
'token' => 'value',
276+
],
277+
'shared' => [
278+
'flash' => 'Value',
279+
]
280+
];
281+
282+
$response = new Response('User/Edit', $props);
283+
$response = $response->toResponse($request);
284+
$page = $response->getData();
285+
286+
$this->assertFalse(isset($page->props->shared));
287+
$this->assertFalse(isset($page->props->auth->token));
288+
$this->assertSame('Jonathan Reinink', $page->props->auth->user->name);
289+
$this->assertSame('[email protected]', $page->props->auth->user->email);
290+
$this->assertSame('value', $page->props->auth->refresh_token);
291+
}
292+
258293
public function test_lazy_props_are_not_included_by_default(): void
259294
{
260295
$request = Request::create('/users', 'GET');

0 commit comments

Comments
 (0)