Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Inertia.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
* @method static \Inertia\MergeProp merge(mixed $value)
* @method static \Inertia\MergeProp deepMerge(mixed $value)
* @method static \Inertia\Response render(string $component, array<array-key, mixed>|\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|\Inertia\ProvidesInertiaProperties $props = [])
* @method static \Illuminate\Http\RedirectResponse back(int $status = 302, array<string, string> $headers = [], mixed $fallback = false)
* @method static \Symfony\Component\HttpFoundation\Response location(string|\Symfony\Component\HttpFoundation\RedirectResponse $url)
* @method static \Inertia\ResponseFactory flash(string|array<string, mixed> $key, mixed $value = null)
* @method static array<string, mixed> getFlashed(?\Illuminate\Http\Request $request = null)
* @method static void macro(string $name, object|callable $macro)
* @method static void mixin(object $mixin, bool $replace = true)
* @method static bool hasMacro(string $name)
Expand Down
14 changes: 14 additions & 0 deletions src/Middleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,23 @@ public function handle(Request $request, Closure $next)
$response->setStatusCode(303);
}

if ($response->isRedirect()) {
$this->reflash($request);
}

return $response;
}

/**
* Reflash the session data for the next request.
*/
protected function reflash(Request $request): void
{
if ($flashed = Inertia::getFlashed($request)) {
$request->session()->flash(SessionKey::FlashData->value, $flashed);
}
}

/**
* Handle empty responses.
*/
Expand Down
30 changes: 29 additions & 1 deletion src/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Inertia;

use BackedEnum;
use Carbon\CarbonInterval;
use Closure;
use GuzzleHttp\Promise\PromiseInterface;
Expand All @@ -16,6 +17,7 @@
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use Inertia\Support\Header;
use UnitEnum;

class Response implements Responsable
{
Expand Down Expand Up @@ -100,7 +102,7 @@ public function __construct(
$this->props = $props;
$this->rootView = $rootView;
$this->version = $version;
$this->clearHistory = session()->pull('inertia.clear_history', false);
$this->clearHistory = session()->pull(SessionKey::ClearHistory->value, false);
$this->encryptHistory = $encryptHistory;
$this->urlResolver = $urlResolver;
}
Expand Down Expand Up @@ -168,6 +170,19 @@ public function cache(string|array $cacheFor): self
return $this;
}

/**
* Add flash data to the response.
*
* @param \BackedEnum|\UnitEnum|string|array<string, mixed> $key
* @return $this
*/
public function flash(BackedEnum|UnitEnum|string|array $key, mixed $value = null): self
{
Inertia::flash($key, $value);

return $this;
}

/**
* Create an HTTP response that represents the object.
*
Expand All @@ -192,6 +207,7 @@ public function toResponse($request)
$this->resolveCacheDirections($request),
$this->resolveScrollProps($request),
$this->resolveOnceProps($request),
$this->resolveFlashData($request),
);

if ($request->header(Header::INERTIA)) {
Expand Down Expand Up @@ -712,6 +728,18 @@ public function resolveOnceProps(Request $request): array
return $onceProps->isNotEmpty() ? ['onceProps' => $onceProps->toArray()] : [];
}

/**
* Resolve flash data from the session.
*
* @return array<string, mixed>
*/
protected function resolveFlashData(Request $request): array
{
$flash = Inertia::getFlashed($request);

return $flash ? ['flash' => $flash] : [];
}

/**
* Determine if the request is an Inertia request.
*/
Expand Down
57 changes: 56 additions & 1 deletion src/ResponseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace Inertia;

use BackedEnum;
use Closure;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Http\Request as HttpRequest;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Redirect;
Expand All @@ -12,8 +14,10 @@
use Illuminate\Support\Traits\Macroable;
use Inertia\Support\Header;
use InvalidArgumentException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirect;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
use UnitEnum;

