A Pattern for Reusable Resource Controllers in Laravel
laravelIn most web apps, we encounter situations where we need to implement repetitive CRUD-type use cases across multiple entities. In our recent projects at Slashnode, we’ve been experimenting with different patterns to try to make it easier and quicker to build out these CRUD-type use cases.
The repetition stems from the fact that most CRUD actions for (basic) resources follow exactly the same sequence of operations. Consider the following methods of a typical resource controller:
Resource Controller Methods
index()
Route is GET (resource)/
. Used to display a list of the resources.
Sequence of Operations:
- Paginate all the records and inject into a view
edit($id)
Route is GET (resource)/{id}
. Used to display a pre-existing resource for editing.
Sequence of Operations:
- Fetch the record
- Generate values required by the form
- Inject the record and values into a form
create()
Route is GET (resource)/create
. Used to show an empty form, allowing the user to create a new resource.
Sequence of Operations:
- Instantiate a new record
- Generate values required by the form
- Inject the record and values into a form
store()
Route is POST (resource)/.
Used to create a new resource from the values entered via a create() form.
Sequence of Operations:
- Validate the Input values
- If fails, redirect back with error messages
- If passes, create a new record and redirect with a success message
update($id)
Route is PUT (resource)/{id}
, Used to update an existing resource based on new values provided via a edit() form.
Sequence of Operations:
- Fetch the record
- Validate the Input values
- If fails, redirect back with error messages
- If passes, update the record and redirect with a success message
destroy($id)
Route is DELETE (resource)/{id}
. Used to delete an existing resource.
Sequence of Operations:
- Fetch the record(s)
- Delete the record(s)
- Redirect
For more information about these routes refer to the Laravel documentation.
Opportunities for Reuse
For the purposes of illustration, consider the pseudo code controller below:
This provides a typical example of the different methods we might implement in a resource controller.
Based on this information, we can start to see opportunities for reusing parts of the code. We should be able to reuse the same controller for different resources (products, categories, orders, etc) by defining the repeated behaviour in an abstract parent class and overriding the following in the concrete implementations:
- The model class.
- The repository class.
- The validation class(es).
- The resource name.
A Pattern for Reuse
For the purposes of illustration, we’ll consider building a resource controller for an e-commerce application.
Note: we’ll also split routes and functionality based on user-types (admin or user). This means we will have separate controllers, routes, etc. for administrators and normal users.
The resource will be a “product,” so our routes will look like this:
OK, nothing too controversial so far.
Let’s do things in reverse and implement the concrete version of the class first. We know what we need to override on a per-resource basis, so we’ll add methods to return the implementation-specific values:
We can see a few important things in here:
- The constructor: we inject the product-specific implementations for the model, repository and validator. This means when the abstract class tries to call “find()”, “update()”, etc. we’ll be calling these methods on the product version of the model / repostiory / validator.
- function userRole() and function resourceName(): these are used to construct view and route paths.
- function formData(): this is used to pass values into the create and edit forms. This will make more sense when you see the abstract parent class.
Now we can see the abstract parent class.
The abstract parent class is quite dense, but it’s quite simple once you break it down. Here are the important points:
- View names: when ever we refer to a route, we need to dynamically generate it using the concrete implementation. This is because the view name will depend on the resource type.
- Route names: likewise, we also need to dynamically generate the routes for redirects. We use the same naming convention for views and routes, so we can use the same method to generate them.
- formData(): form data injects anything we need to display the create / edit form. We merge this with the $item, which is also required in all create / edit actions.
- data(): when ever we need to fetch input data from the user, we defer to the data() function which is actually implemented on the BaseController (not BaseResourceController). This simply pulls a list of the model attributes from the validator. See this reproduced below:
View and Route Naming Structure
As you can see from the code above, the naming convention for routes and views is a critical part of the pattern. We need to use a similar naming convention for both so that we can refer to them using the same methods in the controllers. So with that said, our routes and views will look like this (route – view):
admin.products.index
–views/admin/products/index.blade.php
admin.products.create
–views/admin/products.create.blade.php
admin.products.edit
–views/admin/products.edit.blade.php
You can see and example of one of these views reproduced below:
Conclusion
And that’s it!
We can use the same pattern to create a new resource simply by adding the routes, creating the supporting classes (repository, model, validator), building the concrete implementation of the abstract resource controller and adding the views.
Want to find out more?
We've worked with businesses just like yours to execute successful web projects helping them to optimise operations, improve marketing, and sell more online with custom software solutions. Reach out and tell us about your project for a free no-commitment consultation.
Find out more