Johnny Johnny - 3 months ago 18
PHP Question

Custom Laravel Relations?

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


  • User

  • Role

  • Permission



Let's also say
User
has a many-to-many relation with
Role
, and
Role
has a many-to-many relation with
Permission
.

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 the
Permission
s for a
User
?
There isn't a
BelongsToManyThrough
.

It seems as though you are sort of stuck doing something that doesn't feel quite right and doesn't work with things like
User::with('permissions')
or
User::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 Eloquent
Relation
.

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


Does something like this already exist?

Answer

The closest thing to a solution was what @biship posted in the comments. Where you would manually modify the properties of an existing Relation. 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 the constraints added by the Relation and manually add any new constraints I needed.

My thinking is this... If you're going to be stripping down the constraints each time so that the Relation is just "bare". Why not make a custom Relation that doesn't add any constraints itself and takes a Closure to help facilitate adding constraints?

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 new Custom 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. :)