loading...

Making the case for Laravel model helper methods

mattkingshott profile image Matt Kingshott 👨🏻‍💻 Originally published at itnext.io on ・6 min read

This article is part of a series where I document insights, changes and rethinking that I experienced while refactoring the codebase for Pulse — a painless and affordable site & server monitoring tool designed for developers.

Today, I’d like to talk about why I decided to make a switch from a philosophy I had followed for many years… keeping Eloquent model classes empty. I’ll explain my reasoning for the change and also comment on a couple of issues you should be aware of when using this approach.

The almighty God class

There is some debate among developers about whether Eloquent should be avoided at all costs because it employs too much of Laravel’s “magic” in order to function. After thinking about this for a while, I’ve come to the conclusion that this argument is irrelevant for most developers.

The notion that I should instead create my own decoupled data access layer, and all of the code and tests that go with that, is not realistic for the majority of developers, chiefly because it’s complex and there isn’t time to do it.

Instead, Eloquent is a proven, fully-tested implementation that has worked without issue for every Laravel application I’ve written. That being said, I’ve always kept my model classes light in my apps. By this, I mean that I’ve only used them to define relationships and occasionally, built-in data type casting.

Okay, but why did you decide to do that?

I think it came from watching one of Jeffrey Way’s amazing videos on Laracasts, where he talks about avoiding the use of god classes. A simple example of this is the User model where you could, in theory, create hundreds of functions and have every possible action be run from a user object.

I think I took a concern of creating these types of classes too far and neglected a valuable opportunity to isolate and improve my code.

Creating model helper methods

Let’s explore how we can use “helper” methods on the model classes to clean up our code. Consider the example of a forum. Suppose that certain actions would result in a user being banned e.g.

if (TextValidator::containsHateSpeech($comment->body)) {
    $user->update(["banned\_at" => now()]);
}

Now consider that there are many instances in your application where a user could do something that results in them being banned. You’d need to copy that line of code that updates the user everywhere you needed that logic.

In the case of this example, it’s not really a big deal as we’re only setting a timestamp, but imagine if the logic involved several steps… the risk of errors goes up, as does the difficulty in making changes. Now consider how much better the following code is:

// Somewhere in the application
if (TextValidator::containsHateSpeech($post->body)) {
    $user->ban();
}

// Elsewhere in the application
if (TextValidator::containsHateSpeech($comment->body)) {
    $user->ban();
}

// User model
class User
{

    /\*\*
     \* Prevent the user from participating on the forum.
     \*
     \*/
    public function ban() : void
    {
        $this->update(["banned\_at" => now()]);
    }

}

Another added benefit of this approach is that since the logic exists within a public method on the User model, we can easily write a unit test for it:

/\*\* @test \*/
public function a\_user\_can\_be\_banned()
{
    $adam = factory(User::class, 1)->create(["banned\_at" => null]);

    $this->assertNull($adam->banned\_at);

    $adam->ban();

    $this->assertEquals($adam->fresh()->banned\_at, now());
}

Expanding on this approach

We now know that if we call our ban helper method anywhere in the application, it will ban the user and we have a test to prove it. Let’s build on this example with another. Suppose we wanted to add a conditional to see if a user has been banned. Our code might look like this:

if ($user->banned !== null) {
    // Perform some action
}

As in the earlier example, it is entirely plausible that we would have the same code duplicated throughout the application, which would increase the risk of typing errors and make it difficult to alter the logic should we decide to do so.

By contrast, it becomes much easier to manage if we move the code into a helper method on the User. We can also easily add the inverse of the same method to check if a user is not banned:

// Somewhere in the application
if ($user->isBanned()) {
    // Disallow access
}

// Elsewhere in the application
if ($user->isNotBanned()) {
    // Perform some action
}

// User model
class User
{

    /\*\*
     \* Determine if the user has been banned from participating.
     \*
     \*/
    public function isBanned() : bool
    {
        return $this->banned === null;
    }

    /\*\*
     \* Determine if the user has not been banned from participating.
     \*
     \*/
    public function isNotBanned() : bool
    {
        return ! $this->isBanned();
    }

}

Likewise, writing unit tests to confirm this functionality is a doddle and it gives us the confidence that we need to safely rely on these methods to determine a user’s banned state:

/\*\* @test \*/
public function a\_user\_knows\_if\_it\_is\_banned()
{
    $adam = factory(User::class, 1)->create(["banned\_at" => now()]);

    $eve = factory(User::class, 1)->create(["banned\_at" => null]);

    $this->assertTrue($adam->isBanned());

    $this->assertFalse($eve->isBanned());
}

/\*\* @test \*/
public function a\_user\_knows\_if\_it\_is\_not\_banned()
{
    $adam = factory(User::class, 1)->create(["banned\_at" => null]);

    $eve = factory(User::class, 1)->create(["banned\_at" => now()]);

    $this->assertTrue($adam->isNotBanned());

    $this->assertFalse($eve->isNotBanned());
}

Using traits to organise related functionality

If you have several methods that relate to the same “area” for a model, you may find it helpful to extract them into a separate file. We can make use of a trait to do this, and then simply tell our model to use this trait.

FYI: Laravel does this extensively. Indeed, the User model is a clear example of this as it makes use of Notifiable, Authenticable and Authorizable traits. Another added benefit of traits is the ability to use them in more than one class, so if the functionality is identical in multiple models, you could simply reuse it.

Let’s extract the helpers to a trait and import it within the User model:

trait Bannable
{

    /\*\*
     \* Prevent the user from participating on the forum.
     \*
     \*/
    public function ban() : void
    {
        $this->update(["banned\_at" => now()]);
    }

    /\*\*
     \* Determine if the user has been banned from participating.
     \*
     \*/
    public function isBanned() : bool
    {
        return $this->banned === null;
    }

    /\*\*
     \* Determine if the user has not been banned from participating.    
     \*
     \*/
    public function isNotBanned() : bool
    {
        return ! $this->isBanned();
    }

}

// User class
use App\Traits\Bannable;

class User
{
    use Bannable;
}

Things to be aware of

While the above functionality is perfectly valid, be careful about over-using traits. Due to their extraction of methods, it’s very easy to end up with a god class that doesn’t look like a god class because it’s mostly made up of traits.

I’m also of the opinion that helper methods on the model should mostly be limited to interacting with the model itself and shouldn’t be responsible for doing other things (such as performing validation, logging out a user etc.).

As a result, I mostly favour them performing CRUD operations on themselves or conditionals on their current values, that way, it makes it easier to create unit tests for them. The second you start introducing functionality that works outside of the model, it requires more setup & becomes more difficult to test.

If you have more complex logic, consider dispatching a job or firing an event. That way, you can still unit test that a event was fired or a job was dispatched. That said, I’m personally still inclined to avoid doing this, but there’s nothing stopping you from taking this approach if you don’t have an issue with it.

Wrapping Up

Hopefully you’ve seen how helper methods can improve your code and that I’ve made a reasonable case for their use. I have more articles to share, so if you’re interested in reading them, be sure to follow me here on Medium. You can also follow me on Twitter.

Lastly, if you’re in the market for an affordable and painless site & server monitoring tool that doesn’t require you to have a DevOps degree, please take a moment to check out Pulse. I think you’ll find it to a be a breath of fresh air!

Thanks again and happy coding!


Posted on by:

mattkingshott profile

Matt Kingshott 👨🏻‍💻

@mattkingshott

Founder. Developer. Writer. Lunatic. Created Pulse, IodineJS, Axiom, and more. #PHP #Laravel #Vue #TailwindCSS

Discussion

pic
Editor guide