Laravel controllers with roles - php

I have an application which will be a SaaS and is utilizing user roles. Of course, controllers will need to forward different data depending on user roles or permissions, however I think this approach may lead me to huge controllers and I was wondering if there is a smarter way to do this? For example my user create method:
public function create()
{
if (Auth::user()->isAdmin()) {
$clinics = Clinic::pluck('name', 'id');
$roles = Role::pluck('display_name', 'id');
}
else{
$clinics = Clinic::where('id', Auth::user()->clinic_id)->get()->pluck('name', 'id');
$roles = Role::where('name', '!=', 'admin')->get()->pluck('display_name', 'id');
}
$states = State::pluck('name', 'id');
$cities = City::pluck('name', 'id');
return view('users.create', compact('user', 'clinics', 'states', 'cities', 'roles'));
}
Which is okay now when I only implemented admin and non-admin user, but when roles get complicated, is there a cleaner way to assemble this?

I suggest you to take a look to the Scopes of the Laravel Documentation. You can attach the scopes to your models to achieve the same results.
This solution will not help you deleting code complexity (that is moved in models) but will help you remove code duplication because you will encounter the same "if" multiple times during the development of your application...
A local scope for your clinics could be like this one
class Clinic extens Model {
[...]
public function scopeCanSee($query)
{
$user = Auth::user();
if(!$user->isAdmin())
return $query->where('id', $user->clinic_id);
return $query;
}
}
and in your controller you can then filter the results in this way
public function create()
{
$clinics = Clinic::canSee()->pluck('name', 'id');
[...]
$states = State::pluck('name', 'id');
$cities = City::pluck('name', 'id');
return view('users.create', compact('user', 'clinics', 'states', 'cities', 'roles'));
}
Global Scopes
Another way is to use the Global Scopes (but I haven't tested them)
class Role extends Model
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(new RolesScope);
}
}
class Clinic extends Model
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(new ClinicsScope);
}
}
and scopes similar to
class ClinicsScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$user = Auth::user();
$builder->where('id', $user->clinic_id);
}
}

Related

Accessing a hasMany relation function via Collection

I am trying to get all of the users notifications, and depending on if the user is a buyer or seller (can be both). I have made two functions in my notifications table to filter each other out.
My goal is to ultimately run:
$notifications = Auth::user()->notifications()->getBuyerNotifications();
or
$notifications = Auth::user()->notifications()->getSellerNotifications();
I am running into an issue: Call to undefined method Illuminate\Database\Eloquent\Relations\HasMany
User Model:
public function notifications() {
return $this->hasMany('App\Notification', 'user_id', 'id');
}
Notifications Model:
public function user() {
return $this->belongsTo('App\User', 'id', 'user_id');
}
public static function getBuyerNotifications() {
return self::whereNotNull('buyer_id')
->whereNull('deleted_at')
->get();
}
public static function getSellerNotifications() {
return $this->whereNotNull('seller_id')
->whereNull('deleted_at')
->get();
}
The command I want to run to get all of the users notifications if they're a buyer: $notifications = Auth::user()->notifications()->getBuyerNotifications();
Firstly, you don't need to use whereNull('deleted_at'), you can import the softDeletes Trait in your model:
use Illuminate\Database\Eloquent\SoftDeletes;
...
class Notification extends Model {
use SoftDeletes;
...
}
Laravel will automatically use whereNull('deleted_at') on Eloquent-Builder.
Secondly, you cannot use static method on Illuminate\Database\Eloquent\Relations\HasMany.
Use scope method instead:
public function scopeBuyerNotifications($query) {
return $query->whereNotNull('buyer_id');
}
public function scopeSellerNotifications($query) {
return $query->whereNotNull('seller_id');
}
So you can find the notification like this:
$notifications = Auth::user()->notifications()->sellerNotifications()->get();
$notifications = Auth::user()->notifications()->buyerNotifications()->get();
Auth::user() uses session data.
Try this:
optional(User::find(Auth::id())->notifications)->getBuyerNotifications;
or
$userId = 1; // Example id you can just pass the user Id.
User::find($userId)->notifications->getBuyerNotifications;
You can add two other methods in user model as follows
public function getBuyerNotifications() {
return $this->hasMany('App\Notification', 'buyer_id', 'id');
}
public function getSellerNotifications() {
return $this->hasMany('App\Notification', 'seller_id', 'id');
}
And you can call it directly from the user instance
$user->getBuyerNotifications();
$user->getSellerNotifications();

Laravel - my scope method does not appear to be working

I am using scope to filter conditions for specific users, I have assigned a course to a teacher and when this teacher signs into their account, I only want this teacher to view their course, although my scope method doesn't appear to be working correctly. I am not getting an error, so I am not sure where I have gone wrong. I have added some of my code below, i would be very grateful for any help. Thanks
CoursesController index method;
public function index()
{
$courses = Course::ofTeacher()->get();
return view('admin.courses.index')->with('course', $courses); //pass data down to view
}
Course.php;
<?php
namespace App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Model;
class Course extends Model
{
protected $fillable = [
'id', 'title'
];
public function courses(){
return $this->belongsToMany('App\User');
}
public function teachers() {
return $this->belongsToMany(User::class, 'course_user')
}
public function scopeOfTeacher($query)
{
if (!Auth::user()->isAdmin()) {
return $query->whereHas('teachers', function($q) {
$q->where('user_id', Auth::user()->id);
});
}
return $query;
}
}
User.php;
public function isAdmin() {
return $this->role()->where('role_id', 1)->first();
}

