php - Custom Laravel Relations?

131

Hypothetical situation: Let's say we have 3 models:

  • User
  • Role
  • Permission

Let's also sayUser has a many-to-many relation withRole, andRole has a many-to-many relation withPermission.

So their models might look something like this. (I kept them brief on purpose.)

class User
{
    public function roles() {
        return $this->belongsToMany(Role::class);
    }
}

class Role
{
    public function users() {
        return $this->belongsToMany(User::class);
    }

    public function permissions() {
        return $this->belongsToMany(Permission::class);
    }
}

class Permission
{
    public function roles() {
        return $this->belongsToMany(Role::class);
    }
}

What if you wanted to get all thePermissions for aUser? There isn't aBelongsToManyThrough.

It seems as though you are sort of stuck doing something that doesn't feel quite right and doesn't work with things likeUser::with('permissions') orUser::has('permissions').

class User
{
    public function permissions() {
        $permissions = [];
        foreach ($this->roles as $role) {
            foreach ($role->permissions as $permission) {
                $permissions = array_merge($permissions, $permission);
            }
        }
        return $permissions;
    }
}

This example is, just one example, don't read too much into it. The point is, how can you define a custom relationship? Another example could be the relationship between a facebook comment and the author's mother. Weird, I know, but hopefully you get the idea. Custom Relationships. How?

In my mind, a good solution would be for that relationship to be described in a similar way to how describe any other relationship in Laravel. Something that returns an EloquentRelation.

class User
{
    public function permissions() {
        return $this->customRelation(Permission::class, ...);
    }
}

Does something like this already exist?

26

Answer

Solution:

The closest thing to a solution was what @biship posted in the comments. Where you would manually modify the properties of an existingRelation. This might work well in some scenarios. Really, it may be the right solution in some cases. However, I found I was having to strip down all of theconstraints added by theRelation and manually add any newconstraints I needed.

My thinking is this... If you're going to be stripping down theconstraints each time so that theRelation is just "bare". Why not make a customRelation that doesn't add anyconstraints itself and takes aClosure to help facilitate addingconstraints?

Solution

Something like this seems to be working well for me. At least, this is the basic concept:

class Custom extends Relation
{
    protected $baseConstraints;

    public function __construct(Builder $query, Model $parent, Closure $baseConstraints)
    {
        $this->baseConstraints = $baseConstraints;

        parent::__construct($query, $parent);
    }

    public function addConstraints()
    {
        call_user_func($this->baseConstraints, $this);
    }

    public function addEagerConstraints(array $models)
    {
        // not implemented yet
    }

    public function initRelation(array $models, $relation)
    {
        // not implemented yet
    }

    public function match(array $models, Collection $results, $relation)
    {
        // not implemented yet
    }

    public function getResults()
    {
        return $this->get();
    }
}

The methods not implemented yet are used for eager loading and must be declared as they are abstract. I haven't that far yet. :)

And a trait to make this newCustom Relation easier to use.

trait HasCustomRelations
{
    public function custom($related, Closure $baseConstraints)
    {
        $instance = new $related;
        $query = $instance->newQuery();

        return new Custom($query, $this, $baseConstraints);
    }
}

Usage

// app/User.php
class User
{
    use HasCustomRelations;

    public function permissions()
    {
        return $this->custom(Permission::class, function ($relation) {
            $relation->getQuery()
                // join the pivot table for permission and roles
                ->join('permission_role', 'permission_role.permission_id', '=', 'permissions.id')
                // join the pivot table for users and roles
                ->join('role_user', 'role_user.role_id', '=', 'permission_role.role_id')
                // for this user
                ->where('role_user.user_id', $this->id);
        });
    }
}

// app/Permission.php
class Permission
{
    use HasCustomRelations;

    public function users()
    {
        return $this->custom(User::class, function ($relation) {
            $relation->getQuery()
                // join the pivot table for users and roles
                ->join('role_user', 'role_user.user_id', '=', 'users.id')
                // join the pivot table for permission and roles
                ->join('permission_role', 'permission_role.role_id', '=', 'role_user.role_id')
                // for this permission
                ->where('permission_role.permission_id', $this->id);
        });
    }
}

You could now do all the normal stuff for relations without having to query in-between relations first.

Github

I went a ahead and put all this on Github just in case there are more people who are interested in something like this. This is still sort of a science experiment in my opinion. But, hey, we can figure this out together. :)

142

Answer

Solution:

Have you looked into thehasManyThrough relationship that Laravel offers?

Laravel HasManyThrough

It should help you retrieve all the permissions for a user.

388

Answer

Solution:

I believe this concept already exists. You may choose on using Laravel ACL Roles and Permissions or Gate, or a package known as Entrust by zizaco.

Zizaco - Entrust

Laracast - watch video 13 and 14

Good luck!

People are also looking for solutions to the problem: php - CakePhp: Prevent duplicate calls which insert at the same time same information

Source

Didn't find the answer?

Our community is visited by hundreds of web development professionals every day. Ask your question and get a quick answer for free.

Ask a Question

Write quick answer

Do you know the answer to this question? Write a quick response to it. With your help, we will make our community stronger.

Similar questions

Find the answer in similar questions on our website.