Laravel: Check if all values exists in a relation - php

I have a many to many relationship between User and Role models and want to check if a user has any role of a given list.
So I typed on model:
public function hasAnyRoles(array $roles) {
return $this->roles()->whereIn('name', $roles)->exists();
}
And on UserController:
// User has only "manager" role.
$user->hasAnyRoles(['admin', 'manager']) // true
The problem is that in some parts I need to verify if the user has all roles of the given list. For example:
// User has only "manager" role.
$user->hasRoles(['admin', 'manager']) // false
I wrote it in a "lazy" mode, that generates n + 1 queries:
public function hasRolesLazy($roles) {
return array_reduce($roles, function ($initial, $role) {
return $this->roles()->where('name', $role)->exists();
}, false);
}
How hasRoles(array $roles) method must be constructed to execute only one query in database? I'm a newbie in SQL so I can't figure out many solutions for this.

Try this:
public function hasAnyRoles(array $roles) {
return $this->whereHas('roles', function ($query) use ($roles) {
return $query->whereIn('name', $roles);
})->isNotEmpty();
}

My recommendation would be, to load all of the roles for the user once using the relationship. When you load a relationship by calling it like a property, Laravel will cache it under the hood, so it doesn't repeat any queries. You can then simply use the loaded roles to implement your two methods:
public function hasAnyRoles(array $roles)
{
// When you call $this->roles, the relationship is cached. Run through
// each of the user's roles.
foreach ($this->roles as $role) {
// If the user's role we're looking at is in the provided $roles
// array, then the user has at least one of them. Return true.
if (in_array($role->name, $roles) {
return true;
}
}
// If we get here, the user does not have any of the required roles.
// Return false.
return false;
}
and the second method:
public function hasRoles(array $roles)
{
// Run through each of the roles provided, that the user MUST have
foreach ($roles as $role) {
// Call the $this->roles relationship, which will return a collection
// and be cached. Find the first role that has the name of the role
// we're looking at.
$found = $this->roles->first(function ($r) use ($role) {
return $r->name == $role;
});
// If a role could not be found in the user's roles that matches the
// one we're looking, then the user does not have the role, return
// false
if (!$found) {
return false;
}
}
// If we get here, the user has all of the roles provided to the method
return true;
}
There are of course many different ways to implement these methods, particularly methods found in the Collection class will help you, but the point is that using $this->roles results in only a single query.

Related

How to fetch data from database using nested many to many relationships in Laravel

I'm working on Laravel Access Control Level (ACL) system. where is table contains some many to many to relationship. User table has many to many belongsToMany with Role Table and inversely many to many Role has belongsToMany with User table.Again, Role table has many to many belongsToMany relationship with Permission table and inversely Permission has many to many belongsToMany with Role table.
I want to run a query from user table which is fetch the all permissions of a role. this role is assigned to current user through roles table.
Here is my code sample.
User Model
public function roles()
{
return $this->belongstoMany(Role::class);
}
Role Model
public function users()
{
return $this->belongsToMany(User::class);
}
public function permissions()
{
return $this->belongsToMany(Permission::class);
}
Permission Table
public function roles()
{
return $this->belongsToMany(Role::class);
}
I've tried this query using egar loading...
public function hasPermission($permission)
{
if($this->roles()->with('permissions')->get()->pluck('permissions'))
{
return true;
}
return false;
}
But it always return false.
There are many ways to check if one of the roles has given permission.
One example, add hasPermission method to User.php model
public function hasPermission($permission = '') {
if ( empty($permisison) ) {
return false;
}
/*
Find all user roles that have given permission
*/
$rolesWithPermission = $this
->roles()
->whereHas('permissions', function($query) use ($permission) {
$query->where('name', $permission);
})
->get();
/*
If there is at least one role with given permission,
user has permission
*/
return $rolesWithPermission->count() > 0;
}
Or you can do one query with multiple joins between users, roles and permissions, and filter out the result with where('permissions.name', '=', $permission)

Laravel 5.7 not returning new record when global scope applied

I'm sure I'm missing something simple here but I am completely at a loss so any input would be greatly appreciated.
I have two models, User and Account with a many to many relationship with the model Channel. Accounts can be associated with multiple channels and users can also be associated with multiple channels. This has been created so that users can only access accounts that are associated with channels they are also associated with.
In order to do the filtering, I have applied a global scope to the account model so when I perform a query, it only returns accounts associated with the channels that the user is associated with. This works as intended for everything except newly created accounts.
If I call $account = Account::find($id) on a newly created account it returns null. If I drop the global scope it returns the account.
The only way I have found to fix the problem is if I sync the pivot table for the channel_user table and only include a single channel that is also associated with the account.
It feels like something is being cached somewhere but I'm not sure where to go from here. Please let me know what else you need to know
Account Model:
protected static function boot()
{
parent::boot();
static::addGlobalScope(new ChannelScope);
}
public function channels()
{
return $this->belongsToMany('App\Channel');
}
public function user()
{
return $this->belongsTo('App\User');
}
User Model:
public function accounts() {
return $this->hasMany('App\Account');
}
public function channels(){
return $this->belongsToMany( 'App\Channel' );
}
Channel Model:
public function accounts()
{
return $this->belongsToMany('App\Account');
}
public function users(){
return $this->belongsToMany('App\User');
}
Channel Scope:
public function apply(Builder $builder, Model $model)
{
$channels_ob = Auth::user()->channels;
$channels = array();
foreach ($channels_ob as $channel){
array_push($channels,$channel->id);
}
$builder->whereHas('channels', function ($q) use ($channels){
$q->where('channels.id','=', $channels);});
}
AccountController.php Store:
$account->save();
if (isset($request->chk_channels)){
foreach($request->chk_channels as $channel){
$ch = Channel::where('name',$channel)->get();
$ch_array[] = $ch[0]->id;
}
}
$account->channels()->sync($ch_array);
UserController.php update_channels:
public function update_channels(Request $request, $id)
{
$user = User::find($id);
if ($user->hasPermission('view_all_accounts')){
if (isset($request->chk_channels)){
foreach($request->chk_channels as $channel){
$ch = Channel::where('name',$channel)->get();
$ch_array[] = $ch[0]->id;
}
$user->channels()->sync($ch_array);
}
}
You can't have a column value equivalent to an array. You're building up an array of channels in your scope and then checking equivalency:
$q->where('channels.id','=', $channels);
Perhaps, you want whereIn:
$q->whereIn('channels.id', $channels);

Laravel: create a fake belongsToMany withPivot relationship

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();

Laravel Policies with Many-to-Many relationship

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.

Symfony 2 - Loading roles from database

My roles are stored in the database and I am trying to load them dynamically upon login. What I'm doing is querying for the roles and setting them on the user object in my user provider as seen here:
public function loadUserByUsername($username) {
$q = $this
->createQueryBuilder('u')
->where('u.username = :username')
->setParameter('username', $username)
->getQuery()
;
try {
// The Query::getSingleResult() method throws an exception
// if there is no record matching the criteria.
$user = $q->getSingleResult();
// Permissions
$permissions = $this->_em->getRepository('E:ModulePermission')->getPermissionsForUser($user);
if ($permissions !== null) {
foreach ($permissions as $permission) {
$name = strtoupper(str_replace(" ", "_", $permission['name']));
$role = "ROLE_%s_%s";
if ($permission['view']) {
$user->addRole(sprintf($role, $name, 'VIEW'));
}
if ($permission['add']) {
$user->addRole(sprintf($role, $name, 'ADD'));
}
if ($permission['edit']) {
$user->addRole(sprintf($role, $name, 'EDIT'));
}
if ($permission['delete']) {
$user->addRole(sprintf($role, $name, 'DELETE'));
}
}
}
} catch (NoResultException $e) {
throw new UsernameNotFoundException(sprintf('Unable to find an active admin Entity:User object identified by "%s".', $username), null, 0, $e);
}
return $user;
}
And the user entity:
class User implements AdvancedUserInterface, \Serializable {
....
protected $roles;
....
public function __construct() {
$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);
$this->roles = array();
}
....
public function getRoles() {
$roles = $this->roles;
// Ensure we having something
$roles[] = static::ROLE_DEFAULT;
return array_unique($roles);
}
public function addRole($role) {
$role = strtoupper($role);
$roles = $this->getRoles();
if ($role === static::ROLE_DEFAULT) {
return $this;
}
if (!in_array($role, $roles, true)) {
$this->roles[] = $role;
}
return $this;
}
public function hasRole($role) {
$role = strtoupper($role);
$roles = $this->getRoles();
return in_array($role, $roles, true);
}
}
This works fine and dandy and I see the correct roles when I do:
$this->get('security.context')->getUser()->getRoles()
The problem (I think), is that the token does not know about these roles. Because calling getRoles() on the token is showing only ROLE_USER, which is the default role.
It seems to me that the token is being created before the user is loaded by the UserProvider. I've looked through a lot of the security component but I can't for the life of me find the right part of the process to hook into to set these roles correctly so that the token knows about them.
Update Following the Load roles from database doc works fine, but this does not match my use case as shown here. My schema differs as each role has additional permissions (view/add/edit/delete) and this is why I am attempting the approach here. I don't want to have to alter my schema just to work with Symfony's security. I'd rather understand why these roles are not properly bound (not sure the correct doctrine word here) on my user object at this point.
It looks like you may not be aware of the built in role management that Symfony offers. Read the docs - Managing roles in the database It is actually quite simple to do what you want, all you need to do is implement an interface and define your necessary function. The docs I linked to provide great examples. Take a look.
UPDATE
It looks like the docs don't give you the use statement for the AdvancedUserInterface. Here it is:
// in your user entity
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
then in your role entity:
use Symfony\Component\Security\Core\Role\RoleInterface;
The docs show you how to do the rest.
UPDATE
Take a look at this blog post, which shows how to create roles dynamically:
Dynamically create roles
The problem here stemmed from the fact that I thought I was implementing
Symfony\Component\Security\Core\User\EquatableInterface;
but wasn't (as you can see in the original question, I forgot to add it to my class definition). I'm leaving this here for people if they come across it. All you need is to implement this interface, and add the following method to your user entity.
public function isEqualTo(UserInterface $user) {
if ($user instanceof User) {
// Check that the roles are the same, in any order
$isEqual = count($this->getRoles()) == count($user->getRoles());
if ($isEqual) {
foreach($this->getRoles() as $role) {
$isEqual = $isEqual && in_array($role, $user->getRoles());
}
}
return $isEqual;
}
return false;
}

Categories