Laravel permissions can() function | Authorization

I have users, app_roles, app_permissions, app_permission_app_role, app_role_user.
The tables are self explanatory, I am creating permissions, Then assigning that permissions to new role on role creation, And then i assigns roles to users.
i check permission of the authenticated user like :
#if(auth()->user()->can('some route name'))
Html...
#endif
The above condition checks if the user have access to that content or not based of the assigned role as we know that the role have permissions, And the can('some route name') parameter is a route name. Its working fine.
Where i am stuck !!!
The table app_role_user had user_id, app_role_id, Now i added another column organization_id... (Consider Organizations as groups, Where a user can be a member of that groups, And the owner of the group assigns single role(Can't assign multiple role) to that user). Because now the user can switch between organization and the user can have different roles in different organizations.
I have to clear path for the :
#if(auth()->user()->can('some route name'))
Html...
#endif
Note : : Auth::user()->current_org->id show the id of the organization in which the user is in right now
As well as currently i am saving role_id, user_id, organization_id in app_role_user table.
Here is my AuthServiceProvider class,
I am Dynamically registering permissions with Laravel's Gate :
public function boot(GateContract $gate)
{
$this->registerPolicies();
$this->registerAllPermissions($gate);
}
protected function getPermissions() {
return $this->app->make('App\Repositories\PermissionRepository')->withRoles();
}
private function registerAllPermissions($gate) {
if (Schema::hasTable('app_permissions') and Schema::hasTable('users') and Schema::hasTable('app_roles')) {
cache()->forget('app_permissions_with_roles');
foreach ($this->getPermissions() as $permission) {
$gate->define($permission->name, function ($user) use ($permission) {
return $user->hasPermission($permission);
});
}
}
}
Here is PermissionRepository class :
class PermissionRepository
{
protected $model;
public function __construct(AppPermission $model)
{
$this->model = $model;
}
public function all(){
return $this->model->all();
}
public function withRoles(){
$model = $this->model;
$permissions = cache()->remember('app_permissions_with_roles', 1*60*24, function() use($model) {
return $model->with('roles')->get();
});
return $permissions;
}
}
And here is HasRoles trait having hasPermission(AppPermission $permission) because AuthServiceProvider class needs it in registerAllPermissions.
trait HasRoles {
public function assignRole($role)
{
return $this->roles()->save(
AppRole::whereName($role)->firstOrFail()
);
}
public function hasRole($role)
{
if (is_string($role)) {
return $this->roles->contains('name', $role);
}
return !! $role->intersect($this->roles)->count();
}
public function hasPermission(AppPermission $permission)
{
return $this->hasRole($permission->roles);
}
}
What should i do, I have tried many conditions but nothing worked at all.
Looking forward to hear from you guys.
Thanks for the read, Need serious attention please.
You can try like this
User Model
//add organization_id as pivot field
public function roles(){
return $this->belongsToMany(AppRole::class)->withPivot('organization_id');
}
//define a function
public function orgRoles($orgId){
return $this->roles()->wherePivot('organization_id', $orgId)->get();
}
Now in trait modify hasRole function
public function hasRole($role)
{
$orgId = Auth::user()->current_org->id;
if (is_string($role)) {
return $this->orgRoles($orgId)->contains('name', $role);
}
return !! $role->intersect($this->orgRoles($orgId))->count();
}

Using a polymorphic relation in a child model causes an infinite loop?

This question was already asked here but it received no answer. Now I face the same problem but in laravel 5.4. I have a model Book, a model ReadingSession and a model Comment. A book has many reading sessions and has many comments but the reading session can also have comments. So I have my relations defined like this:
Book.php
protected $with = [
'author',
'readingSessions',
'userRating',
'ratings',
'comments'
];
public function users()
{
return $this->belongsToMany(User::class, 'user_book');
}
public function author()
{
return $this->belongsTo(Author::class);
}
public function allReadingSessions()
{
return $this->hasMany(ReadingSession::class);
}
public function readingSessions()
{
return $this->hasMany(ReadingSession::class)
->where('user_id', Auth::user()->id);
}
public function ratings()
{
return $this->hasMany(Rating::class);
}
public function userRating()
{
return $this->hasMany(Rating::class)
->where('user_id', Auth::user()->id);
}
public function comments()
{
return $this->morphMany('App\Models\Comment', 'commentable');
}
ReadingSession.php
protected $with = ['comments'];
public function user()
{
return $this->belongsTo(User::class);
}
public function book()
{
return $this->belongsTo(Book::class);
}
public function comments()
{
return $this->morphMany('App\Models\Comment', 'commentable');
}
Comment.php
public function commentable()
{
return $this->morphTo();
}
These seems to create an infinite loop. Can anyone hint me on what I'm doing wrong?
The main reason you might have an infinite loop there is if you are trying to load automatically a relationship that in turn tries to do the same with the previous model.
Putting it into an example:
Book.php
protected $with = [
'author',
];
public function author()
{
return $this->belongsTo(Author::class);
}
Author.php
protected $with = [
'books',
];
public function books()
{
return $this->hasMany(Book::class);
}
In this case, every time you fetch an author it will fetch automatically his books that in turn will try to fetch the author and on and on...
One other thing that might happen and it's harder to realize is when using the $appends property on some accessors. If you are trying automatically had a variable into a model through the $appends and if that accessor fetches a relation or uses a relation in some way you might get an infinite loop again.
Example:
Author.php
protected $appends = [
'AllBooks',
];
public function books()
{
return $this->hasMany(Book::class);
}
public function getAllBooksAttribute() {
return $this->books->something...
}
In this case, every time the app tries to resolve your Author model it will fetch the books, that in turn will fetch the Author, that in turn will fetch the books again and on and on...
From your snippets, is not clear what is causing the problem but this answer might give some leads where to search for it.
To solve it, you might remove the relation from the $with and load it manually: $author->load('books') or Author::with('books')->where...
You can also load a relation of a relation in this way, for example: $author->load('books', 'books.comments') or Author::with('books', 'books.comments')->where...
It all comes down what you are trying to achieve. So you have to evaluate what and what not you should auto-load.
Be careful when loading automatically relations on your models and when adding accessors to $appends, especially if they use relations. It is an awesome feature but can bite hard sometimes.

Distant HasManyThrough

I have four Models:
User
Client
Store
Opportunity
The relationships are defined as such:
User hasMany Client
Client hasMany Store
Store hasMany Opportunity
User hasManyThrough Store, Client (this works)
The problem is that I'm attempting to access the User->Opportunity relationship via built-in Laravel relationships, but it doesn't seem as if I can do it without a custom Query or an additional user_id column on the opportunities table to allow direct access (even though one can be inferred from the Store->Client relationship). I'm also not a fan of nested foreach loops if they can be avoided.
My question:
Is there a way to go one level deeper and directly access a User's Opportunities in this scenario? The actual Model code and all relevant relationships are as follows:
User
class User extends Eloquent{
public function clients(){
return $this->hasMany('Client');
}
public function stores(){
return $this->hasManyThrough('Store', 'Client');
}
public function proposals(){
return $this->hasMany('Proposal');
}
public function opportunities(){ //This does the job, but I feel like it could be better
return Opportunity::join('stores', 'stores.id', '=', 'opportunities.store_id')->
join('clients', 'clients.id', '=', 'stores.client_id')->
join('users', 'users.id', '=', 'clients.user_id')->
select('opportunities.*')->
where('users.id', $this->id);
}
public function getOpportunitiesAttribute(){ //This just helps mimic the hasManyThrough shorthand
return $this->opportunities()->get();
}
}
Client
class Client extends Eloquent{
public function stores(){
return $this->hasMany('Store');
}
public function user(){
return $this->belongsTo('User');
}
public function opportunities(){
return $this->hasManyThrough('Opportunity', 'Store');
}
}
Store
class Store extends Eloquent {
public function client(){
return $this->belongsTo('Client');
}
public function opportunities(){
return $this->hasMany('Opportunity');
}
}
Opportunity
class Opportunity extends Eloquent {
public function store(){
return $this->belongsTo('Store');
}
}
I don't think there is such method in Laravel. You have to create your custom query. This custom query can be very expensive since multiple queries will be performed. Thus, the optimum solution for this, according to me, is to relate User and Opportunity with a foreign key.
However, if you don't desire to link User and Opportunity with a foreign key, then you can create a custom query to handle this. Simply add a "hasManyThrough" relation between Opportunity and Client model like,
<?php
class Client extends Eloquent{
public function store(){
return $this->hasMany('Store');
}
public function user(){
return $this->belongsTo('User');
}
public function opportunity(){
return $this->hasManyThrough('Opportunity', 'Store');
}
}
Then create a static function in User model.
<?php
class User extends Eloquent implements UserInterface, RemindableInterface {
use UserTrait, RemindableTrait;
public function client(){
return $this->hasMany('Client');
}
public function store(){
return $this->hasManyThrough('Store', 'Client');
}
public static function getOpportunityOfUser($userId)
{
$clients = User::find($userId)->client;
foreach ($clients as $client) {
$opportunities[] = Client::find($client->id)->opportunity;
}
return $opportunities;
}
}
Now you can access Opportunity realted to a User in one go like,
Route::get('/', function()
{
return $usersOpportunities = User::getOpportunityOfUser(1);
});
This will return all opportunity of all clients related to User with id '1'.
I created a HasManyThrough relationship with unlimited levels: Repository on GitHub
After the installation, you can use it like this:
class User extends Model {
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
public function opportunities() {
return $this->hasManyDeep(Opportunity::class, [Client::class, Store::class]);
}
}

Categories