class ResponseFactory
{
Expand Down Expand Up @@ -154,7 +158,7 @@ public function resolveUrlUsing(?Closure $urlResolver = null): void
*/
public function clearHistory(): void
{
session(['inertia.clear_history' => true]);
session([SessionKey::ClearHistory->value => true]);
}

/**
Expand Down Expand Up @@ -309,4 +313,55 @@ public function location($url): SymfonyResponse

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

/**
* Flash data to be included with the next response. Unlike regular props,
* flash data is not persisted in the browser's history state, making it
* ideal for one-time notifications like toasts or highlights.
*
* @param \BackedEnum|\UnitEnum|string|array<string, mixed> $key
*/
public function flash(BackedEnum|UnitEnum|string|array $key, mixed $value = null): self
{
$flash = $key;

if (! is_array($key)) {
$key = match (true) {
$key instanceof BackedEnum => $key->value,
$key instanceof UnitEnum => $key->name,
default => $key,
};

$flash = [$key => $value];
}

session()->now(SessionKey::FlashData->value, [
...$this->getFlashed(),
...$flash,
]);

return $this;
}

/**
* Create a new redirect response to the previous location.
*
* @param array<string, string> $headers
*/
public function back(int $status = 302, array $headers = [], mixed $fallback = false): RedirectResponse
{
return Redirect::back($status, $headers, $fallback);
}

/**
* Retrieve the flashed data from the session.
*
* @return array<string, mixed>
*/
public function getFlashed(?HttpRequest $request = null): array
{
$request ??= request();

return $request->hasSession() ? $request->session()->get(SessionKey::FlashData->value, []) : [];
}
}
16 changes: 16 additions & 0 deletions src/SessionKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Inertia;

enum SessionKey: string
{
/*
* Session key for clearing the Inertia history.
*/
case ClearHistory = 'inertia.clear_history';

/**
* Session key for flash data.
*/
case FlashData = 'inertia.flash_data';
}
78 changes: 78 additions & 0 deletions tests/ResponseFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -635,4 +635,82 @@ public function test_once_prop_is_included_in_once_props_by_default(): void
],
]);
}

public function test_flash_data_is_flashed_to_session_on_redirect(): void
{
Route::middleware([StartSession::class, ExampleMiddleware::class])->post('/flash-test', function () {
return Inertia::flash(['message' => 'Success!'])->back();
});

$response = $this->post('/flash-test', [], [
'X-Inertia' => 'true',
]);

$response->assertRedirect();
$this->assertEquals(['message' => 'Success!'], session('inertia.flash_data'));
}

public function test_render_with_flash_includes_flash_in_page(): void
{
Route::middleware([StartSession::class, ExampleMiddleware::class])->post('/flash-test', function () {
return Inertia::flash('type', 'success')
->render('User/Edit', ['user' => 'Jonathan'])
->flash(['message' => 'User updated!']);
});

$response = $this->post('/flash-test', [], [
'X-Inertia' => 'true',
]);

$response->assertSuccessful();
$response->assertJson([
'component' => 'User/Edit',
'props' => [
'user' => 'Jonathan',
],
'flash' => [
'message' => 'User updated!',
'type' => 'success',
],
]);

// Flash data should not persist in session after being included in response
$this->assertNull(session('inertia.flash_data'));
}

public function test_render_without_flash_does_not_include_flash_key(): void
{
Route::middleware([StartSession::class, ExampleMiddleware::class])->get('/no-flash', function () {
return Inertia::render('User/Edit', ['user' => 'Jonathan']);
});

$response = $this->get('/no-flash', [
'X-Inertia' => 'true',
]);

$response->assertSuccessful();
$response->assertJson([
'component' => 'User/Edit',
]);
$response->assertJsonMissing(['flash']);
}

public function test_multiple_flash_calls_are_merged(): void
{
Route::middleware([StartSession::class, ExampleMiddleware::class])->post('/create', function () {
Inertia::flash('foo', 'value1');
Inertia::flash('bar', 'value2');

return Inertia::render('User/Show');
});

$response = $this->post('/create', [], ['X-Inertia' => 'true']);

$response->assertJson([
'flash' => [
'foo' => 'value1',
'bar' => 'value2',
],
]);
}
}