Skip to content

Commit 756bc2b

Browse files
authored
[3.x] Exception handling via Inertia::handleExceptionsUsing() (#823)
* Exception handling via `Inertia::handleExceptionsUsing()` * Added `usingMiddleware` method
1 parent 981f70f commit 756bc2b

5 files changed

Lines changed: 399 additions & 0 deletions

File tree

src/ExceptionResponse.php

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
namespace Inertia;
4+
5+
use Illuminate\Contracts\Http\Kernel as KernelContract;
6+
use Illuminate\Contracts\Support\Responsable;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Routing\Router;
9+
use Symfony\Component\HttpFoundation\Response;
10+
use Throwable;
11+
12+
class ExceptionResponse implements Responsable
13+
{
14+
protected ?string $component = null;
15+
16+
/** @var array<string, mixed> */
17+
protected array $props = [];
18+
19+
protected bool $includeSharedData = false;
20+
21+
protected ?string $rootView = null;
22+
23+
/** @var class-string<Middleware>|null */
24+
protected ?string $middlewareClass = null;
25+
26+
public function __construct(
27+
public readonly Throwable $exception,
28+
public readonly Request $request,
29+
public readonly Response $response,
30+
protected readonly Router $router,
31+
protected readonly KernelContract $kernel,
32+
) {}
33+
34+
/**
35+
* @param array<string, mixed> $props
36+
*/
37+
public function render(string $component, array $props = []): static
38+
{
39+
$this->component = $component;
40+
$this->props = $props;
41+
42+
return $this;
43+
}
44+
45+
/**
46+
* @param class-string<Middleware> $middlewareClass
47+
*/
48+
public function usingMiddleware(string $middlewareClass): static
49+
{
50+
$this->middlewareClass = $middlewareClass;
51+
52+
return $this;
53+
}
54+
55+
public function withSharedData(): static
56+
{
57+
$this->includeSharedData = true;
58+
59+
return $this;
60+
}
61+
62+
public function rootView(string $rootView): static
63+
{
64+
$this->rootView = $rootView;
65+
66+
return $this;
67+
}
68+
69+
public function statusCode(): int
70+
{
71+
return $this->response->getStatusCode();
72+
}
73+
74+
/**
75+
* @param \Illuminate\Http\Request $request
76+
*/
77+
public function toResponse($request): Response
78+
{
79+
if ($this->component === null) {
80+
return $this->response;
81+
}
82+
83+
$middleware = $this->resolveMiddleware();
84+
85+
if ($middleware) {
86+
Inertia::version(fn () => $middleware->version($this->request));
87+
Inertia::setRootView($this->rootView ?? $middleware->rootView($this->request));
88+
} elseif ($this->rootView) {
89+
Inertia::setRootView($this->rootView);
90+
}
91+
92+
if ($this->includeSharedData && $middleware) {
93+
Inertia::share($middleware->share($this->request));
94+
95+
foreach ($middleware->shareOnce($this->request) as $key => $value) {
96+
if ($value instanceof OnceProp) {
97+
Inertia::share($key, $value);
98+
} else {
99+
Inertia::shareOnce($key, $value);
100+
}
101+
}
102+
}
103+
104+
return Inertia::render($this->component, $this->props)
105+
->toResponse($this->request)
106+
->setStatusCode($this->response->getStatusCode());
107+
}
108+
109+
protected function resolveMiddleware(): ?Middleware
110+
{
111+
if ($this->middlewareClass) {
112+
return app($this->middlewareClass);
113+
}
114+
115+
$class = $this->resolveMiddlewareFromRoute() ?? $this->resolveMiddlewareFromKernel();
116+
117+
if ($class) {
118+
return app($class);
119+
}
120+
121+
return null;
122+
}
123+
124+
/**
125+
* @return class-string<Middleware>|null
126+
*/
127+
protected function resolveMiddlewareFromRoute(): ?string
128+
{
129+
$route = $this->request->route();
130+
131+
if (! $route) {
132+
return null;
133+
}
134+
135+
foreach ($this->router->gatherRouteMiddleware($route) as $middleware) {
136+
if (! is_string($middleware)) {
137+
continue;
138+
}
139+
140+
$class = head(explode(':', $middleware));
141+
142+
if (is_a($class, Middleware::class, true)) {
143+
return $class;
144+
}
145+
}
146+
147+
return null;
148+
}
149+
150+
/**
151+
* @return class-string<Middleware>|null
152+
*/
153+
protected function resolveMiddlewareFromKernel(): ?string
154+
{
155+
foreach ($this->kernel->getMiddlewareGroups() as $group) {
156+
foreach ($group as $middleware) {
157+
if (is_string($middleware) && is_a($middleware, Middleware::class, true)) {
158+
return $middleware;
159+
}
160+
}
161+
}
162+
163+
return null;
164+
}
165+
}

src/Inertia.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* @method static \Inertia\Response render(string $component, array<array-key, mixed>|\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|\Inertia\ProvidesInertiaProperties $props = [])
2525
* @method static \Illuminate\Http\RedirectResponse back(int $status = 302, array<string, string> $headers = [], mixed $fallback = false)
2626
* @method static \Symfony\Component\HttpFoundation\Response location(string|\Symfony\Component\HttpFoundation\RedirectResponse $url)
27+
* @method static void handleExceptionsUsing(callable $callback)
2728
* @method static \Inertia\ResponseFactory flash(string|array<string, mixed> $key, mixed $value = null)
2829
* @method static array<string, mixed> getFlashed(?\Illuminate\Http\Request $request = null)
2930
* @method static void withoutSsr(array<int, string>|string $paths)

src/ResponseFactory.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
use BackedEnum;
66
use Closure;
7+
use Illuminate\Contracts\Debug\ExceptionHandler;
8+
use Illuminate\Contracts\Http\Kernel;
79
use Illuminate\Contracts\Support\Arrayable;
810
use Illuminate\Http\Request as HttpRequest;
11+
use Illuminate\Routing\Router;
912
use Illuminate\Support\Arr;
1013
use Illuminate\Support\Facades\App;
1114
use Illuminate\Support\Facades\Redirect;
@@ -334,6 +337,31 @@ public function location($url): SymfonyResponse
334337
return $url instanceof RedirectResponse ? $url : Redirect::away($url);
335338
}
336339

340+
/**
341+
* Register a callback to handle HTTP exceptions for Inertia requests.
342+
*/
343+
public function handleExceptionsUsing(callable $callback): void
344+
{
345+
/** @var \Illuminate\Foundation\Exceptions\Handler $handler */
346+
$handler = app(ExceptionHandler::class);
347+
348+
$handler->respondUsing(function ($response, $e, $request) use ($callback) {
349+
$result = $callback(new ExceptionResponse(
350+
$e,
351+
$request,
352+
$response,
353+
app(Router::class),
354+
app(Kernel::class),
355+
));
356+
357+
if ($result instanceof ExceptionResponse) {
358+
return $result->toResponse($request);
359+
}
360+
361+
return $result ?? $response;
362+
});
363+
}
364+
337365
/**
338366
* Flash data to be included with the next response. Unlike regular props,
339367
* flash data is not persisted in the browser's history state, making it

0 commit comments

Comments
 (0)