Laravel Catch All Relationship
Sometimes you want a "catch all" relationship in your Laravel model. The most common case is the "admin" User able to access all entities of a given other model, with no explicit belongsToMany
or hasMany
relationships described in the database, while other Users can access only assigned entities.
Of course it is possible to provide a classic relationship function, and then another function which returns the result of that explicit relationship or a Entity::all()
result set on the basis of your specific condition (e.g. an assigned role). But this break the useful chaining of Eloquent queries, and you are no longer able to perform a
$user->entities()->where('name', 'foobar')->orderBy('created_at', 'desc')->get();
kind of query I (and probably you) like to run.
So, I've managed to create a custom kind of Illuminate\Database\Eloquent\Relations\Relation
able to retrieve all entities for a given Model, to be used in my relationship function.
The code looks like:
<?php
namespace App\Helpers;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class RelateToAll extends Relation
{
private $catchAll;
private $ownerKey;
public function __construct(Builder $query, Model $parent, $ownerKey, $catchAll = null)
{
/*
It is possible to provide own callback to query all models.
By default, it assumes that ID is numeric and is > 0
*/
if (is_null($catchAll)) {
$catchAll = function($query) use ($ownerKey) {
$query->where($ownerKey, '>', 0);
};
}
$this->related = $parent;
$this->ownerKey = $ownerKey;
$this->catchAll = $catchAll;
parent::__construct($query, $parent);
}
public function addConstraints()
{
if (static::$constraints) {
$query = $this->getRelationQuery();
($this->catchAll)($query);
}
}
public function addEagerConstraints(array $models)
{
/*
Warning! It is not possible to eager load this relation!
*/
throw new \Exception("No eager loading for catch all relation!", 1);
}
public function initRelation(array $models, $relation)
{
foreach ($models as $model) {
$model->setRelation($relation, $this->related->newCollection());
}
return $models;
}
public function match(array $models, Collection $results, $relation)
{
return $this->matchMany($models, $results, $relation);
}
public function getResults()
{
return $this->query->get();
}
}
and I've also provided a Trait to extend my "catch all" models
<?php
namespace App\Models\Concerns;
trait CustomRelationships
{
public function relatoToAll($related, $foreignKey = 'id')
{
$instance = $this->newRelatedInstance($related);
return new \App\Helpers\RelateToAll($instance->newQuery(), $this, $foreignKey);
}
}
so that now my App\Models\User
class looks like
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\Models\Concerns\CustomRelationships;
class User extends Authenticatable
{
use CustomRelationships;
public function entities()
{
if ($this->role == 'admin') {
return $this->relatoToAll(Entity::class);
}
else {
return $this->belongsToMany(Entity::class);
}
}
}
The described RelateToAll
class permits to define a custom query matching all given models (the provided one assumes there is a numeric id
always major than 0: valid in most situations).
To be noticed that this implementation cannot be leveraged with eager loading, as the kind of relationship (and the generated query) change for each particular User (due the if
coded in the relationship function): trying to execute something like
User::where('id', '>', 0)->with('entities')->get();
the belongsToMany
relationship is used to map each User to the assigned Entities, which looks into explicit connections within the database.