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 Permissions 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?
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. :)
Have you looked into the hasManyThrough relationship that Laravel offers?
Laravel HasManyThrough
It should help you retrieve all the permissions for a user.
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!
Related
I have a belongsToMany() relationship between a User and a Group. The user has a level within any group he belongs to.
public function groups()
{
return $this->belongsToMany('App\Group', 'user_group', 'user_id', 'group_id')
->withPivot('level');
}
This works great.
However if the User is an admin, I would like the groups function to return ALL Groups with level = 3, regardless of whether that relationship exists in the pivot table or not.
I can successfully create a Collection which mirrors the data structure as follows:
\App\Group::all()->transform(function ($item, $key) use ($uid) {
$item->pivot = collect(['user_id'=>$uid,'group_id'=>$item->id,'level'=>3]);
return $item;
});
However, I cannot substitute the two outputs as one returns a belongsTo relationship instance and the other returns a Collection. This means I can call ->get() on the former but not the latter.
I thought about using the DB:: facade and creating a Builder for the latter, but I cannot add the Pivot values manually.
Any thoughts on how to achieve this?
-- UPDATE --
I am currently cheating by adding the ->get() inside the groups() method, but this is messy and I would still like to know if there is a better way to solve this problem.
public function groups()
{
if ($this->isAdmin()) {
return \App\Group::all()->transform(function ($item, $key) use ($uid) {
$item->pivot = collect(['user_id'=>$uid,'group_id'=>$item->id,'level'=>3]);
return $item;
});
} else {
return $this->belongsToMany('App\Group', 'user_group', 'user_id', 'group_id')
->withPivot('level')->get();
}
}
So this solution should work(not tested), but it is not the "cleanest" it would be better to access all groups through some other mechanism but because I don't know your admin implemention it is hard to guess.
public function groups()
{
return $this->belongsToMany('App\Group', 'user_group', 'user_id', 'group_id')
->withPivot('level');
}
public function scopeSpecialGroups($query)
{
return $query->when($this->role === 'admin',function($query){
return Group::where('level', '>', 3');
})->when($this->role != 'admin',function($query){
return $query->with('groups');
});
}
Then you should be able to call User::specialGroups()->get();
Role and Permission are two models and since they have many-to-many relationship, I have an intermediate table called permission_role table. But this doesn't have a Model. I am trying to attach a Permission to a Role. But $this->authorize('create', RolePermission::class); always fails with error "This action is unauthorized."
Route:
Route::post('/rolepermissions/{role}/addpermission', 'RolePermissionController#store')
->name('rolepermission.store');
RolePermissionController:
public function store(StoreRolePermission $request, Role $role)
{
$this->authorize('create', RolePermission::class);
...
}
RolePermissionPolicy:
public function create(User $user)
{
if (($user->usertype == 'ADMIN') || ($user->usertype == 'SUPERADMIN'))
{
return true;
}
else
{
return false;
}
}
Is it because the intermediate table does not have an associated Model?
I had a similar scenario, and I'm doing:
Auth:user()->can('create', RolePermission::class);, instead of authorize.
I think (have not tested) you can also do:
$this->authorize('create', [RolePermission::class, Auth::user()]). The second argument in the array will get passed as the only param in your policy.
I'm very new to Laravel, I got some help to solve a similar issue, hope this works for you.
I have two models User and Child.
class User extends Model {
public function children() {
return $this->belongsToMany('App\Child', 'user_child')->withPivot('relation_id');
}
public function connections() {
// How to get the distinctly aggregate of my children.friends.contacts as a relationship here?
}
}
class Child extends Model {
public function contacts() {
return $this->belongsToMany('App\User', 'user_child')->withPivot('relation_id');
}
public function friends() {
return $this->belongsToMany('App\Child', 'child_connection', 'child_id1', 'child_id2');
}
}
I would like to eagerly load the distant relationship which I name 'connections' which are the contacts (users) of my children's friends.
$user = User::with('children', 'children.friends', 'connections');
Any ideas how to do this elegantly?
Thank you!
Try
public function connections() {
return $this->belongsToMany('App\Child', 'user_id')->with('friends.contacts;);
}
OMG What a long shot!
If by any miracle that works, then doing
$user = User::find(1);
$user->connections(); //should bring 'children', 'friends' and 'contacts' in one query.
Well it should.
If you find an answer, please post it. I'm really interested in this.
I have two models, User and Appointment. Users can have clients which are also users. These clients are referenced in appointments under client_id:
The appointment table looks like this:
Appointment
id|user_id|client_id
I'd like to pull all clients per user (uniques) but having difficulty crafting the relationship (hasManyThrough – User has many Users (clients) through Appointments).
Based on my understanding of the Laravel docs, the following should work:
User.php
public function clients()
{
return $this->hasManyThrough('App\User', 'App\Appointment', 'client_id', 'user_id', 'id');
}
-
Appointment.php
public function user()
{
return $this->belongsTo('App\User');
}
public function client()
{
return $this->belongsTo('App\User', 'client_id');
}
Alas it does not get me what I'm looking for. Should I craft my own query?
That's a little tricky. The documentation Has Many Through example is cascade like structure, which tells me that this type of relationship is not applicable to your case (but there are many undocumented things, so it could also be possible).
I propose the following relationships if you don't want to deal with undocumented cases:
User.php
public function appointmentsInWhichIsProvider()
{
return $this->hasMany('App\Appointment');
}
public function appointmentsInWhichIsClient()
{
return $this->hasMany('App\Appointment', 'client_id');
}
Appointment.php
public function user()
{
return $this->belongsTo('App\User');
}
public function client()
{
return $this->belongsTo('App\User', 'client_id');
}
If you want to get all the clients of an user, you can do something like this (not pretty at all, but should do the trick):
$user = User::find(1);
$userClients = [];
foreach ($user->appointmentsInWhichIsProvider as $appointment) {
array_push($userClients, $appointment->client);
}
var_dump($userClients) // Collection containing all the user clients.
I have 3 models: User, Role, Tool where each user could have many roles, and each role could have many tools.
The many to many relationships work well in each case. I can access:
User::find(1)->roles
Tool::find(1)->roles
Role::find(1)->tools
Role::find(1)->users
My tables are:
users
id
name
roles
id
name
tools
is
name
role_user
id
role_id
user_id
role_tool
id
role_id
tool_id
In each model:
//In User Model
public function roles()
{
return $this->belongsToMany('Rol');
}
//In Role Model
public function users()
{
return $this->belongsToMany('User');
}
public function tools()
{
return $this->belongsToMany('Tool');
}
//In Tool Model
public function roles()
{
return $this->belongsToMany('Rol');
}
I need to get all the tools of a single user like: User::find(1)->roles()->tools. How can I do that?
Get all the roles of the user, then in a loop you get all tools of the role and merge them to an array where you store all tools.
$tools = array();
foreach(User::find(1)->roles as $role)
$tools = array_merge($tools, $role->tools->toArray());
This runs a query for every iteration, so for better performance you should use eager loading.
$tools = array();
foreach (User::find(1)->load('roles.tools')->roles as $role) {
$tools = array_merge($tools, $role->tools->toArray());
}
Now you can place this to a function called tools() in your User model.
public function tools()
{
$tools = array();
foreach ($this->load('roles.tools')->roles as $role) {
$tools = array_merge($tools, $role->tools->toArray());
}
return $tools;
}
You can call it like this: User::find(1)->tools().
I don't think that the framework has a better solution. One other method is to use the Fluent Query Builder and create your own query but I don't see how that would be better.
Define a hasManyThrough relationship in User::find(1)->roles()->tools
class User extends Eloquent {
public function tools()
{
return $this->hasManyThrough('Tool', 'Role');
}
}
Then you can access straight forward:
$user->tools