Skip to content

[9.x] Introduce Laravel Precognition #44339

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

Merged
merged 4 commits into from
Sep 29, 2022
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
8 changes: 7 additions & 1 deletion src/Illuminate/Foundation/Http/FormRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,14 @@ protected function getValidatorInstance()
*/
protected function createDefaultValidator(ValidationFactory $factory)
{
$rules = $this->container->call([$this, 'rules']);

if ($this->isPrecognitive()) {
$rules = $this->filterPrecognitiveRules($rules);
}

return $factory->make(
$this->validationData(), $this->container->call([$this, 'rules']),
$this->validationData(), $rules,
$this->messages(), $this->attributes()
)->stopOnFirstFailure($this->stopOnFirstFailure);
}
Expand Down
1 change: 1 addition & 0 deletions src/Illuminate/Foundation/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class Kernel implements KernelContract
* @var string[]
*/
protected $middlewarePriority = [
\Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace Illuminate\Foundation\Http\Middleware;

use Illuminate\Container\Container;
use Illuminate\Foundation\Routing\PrecognitionCallableDispatcher;
use Illuminate\Foundation\Routing\PrecognitionControllerDispatcher;
use Illuminate\Http\Response;
use Illuminate\Routing\Contracts\CallableDispatcher as CallableDispatcherContract;
use Illuminate\Routing\Contracts\ControllerDispatcher as ControllerDispatcherContract;

class HandlePrecognitiveRequests
{
/**
*The container instance.
*
* @var \Illuminate\Container\Container
*/
protected $container;

/**
* Create a new middleware instance.
*
* @param \Illuminate\Container\Container $container
* @return void
*/
public function __construct(Container $container)
{
$this->container = $container;
}

/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Illuminate\Http\Response
*/
public function handle($request, $next)
{
if (! $request->isAttemptingPrecognition()) {
return $this->appendVaryHeader($request, $next($request));
}

$this->prepareForPrecognition($request);

return tap($next($request), function ($response) use ($request) {
$this->appendVaryHeader($request, $response->header('Precognition', 'true'));
});
}

/**
* Prepare to handle a precognitive request.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
protected function prepareForPrecognition($request)
{
$request->attributes->set('precognitive', true);

$this->container->bind(CallableDispatcherContract::class, fn ($app) => new PrecognitionCallableDispatcher($app));
$this->container->bind(ControllerDispatcherContract::class, fn ($app) => new PrecognitionControllerDispatcher($app));
}

/**
* Append the appropriate "Vary" header to the given response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Response $response
* @return \Illuminate\Http\Response $response
*/
protected function appendVaryHeader($request, $response)
{
return $response->header('Vary', implode(', ', array_filter([
$response->headers->get('Vary'),
'Precognition',
])));
}
}
21 changes: 21 additions & 0 deletions src/Illuminate/Foundation/Precognition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Illuminate\Foundation;

class Precognition
{
/**
* Get the "after" validation hook that can be used for precognition requests.
*
* @param \Illuminate\Http\Request $request
* @return \Closure
*/
public static function afterValidationHook($request)
{
return function ($validator) use ($request) {
if ($validator->messages()->isEmpty() && $request->headers->has('Precognition-Validate-Only')) {
abort(204);
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Foundation\Console\CliDumper;
use Illuminate\Foundation\Http\HtmlDumper;
use Illuminate\Foundation\MaintenanceModeManager;
use Illuminate\Foundation\Precognition;
use Illuminate\Foundation\Vite;
use Illuminate\Http\Request;
use Illuminate\Log\Events\MessageLogged;
Expand Down Expand Up @@ -96,7 +97,15 @@ public function registerDumper()
public function registerRequestValidation()
{
Request::macro('validate', function (array $rules, ...$params) {
return validator()->validate($this->all(), $rules, ...$params);
$rules = $this->isPrecognitive()
? $this->filterPrecognitiveRules($rules)
: $rules;

return tap(validator($this->all(), $rules, ...$params), function ($validator) {
if ($this->isPrecognitive()) {
$validator->after(Precognition::afterValidationHook($this));
}
})->validate();
});

Request::macro('validateWithBag', function (string $errorBag, array $rules, ...$params) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Illuminate\Foundation\Routing;

use Illuminate\Routing\CallableDispatcher;
use Illuminate\Routing\Route;

class PrecognitionCallableDispatcher extends CallableDispatcher
{
/**
* Dispatch a request to a given callable.
*
* @param \Illuminate\Routing\Route $route
* @param callable $callable
* @return mixed
*/
public function dispatch(Route $route, $callable)
{
$this->resolveParameters($route, $callable);

abort(204);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Illuminate\Foundation\Routing;

use Illuminate\Routing\ControllerDispatcher;
use Illuminate\Routing\Route;
use RuntimeException;

class PrecognitionControllerDispatcher extends ControllerDispatcher
{
/**
* Dispatch a request to a given controller and method.
*
* @param \Illuminate\Routing\Route $route
* @param mixed $controller
* @param string $method
* @return void
*/
public function dispatch(Route $route, $controller, $method)
{
$this->ensureMethodExists($controller, $method);

$this->resolveParameters($route, $controller, $method);

abort(204);
}

/**
* Ensure that the given method exists on the controller.
*
* @param object $controller
* @param string $method
* @return $this
*/
protected function ensureMethodExists($controller, $method)
{
if (method_exists($controller, $method)) {
return $this;
}

$class = $controller::class;

throw new RuntimeException("Attempting to predict the outcome of the [{$class}::{$method}()] method but the method is not defined.");
}
}
31 changes: 27 additions & 4 deletions src/Illuminate/Foundation/Validation/ValidatesRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Illuminate\Foundation\Validation;

use Illuminate\Contracts\Validation\Factory;
use Illuminate\Foundation\Precognition;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

Expand All @@ -22,10 +23,22 @@ public function validateWith($validator, Request $request = null)
$request = $request ?: request();

if (is_array($validator)) {
$validator = $this->getValidationFactory()->make($request->all(), $validator);
$rules = $request->isPrecognitive()
? $request->filterPrecognitiveRules($validator)
: $validator;

$validator = $this->getValidationFactory()->make($request->all(), $rules);
} elseif ($request->isPrecognitive()) {
$validator->setRules(
$request->filterPrecognitiveRules($validator->getRules())
);
}

return $validator->validate();
return tap($validator, function ($validator) use ($request) {
if ($request->isPrecognitive()) {
$validator->after(Precognition::afterValidationHook($request));
}
})->validate();
}

/**
Expand All @@ -42,9 +55,19 @@ public function validateWith($validator, Request $request = null)
public function validate(Request $request, array $rules,
array $messages = [], array $customAttributes = [])
{
return $this->getValidationFactory()->make(
$rules = $request->isPrecognitive()
? $request->filterPrecognitiveRules($rules)
: $rules;

$validator = $this->getValidationFactory()->make(
$request->all(), $rules, $messages, $customAttributes
)->validate();
);

return tap($validator, function ($validator) use ($request) {
if ($request->isPrecognitive()) {
$validator->after(Precognition::afterValidationHook($request));
}
})->validate();
}

/**
Expand Down
30 changes: 30 additions & 0 deletions src/Illuminate/Foundation/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Illuminate\Foundation\Mix;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Queue\CallQueuedClosure;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\HtmlString;
use Symfony\Component\HttpFoundation\Response;
Expand Down Expand Up @@ -601,6 +602,35 @@ function policy($class)
}
}

if (! function_exists('precognitive')) {
/**
* Handle a Precognition controller hook.
*
* @param null|callable $callable
* @return mixed
*/
function precognitive($callable = null)
{
$callable ??= function () {
//
};

$payload = $callable(function ($default, $precognition = null) {
$response = request()->isPrecognitive()
? ($precognition ?? $default)
: $default;

abort(Router::toResponse(request(), value($response)));
});

if (request()->isPrecognitive()) {
abort(204);
}

return $payload;
}
}

if (! function_exists('public_path')) {
/**
* Get the path to the public folder.
Expand Down
45 changes: 45 additions & 0 deletions src/Illuminate/Http/Concerns/CanBePrecognitive.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Illuminate\Http\Concerns;

use Illuminate\Support\Collection;

trait CanBePrecognitive
{
/**
* Filter the given array of rules into an array of rules that are included in precognitive headers.
*
* @param array $rules
* @return array
*/
public function filterPrecognitiveRules($rules)
{
if (! $this->headers->has('Precognition-Validate-Only')) {
return $rules;
}

return Collection::make($rules)
->only(explode(',', $this->header('Precognition-Validate-Only')))
->all();
}

/**
* Determine if the request is attempting to be precognitive.
*
* @return bool
*/
public function isAttemptingPrecognition()
{
return $this->header('Precognition') === 'true';
}

/**
* Determine if the request is precognitive.
*
* @return bool
*/
public function isPrecognitive()
{
return $this->attributes->get('precognitive', false);
}
}
3 changes: 2 additions & 1 deletion src/Illuminate/Http/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
*/
class Request extends SymfonyRequest implements Arrayable, ArrayAccess
{
use Concerns\InteractsWithContentTypes,
use Concerns\CanBePrecognitive,
Concerns\InteractsWithContentTypes,
Concerns\InteractsWithFlashData,
Concerns\InteractsWithInput,
Macroable;
Expand Down
Loading