DRY resource controllers in Laravel
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 store()
and 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 store()
and 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.
tl;dr
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.
Good parenting
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 GenericResourceController
:
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 store()
and 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 store()
and 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 update()
action.
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 store()
and update()
:
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 navigationSetting 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 navigationValidating 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 products
and 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, product_id
and 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 store()
and 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:
Syncing intermediaries
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:
Thankfully, the sync()
function automagically deals with both new and removed related ids, so there's no need to code all this from scratch.
Subtle complications
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 navigationGiving feedback
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:
Redirect
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.
Deleting data
As well as abstracting out GET
and 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
The 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.
Removing intermediaries
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 $intermediaries
property.
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.
Feedback
Next, to tell the user that the object was deleted successfully, we make some adjustments to the existing setSuccessMessage()
method.
Redirect
Finally, the only thing we need to do to set up the redirect after the destroy action is to add another element to our $redirectAfter
property.
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 GET
, PUT
and 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 store()
, update()
or 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.
Conclusion
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
Comments
comments powered by Disqus