Viblo CTF
0

Implementing custom route validators for Laravel

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:

  1. Matching route according to header information.
  2. 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 method
  • SchemeValidator - matches HTTPS/HTTP schema
  • HostValidator - matches domain for multi-domain applications
  • UriValidator - 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' => '[email protected]',
    'accept' => 'text/html',
]);

Route::get('/output', [
    'uses' => '[email protected]',
    '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' => '[email protected]',
    'locale' => 'en',
]);
Route::get('/translated', [
    'uses' => '[email protected]',
    '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

Viblo
Let's register a Viblo Account to get more interesting posts.