Laravel-permission: Using 2 Models for scoping results from the same roles

Created on 27 Aug 2018  路  10Comments  路  Source: spatie/laravel-permission

Hi. I have 2 Models for User. One for keeping all the relationships and scopes for the frontend and another for the admin area which has different scopes and so on.

The frontend user is located at App/Models/User.php and the Admin one App/Models/Admin/User.php

They are both almost identical bar the scopes and relationships.

What I'm trying to do is in the backend controllers use App/Models/Admin/User.php but when I call the role it doesn't get any results:

use App/Model/Admin/User;

$user = User::role('customer')->get();

But if I change the use App/Model/Admin/User; to use App/Model/User; it works fine.

Heres the model for Admin/User.php which is identical on the User.php:

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Cashier\Billable;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use Notifiable;
    use HasRoles;

    protected $guard_name = 'web';

Any help or info would be greatful

support

Most helpful comment

There are a couple of methods to work around this.
First if you only have one entity that extends User functionality, you can add it to the morphMap array. Adding this to the boot() method in AppServiceProvider.php.

    public function boot()
    {
        //
        Relation::morphMap([
            User::class => Teacher::class
        ]);

The other way, if you have more logical entities, like Admins, Managers, all that extend User class.
You could override the getMorphClass on the User model to always return User::class.

    public function getMorphClass()
    {
        return self::class;
    }

In theory, you could instead return a meaningful equivalent string and also add the Relation::morphMap for that string, but I haven't tested that one.

The only caveat is that this will override all MorphTo or MorphToMany relations to give the overridden specific string every time you're building a relation. So keep that in mind.

All 10 comments

Oh the joys of polymorphic relations.
When you assign roles (or permissions for that matter) to a user, its model class name is recorded in the pivot table, essentially hard-linking it to that specific model.

So, if you assign roles to an App/Models/User' object, it recordsApp/Models/Userin the database, and therefore an attempt to view relations by another object such asApp/Models/Admin/User` will not return those roles assigned to the other object.

One way I've gotten around this is by reverting to v1 of this package, which doesn't use polymorphism. It's a simpler relation structure and skips this issue because it assumes only one type of user model. This has its pros and cons.
(Note: if you wanna give it a try, revert and delete the migrations for the v2 and rebuild all your roles/permissions assignments fresh in v1 -- the 2 schemas are incompatible with each other.)

Ah brilliant, I'll give that a shot and see how I get on. If not I'll have to keep it on v2 and just suck it up lol...

Many thanks for the reply.

Is there another way of achieving what @cloudwales described, other than reverting to v1 of the package?

Any help would be greatly appreciated. From what I've read, I'm not the only one looking for a workarround to this issue.

@laxsmi,
A few questions:

  • Can you point me to your research which suggests high demand for this feature? (because I'd like to investigate their specific use-cases to find a solution).
  • What about v2 of the package do you want/need that v1 doesn't offer? One big difference is the polymorphism, but of course that's also the cause of the main pain point this discussion is about
  • Can you share code for a project where we could collaborate or at least discuss with specifics?

I don't know that there is a _high demand_ but I believe that #292 this fork for instance aims at something that could be achieved easily by extending a model, and scoping the new model based on roles or permissions.

My scenario is this one: I have different models (e.g. Student and Teacher) extending User. I was planning to restrict the scope of these new models to the users who have been assigned these roles (using Laravel global scopes). From these models, I would then be call Student::all() and all other methods without the need to re-write code like \App\User::role('admin')->get(); for every method within the model.

However, since the roles are assigned to the User model, I am not able to retrieve the users from another model...

I'm facing this issue within similar use cases. I have users and admins.

Admin is a model who extends User. User is the main model, predefined by Laravel.

class Admin extends User
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('admin', function (Builder $builder) {
            $builder->whereHas('roles', function ($query) {
                $query->where('guard_name', 'admin');
                $query->where('name', 'admin');
            });
        });
    }
}
class User extends Authenticatable implements CanBeOwner

This is exactly what I was talking about. Thank you @drbyte for suggesting to reverting to the v1 of the package - I wasn't aware that it was still maintained, but this can be a viable solution. Since my project is also using Backpack, though, this doesn't work for me: the permissions module of Backpack requires the v2.

So I reverted to using the User model only, with local scopes:

   public function scopeTeacher($query)
    {
        return User::role('teacher');
    }

which allows me to do stuff like User::teacher()->get() whenever I need to perform operations on what would have been my Teacher model.

This isn't as clean as I'd like to be, but it works.

@laxsmi , your solution is better than you think. I'll refactor my code to be more like yours. Thank you.

@laxsmi thanks, We have the same query "Teacher" hahaha

There are a couple of methods to work around this.
First if you only have one entity that extends User functionality, you can add it to the morphMap array. Adding this to the boot() method in AppServiceProvider.php.

    public function boot()
    {
        //
        Relation::morphMap([
            User::class => Teacher::class
        ]);

The other way, if you have more logical entities, like Admins, Managers, all that extend User class.
You could override the getMorphClass on the User model to always return User::class.

    public function getMorphClass()
    {
        return self::class;
    }

In theory, you could instead return a meaningful equivalent string and also add the Relation::morphMap for that string, but I haven't tested that one.

The only caveat is that this will override all MorphTo or MorphToMany relations to give the overridden specific string every time you're building a relation. So keep that in mind.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

younus93 picture younus93  路  4Comments

bhulsman picture bhulsman  路  3Comments

ghost picture ghost  路  3Comments

neoreids picture neoreids  路  3Comments

ionesculiviucristian picture ionesculiviucristian  路  4Comments