That ResourceRegistrar thingy

Lets extend.

Posted by lagbox on May 2, 2016 extending laravel resource registrar

That ResourceRegistrar thingy

The ResourceRegistrar is the class used to register your resource routes. Route::resource is creating an instance of ResourceRegister and passing arguments to its register method.

This class can be extended very easily for many purposes. The Route::resource method takes a 3rd argument which is an options array that can be leveraged at will to allow for new features. I keep an extended version of ResourceRegistrar and the Router itself in my projects with extra features I find useful.

Currently, as of Laravel 5.2.21, we have the ability to adjust the resource name that ends up being used as route parameters. This is done by using the options array that is passed to the Registrar.

Route::resource('users', 'UserController', [
    'parameters' => ['users' => 'person']
]);

// GET users/{person}
// GET users/{person}/edit
// ...

OR

Route::resource('users', 'UserController', [
    'parameters' => 'singular'
]);

// GET users/{user}
// GET users/{user}/edit
// ...

There is also the ability to define these globally, but it is out of the scope of this document.

You can easily extend this class and bind it to the IoC container and it will be used by the Router. This was designed to be extendable and the router itself checks if a binding exists, if not it will use the default ResourceRegistrar.

ResourceRegistrar parameter naming PR

Lets Extend

Lets say we want to be able to add the ability to add 'where' constraints to the parameters that end up being created for our resource routes. There currently is no way to do this at the resource level, one would have to define a global pattern, which may not be ideal.

Lets use the options array to pass along these constraints, then we will extend the ResourceRegistrar to handle this.

Route::resource('services', 'ServiceController', [
    'where' => ['services' => '[0-9]+']
]);

This isn't too shabby, we only allow a numeric to match, lets get onto adjusting the Registrar.

public function register($name, $controller, $options = [])
{
    ...

    foreach ($this->getResourceMethods($defaults, $options) as $m) {
        $this->addWheres(
            $this->{'addResource'.ucfirst($m)}($name, $base, $controller, $options),
            $options
        );
    }
}

We have adjusted the part of the register call that is involved with calling the methods that actually register the routes for a resource based on what 'defaults' we want. By default we get ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy'] routes defined. These method calls return the route that is created so we will use this to our advantage to be able to make calls to those routes to add the 'where' pattern to each.

/**
 * Add any parameter patterns to the route.
 *
 * @param  \Illuminate\Routing\Route $route
 * @param  array  $options
 * @return \Illuminate\Routing\Route
 */
protected function addWheres($route, array $options = [])
{
    if (empty($options['where'])) {
        return $route;
    }
    if (! isset($this->wheres)) {
        array_walk($options['where'], function ($item, $key) use (&$wheres) {
            $wheres[$this->getResourceWildcard($key)] = $item;
        });
        $this->wheres = $wheres;
    }
    return $route->where($this->wheres);
}

This method is checking the $options array to see if any 'where's were set and if so we will procede to process them. We check the parameter names against the getResourceWildcard call to make sure we have the correct parameter name, as they can be changed globally without our knowledge. Once we have matched those we will then apply the needed 'where' conditions to the route.

Gist version of above code

Last thing we need to do to allow for this to work is bind our extended ResourceRegistrar to the container. In a service provider:

public function register()
{
    $this->app->bind(
        \Illuminate\Routing\ResourceRegistrar::class,
        \App\Lib\ResourceRegistrar::class
    );
}

Gist for binding the extended ResourceRegistrar

Now the router will use our extended version of the ResourceRegistrar which allows our extended features. Upside? this is all backwards compatible. Since we are using the options array, we are not risking breaking anything. If your ResourceRegistrar isn't extended to handle these options, they will just be ignored. By break I mean won't error out.

PR to add this to the ResourceRegistrar

Another random problem solved

Someone had asked how can they have a certain route parameter added to their show routes. In this case they wanted a {slug} param added to the route definition but only for show routes. This is not difficult to do by overriding the method that registers the show route. Only issue is that the 'show' route is defined before 'edit' so the extra param would conflict with it. To get around this we adjust the order of the resource defaults variable.

Route::resource('something', 'SomethingController', ['slug' => true]);

// ResourceRegistrar

protected $resourceDefaults = ['index', 'create', 'store', 'edit', 'show', 'update', 'destroy'];

public function addResourceShow($name, $base, $controller, $options)
{
    $uri = $this->getResourceUri($name). '/{'. $base .'}';

    if (isset($options['slug']) && $options['slug']) {
        $uri .= '/{slug}';
    }

    $action = $this->getResourceAction($name, $controller, 'show', $options);

    return $this->router->get($uri, $action);
}

Gist version

Random stuff

Lets say we want a one-off ResourceRegistrar which only defines certain routes, and we dont feel like passing the options array to each resource call with the limited routes we want. We will only be using the 'index' and 'show' routes.

We can leverage PHP7's new anonymous class feature for this, if you so wish. We can create an anonymous class that extends ResourceRegistrar and call register on it instead of Route::resource.

use Illuminate\Routing\ResourceRegistrar;

$rr = new class (Route::getFacadeRoot()) extends ResourceRegistrar {
    protected $resourceDefaults = ['index', 'show'];
    protected $parameters = 'singular';
};

$rr->register('something', 'SomeController');
$rr->register('foos', 'FooController');
$rr->register('bars', 'BarController');

// GET something
// GET something/{something}
// GET foos
// GET foos/{foo}
// GET bars
// GET bars/{bar}

Notice

I will be updating this over time. Things will change, some might be PRed or adjusted.