Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change the default invalid form handling to follow redirects internally #36

Merged
merged 14 commits into from
Nov 11, 2021
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
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