diff --git a/src/Inertia.php b/src/Inertia.php index a967ed8f..c5986616 100644 --- a/src/Inertia.php +++ b/src/Inertia.php @@ -18,6 +18,9 @@ * @method static void mixin(object $mixin, bool $replace = true) * @method static bool hasMacro(string $name) * @method static void flushMacros() + * @method static void persist(string|array|\Illuminate\Contracts\Support\Arrayable $props) + * @method static array getPersisted() + * @method static void flushPersisted() * * @see \Inertia\ResponseFactory */ diff --git a/src/Middleware.php b/src/Middleware.php index bf7f22de..fcb98f4e 100644 --- a/src/Middleware.php +++ b/src/Middleware.php @@ -19,6 +19,13 @@ class Middleware */ protected $rootView = 'app'; + /** + * The properties that should always be included on Inertia responses, regardless of "only" or "except" requests. + * + * @var array + */ + protected $persisted = []; + /** * Determines the current asset version. * @@ -83,6 +90,7 @@ public function handle(Request $request, Closure $next) }); Inertia::share($this->share($request)); + Inertia::persist($this->persisted); Inertia::setRootView($this->rootView($request)); $response = $next($request); diff --git a/src/Response.php b/src/Response.php index 035f4ced..17772ed5 100644 --- a/src/Response.php +++ b/src/Response.php @@ -23,6 +23,7 @@ class Response implements Responsable protected $component; protected $props; + protected $persisted; protected $rootView; protected $version; protected $viewData = []; @@ -30,10 +31,11 @@ class Response implements Responsable /** * @param array|Arrayable $props */ - public function __construct(string $component, $props, string $rootView = 'app', string $version = '') + public function __construct(string $component, array $props, string $rootView = 'app', string $version = '', array $persisted = []) { $this->component = $component; $this->props = $props instanceof Arrayable ? $props->toArray() : $props; + $this->persisted = $persisted; $this->rootView = $rootView; $this->version = $version; } @@ -158,7 +160,10 @@ public function resolveArrayableProperties(array $props, Request $request, bool */ public function resolveOnly(Request $request, array $props): array { - $only = array_filter(explode(',', $request->header(Header::PARTIAL_ONLY, ''))); + $only = array_merge( + array_filter(explode(',', $request->header(Header::PARTIAL_ONLY, ''))), + $this->persisted + ); $value = []; diff --git a/src/ResponseFactory.php b/src/ResponseFactory.php index 4fac273a..3331d0d2 100644 --- a/src/ResponseFactory.php +++ b/src/ResponseFactory.php @@ -24,6 +24,9 @@ class ResponseFactory /** @var array */ protected $sharedProps = []; + /** @var array */ + protected $persisted = []; + /** @var Closure|string|null */ protected $version; @@ -66,6 +69,30 @@ public function flushShared(): void $this->sharedProps = []; } + /** + * @param string|array|Arrayable $props + */ + public function persist($props): void + { + if (is_array($props)) { + $this->persisted = array_merge($this->persisted, $props); + } elseif ($props instanceof Arrayable) { + $this->persisted = array_merge($this->persisted, $props->toArray()); + } else { + $this->persisted[] = $props; + } + } + + public function getPersisted(): array + { + return $this->persisted; + } + + public function flushPersisted(): void + { + $this->persisted = []; + } + /** * @param Closure|string|null $version */ @@ -101,7 +128,8 @@ public function render(string $component, $props = []): Response $component, array_merge($this->sharedProps, $props), $this->rootView, - $this->getVersion() + $this->getVersion(), + $this->persisted ); } diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index 027cd93d..b5df6d9f 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -239,10 +239,39 @@ public function rootView(Request $request): string $response->assertViewIs('welcome'); } - private function prepareMockEndpoint($version = null, $shared = [], $middleware = null): \Illuminate\Routing\Route + public function test_middleware_can_set_persisted_properties(): void + { + $shared = [ + 'shared' => [ + 'flash' => 'The user has been updated.' + ] + ]; + + $this->prepareMockEndpoint(null, $shared, null, ['shared']); + + $response = $this->get('/', [ + 'X-Inertia' => 'true', + 'X-Inertia-Partial-Component' => 'User/Edit', + 'X-Inertia-Partial-Data' => 'user' + ]); + + $response->assertOk(); + $response->assertJson([ + 'props' => [ + 'shared' => [ + 'flash' => 'The user has been updated.' + ], + 'user' => [ + 'name' => 'Jonathan', + ] + ] + ]); + } + + private function prepareMockEndpoint($version = null, $shared = [], $middleware = null, $persisted = []): \Illuminate\Routing\Route { if (is_null($middleware)) { - $middleware = new ExampleMiddleware($version, $shared); + $middleware = new ExampleMiddleware($version, $shared, $persisted); } return Route::middleware(StartSession::class)->get('/', function (Request $request) use ($middleware) { diff --git a/tests/ResponseFactoryTest.php b/tests/ResponseFactoryTest.php index 482c64fe..1a1ed4c9 100644 --- a/tests/ResponseFactoryTest.php +++ b/tests/ResponseFactoryTest.php @@ -150,6 +150,22 @@ public function test_can_flush_shared_data(): void $this->assertSame([], Inertia::getShared()); } + public function test_can_persist_properties(): void + { + Inertia::persist('auth.user'); + $this->assertSame(['auth.user'], Inertia::getPersisted()); + Inertia::persist(['posts']); + $this->assertSame(['auth.user', 'posts'], Inertia::getPersisted()); + } + + public function test_can_flush_persisted_data(): void + { + Inertia::persist('auth.user'); + $this->assertSame(['auth.user'], Inertia::getPersisted()); + Inertia::flushPersisted(); + $this->assertSame([], Inertia::getPersisted()); + } + public function test_can_create_lazy_prop(): void { $factory = new ResponseFactory(); diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 6be28af6..2d8cf286 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -326,6 +326,40 @@ public function test_lazy_props_are_included_in_partial_reload(): void $this->assertSame('A lazy value', $page->props->lazy); } + public function test_persist_props_on_partial_reload(): void + { + $request = Request::create('/user/123', 'GET'); + $request->headers->add(['X-Inertia' => 'true']); + $request->headers->add(['X-Inertia-Partial-Component' => 'User/Edit']); + $request->headers->add(['X-Inertia-Partial-Data' => 'data']); + + $props = [ + 'auth' => [ + 'user' => new LazyProp(function () { + return [ + 'name' => 'Jonathan Reinink', + 'email' => 'jonathan@example.com', + ]; + }), + 'token' => 'value', + ], + 'data' => [ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@example.com', + ] + ]; + + $response = new Response('User/Edit', $props, 'app', '123', ['auth.user']); + $response = $response->toResponse($request); + $page = $response->getData(); + + $this->assertFalse(isset($page->props->auth->token)); + $this->assertSame('Jonathan Reinink', $page->props->auth->user->name); + $this->assertSame('jonathan@example.com', $page->props->auth->user->email); + $this->assertSame('Taylor Otwell', $page->props->data->name); + $this->assertSame('taylor@example.com', $page->props->data->email); + } + public function test_top_level_dot_props_get_unpacked(): void { $props = [ diff --git a/tests/Stubs/ExampleMiddleware.php b/tests/Stubs/ExampleMiddleware.php index 16ef7885..3c3822ef 100644 --- a/tests/Stubs/ExampleMiddleware.php +++ b/tests/Stubs/ExampleMiddleware.php @@ -19,10 +19,16 @@ class ExampleMiddleware extends Middleware */ protected $shared = []; - public function __construct($version = null, $shared = []) + /** + * @var array + */ + protected $persisted = []; + + public function __construct($version = null, $shared = [], $persisted = []) { $this->version = $version; $this->shared = $shared; + $this->persisted = $persisted; } /**