The Case for Model-Centric Laravel

Image for post
Image for post

Laravel is a web MVC framework, providing a solid foundation for quickly and easily spinning up web applications. If you’ve spent any amount of time in the Laravel community, you’ll probably know that the framework is designed to deliver Controller-centric application logic, meaning that most of your application’s processes are implemented directly inside of Controller classes.

For example, take the documentation on Laravel’s included system for Validation. You’ll see that the logic for validating data is designed for use inside of a Controller with an incoming Request object. Similarly, see how the built-in File Storage system allows you to push uploaded files directly from the Controller. These are just a couple of examples, but you can continue reading through the Laravel documentation to find many more examples of how the framework is literally designed for Controller-centric logic.

An Alternate Perspective

Why?

Organizing logic into model functions modularizes your application logic, which makes it easier to re-use between Web and API controllers, Background Jobs, Seeders, and even Tinker, rather than manually recreating it each time it is needed. The other major benefit is that it allows you to develop a significant chunk of logic via “configuration” by simply including traits and setting model variables. In fact, this is how the base Model class already functions, with variables to configure model behavior, such as $hidden, $fillable, $dates, $dateFormat, etc.

A Good Starting Point

Firstly, most of the Controller-centric components, such as the Validation system, also provide methods for manual usage outside of the Controller.

Secondly, there are a number of existing Laravel components which enhance your Models directly, including the Laravel Cashier Billable trait which provides member functions for creating, associating, and removing payment methods.

Lastly, the Model class itself includes functionality for extension and customization, such as attribute accessors and mutators, the boot function, and the ability to boot traits.

Technique and Examples

Side Note

Lastly, please understand that this is not a tutorial on how to use the helium/laravel-helpers package, but rather, a discussion of the general technique. We will publish formal tutorials on each component once we have re-built the package.

Example 1: Model Phone Numbers

In my experience, the easiest way to standardize your formatting on the back-end is to strip out any special characters and store just the digits, then handle formatting on the front-end for display purposes only.

Herein lies the Controller-centric approach:

class UserController extends Controller
{
protected function stripPhoneFormatting(array $data) {
$data['phone_number'] = preg_replace(
'/[^0-9]/',
'',
$data['phone_number']
);
return $data;
}
public function store(Request $request) {
$data = $this->stripPhoneFormatting($request->all());
return User::create($data);
}
public function update(User $user, Request $request) {
$data = $this->stripPhoneFormatting($request->all());
$user->update($data); return $user;
}
}

Now, consider the Model-centric method:

class User extends Model
{
use HasPhoneNumbers;
}

class UserController extends Controller
{
public function store(Request $request)
{
return User::create($request->all());
}
public function update(User $user, Request $request)
{
$user->update($request->all());
return $user;
}
}

With the HasPhoneNumbers trait, the phone_number field is automatically stripped of all special characters upon being set, with no need to write and apply a helper function each time you need to update a Model.

For more advanced configuration, such as custom phone number fields, set the phoneNumbers array on the Model:

class User extends Model
{
use HasPhoneNumbers;
protected $phoneNumbers = [
'home_phone_number',
'cell_phone_number',
'office_phone_number'
];
}

Example 2: Model Self-Validation

class UserController extends Controller
{
public function store(Request $request)
{
$validatedData = $request->validate([
'name' => 'required|string',
'email' => 'required|email',
'age' => 'nullable|integer'
]);
return User::create($validatedData);
}
public function update(User $user, Request $request)
{
$validatedData = $request->validate([
'name' => 'sometimes|required|string',
'username' => 'sometimes|required|string',
'age' => 'sometimes|required|integer'
]);
$user->update($validatedData); return $user;
}
}

Now consider the Model-centric approach:

class User extends Model
{
use SelfValidates;

protected $validatesOnSave = true;
protected $validationRules = [
'name' => 'required|string',
'email' => 'required|email',
'age' => 'required|integer'
];
}

class UserController extends Controller
{
public function store(Request $request)
{
return User::create($request->all());
}
public function update(User $user, Request $request)
{
$user->update($request->all());
return $user;
}
}

Although this example obscures the logic that actually performs validation on the model, that’s kind of the point. In short, just before the model is saved (i.e., create, update, or save is called), the internal $attributes array is validated against your defined ruleset. Not only does this again reduce the need to reproduce logic each time you update a Model, but it also protects the Model from errant updates that are applied manually after the initial Controller validation, since it is validated at the last possible instance before being saved to the database.

For more advanced configuration, such as conditional validation, simply write a validationRules function, instead of an attribute.

class User extends Model
{
use SelfValidates;
protected $validatesOnSave = true; protected function validationRules()
{
$rules = [
'user_type' => [
'required',
Rule::in(['admin', 'user'])
],
'name' => 'required|string',
'email' => 'required|email',
'age' => 'required|integer'
];
if ($this->user_type = 'admin') {
$rules['admin_group_id'] =
'required|exists:admin_groups,id';
}
return $rules;
}
}

Example 3: Combining Requirements

The previous two examples may not seem to have any significant gains in simplicity over the traditional Controller-centric method, at least just by looking at the number of lines of code. However, consider combining the previous two examples into one User Model and Controller, then add in other real-world requirements, such as uploading files S3. As you can imagine, the Controller-side code can start to get a bit bloated, and becomes very repetitive.

Now take a look at the Model-centric perspective. Sure, there’s a bit of configuration you’ll have to include in each Model class, but remember that you only have to write the configuration once for each model. Plus, all you have to do is handle writing the configuration variables each time, rather than re-writing the same logic each time it’s needed since the logic is implemented once in the Trait and reused.

The final benefit is the fact that there’s no risk that you or another developer will forget to include a certain bit of logic when adding new endpoints to your app. Whether it’s a Background Job, Controllers, or Seeders, you will almost always want to make sure that the same requirements are enforced. The Model-centric approach ensures that rules are always applied consistently across the board, and removes the requirement on the developer to be actively aware of all of the requirements at all times.

Conclusion

With the size and diversity of the Laravel community, I hope that this alternate take on the framework inspires others to explore and test the boundaries with what you can do with it! Like with life, it’s easy to get stuck in one mode of thinking without realizing what else is out there, so you should experiment, play around, and see what is possible. And if anybody comes up with an interesting idea, be sure to share it and continue this discussion for the benefit of everybody in this community!

Written by

Software That Works

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store