get members who have a role - spatie permission - php

I have a question, how do I get users who do not have a role?
I use spatie permission
ex:
User::notRole('manager')->get()
Here is a query that performs the required, but the user must have at least one role because it is used whereHas,
otherwise, it will be a result null
If the user doesn't have a role I can't get him among the users who don't have a role manager
public function scopeDoesntHaveRole(Builder $query, $roles, $guard = null): Builder
{
if ($roles instanceof Collection) {
$roles = $roles->all();
}
if (!is_array($roles)) {
$roles = [$roles];
}
$roles = array_map(function ($role) use ($guard) {
if ($role instanceof Role) {
return $role;
}
$method = is_numeric($role) ? 'findById' : 'findByName';
return $this->getRoleClass()->{$method}($role, $guard ?: $this->getDefaultGuardName());
}, Arr::wrap($roles));
return $query->whereHas('roles', function (Builder $subQuery) use ($roles) {
$roleClass = $this->getRoleClass();
$key = (new $roleClass())->getKeyName();
$subQuery->whereNotIn(config('permission.table_names.roles') . ".$key", \array_column($roles, $key));
});
}

I think you can approach it like this :
use Illuminate\Database\Eloquent\Builder;
$users= User::whereDoesntHave('roles', function (Builder $query) {
$query->where('name', 'manager');
})->get();
since on their documentation they have an example:
$users_without_any_roles = User::doesntHave('roles')->get();
https://spatie.be/docs/laravel-permission/v5/basic-usage/basic-usage#content-eloquent

Related

Detach role from the user and delete the user in bulk along with livewire traits

Description
I have a "belongsToMany" eloquent relationship between users and roles . I am creating a CRUD system with capability to do bulk actions. I followed the Livewire Screencasts Bulk Export/Delete and Refactoring For Re-Usability. I have created a trait for the bulk actions, so I'm able to use it out of the box.
I need to detach the role from the user and then delete the user in bulk. I am unable to call roles relationship on a public method property and detach the same. This is how I detach the role for a single user $this->user->roles()->detach(); but I'm unable to do it with $this->selectedRowsQuery->roles()->detach(); in case of bulk user deletion.
Stripped-down, copy-pastable code snippets
Livewire/Backend/UserController
public $showUserBulkDeletionModal = false;
public function confirmDeleteBulk()
{
$deleteCount = $this->selectedRowsQuery->count();
$this->selectedRowsQuery->roles()->detach();
$this->selectedRowsQuery->delete();
$this->showUserBulkDeletionModal = false;
$this->notify('You\'ve deleted '.$deleteCount.' users');
}
public function getRowsQueryProperty()
{
$query = User::query()
->when($this->filters['email'], fn($query, $email) => $query->where('email', 'like', '%'.$email.'%'))
->when($this->filters['role'], fn($query, $role) => $query->whereHas('roles', fn ($query) => $query->where('id', $role)))
->when($this->filters['search'], fn($query, $search) => $query->where('name', 'like', '%'.$search.'%'))
->when($this->filters['date-min'], fn($query, $created_at) => $query->where('created_at', '>=', Carbon::createFromFormat('d/m/Y', $created_at)))
->when($this->filters['date-max'], fn($query, $created_at) => $query->where('created_at', '<=', Carbon::createFromFormat('d/m/Y', $created_at)));
return $this->applySorting($query);
}
Livewire/Traits/WithBulkActions
trait WithBulkActions
{
public $selectPage = false;
public $selectAll = false;
public $selected = [];
public function renderingWithBulkActions()
{
if ($this->selectAll) $this->selectPageRows();
}
public function updatedSelected()
{
$this->selectAll = false;
$this->selectPage = false;
}
public function updatedSelectPage($value)
{
if ($value) return $this->selectPageRows();
$this->selectAll = false;
$this->selected = [];
}
public function selectPageRows()
{
$this->selected = $this->rows->pluck('id')->map(fn($id) => (string) $id);
}
public function selectAll()
{
$this->selectAll = true;
}
public function getSelectedRowsQueryProperty()
{
return (clone $this->rowsQuery)
->unless($this->selectAll, fn($query) => $query->whereKey($this->selected));
}
}
Context
Livewire version: 2.3.6
Laravel version: 8.23.1
Alpine version: 2.8.0
Browser: Chrome
This line won't work:
$this->selectedRowsQuery->roles()->detach();
$this->selectedRowsQuery is a Collection, and your code isn't smart enough to know which instance of roles() you're trying to detach (delete). You simply need to do this in a loop:
foreach ($this->selectedRowsQuery as $queryRow) {
$queryRow->roles()->detach();
$queryRow->delete(); // Can do this here (single delete), or below
}
$this->selectedRowsQuery->delete(); // Can do this here (batch delete), or above
Edit: At the time of the foreach(), $this->selectedRowsQuery is still an instance of the Builder class, which is incompatible with foreach() until a Closure (get(), cursor(), etc.) is passed. To handle this, simply adjust your code as:
foreach ($this->selectedRowsQuery->get() as $queryRow) {
$queryRow->roles()->detach();
...
}
Note: ->get() is more widely used, but ->cursor() is available and generally more performant for larger loops.

