Skip to content

Commit

Permalink
Merge pull request #36 from tonysm/invalid-forms-with-422
Browse files Browse the repository at this point in the history
Change the default invalid form handling to follow redirects internally
  • Loading branch information
tonysm authored Nov 11, 2021
2 parents 4b84ffc + a369b3e commit 6ff03eb
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 27 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ php artisan turbo:install --jet --stimulus

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.

You may add the middleware to the "web" route group on your HTTP Kernel:
**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:

```php
\Tonysm\TurboLaravel\Http\Middleware\TurboMiddleware::class,
Expand All @@ -90,8 +90,8 @@ class Kernel extends HttpKernel
{
protected $middlewareGroups = [
'web' => [
// ...
\Tonysm\TurboLaravel\Http\Middleware\TurboMiddleware::class,
// other middlewares...
],
];
}
Expand Down
2 changes: 2 additions & 0 deletions src/Facades/Turbo.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
* @method static bool shouldBroadcastToOthers
* @method static string domId(Model $model, string $prefix = "")
* @method static Broadcaster broadcaster()
* @method static self withoutRegisteringMiddleware()
* @method static bool shouldRegisterMiddleware()
*/
class Turbo extends Facade
{
Expand Down
6 changes: 5 additions & 1 deletion src/Http/Middleware/RouteRedirectGuesser.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@

class RouteRedirectGuesser
{
public function guess(string $routeName): string
public function guess(string $routeName): ?string
{
if (! Str::endsWith($routeName, '.store') && ! Str::endsWith($routeName, '.update')) {
return null;
}

$creating = Str::endsWith($routeName, '.store');

$lookFor = $creating
Expand Down
90 changes: 81 additions & 9 deletions src/Http/Middleware/TurboMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
namespace Tonysm\TurboLaravel\Http\Middleware;

use Closure;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Cookie;
use Tonysm\TurboLaravel\Facades\Turbo as TurboFacade;
use Tonysm\TurboLaravel\Turbo;

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

/**
* Encrypted cookies to be added to the internal requests following redirects.
*
* @var array
*/
private array $encryptedCookies;

public function __construct(RouteRedirectGuesser $redirectGuesser)
{
$this->redirectGuesser = $redirectGuesser;
Expand All @@ -28,6 +40,8 @@ public function __construct(RouteRedirectGuesser $redirectGuesser)
*/
public function handle($request, Closure $next)
{
$this->encryptedCookies = $request->cookies->all();

if ($this->turboNativeVisit($request)) {
TurboFacade::setVisitingFromTurboNative();
}
Expand Down Expand Up @@ -59,21 +73,79 @@ private function turboResponse($response, Request $request)
return $response;
}

// Turbo expects a 303 redirect. We are also changing the default behavior of Laravel's failed
// validation redirection to send the user to a page where the form of the current resource
// is rendered (instead of just "back"), since Frames could have been used in many pages.
// We get the response's encrypted cookies and merge them with the
// encrypted cookies of the first request to make sure that are
// sub-sequent request will use the most up-to-date values.

$responseCookies = collect($response->headers->getCookies())
->mapWithKeys(fn (Cookie $cookie) => [$cookie->getName() => $cookie->getValue()])
->all();

$this->encryptedCookies = array_replace_recursive($this->encryptedCookies, $responseCookies);

// When throwing a ValidationException and the app uses named routes convention, we can guess
// the form route for the current endpoint, make an internal request there, and return the
// response body with the form over a 422 status code, which is better for Turbo Native.

if ($response->exception instanceof ValidationException && ($formRedirectUrl = $this->getRedirectUrl($request, $response))) {
$response->setTargetUrl($formRedirectUrl);

return tap($this->handleRedirectInternally($request, $response), function () use ($request) {
App::instance('request', $request);
Facade::clearResolvedInstance('request');
});
}

return $response->setStatusCode(303);
}

private function getRedirectUrl($request, $response)
{
if ($response->exception->redirectTo) {
return $response->exception->redirectTo;
}

return $this->guessFormRedirectUrl($request);
}

$response->setStatusCode(303);
private function kernel(): Kernel
{
return App::make(Kernel::class);
}

if ($response->exception instanceof ValidationException && ! $response->exception->redirectTo) {
$response->setTargetUrl(
$this->guessRedirectingRoute($request) ?: $response->getTargetUrl()
/**
* @param Request $request
* @param Response $response
*
* @return Response
*/
private function handleRedirectInternally($request, $response)
{
$kernel = $this->kernel();

do {
$response = $kernel->handle(
$request = $this->createRequestFrom($response->headers->get('Location'), $request)
);
} while ($response->isRedirect());

if ($response->isOk()) {
$response->setStatusCode(422);
}

return $response;
}

private function createRequestFrom(string $url, Request $baseRequest)
{
$request = Request::create($url, 'GET');

$request->headers->replace($baseRequest->headers->all());
$request->cookies->replace($this->encryptedCookies);

return $request;
}

/**
* @param \Illuminate\Http\Request $request
* @return bool
Expand All @@ -86,7 +158,7 @@ private function turboVisit($request)
/**
* @param \Illuminate\Http\Request $request
*/
private function guessRedirectingRoute($request)
private function guessFormRedirectUrl($request)
{
$route = $request->route();
$name = optional($route)->getName();
Expand All @@ -99,7 +171,7 @@ private function guessRedirectingRoute($request)

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

if (! Route::has($formRouteName)) {
if (! $formRouteName || ! Route::has($formRouteName)) {
return null;
}

Expand Down
19 changes: 19 additions & 0 deletions src/Turbo.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ class Turbo
*/
private bool $broadcastToOthersOnly = false;

/**
* Whether or not the turbo middleware should be automatically added to the "web" middleware group stack.
*
* @var bool
*/
private bool $registerMiddleware = true;

public function isTurboNativeVisit(): bool
{
return $this->visitFromTurboNative;
Expand All @@ -36,6 +43,18 @@ public function setVisitingFromTurboNative(): self
return $this;
}

public function withoutRegisteringMiddleware(): self
{
$this->registerMiddleware = true;

return $this;
}

public function shouldRegisterMiddleware(): bool
{
return $this->registerMiddleware;
}

/**
* @param bool|Closure $toOthers
*
Expand Down
6 changes: 6 additions & 0 deletions src/TurboServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
Expand All @@ -14,6 +15,7 @@
use Tonysm\TurboLaravel\Broadcasters\LaravelBroadcaster;
use Tonysm\TurboLaravel\Commands\TurboInstallCommand;
use Tonysm\TurboLaravel\Facades\Turbo as TurboFacade;
use Tonysm\TurboLaravel\Http\Middleware\TurboMiddleware;
use Tonysm\TurboLaravel\Http\MultiplePendingTurboStreamResponse;
use Tonysm\TurboLaravel\Http\PendingTurboStreamResponse;
use Tonysm\TurboLaravel\Http\TurboResponseFactory;
Expand Down Expand Up @@ -43,6 +45,10 @@ public function boot()
$this->bindBladeMacros();
$this->bindRequestAndResponseMacros();
$this->bindTestResponseMacros();

if (TurboFacade::shouldRegisterMiddleware()) {
Route::prependMiddlewareToGroup('web', TurboMiddleware::class);
}
}

public function register()
Expand Down
Loading

0 comments on commit 6ff03eb

Please sign in to comment.