Laravel Eloquent Relationships make codes cleaner

Just now I dare myself to actually implement Laravel’s eloquent relationships to this site. Previously I was too afraid, thinking that it would be too complicated. I resorted to query builder instead. It took me some efforts to join from post_types and users tables to posts table, and I am amazed at how I decided to take a more complicated route to achieve pretty much the same results.

Yes, Eloquent relationships are more succinct when we have implemented it to our models. In the following sections I will try to demonstrate the difference between the two and how eloquent creates cleaner code.

Before Eloquent - Query Builder

What we did to fetch a post was originally like the following:

use App\Models\Post;

// Fetch published posts of a certain type,
// the amount of which is limited
$type = 'news';
$limit = 15;

Post::select('posts.id',
           'posts.created_at as created',
           'post_types.code as type',
           'post_types.name as type_name',
           'posts.slug',
           'posts.title',
           'users.name as author',
           'posts.body')
    ->where('is_published',1)
    ->where('post_types.code',$type)
    ->orderBy('created','desc')
    ->join('users',
           'users.id',
           '=','posts.users_id')
    ->join('post_types',
           'post_types.id',
           '=','posts.post_types_id')
    ->limit($limit)
    ->get();

// Fetch a particular post by selecting its
// ID
$id='put-post-id-here';
Post::select('posts.id',
    'posts.created_at as created',
    'post_types.code as type',
    'posts.slug',
    'posts.title',
    'users.name as author',
    'users.id as author_id',
    'posts.body',
    'posts.is_published')
        ->where('posts.id',$id)
        ->join('users',
               'users.id',
               '=','posts.users_id')
        ->join('post_types',
               'post_types.id',
               '=','posts.post_types_id')
        ->firstOrFail();

The joins are necessary for me to be able to access values of its post types and author information.

Sets up Eloquent Relationships

Now let us implement Eloquent, by first adding the required functions to our models. First we want to access posts from our User model.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Authenticatable
{
    /**
     * Get the posts for this user
     */
    public function posts(): HasMany
    {
        return $this->hasMany(
                    Post::class,
                    'users_id');
    }
}

Now we also want to do the same from our PostType model.

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Model;

class PostType extends Model
{
    /**
     * Get the posts for this PostType
     */
    public function posts(): HasMany
    {
        return $this->hasMany(
                    Post::class,
                    'post_types_id');
    }
}

Note that we use the hasMany methods to define that each users and post types can have many posts. We also supplied our foreign keys users_id and post_types_id because we used nonstandard column names in our table migrations. Eloquent would normally use snake case of our model name and append it with _id. So for example, if in our posts table, our foreign key for post type is post_type_id, we can just return $this->hasMany(Post::class); and the method would just work.

Now we need to define the inverse of hasMany in our Post model. To define the inverse of hasMany we can use belongsTo method:

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Get the owner of a post
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(
                    User::class,
                    'users_id');
    }

    /**
     * Get the type of a post
     */
    public function type(): BelongsTo
    {
        return $this->belongsTo(
                    PostType::class,
                    'post_types_id');
    }
}

Now we can access our post’s user and type effortlessly:

use App\Models\Post;

// Get the model of the post's owner
Post::first()->user;

//Get the model of our post type
Post::first()->type

Then if we want to say, get the name of the post’s author, or if we want to get the code of our post type, we can do:

use App\Models\Post;

// Get the name of the post's author
Post::first()->user->name;

// Get the code of the post's type
Post::first()->type->code;

Or even accessing posts created by a particular user:

use App\Models\User;

User::first()->posts;

Or get posts of a certain post type:

use App\Models\PostType;

PostType::first()->posts;

We can even use query builder alongside with Eloquent. The following achieves similar results as our earlier two query builder examples.

use App\Models\PostType;
use App\Models\Post;

// Fetch published posts of a certain type,
// the amount of which is limited
$type = 'news';
$limit = 15;
PostType::where('code',$type)
    ->first()
    ->posts()
    ->where('is_published',true)
    ->orderBy('created_at','desc')
    ->limit($limit)
    ->get();

// Fetch a particular post by selecting its
// ID
$id='put-post-id-here';
Post::where('id',$id)
    ->firstOrFail();

The primary difference is that now we don’t have to perform joins. The difference would be on how we access the attributes.

When using query builder, we aliased certain columns for easy accessing. For example users.name is aliased to author when users table is joined to the posts table in our previous query builder example. Therefore assume that we store a particular Post model after the join operation into a variable $post, we can access the author name attribute by $post->author.

If the same post model is implementing eloquent relationships like we described above, there is no need to alias users.id to author, all we have to do is to get the user of a post, and access the name attribute: $post->user->name. Therefore we also made modifications to our controller methods to reflect the changes. We are also adjusting our blade templates, as some still directly access models for displaying contents.

Conclusion

The change is done with minimal differences from the perspective of casual website viewers. However behind the scene the code is being tidied out. Our code can be cleaner as we no longer have to manually join database tables.

In the future we wish to implement user roles using eloquent relationship, and therefore allowing us to conveniently differentiate user’s permissions based on their role that is accessible directly from their user model.