Laravel 5.6 apply pivot scope in relationship

I need to apply a scope form a pivot class in a morphedByMany relationship included in a User class. This is the relationship:
public function nodes($nodeClass)
{
return
$this->morphedByMany(
$nodeClass,
'node',
'users_permissions',
'user_id',
'node_id',
'id',
'id'
)
->using('App\Models\UserNode');
}
Then, I have a role scope in the pivot class (UserNode). That scope is not our code, it is from HasRoles Traits from spatie/laravel-permission package:
public function scopeRole(Builder $query, $roles, $guard = null): Builder
{
if ($roles instanceof Collection) {
$roles = $roles->all();
}
if (! is_array($roles)) {
$roles = [$roles];
}
$roles = array_map(function ($role) use ($guard) {
if ($role instanceof Role) {
return $role;
}
$method = is_numeric($role) ? 'findById' : 'findByName';
$guard = $guard ?: $this->getDefaultGuardName();
return $this->getRoleClass()->{$method}($role, $guard);
}, $roles);
return $query->whereHas('roles', function ($query) use ($roles) {
$query->where(function ($query) use ($roles) {
foreach ($roles as $role) {
$query->orWhere(config('permission.table_names.roles').'.id', $role->id);
}
});
});
}
When I try to get specific class of nodes inside User class like this it works perfectly:
$nodes = $this->nodes($nodeClass)->get();
However, I don't know how I can apply properly the scope 'role' to the pivot class of the relation because wherePivot or wherePivotIn does not accept scopes, relations or closures. I have also tried other things with the same result like:
$nodes = $this->nodes($nodeClass)->whereHas('pivot', function($q) use ($roles) {
$q->role($roles);
})->get();
Is there any way to apply the pivot class' scope to this relation?
Thanks in advance

Getting all related fields to related field in Eloquent

I have this DB structure:
classroom_user is Elqouent many-to-many pivot.
User.php:
public function classrooms() {
return $this->belongsToMany(ClassRoom::class, 'classroom_user', 'classroom_id', 'user_id');
}
ClassRoom.php:
public function users()
{
return $this->belongsToMany(User::class, 'classroom_user', 'user_id', 'classroom_id');
}
I have a $user and want to write Eloquent method which gets all users who are in at least one of classrooms the $useralso is in. Something like $user->classrooms->users. But this falls with error Property [users] does not exist on this collection instance.. How can I do this?
I think you can do it like this:
$users = User::whereHas('classrooms', function($query) {
$query->whereHas('users', function($query) {
$query->where('id', auth()->user()->id);
});
})->get();
You could store this as a scope in your User model:
public function scopeClassmatesOf($query, $user)
{
$query->whereHas('classrooms', function($query) use($user) {
$query->whereHas('users', function($query) use($user) {
$query->where('id', $user->id);
});
})
}
And then call it from a controller like:
$user = auth()->user();
$users = User::classmatesOf($user)->get();
Your asking for users of a collection. $user->classrooms is a collection of classrooms.
If you do $user->classrooms->first()->users you will get all the users from the first classroom.
To get all users from all classrooms, you can do something like;
$users = $user->classrooms->map(function ($classroom) {
return $classroom->users;
});
Note that will return duplicate users and authenticated user him self witin the collection. So, you might need to do some filtering to the result.
This worked for me:
public function classmates() {
$self = $this;
$a = User::with('classrooms')->whereHas('classrooms', function ($query) use ($self) {
$query->whereHas('users', function ($query) use ($self) {
$query->where('users.id', $self->id);
});
});
return $a;
}

