Easy Role Management with Pivot Models

If you have ever developed any kind of SaaS app (like Spark), you know what team level role management means. Instead of picking a bad strategy for handling roles, we can bring simple solution by using Pivot Models.

Warming up

Most of us don’t need a robust permission handling in our application. What do we mean under simple role management? Basically that, you have a team, the team has an owner and some members. We want to determine who is the owner, who has a bit more rights in the team and who is a basic member with the basic permissions.

In code, that means we have a User model, a Team model and a pivot table that establishes the connection between the two models. Since, most of the time this structure is present, it’s time to roll out a simple solution for handling roles that stored in the pivot table.

The Pivot Table

The pivot table’s structure should be simple and clean. A standard table blueprint should look like the following:

Schema::create('team_user', function (Blueprint $table) {
    $table->unsignedInteger('team_id')->index();
    $table->unsignedInteger('user_id')->index();
    $table->string('role')->default('member');
    $table->unique(['team_id', 'user_id']);

    // Foreign constraints, timestamps etc..
});

As we see, we store the role attribute in the pivot table. The default value is member, that means if we don’t give any value when inserting a new record, the role column’s value falls back to member automatically.

An advantage of this structure is that a user can have multiple teams and in each team can own a different role.

The Eloquent Relationship

The type of this relationship is many-to-many. Let’s set up the relationship from the user’s side before moving on the pivot models.

public function teams()
{
    return $this->belongsToMany(Team::class, 'team_user')
        ->using(Role::class)
        ->as('role')
        ->withPivot('role');
}

We can see, it’s a normal belongsToMany() relation, where we retrieve the pivot data with the withPivot() method.

We have to use the withPivot() method to fill the pivot model with the stored data in the pivot table. If you have more than one column that you want to retrieve from the pivot table, you can pass an array as an argument. It will fill up the pivot model with the given attributes.

Besides that, we see two more things. First of all, the using() method, what defines the pivot model what we want to use instead of the default Illuminate\Database\Eloquent\Relations\Pivot class.

The second thing is the as() method’s parameter overrides the default “pivot” key. That means:

// default
$user->team->pivot;

// with the as('role')
$user->team->role;

Using this approach makes it a bit more understandable, so it’s a good point to override the default value with a more proper one.

Attaching Users to the Team

Since we have the relationship, we can easily attach the users to the team. Please note, we introduced the relationship only the on the users’ end, but it would work exactly the same by replacing the model and pasting the code to the team model. (And in real life you should do that).

So according to the documentation, we can do the following:

// the user is an owner
$user->teams()->attach(1, ['role' => 'owner']);

// omit the role to use the default "member"
$user->teams()->attach(1);

As you see, by adding the extra parameter to the attach() method, we can specify what role we want to assign to the user.

If you want to update the roles on the pivot table, you may use the updateExistingPivot() method. See more here.

Generating Pivot Models

It is not a well-known feature, but in Laravel, you can generate pivot models easily. All you have to do to add a -p flag to your make:model command:

php artisan make:model Role -p
Pivot models extend the Illuminate\Database\Eloquent\Relations\Pivot class.

The cool thing, we can do anything we want from now. We can handle it as a normal Eloquent model. We can write methods, accessors or mutators.

Extending the Pivot Model

As we mentioned, we can easily extend the pivot model with any functionality we need. For example, what if we want to check if the role matches with the given role(s). We can implement the following in the pivot model:

public function hasPermission($roles)
{
    return in_array($this->role, (array) $roles);
}

And we may use it like this:

$user->teams->first()->role->hasPermission('owner'); // false

$user->teams->first()->role->hasPermission(['owner', 'member']); // true

Of course in real life, you should retrieve the team model for example from the session, or with route-model binding. It’s up to your project.

The point is, from now, you can refactor the whole logic to a middleware or a blade directive. If you have some actions or some elements on your page that only owners can view, it’s a perfect use case for using an approach like this.

Also, as we mentioned, you can write accessors or mutators on your pivot model. For example, if we want to get the translated label of the role, we could do the following:

protected $appends = [
    'label',
];

public function getLabelAttribute()
{
   return trans('roles'.$this->role);
}

As you can see, we can append the attribute to the JSON or array form of the pivot model. Like in the case of normal eloquent models.

Summary

We believe, this solution covers the needs of most of us. But of course, if you handle permissions besides roles, you may use a more complex solution – like a package.

Otherwise, this approach shows perfectly the potential of the framework. If you know the little details and the big picture too, you choose the best solution for your application, or better you can do it yourself.