Skip to content

Commit

Permalink
Introduce Laravel Precognition
Browse files Browse the repository at this point in the history
  • Loading branch information
timacdonald committed Sep 28, 2022
1 parent 93ed7f4 commit 1344e2e
Show file tree
Hide file tree
Showing 22 changed files with 1,334 additions and 15 deletions.
9 changes: 8 additions & 1 deletion src/Illuminate/Foundation/Http/FormRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidatesWhenResolvedTrait;
use Illuminate\Validation\ValidationException;

Expand Down Expand Up @@ -109,8 +110,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 @@ -71,6 +71,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,82 @@
<?php

namespace Illuminate\Foundation\Http\Middleware;

use Illuminate\Container\Container;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Foundation\Precognition;
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',
])));
}
}
23 changes: 23 additions & 0 deletions src/Illuminate/Foundation/Precognition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Illuminate\Foundation;

use Illuminate\Http\Response;

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 @@ -4,6 +4,7 @@

use Illuminate\Contracts\Foundation\MaintenanceMode as MaintenanceModeContract;
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 @@ -73,7 +74,15 @@ public function register()
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,24 @@
<?php

namespace Illuminate\Foundation\Routing;

use Illuminate\Http\Response;
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,46 @@
<?php

namespace Illuminate\Foundation\Routing;

use Illuminate\Http\Response;
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

0 comments on commit 1344e2e

Please sign in to comment.