Laravel many to many that match all data from array

I have products which have a many to many relationship with filters
The user may choose multiple filters and I want to display all products that match the selected filters. But by matching them I mean containing all of them (not only some of them). Here's an example to explain what I mean, let's say that the user is on the cars category page and he wants to filter all cars that are from year 2013 AND have 4x4. Now if the user selects those filters it will show all the cars that are from year 2013 OR have 4x4.
Here's my code in the controller:
public function showFilteredProducts(Request $request)
{
$products = collect([]);
$this->request = $request;
foreach ($request->filters as $filter_id => $active) {
$this->filter_id = $filter_id;
$queriedProducts = Product::whereHas('filters', function($query) {
$query->where('filters.id', $this->filter_id);
})
->whereHas('category', function($query) {
$query->where('slug', $this->request->category_slug);
})
->get();
foreach ($queriedProducts as $product) {
if (!$products->contains($product)) {
$products[] = $product;
}
}
}
return response()->json($products->chunk(3));
}
As i explained this now returns the products if they match only one of the filters, but I want them to match all of them.
try this
public function showFilteredProducts(Request $request)
{
$filters = $request->filters;
$query = Product::query();
foreach ($filters as $filter_id => $active) {
$query = $query->whereHas('filters', function($query) use ($filter_id) {
$query->where('filters.id', $filter_id);
});
}
$query = $query->whereHas('category', function($query) use ($request) {
$query->where('slug', $request->category_slug);
})
$products = $query->get();
return $products->chunk(3);
}
alternatively, based on your previous code, you can use array_intersect like this:
public function showFilteredProducts(Request $request)
{
$products = collect([]);
$this->request = $request;
foreach ($request->filters as $filter_id => $active) {
$this->filter_id = $filter_id;
$queriedProducts = Product::whereHas('filters', function($query) {
$query->where('filters.id', );
})
->whereHas('category', function($query) {
$query->where('slug', $this->request->category_slug);
})
->get();
$products = array_intersect($queriedProducts, $products);
}
return response()->json($products->chunk(3));
}
I think you want to use orWhereHas() instead of whereHas() on the second table that you are checking against.

Passing an array in the construct of a controller with laravel

I am wanting to find out how I can pass in roles as an array because when I try to do this in my construct of my controller it always seems to only be Administrator.
The following is my app/controllers/UserscController.php
class UsersController extends BaseController {
public function __construct()
{
parent::__construct();
$this->beforeFilter('role:Administrator,Owner');
}
}
The following is my app/filters.php
Route::filter('role', function($route, $request, $roles)
{
if(Auth::guest() !== true)
{
if(!empty($roles))
{
$roles = explode(',', $roles);
if(count($roles) > 0)
{
foreach($roles as $role)
{
if (Auth::user()->role->role_name == $role)
{
return;
}
}
}
}
return Redirect::to('/youshallnotpass');
}
});
You can set the roles as a parameter in your controller. To load the roles into your filter you would do this:
Route::filter('role', function($route, $request)
{
if(Auth::guest() !== true)
{
$roles = $route->parameter('roles');
if(!empty($roles))
{
foreach($roles as $role)
{
if (Auth::user()->role->role_name == $role)
{
return;
}
}
}
return Redirect::to('/youshallnotpass');
}
});
Note:
Before laraval 4.1 you would use: $roles = $route->getParameter('roles'); instead of $roles = $route->parameter('roles');
Hope this helps!
Laravel already explodes your filter parameters on the comma, to allow passing multiple parameters to a filter. So, in your case, you've actually passed two parameters to your filter: the first parameter has the value 'Administrator', and the second parameter has the value 'Owner'.
So, two quick options are:
Change the delimiter you're using in your string. In your controller: $this->beforeFilter('role:Administrator;Owner'); and then in your filter: $roles = explode(';', $roles);
Or, leave your controller code alone and use PHP's func_get_args() function in your filter:
Route::filter('role', function($route, $request)
{
if(Auth::guest() !== true)
{
$roles = array_slice(func_get_args(), 2);
foreach($roles as $role)
{
if (Auth::user()->role->role_name == $role)
{
return;
}
}
return Redirect::to('/youshallnotpass');
}
});

Categories