DEV Community

bakle
bakle

Posted on

Laravel fit models.

Time ago in the Laravel community, people were talking about how to balance responsibilities between models and controllers. Some people say that it was better making fat models and thin controllers, while others say the opposite. This time I'm going to talk about a strategy to reduce a model when it becomes too "fat".

The concept of "fit models" came up from the need of reducing the amount of methods within a model. Months ago I was looking and thinking for a better way to reduce the amount of methods in a "fat" model and I came up with a solution, and searching for some articles I didn't found anyone who applied this, until I recently found that Freek Van Der Herten tried this same approach.

In a medium-large scale application is normal to have lots of methods for relationships, accessors, mutators and more.

Let's look at this example:


class User extends Authenticatable
{
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'birth_date' => 'datetime',
    ];

    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }

    public function followers(): HasMany
    {
        return $this->hasMany(Follower::class);
    }

    public function favorites(): HasMany
    {
        return $this->hasMany(Favorite::class);
    }

    public function socialNetworks(): HasMany
    {
        return $this->hasMany(SocialNetwork::class);
    }

    public function addresses(): HasMany
    {
        return $this->hasMany(Address::class);
    }

    public function phones(): HasMany
    {
        return $this->hasMany(Phone::class);
    }

    public function messages(): HasMany 
    {
        return $this->hasMany(Message::class);
    }

    public function getFullNameAttribute(): string
    {
        return $this->first_name . ' ' . $this->last_name;
    }

    public function getShortNameAttribute(): string
    {
        return $this->first_name . ' ' . Str::limit($this->last_name, 1);
    }

    public function getAgeAttribute(): string
    {
        return now()->diffInYears($this->birth_date);
    }

    public function getCreatedDateAttribute(): string
    {
        return $this->created_at->toFormattedDateString();
    }

    public function getProfilePicturePathAttribute(): string
    {
        return storage_path('pictures/' . $this->id . '/' . $this->profile_picture);
    }

    public function setFirstNameAttribute(string $firstName)
    {
        $this->attributes['first_name'] = Str::studly($firstName);
    }

    public function setLastNameAttribute(string $lastName)
    {
        $this->attributes['last_name'] = Str::studly($lastName);
    }
}

Enter fullscreen mode Exit fullscreen mode

This User model has some relationships, accessors and mutators, also remember that methods could increase a lot. At this time we can see too many things in our code and it is hard to read despite it's not a "fat" model.

Moreover, in a collaborative project developers try to define how to organize those methods. In the previous code we saw that relationships were first, then accessors and then mutators. When new relationships or accessors come in, developers will try to keep that structure, but when the team is growing new developers will add new methods wherever they want, making this previous structure a mess.

Traits to the rescue

So, what are traits? In simple words, they are a way to reuse code and reduce limitations of single inheritance. Although, a trait is something that can't be inheritable and is more like an extension of a class.

What if we separate each type of methods and put them in a trait? This way we can create a specific trait for each one. For example, we can create a HasUserAccessors for keeping all user's accessors and a HasUserMutators for keeping all user's mutators.

With this approach our User model will look like this:

class User extends Authenticatable
{
    use HasFactory, Notifiable;
    use HasUserAccessors;
    use HasUserMutators;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'birth_date' => 'datetime',
    ];

    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }

    public function followers(): HasMany
    {
        return $this->hasMany(Follower::class);
    }

    public function favorites(): HasMany
    {
        return $this->hasMany(Favorite::class);
    }

    public function socialNetworks(): HasMany
    {
        return $this->hasMany(SocialNetwork::class);
    }

    public function addresses(): HasMany
    {
        return $this->hasMany(Address::class);
    }

    public function phones(): HasMany
    {
        return $this->hasMany(Phone::class);
    }

    public function messages(): HasMany 
    {
        return $this->hasMany(Message::class);
    }

}

Enter fullscreen mode Exit fullscreen mode

Better?

In fact, we can go further by creating a HasUserRelationships:

class User extends Authenticatable
{
    use HasFactory, Notifiable;
    use HasUserAccessors;
    use HasUserMutators;
    use HasUserRelationships;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'birth_date' => 'datetime',
    ];

}

Enter fullscreen mode Exit fullscreen mode

I know that some of you will say that is not a valid approach because we are not reusing a trait and it goes against the definition of trait, but remember that just because something can be reused doesn't mean that you have to. For example, classes can be extended but do you extend every class? Of course not, you create them for specific purposes with no need to apply inheritance.

Remember when I said that traits are not inheritable? It is because you cannot have a class that extends a trait, also you can't hide a trait's properties. If you have a private property inside a trait, when you use that trait in a class that class will own the property, it is like creating the property directly inside the class.

This is why I think using traits to reduce models is totally valid because is like creating methods inside the model but making the model looks fit.

Top comments (4)

Collapse
 
fireynis profile image
Jeremy MacArthur

This just seems like you moved the code to other files, it's not actually cleaning up the code. I guess at least it is compartmentalized for the next person.

Collapse
 
bakle profile image
bakle

I never talked about cleaning up code. This article refers to reduce a lot of methods in a model. It doesn't mean that we are cleaning up, as you say, we're just moving groups of methods to a specfic file, in this cases to traits to have a light or fit model that is easy to read.

Collapse
 
fireynis profile image
Jeremy MacArthur

But don't you have the same amount of methods but just spread over a few files?

Thread Thread
 
bakle profile image
bakle

Yes but not directly inside the model. That is why I refer to "reducing the number of methods within the model", this make the model itself easier to read. Extract methods into specific files is one of the principles of refactoring.