diff --git a/CHANGELOG.md b/CHANGELOG.md index 28327d39..ab6fa08b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased](https://github.com/inertiajs/inertia-laravel/compare/v1.2.0...1.x) +- Add "always" props using new `Inertia::always()` wrapper ([#627](https://github.com/inertiajs/inertia-laravel/pull/627)) + ## [v1.2.0](https://github.com/inertiajs/inertia-laravel/compare/v1.1.0...v1.2.0) - 2024-05-17 * [1.x] Make commands lazy by [@timacdonald](https://github.com/timacdonald) in https://github.com/inertiajs/inertia-laravel/pull/601 diff --git a/src/AlwaysProp.php b/src/AlwaysProp.php new file mode 100644 index 00000000..5c9ee6ec --- /dev/null +++ b/src/AlwaysProp.php @@ -0,0 +1,24 @@ +value = $value; + } + + public function __invoke() + { + return is_callable($this->value) ? App::call($this->value) : $this->value; + } +} diff --git a/src/Inertia.php b/src/Inertia.php index c5986616..aee499ea 100644 --- a/src/Inertia.php +++ b/src/Inertia.php @@ -12,15 +12,13 @@ * @method static void version(\Closure|string|null $version) * @method static string getVersion() * @method static \Inertia\LazyProp lazy(callable $callback) + * @method static \Inertia\AlwaysProp always(mixed $value) * @method static \Inertia\Response render(string $component, array|\Illuminate\Contracts\Support\Arrayable $props = []) * @method static \Symfony\Component\HttpFoundation\Response location(string|\Symfony\Component\HttpFoundation\RedirectResponse $url) * @method static void macro(string $name, object|callable $macro) * @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 fcb98f4e..24caf9ad 100644 --- a/src/Middleware.php +++ b/src/Middleware.php @@ -19,13 +19,6 @@ 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. * @@ -60,9 +53,7 @@ public function version(Request $request) public function share(Request $request) { return [ - 'errors' => function () use ($request) { - return $this->resolveValidationErrors($request); - }, + 'errors' => Inertia::always($this->resolveValidationErrors($request)), ]; } @@ -90,7 +81,6 @@ 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 7e1373a9..fc7dcf04 100644 --- a/src/Response.php +++ b/src/Response.php @@ -23,7 +23,6 @@ class Response implements Responsable protected $component; protected $props; - protected $persisted; protected $rootView; protected $version; protected $viewData = []; @@ -31,11 +30,10 @@ class Response implements Responsable /** * @param array|Arrayable $props */ - public function __construct(string $component, array $props, string $rootView = 'app', string $version = '', array $persisted = []) + public function __construct(string $component, array $props, string $rootView = 'app', string $version = '') { $this->component = $component; $this->props = $props instanceof Arrayable ? $props->toArray() : $props; - $this->persisted = $persisted; $this->rootView = $rootView; $this->version = $version; } @@ -129,6 +127,8 @@ public function resolveProperties(Request $request, array $props): array $props = $this->resolveExcept($request, $props); } + $props = $this->resolveAlways($props); + $props = $this->resolvePropertyInstances($props, $request); return $props; @@ -164,10 +164,7 @@ public function resolveArrayableProperties(array $props, Request $request, bool */ public function resolveOnly(Request $request, array $props): array { - $only = array_merge( - array_filter(explode(',', $request->header(Header::PARTIAL_ONLY, ''))), - $this->persisted - ); + $only = array_filter(explode(',', $request->header(Header::PARTIAL_ONLY, ''))); $value = []; @@ -190,6 +187,21 @@ public function resolveExcept(Request $request, array $props): array return $props; } + /** + * Resolve `always` properties that should always be included on all visits, regardless of "only" or "except" requests. + */ + public function resolveAlways(array $props): array + { + $always = array_filter($this->props, static function ($prop) { + return $prop instanceof AlwaysProp; + }); + + return array_merge( + $always, + $props + ); + } + /** * Resolve all necessary class instances in the given props. */ @@ -204,6 +216,10 @@ public function resolvePropertyInstances(array $props, Request $request): array $value = App::call($value); } + if ($value instanceof AlwaysProp) { + $value = App::call($value); + } + if ($value instanceof PromiseInterface) { $value = $value->wait(); } diff --git a/src/ResponseFactory.php b/src/ResponseFactory.php index 3331d0d2..28144c7d 100644 --- a/src/ResponseFactory.php +++ b/src/ResponseFactory.php @@ -24,9 +24,6 @@ class ResponseFactory /** @var array */ protected $sharedProps = []; - /** @var array */ - protected $persisted = []; - /** @var Closure|string|null */ protected $version; @@ -69,30 +66,6 @@ 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 */ @@ -115,6 +88,14 @@ public function lazy(callable $callback): LazyProp return new LazyProp($callback); } + /** + * @param mixed $value + */ + public function always($value): AlwaysProp + { + return new AlwaysProp($value); + } + /** * @param array|Arrayable $props */ @@ -128,8 +109,7 @@ public function render(string $component, $props = []): Response $component, array_merge($this->sharedProps, $props), $this->rootView, - $this->getVersion(), - $this->persisted + $this->getVersion() ); } diff --git a/tests/AlwaysPropTest.php b/tests/AlwaysPropTest.php new file mode 100644 index 00000000..c1756e53 --- /dev/null +++ b/tests/AlwaysPropTest.php @@ -0,0 +1,47 @@ +assertSame('An always value', $alwaysProp()); + } + + public function test_can_accept_scalar_values(): void + { + $alwaysProp = new AlwaysProp('An always value'); + + $this->assertSame('An always value', $alwaysProp()); + } + + public function test_can_accept_callables(): void + { + $callable = new class { + public function __invoke() { + return 'An always value'; + } + }; + + $alwaysProp = new AlwaysProp($callable); + + $this->assertSame('An always value', $alwaysProp()); + } + + public function test_can_resolve_bindings_when_invoked(): void + { + $alwaysProp = new AlwaysProp(function (Request $request) { + return $request; + }); + + $this->assertInstanceOf(Request::class, $alwaysProp()); + } +} diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php index b5df6d9f..4fb9f5d4 100644 --- a/tests/MiddlewareTest.php +++ b/tests/MiddlewareTest.php @@ -2,7 +2,6 @@ namespace Inertia\Tests; -use Closure; use LogicException; use Inertia\Inertia; use Inertia\Middleware; @@ -13,6 +12,7 @@ use Illuminate\Support\Facades\Session; use Inertia\Tests\Stubs\ExampleMiddleware; use Illuminate\Session\Middleware\StartSession; +use Inertia\AlwaysProp; class MiddlewareTest extends TestCase { @@ -125,7 +125,7 @@ public function test_it_will_instruct_inertia_to_reload_on_a_version_mismatch(): public function test_validation_errors_are_registered_as_of_default(): void { Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { - $this->assertInstanceOf(Closure::class, Inertia::getShared('errors')); + $this->assertInstanceOf(AlwaysProp::class, Inertia::getShared('errors')); }); $this->withoutExceptionHandling()->get('/'); @@ -239,39 +239,10 @@ public function rootView(Request $request): string $response->assertViewIs('welcome'); } - 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 + private function prepareMockEndpoint($version = null, $shared = [], $middleware = null): \Illuminate\Routing\Route { if (is_null($middleware)) { - $middleware = new ExampleMiddleware($version, $shared, $persisted); + $middleware = new ExampleMiddleware($version, $shared); } return Route::middleware(StartSession::class)->get('/', function (Request $request) use ($middleware) { diff --git a/tests/ResponseFactoryTest.php b/tests/ResponseFactoryTest.php index 1a1ed4c9..b86754b9 100644 --- a/tests/ResponseFactoryTest.php +++ b/tests/ResponseFactoryTest.php @@ -15,6 +15,7 @@ use Illuminate\Session\Middleware\StartSession; use Illuminate\Session\NullSessionHandler; use Illuminate\Session\Store; +use Inertia\AlwaysProp; class ResponseFactoryTest extends TestCase { @@ -150,22 +151,6 @@ 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(); @@ -176,6 +161,16 @@ public function test_can_create_lazy_prop(): void $this->assertInstanceOf(LazyProp::class, $lazyProp); } + public function test_can_create_always_prop(): void + { + $factory = new ResponseFactory(); + $alwaysProp = $factory->always(function () { + return 'An always value'; + }); + + $this->assertInstanceOf(AlwaysProp::class, $alwaysProp); + } + public function test_will_accept_arrayabe_props() { Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/', function () { diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index eb925cbc..d36cdaae 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -2,7 +2,6 @@ namespace Inertia\Tests; -use Exception; use Mockery; use Inertia\LazyProp; use Inertia\Response; @@ -16,6 +15,7 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\ResourceCollection; +use Inertia\AlwaysProp; class ResponseTest extends TestCase { @@ -381,7 +381,7 @@ 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 + public function test_always_props_are_included_on_partial_reload(): void { $request = Request::create('/user/123', 'GET'); $request->headers->add(['X-Inertia' => 'true']); @@ -389,30 +389,29 @@ public function test_persist_props_on_partial_reload(): void $request->headers->add(['X-Inertia-Partial-Data' => 'data']); $props = [ - 'auth' => [ - 'user' => new LazyProp(function () { - return [ - 'name' => 'Jonathan Reinink', - 'email' => 'jonathan@example.com', - ]; - }), - 'token' => 'value', - ], + 'user' => new LazyProp(function () { + return [ + 'name' => 'Jonathan Reinink', + 'email' => 'jonathan@example.com', + ]; + }), 'data' => [ 'name' => 'Taylor Otwell', - 'email' => 'taylor@example.com', - ] + ], + 'errors' => new AlwaysProp(function () { + return [ + 'name' => 'The email field is required.' + ]; + }) ]; - $response = new Response('User/Edit', $props, 'app', '123', ['auth.user']); + $response = new Response('User/Edit', $props, 'app', '123'); $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('The email field is required.', $page->props->errors->name); $this->assertSame('Taylor Otwell', $page->props->data->name); - $this->assertSame('taylor@example.com', $page->props->data->email); + $this->assertFalse(isset($page->props->user)); } public function test_top_level_dot_props_get_unpacked(): void diff --git a/tests/Stubs/ExampleMiddleware.php b/tests/Stubs/ExampleMiddleware.php index 3c3822ef..16ef7885 100644 --- a/tests/Stubs/ExampleMiddleware.php +++ b/tests/Stubs/ExampleMiddleware.php @@ -19,16 +19,10 @@ class ExampleMiddleware extends Middleware */ protected $shared = []; - /** - * @var array - */ - protected $persisted = []; - - public function __construct($version = null, $shared = [], $persisted = []) + public function __construct($version = null, $shared = []) { $this->version = $version; $this->shared = $shared; - $this->persisted = $persisted; } /**