DRY resource controllers in Laravel
Tags: Laravel PHP
Resource controllers in Laravel provide a convenient way to set up the structure we need to handle CRUD operations for our data. To create a new resource controller, all we need is single command:
But let's say we've set up our views for creating and editing data, and now we want to save the contents of our form inputs to the database, via the
update() actions. Whether we're saving a new record or updating an old one, the process is usually the same:
- instantiate either a new object or an object corresponding to the existing database record
- validate the form data
- set the form data in the object
- save the object
- sync many-to-many relationships
- set user feedback in the session
- redirect to the next view
We could code this up individually for each
update() action in each controller. But if we coded it just once, it would save time and make errors less likely. The same is true for delete actions. In this post, I'll detail the process I'm currently working with to make things a bit more DRY. I'm assuming a basic familiarity with Laravel, object-oriented PHP and relational databases.
For the resource controller classes in your Laravel app, use a parent class that contains all the common form submission and deletion code that you'll need. For resources that require special treatment, individual functions can be overwritten in the child controller. See the sample code for the
GenericResourceController and an example of a child class.
The first step is to create a new controller class, say
GenericResourceController. We'll be using this as the parent class for all controllers that need to implement this approach to handling form submissions:
Individual object types in our app will each have a controller which is a child of
Requests, ids and objects
We need to set up a few important properties in the parent class. First, the form submission is passed into both the
update() actions as a
$request parameter. We'll be dividing our code into discrete functions, but we don't want to be playing parameter ping-pong with the request variable. Instead, we're going to declare a
$request property in
GenericResourceController, and set the value inside the
update() actions. Because
$request is now a property of the class, it will persist between function calls. The data from
$request is then going to be set in an object, so the second property we need is an
$object. As far as the parent class is concerned, it doesn't matter what type of object it is. That's for the child class to determine. For example, in
ProductController, the object would be a product, and would be an instance of the
App\Product model (see below). Finally, we need an
$id property to store the id from the
Everything that happens from now on, whether we're storing new data or updating an existing record, is virtually the same, so we create a new function,
processFormSubmission(), and call it in both
Configuring child properties
There are a number of properties that need to be customised in each child controller class. We'll set these as stubs in
GenericResourceController so we can just copy and paste them into the child. The first property is the name of the object model that we're using:
It's important to preface the model name with 'App\', otherwise it may not be recognised when we use it later on. Also, we mustn't forget to add the name of the model to the list of included classes above the child class, e.g.:Skip to navigation
Setting the object
We'll need a function which sets the object. If we're creating a new record in the database, we want a new, clean object, but if we're updating existing data, we want our object to correspond to an existing record, which we find by its id:Skip to navigation
Validating submitted data
The next step is to validate the form data. This requires another property that gets overwritten in the child class, an array of field names and their validation rules:
In the child classes, we'll populate the
$fields property like so:
It's important to include field names even if there are no associated validation rules, as in
description in the example above. The reason is that we'll be using these field names later when it comes to setting and saving data in the object.
Next, we need to do the validation. We create a function to do this, and call it in
processFormSubmission(). If the app is using URL-friendly slugs rather than ids, we can add slug validation here, too. The challenge with slugs is that, like ids, they must be unique. However, we only want to worry about this if we're creating a new database record or changing the slug of an existing record. If we are updating an existing record and the slug hasn't changed, we don't want to add a unique restriction, because there'll be a clash with the existing record when it tries to validate. Here's the code:
Set data and save
If all the submitted data passes our validation rules, we need to get it into the object. Again, we call a function from
processFormSubmission(). As with validation, we cycle through the field names in the
$fields property to set the data, and finish off with a save function:
Syncing many-to-many relationships
Sometimes, we need a many-to-many relationship between two tables. For example, we might want each product to be associated with several tags, and vice versa. In situations like this, we typically have two tables in the database, say
tags, which are connected by an intermediary table,
product_tag. By default, Laravel expects the name of the intermediary table to be the singular of the two table names in alphabetical order and separated by an underscore, but this can be overridden in the
belongsToMany() function in the model (see below). This intermediary table has two columns,
tag_id. This is an essential but powerful concept in relational databases.
How to input an array of ids
When we submit our input form, we pass in an array of ids which correspond to records in the foreign table through the
$request parameter in our
update() actions. In HTML, we can do this by having a
<select> element in our form whose name is followed by square brackets:
Since I'm focussing on controllers in this post, I won't go into this any more here, but if this is new to you, I can recommend a great YouTube series on Laravel by DevMarketer, and in particular the lessons on many-to-many relationships cover this in detail.
Defining the intermediaries
So, let's set this up in our parent controller. First, we need to tell the child controller which relationships the model is expecting. Because there could be several many-to-many relationships, we're going to declare these in an array. The key of each array element is a string which gives the name of the related table, exactly as it's defined in the model. To establish the many-to-many relationship, the model should contain a function which returns the
belongsToMany() QueryBuilder function. It's the name of the function that declares this which we need. So, in the example below, 'tags' is the string we want for the key:
The value is an array of switches which tell us which actions we want to perform on the intermediaries. We'll be coming back to this when we code up the delete functionality, but for now, we'll set as follows:
$intermediaries = ['tags' => ['save' => true]];
This might seem a bit complicated, but it'll become clear as we go on. We add an empty array to the
GenericResourceController, which will remind us to declare it in the child controllers:
Now, we want to create a set of functions in
GenericResourceController which handles the CRUD operations on the intermediary table. Let's call the first one
handleIntermediaries(). For each of the relationships declared in the
$intermediaries property, we want to check if there is an array in the
$request property which has name of the key (e.g. 'tags'). Then we determine whether the 'save' action is permitted in this controller. If so, we call another function,
syncIntermediary(), which uses the
sync() function to update the intermediary table with the ids corresponding to records in the foreign table.
We need to be careful when there are no related objects to sync, for example, when a product has no tags. If there are no related objects, the corresponding
$request parameter will have a null value. We can't pass null values into
sync() because it will return an error, so we create an empty array and pass that in. Here's the code:
sync() function automagically deals with both new and removed related ids, so there's no need to code all this from scratch.
We can see why we need to tell the controller which actions it is allowed to carry out in the
$intermediaries property if we consider an example. Let's say we have a many-to-many relationship between products and tags. When we create or update a product, the form we use will no doubt have an input which allows us to select one or more tags. When we pass the tag list into the controller through the
$request parameter, it will be stored either as an array, if there are one or more tags, or null if there are no tags. Conversely, let's say we also have a form to edit tags. This form probably won't have an input which allows us to attach products to the tag. So, when we try to reference 'products' in the
$request parameter, what will we get? A null value. This means we get null in two situations: when there's a zero-length array or when there's no 'products' input in the form. If we didn't tell the controller which actions are allowed, the
TagController would sync an empty array with products, which would remove all the products associated with that tag. If we just wanted to change the name of the tag, this is not the result we'd want. As we'll see later, we might want a controller to remove related objects when we delete, but not when we store or update.
With this setup, it'll be fairly easy to add delete functionality later on.Skip to navigation
After saving the data and handling many-to-many relationships, we need to set a success message in the session using another function. There needs to be a property, again overwritten in the child class, which tells the user in plain language what type of object was saved,
$objectLang. We'll also be using the success message function in the
destroy() action, so we'll set up the scaffolding for that now:
Finally, we need to redirect to the next page. We set up a property containing the routes to which the app is redirected. This is overwritten in the child class. Then, we need to write a function that returns the route. This should take into account the parameters that will be passed via the URL. Here, I'm just assuming the extra parameter will be the object id. If it's something else, such as a slug, we'll need to figure out a way to handle this. Perhaps in
$redirectAfter, each array element could be a subarray which includes the route and the parameter which needs to be included. I'll leave this for now, however.
As well as abstracting out
POST actions in the
GenericResourceController, we can also use it to handle
DELETE actions. In fact, we've done most of the groundwork for delete actions in the functions we've already written. The process is straightforward enough:
- instantiate the object
- remove many-to-many relationships
- delete the object
- set user feedback in the session
- redirect to the next view
Coding the process
destroy() action is the gateway to this process. In it, we'll set the object id, add a function which handles each of the above steps in turn, and finally redirect to the next view.
There's already a function,
setObject() which instantiates the object, so we don't need to change anything here, but the process for dealing with intermediaries needs a few tweaks. First, in our child controllers, we need to add a 'destroy' option to the
Then, in the parent controller, we alter the
handleIntermediaries() function so that it deletes intermediaries only if we've set permission in the
$intermediaries property. Finally, just to keep the code clean, we add another function which uses
detach() to remove the related objects.
Deleting the object
To keep the code clean and consistent, we create a function which calls the
delete() method on our object.
Next, to tell the user that the object was deleted successfully, we make some adjustments to the existing
Finally, the only thing we need to do to set up the redirect after the destroy action is to add another element to our
Extending this approach
With the delete functionality coded, we now have a DRY framework for handling data submitted to a controller by a form using
DELETE methods. See the complete code for the
GenericResourceController and a sample child class.
For straightforward forms consisting of one input for each database field, we don't even need a
destroy() action to be declared in the child controller. But if there's a child class in your app with special requirements, any of the functions in
GenericResourceController can be overwritten, and to disable a function, we can overwrite it in the child class with an empty function:
While this example illustrates how to set up DRY submit-save-redirect patterns, if you squint a little, you can see how other common tasks could be handled in a similar way. An example would be submit-send email-redirect, which fires off an email after a form is submitted.
The programming pattern I've used here, using a parent class for all the common functions, has its advantages, but it does limit the range of uses of the child resource controller. In order to extend the uses of resource controllers, it might make sense to use another programming pattern, such as the decorator pattern. Instead of putting our common functions in a parent class, we set up a new class which contains our common functions, e.g.
GenericResourceDecorator, and then instantiate it as a property in the resource controller object. I might try this in a future project.
I like this process because it's generic enough to apply to almost any form submission environment in a Laravel app, but flexible enough to handle variations in a child class. It might be a little bit involved for a small project, but if an app has lots of different object types, this could really save time and reduce the number of coding errors. I'd be interested to hear about other smart ways of dealing with form data that are out there.Skip to navigation