Make Laravel models better with PHP traits

I suppose that most web-developers have met a problem of “fat” models at some point of their career. Even good developers following SOLID principles by keeping validation rules and other business logic out of models, sometimes face this problem.

Luckily, there’s a stupidly easy solution which I’ve never seen in any Laravel project before. That’s why I decided to write this short article.

A typical fat model

Let’s start with an example of a “fat” model:

<?php namespace MyApp\Models;

class User extends BaseModel
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function orders()
    {
        return $this->hasMany('Order');
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function projects()
    {
        return $this->belongsToMany('Project')
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function articles()
    {
        return $this->hasMany('Article');
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function comments()
    {
        return $this->hasMany('Comment');
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function role()
    {
        return $this->belongsTo('Role');
    }

    /**
     * @param \Illuminate\Database\Eloquent\Builder $builder
     * @param int $roleId
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeRoleId($builder, $roleId = 0)
    {
        if (!$roleId)
        {
            return $builder;
        }

        return $builder->where('role_id', '=', $roleId);
    }
}

As you see, relationships and scopes bloat the code and make itdifficult to read.

PHP Traits rush to help!

Traits enable a developer to use sets of methods in several independent classes.

And that’s exactly what we are looking for!

Let’s see what the code will look like if we replace the “orders” relation with a trait.

Here’s the Orders trait:

<?php namespace MyApp\Models\Relations\HasMany;

trait Orders
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function orders()
    {
        return $this->hasMany('Order');
    }
}

I prefer to put relations into a separate Relations directory inside of the Models directory (pay attention to the namespace).

And here’s the class after our small refactoring:

<?php namespace MyApp\Models;

class User extends BaseModel
{
    // Here's the trait!
    use Relations\HasMany\Orders;

    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function projects()
    {
        return $this->belongsToMany('Project')
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function articles()
    {
        return $this->hasMany('Article');
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function comments()
    {
        return $this->hasMany('Comment');
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function role()
    {
        return $this->belongsTo('Role');
    }

    /**
     * @param \Illuminate\Database\Eloquent\Builder $builder
     * @param int $roleId
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeRoleId($builder, $roleId = 0)
    {
        if (!$roleId)
        {
            return $builder;
        }

        return $builder->where('role_id', '=', $roleId);
    }
}

The difference is not very obvious but let me show you the same class after I extracted all relashionships and scopes into separate traits:

<?php namespace MyApp\Models;

class User extends BaseModel
{
    // Has Many Relations
    use Relations\HasMany\Orders;
    use Relations\HasMany\Articles;
    use Relations\HasMany\Comments;

    // Belongs To Relations
    use Relations\BelongsTo\Role;

    // Belongs To Many Relations
    use Relations\BelongsToMany\Projects;

    // Scopes
    use Scopes\RoleId;
}

Hopefuly, now you’re convinced that it’s much better and easier to read. Our former model has a good chance to become one of the new Victoria’s Secret Angels ;)

Conclusions

Please don’t extract every single relation into a separate trait. If you have to decribe a relation which is not following Laravel’s default naming conventions, it could be better not to extract it as it gives more information to a person who reads the code. But if your relation is simple and follows conventions, then moving it to a trait is a good idea. It can even be reused in situations when, for example, two models have similar belongsTo relationships.

Despite of being available since PHP 5.4, traits are not very widespread in PHP community and this short article shows a good example of benefits you can get by using them in similar scenarios.

← Back