Skip to content

Commit 6ff03eb

Browse files
authored
Merge pull request #36 from tonysm/invalid-forms-with-422
Change the default invalid form handling to follow redirects internally
2 parents 4b84ffc + a369b3e commit 6ff03eb

File tree

7 files changed

+197
-27
lines changed

7 files changed

+197
-27
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ php artisan turbo:install --jet --stimulus
7272

7373
The package ships with a middleware which applies some conventions on your redirects, specially around how failed validations are handled automatically by Laravel. Read more about this in the [Conventions](#conventions) section of the documentation.
7474

75-
You may add the middleware to the "web" route group on your HTTP Kernel:
75+
**The middleware is automatically prepended to your web route group middleware stack**. You may want to add the middleware to other groups, when doing so, make sure it's at the top of the middleware stack:
7676

7777
```php
7878
\Tonysm\TurboLaravel\Http\Middleware\TurboMiddleware::class,
@@ -90,8 +90,8 @@ class Kernel extends HttpKernel
9090
{
9191
protected $middlewareGroups = [
9292
'web' => [
93-
// ...
9493
\Tonysm\TurboLaravel\Http\Middleware\TurboMiddleware::class,
94+
// other middlewares...
9595
],
9696
];
9797
}

src/Facades/Turbo.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
* @method static bool shouldBroadcastToOthers
1818
* @method static string domId(Model $model, string $prefix = "")
1919
* @method static Broadcaster broadcaster()
20+
* @method static self withoutRegisteringMiddleware()
21+
* @method static bool shouldRegisterMiddleware()
2022
*/
2123
class Turbo extends Facade
2224
{

src/Http/Middleware/RouteRedirectGuesser.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66

77
class RouteRedirectGuesser
88
{
9-
public function guess(string $routeName): string
9+
public function guess(string $routeName): ?string
1010
{
11+
if (! Str::endsWith($routeName, '.store') && ! Str::endsWith($routeName, '.update')) {
12+
return null;
13+
}
14+
1115
$creating = Str::endsWith($routeName, '.store');
1216

1317
$lookFor = $creating

src/Http/Middleware/TurboMiddleware.php

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
namespace Tonysm\TurboLaravel\Http\Middleware;
44

55
use Closure;
6+
use Illuminate\Contracts\Http\Kernel;
67
use Illuminate\Http\RedirectResponse;
78
use Illuminate\Http\Request;
9+
use Illuminate\Http\Response;
10+
use Illuminate\Support\Facades\App;
11+
use Illuminate\Support\Facades\Facade;
812
use Illuminate\Support\Facades\Route;
913
use Illuminate\Support\Str;
1014
use Illuminate\Validation\ValidationException;
15+
use Symfony\Component\HttpFoundation\Cookie;
1116
use Tonysm\TurboLaravel\Facades\Turbo as TurboFacade;
1217
use Tonysm\TurboLaravel\Turbo;
1318

@@ -16,6 +21,13 @@ class TurboMiddleware
1621
/** @var \Tonysm\TurboLaravel\Http\Middleware\RouteRedirectGuesser */
1722
private $redirectGuesser;
1823

24+
/**
25+
* Encrypted cookies to be added to the internal requests following redirects.
26+
*
27+
* @var array
28+
*/
29+
private array $encryptedCookies;
30+
1931
public function __construct(RouteRedirectGuesser $redirectGuesser)
2032
{
2133
$this->redirectGuesser = $redirectGuesser;
@@ -28,6 +40,8 @@ public function __construct(RouteRedirectGuesser $redirectGuesser)
2840
*/
2941
public function handle($request, Closure $next)
3042
{
43+
$this->encryptedCookies = $request->cookies->all();
44+
3145
if ($this->turboNativeVisit($request)) {
3246
TurboFacade::setVisitingFromTurboNative();
3347
}
@@ -59,21 +73,79 @@ private function turboResponse($response, Request $request)
5973
return $response;
6074
}
6175

62-
// Turbo expects a 303 redirect. We are also changing the default behavior of Laravel's failed
63-
// validation redirection to send the user to a page where the form of the current resource
64-
// is rendered (instead of just "back"), since Frames could have been used in many pages.
76+
// We get the response's encrypted cookies and merge them with the
77+
// encrypted cookies of the first request to make sure that are
78+
// sub-sequent request will use the most up-to-date values.
79+
80+
$responseCookies = collect($response->headers->getCookies())
81+
->mapWithKeys(fn (Cookie $cookie) => [$cookie->getName() => $cookie->getValue()])
82+
->all();
83+
84+
$this->encryptedCookies = array_replace_recursive($this->encryptedCookies, $responseCookies);
85+
86+
// When throwing a ValidationException and the app uses named routes convention, we can guess
87+
// the form route for the current endpoint, make an internal request there, and return the
88+
// response body with the form over a 422 status code, which is better for Turbo Native.
89+
90+
if ($response->exception instanceof ValidationException && ($formRedirectUrl = $this->getRedirectUrl($request, $response))) {
91+
$response->setTargetUrl($formRedirectUrl);
92+
93+
return tap($this->handleRedirectInternally($request, $response), function () use ($request) {
94+
App::instance('request', $request);
95+
Facade::clearResolvedInstance('request');
96+
});
97+
}
98+
99+
return $response->setStatusCode(303);
100+
}
101+
102+
private function getRedirectUrl($request, $response)
103+
{
104+
if ($response->exception->redirectTo) {
105+
return $response->exception->redirectTo;
106+
}
107+
108+
return $this->guessFormRedirectUrl($request);
109+
}
65110

66-
$response->setStatusCode(303);
111+
private function kernel(): Kernel
112+
{
113+
return App::make(Kernel::class);
114+
}
67115

68-
if ($response->exception instanceof ValidationException && ! $response->exception->redirectTo) {
69-
$response->setTargetUrl(
70-
$this->guessRedirectingRoute($request) ?: $response->getTargetUrl()
116+
/**
117+
* @param Request $request
118+
* @param Response $response
119+
*
120+
* @return Response
121+
*/
122+
private function handleRedirectInternally($request, $response)
123+
{
124+
$kernel = $this->kernel();
125+
126+
do {
127+
$response = $kernel->handle(
128+
$request = $this->createRequestFrom($response->headers->get('Location'), $request)
71129
);
130+
} while ($response->isRedirect());
131+
132+
if ($response->isOk()) {
133+
$response->setStatusCode(422);
72134
}
73135

74136
return $response;
75137
}
76138

139+
private function createRequestFrom(string $url, Request $baseRequest)
140+
{
141+
$request = Request::create($url, 'GET');
142+
143+
$request->headers->replace($baseRequest->headers->all());
144+
$request->cookies->replace($this->encryptedCookies);
145+
146+
return $request;
147+
}
148+
77149
/**
78150
* @param \Illuminate\Http\Request $request
79151
* @return bool
@@ -86,7 +158,7 @@ private function turboVisit($request)
86158
/**
87159
* @param \Illuminate\Http\Request $request
88160
*/
89-
private function guessRedirectingRoute($request)
161+
private function guessFormRedirectUrl($request)
90162
{
91163
$route = $request->route();
92164
$name = optional($route)->getName();
@@ -99,7 +171,7 @@ private function guessRedirectingRoute($request)
99171

100172
// If the guessed route doesn't exist, send it back to wherever Laravel defaults to.
101173

102-
if (! Route::has($formRouteName)) {
174+
if (! $formRouteName || ! Route::has($formRouteName)) {
103175
return null;
104176
}
105177

src/Turbo.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ class Turbo
2424
*/
2525
private bool $broadcastToOthersOnly = false;
2626

27+
/**
28+
* Whether or not the turbo middleware should be automatically added to the "web" middleware group stack.
29+
*
30+
* @var bool
31+
*/
32+
private bool $registerMiddleware = true;
33+
2734
public function isTurboNativeVisit(): bool
2835
{
2936
return $this->visitFromTurboNative;
@@ -36,6 +43,18 @@ public function setVisitingFromTurboNative(): self
3643
return $this;
3744
}
3845

46+
public function withoutRegisteringMiddleware(): self
47+
{
48+
$this->registerMiddleware = true;
49+
50+
return $this;
51+
}
52+
53+
public function shouldRegisterMiddleware(): bool
54+
{
55+
return $this->registerMiddleware;
56+
}
57+
3958
/**
4059
* @param bool|Closure $toOthers
4160
*

src/TurboServiceProvider.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Http\Request;
66
use Illuminate\Support\Facades\Blade;
77
use Illuminate\Support\Facades\Response;
8+
use Illuminate\Support\Facades\Route;
89
use Illuminate\Support\ServiceProvider;
910
use Illuminate\Support\Str;
1011
use Illuminate\Testing\TestResponse;
@@ -14,6 +15,7 @@
1415
use Tonysm\TurboLaravel\Broadcasters\LaravelBroadcaster;
1516
use Tonysm\TurboLaravel\Commands\TurboInstallCommand;
1617
use Tonysm\TurboLaravel\Facades\Turbo as TurboFacade;
18+
use Tonysm\TurboLaravel\Http\Middleware\TurboMiddleware;
1719
use Tonysm\TurboLaravel\Http\MultiplePendingTurboStreamResponse;
1820
use Tonysm\TurboLaravel\Http\PendingTurboStreamResponse;
1921
use Tonysm\TurboLaravel\Http\TurboResponseFactory;
@@ -43,6 +45,10 @@ public function boot()
4345
$this->bindBladeMacros();
4446
$this->bindRequestAndResponseMacros();
4547
$this->bindTestResponseMacros();
48+
49+
if (TurboFacade::shouldRegisterMiddleware()) {
50+
Route::prependMiddlewareToGroup('web', TurboMiddleware::class);
51+
}
4652
}
4753

4854
public function register()

0 commit comments

Comments
 (0)