Implementing custom route validators for Laravel
Bài đăng này đã không được cập nhật trong 8 năm
In many cases default route matching system is not enough to provide required application code flexibility.
This article is a tutorial which explains how to use Laravel Route validators to extend router for two special cases:
- Matching route according to header information.
- Providing path-based locale detection on core level.
What Route validators are?
A Validator is a class which must implement ValidatorInterface
contain at least one method match
which returns boolean result, determining if provided request matches route definition.
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class CustomValidator implements ValidatorInterface
{
public function matches(Route $route, Request $request)
{
// Matching algorithm.
}
}
By default Laravel provides 4 validators:
MethodValidator
- matches HTTP request methodSchemeValidator
- matches HTTPS/HTTP schemaHostValidator
- matches domain for multi-domain applicationsUriValidator
- matches request URI by regex
Validators are stored in static $validators
variable of Illuminate\Routing\Route
class. This you can simply override in your service provider on application boot.
Route::$validators = array_merge(Route::getValidators(), [
$this->app->make(CustomValidator::class),
// any other validator constructor.
]);
Writing header validator
Some applications require using different route actions according to header information. The basic example is routing to different controllers according to Accept
header.
Let's try to route html
and json
requests to different controllers.
First we need to define our routes. The trick here is that Laravel supports not only default route configurations like middleware or prefix, but any custom ones.
Route::get('/output', [
'uses' => 'OutputController@html',
'accept' => 'text/html',
]);
Route::get('/output', [
'uses' => 'OutputController@json',
'accept' => 'application/json',
]);
As we can see the route will successfully store accept
key in action
definition. So let's use it in our validator.
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class AcceptValidator implements ValidatorInterface
{
public function matches(Route $route, Request $request)
{
$action = $route->getAction();
if (isset($action['accept'])) {
return $request->getAcceptableContentTypes()[0] == $action['accept'];
}
// Return TRUE in case route has no accept definition.
return true;
}
}
Of course, this is a very basic example and in real application it will require a lot of additional checks for customized headers.
After you connect this validator in your service provider, requests with different accept headers will be matched to different controller methods without additional logic required in controller.
Writing URI locale validator
Lots off applications require locale routing and determining locale from requested path. One of simplest ways to implement this is to define validator.
The difference from previous example is that we need to store matched locale to Route and exclude prefix from URI matching.
Moreover we also need to have some value to match all locales (e. g. an asterisk *
).
Route::get('/translated', [
'uses' => 'Controller@english',
'locale' => 'en',
]);
Route::get('/translated', [
'uses' => 'Controller@index',
'locale' => '*',
]);
So in our case request to /en/translated
will be routed to english
method of controller, while any other like /jp/translated
to index
method.
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
class LocaleUriValidator implements ValidatorInterface
{
public function matches(Route $route, Request $request)
{
$action = $route->getAction();
$path = explode(
$request->path() == '/' ? '/' : '/'.$request->path()
);
if (isset($action['locale'])) {
if ($action['locale'] === '*') {
return true;
}
if ($action['locale'] === $path[1]) {
$route->setParameter('locale', $locale);
unset($path[1]);
}
}
return preg_match(
$route->getCompiled()->getRegex(),
rawurldecode(implode('/', $path))
);
}
}
As far as Request object does not allow to modify path information in existing instance, we need to run matching of URI ourselves.
This validator can be added to your validators array instead of default UriValidator
or after it, depending on application logic.
What else can it be used for?
There are much more cases which may require custom validator usages. One of the best examples is version matching from Accept
header in Dingo API package.
But the main goal is to make route definitions more flexible and exclude unnecessary duplicate logic from controllers across the application.
All rights reserved