DRY resource controllers in Laravel

My selfie

James Turner,

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:

# Command line
?> php artisan make:controller SomeController --resource

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:

  1. instantiate either a new object or an object corresponding to the existing database record
  2. validate the form data
  3. set the form data in the object
  4. save the object
  5. sync many-to-many relationships
  6. set user feedback in the session
  7. 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.

Skip to navigation

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.

Skip to navigation

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:

<?php
// GenericResourceController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Session;

class GenericResourceController extends Controller {
	//
}

Individual object types in our app will each have a controller which is a child of GenericResourceController:

<?php
// SomeController.php
// ...
class SomeController extends GenericResourceController {
 // 
}
Skip to navigation

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.

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	protected $request;
	protected $object;
	protected $id = false;
	
	public function store(request $request){
		$this->request = $request;
	}

	public function update(request $request, $id){
		$this->request = $request;
		$this->id = $id;
	}
}

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():

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// properties...
	public function store(Request $request){
		$this->request = $request;
		$this->processFormSubmission();
	}
	public function update(Request $request, $id){
		$this->request = $request;
		$this->id = $id;
		$this->processFormSubmission();
	}
	public function processFormSubmission(){
		//
	}
}
Skip to navigation

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:

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	protected $request;
	protected $object;
	protected $id = false;
	
	// child properties
	protected $model = ''; // e.g. 'App\Product'
	
	// ...
}

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.:

<?php
// ProductController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Session;
use App\Product; // don't forget this

class ProductController extends GenericResourceController {
	protected $model = 'App\Product';
	// ...
}
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:

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// ...
	protected function processFormSubmission(){
		$this->setObject();
	}
	
	protected function setObject(){
		$model = $this->model;
		if($this->id === false){
			$this->object = new $model;
		} else {
			$this->object = $model::find($this->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:

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// ...
	protected $model;
	protected $fields = [];
	// functions...
}

In the child classes, we'll populate the $fields property like so:

<?php
// ProductController.php
// ...
class ProductController extends GenericResourceController {
	// ...
	protected $fields = [
		'product_name' => 'required|max:255',
		'description' => '',
		'slug' => 'required|alpha_dash|min:5|max:255',
		'price' => 'required|float',
		// etc...
	];
	// ...
}

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:

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// ...
	protected function processFormSubmission(){
		$this->setObject();
		$this->validateRequest();
	}
	// ...
	protected function validateRequest(){
		if(
			isset($this->fields['slug']) &&
			$this->request->input('slug') != $this->object->slug
		){
			$this->fields['slug'] = 'required|alpha_dash|min:5|max:255|unique:' . $this->object->getTable() . ',slug';
		}
		$this->validate($this->request, $this->fields);
	}
}
Skip to navigation

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:

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// ...
	protected function processFormSubmission(){
		$this->setObject();
		$this->validateRequest();
		$this->setObjectData();
		$this->saveObject();
	}
	// ...
	protected function setObjectData(){
		foreach($this->fields as $field => $rules){
			$this->object->$field = $this->request->$field;
		}
	}
	protected function saveObject(){
		$this->object->save();
	}
}
Skip to navigation

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:

<!-- HTML -->
<select name="tags[]">...</select>

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:

<?php
// ...
class Product extends Model {
	public function tags(){
		return $this->belongsToMany('App\Tag');
	}
}

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:

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	protected $model;
	protected $fields = [];
	protected $intermediaries = [];
	// ...
}

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.

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// ...
	protected function processFormSubmission(){
		$this->setObject();
		$this->validateRequest();
		$this->setObjectData();
		$this->saveObject();
		$this->handleIntermediaries('save');
	}
	// ...
	protected function handleIntermediaries($action){
		if(isset($this->intermediaries) && is_array($this->intermediaries)){
			foreach($this->intermediaries as $intermediary => $isAllowed){
				if($action == 'save' && $isAllowed['save']){
					$this->syncIntermediary($intermediary);
				}
			}
		}
	}
	
}

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:

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	//...
	protected function syncIntermediary($intermediary){
		if(
			is_array($this->request->$intermediary) &&
			count($this->request->$intermediary) > 0
		){
			$idList = $this->request->$intermediary;
		} else {
			$idList = [];
		}
		$this->object->$intermediary()->sync($idList);
	}
}

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 navigation

Giving 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:

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// ...
	protected $objectLang = ''; // e.g. 'product'
	// ...
	protected function processFormSubmission(){
		$this->setObject();
		$this->validateRequest();
		$this->setObjectData();
		$this->saveObject();
		$this->handleIntermediaries('save');
		$this->setSuccessMessage('save');
	}
	// ...
	protected function setSuccessMessage($messageType){
		$messages = array(
			'save' => 'The ' . $this->objectLang . ' was successfully saved.',
		);
		Session::flash('success', $messages[$messageType]);
	}
}
Skip to navigation

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.

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// ...
	protected $redirectAfter = [
		'store' => '', // e.g. 'products.show'
		'update' => '',
	];
	// ...
	public function store(){
		// ...
		return $this->redirect('store');
	}
	public function update(){
		// ...
		return $this->redirect('update');
	}
	// ...
	protected function redirect($action){
		$route = $this->redirectAfter[$action];
		if(strpos($route, 'index') !== false){
			return redirect()->route($route);
		} else {
			return redirect()->route($route, $this->id);
		}
	}
}
Skip to navigation

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:

  1. instantiate the object
  2. remove many-to-many relationships
  3. delete the object
  4. set user feedback in the session
  5. 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.

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// ...
	public function destroy($id){
		$this->id = $id;
		$this->destroyObject();
		return $this->redirect('destroy');
	}
	protected function destroyObject(){
		$this->setObject();
		$this->handleIntermediaries('destroy');
		$this->deleteObject();
		$this->createSuccessMsg('destroy');
	}
}

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.

<?php
// ProductController.php
// ...
class ProductController extends GenericResourceController {
	// ...
	protected $intermediaries = ['tags' => ['save' => true, 'destroy' => true]];
	// ...
}

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.

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// ...
	protected function handleIntermediaries($action){
		if(isset($this->intermediaries) && is_array($this->intermediaries)){
			foreach($this->intermediaries as $intermediary => $isAllowed){
				if($action == 'save' && $isAllowed['save']) {
					$this->syncIntermediary($intermediary);
				} elseif($action == 'destroy' && $isAllowed['destroy']){
					$this->detachIntermediary($intermediary);
				}
			}
		}
	}
	protected function detachIntermediary($intermediary){
		$this->object->$intermediary()->detach();
	}
	// ...
}

Deleting the object

To keep the code clean and consistent, we create a function which calls the delete() method on our object.

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// ...
	protected function deleteObject(){
		$this->object->delete();
	}
	// ...

Feedback

Next, to tell the user that the object was deleted successfully, we make some adjustments to the existing setSuccessMessage() method.

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// ...
	protected function setSuccessMessage($messageType){
		$messages = array(
			'save' => 'The ' . $this->objectLang . ' was successfully saved.',
			'destroy' => 'The ' . $this->objectLang . ' was successfully deleted.'
		);
		Session::flash('success', $messages[$messageType]);
	}
}

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.

<?php
// GenericResourceController.php
// ...
class GenericResourceController extends Controller {
	// ...
	protected $redirectAfter = [
		'store' => '', // e.g. 'products.show'
		'update' => '',
		'destroy' => '',
	];
	// ...
}
Skip to navigation

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:

<?php
class ProductController extends GenericResourceController {
	//...
	protected function handleIntermediaries() {
		// don't add any code to disable the 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.

Skip to navigation

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