davestewart's avatar

Reducing code duplication on resource controllers

Bit of a long post, but hopefully an interesting area (and hopefully one which has been solved before) so I would appreciate the input :)

I'm mainly a front end guy, and if there's one thing I hate about back end code, it's the amount of repetition of mundane tasks. I find this especially apparent with the resource controller lifecycle.

The way I see it, you have 7 core tasks, each of which requires almost identical code across however many entities you have in your app:

          | Query | Paginate | View | Form | Validate | Errors | Redirect |
|---------|-------|----------|------|------|----------|--------|----------|
| Index   | x     | x        | x    |      |          |        |          |
| Show    | x     |          | x    |      |          |        |          |
| Create  |       |          | x    | x    | x        | x      |          |
| Edit    | x     |          | x    | x    | x        | x      |          |
| Update  | x     |          |      |      |          |        | x        |
| Store   | x     |          |      |      |          |        | x        |
| Destroy | x     |          |      |      |          |        | x        |

User input also requires management of:

  • Fields
  • Labels
  • Controls
  • Rules
  • Errors
  • Prompts

At the start of my most recent project I bit the bullet and developed a small set of service classes, meta objects, controller traits and default views to abstract away all the repetition.

It allows me to set up resource controllers with complete end-to-end functionality with as little code as this:

// ClientController

use CrudMethods;

public function __construct()
{
    $crud       = new CrudMeta('entities/clients', 'app\data\entities\Client'); // route, model
    $meta       = new ClientMeta();
    $this->crud = new CrudService($crud, $meta);
}

Controller methods, which are supplied by default by the CrudMethods trait, are of the form:

public function edit($id)
{
    return $this->crud->edit($id)->response;
}

The generic CrudService instance ($this->crud) for each resource gets its values from a Meta class, one of which must be created for each database entity. The Meta class' job is to hold all data pertaining to entities, forms, language, validation etc, in one place:

class ClientMeta extends ModelMeta
{
    // natural language for views
    public $singular    = 'client';
    public $plural      = 'clients';

    // validation, separate from model and controller
    public $rules =
    [
        'name'              => 'required|string|min:2|max:50',
    ];

    // default form controls 
    public $controls =
    [
        'name'              => 'text',
        'slug'              => 'text',
    ];

    // fields which should be available for each resource type
    public $fields =
    [
        'index'             => 'name slug',
        'create'            => 'name slug',
        'show'              => 'name slug',
        'edit'              => 'name',
    ];

}

With the parameters set in the constructor, the CrudMeta class for each entity, and the functionality in the CrudMethods traits (or overridden methods) the CrudService itself then has enough information to manage:

  • the 7 resource methods
  • querying
  • validation and errors
  • collating view data
  • returning responses

Finally, a set of default (and overridable) views handle the rendering of the data for the various resource types.

Overall, it all works great, and has until this point been flexible enough that I can override methods, or manipulate the flow when required, though some of the architecture choices around entities, querying, pagination, and DI are perhaps at this point less elegant than I had hoped.

Anyway, the main aim of this post is to get some feedback on this approach from those with more experience in this area:

  • I assume this problem has been solved like this before; what are the pros and cons?
  • Are there any existing libraries that have already solved this problem?
  • Is there a danger of this kind of approach reaching a limit, where the costs of abstraction outweigh the benefits of clarity?
  • by abstracting the functionality away from controllers, form requests etc, is there a danger of too much magic, fragility or tight-coupling?
  • any other thoughts on this kind of approach?

As I said, I'm mainly front end, so my back end work is never particularly specialised; my chief aim is to reduce the boilerplate that seems so prevalent and wasteful.

Thanks for reading, Cheers, Dave

0 likes
1 reply

Please or to participate in this conversation.