Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

FrazeColder's avatar

Laravel relationship: Accessing method of another model in a relationshop retrievs always the first entry (wrong entry)

Hi,

I have found a very strange behavior. I have a User model, a Role model, many other models like a Comments model or a Page model because a page can have comments and so on. However, I have simplified the problem and even created a new project to reproduce this problem. And I am facing the same problem as I am in my own project. I have also attached the repo so you can test it on your own (download link at the end of this text).

My problem is that a User has one Role which is assigned by the role_id in the User table. No problem so far. However, in my User model I have assigned a belongsTo relationship to the Role model to get the role of the user.

Why I am doing this? Because only people with certain roles should be able to see certain pages or comments for exmaple. So, I am basically checking if the user has the permission to see certain pages.

The problem I am facing now is that any user I choose, always has the superadmin role. The superadmin role is the first role along my two other roles in my Role table. Besides superadmin I also have to role admin and user. Please check out the repo, I have also created migrations and seeders so you can easily reproduce the problem.

But this problem only occurs when my Page model performs eager loading via the protected $with variable or in the controller when using $page->load('comments'). When eager loading is activated, the user, no matter which one, always has the first role which there is superadmin.

For me this is not normal and sounds like a bug? I am also very confused because I only have this problem with my role relationship and no other relationship. Maybe I cannot use the name role?

What I am basically trying to do is to check weather the user is admin or not via the ->isAdmin() function in my User model. Which also is very strange when I call the ->isAdmin() method via php artisan tinker or basically inside my view the user has the correct role. But why doesn't the user has the correct role when accessing it in a relationship?

Here you can download my repo. After you have downloaded it, please run the migrations and seeders and call the URI /page/1. There you can see a dump which says superadmin although User 2 has the role called user. Then switch the $with attribute in my Page model form comments to test and you can see the user will have the correct role as the role is resolved in the view itself.

Note 1: I wanted to keep the repo as small as possible. So, I have not implemented a login and authenticate feature. All queries are made on the User with the id 2. This user has the role called user. However, I have tested if it makes any difference if I am using the auth user or just any User from the database. It doesn't make any difference. So, it also has nothing to do with auth.

Note 2: Even when performing a refresh on my model the role relationship still is wrong!

Note 3: Why I even want to know if the user is admin or not? Because as I mentioned earlier, my page can have comments. Comments have a page status (publish, pending, draft, etc.) and I only want to show to normal users the published comments and to the admins the published and also pending comments so they can approve them. For this I need to perform a if-else statement and have to check (inside the relationship) via the isAdmin() function weather to load only published or also pending comments.

Note 4: The user is always the correct when. Even when doing this with auth()->user(). However, the role relation is wrong because it always is the first entry in my database which there is superadmin!

I am using PHP 7.4.11 and Laravel 8.15.0. I will post my code here as well:

Role migration:

Schema::create('roles', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name')->unique();
    $table->string('label')->unique();
    $table->timestamps();
});

User migration:

Schema::create('users', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->unsignedBigInteger('role_id')->index();
    $table->string('name', 255)->unique();
    $table->string('email')->unique()->nullable();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password')->nullable();
    $table->timestamps();

    $table->foreign('role_id')
        ->references('id')
        ->on('roles');
});

Page migration:

Schema::create('pages', function (Blueprint $table) {
    $table->increments('id');
    $table->unsignedBigInteger('user_id')->index();
    $table->char('heading')->index();
    $table->char('slug')->index()->unique();
    $table->mediumText('content');
    $table->timestamps();

    $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onDelete('restrict');
});

Role seeder:

\DB::table('roles')->delete();

\DB::table('roles')->insert(array (
    0 =>
    array (
        'id' => 1,
        'name' => 'superadmin',
        'label' => 'Superadmin',
        'created_at' => date(now()),
        'updated_at' => date(now()),
    ),
    1 =>
    array (
        'id' => 2,
        'name' => 'admin',
        'label' => 'Admin',
        'created_at' => date(now()),
        'updated_at' => date(now()),
    ),
    2 =>
    array (
        'id' => 3,
        'name' => 'user',
        'label' => 'User',
        'created_at' => date(now()),
        'updated_at' => date(now()),
    ),
));

User seeder

\DB::table('users')->delete();

\DB::table('users')->insert(array (
    0 =>
        array (
            'id' => 1,
            'role_id' => 1,
            'name' => 'MyName',
            'email' => '[email protected]',
            'email_verified_at' => date(now()),
            'password' => 'mypassword',
            'created_at' => date(now()),
            'updated_at' => date(now())
        ),
    1 =>
        array (
            'id' => 2,
            'role_id' => 3,
            'name' => 'ThisIsMyName',
            'email' => '[email protected]',
            'email_verified_at' => date(now()),
            'password' => 'mypassword',
            'created_at' => date(now()),
            'updated_at' => date(now())
        )
));

Page seeder:

\DB::table('pages')->delete();

\DB::table('pages')->insert(array (
    0 =>
        array (
            'id' => 1,
            'user_id' => 1,
            'heading' => 'My Test Heading',
            'slug' => 'my-test-heading',
            'content' => 'This is my content',
            'created_at' => date(now()),
            'updated_at' => date(now())
        ),
    1 =>
        array (
            'id' => 2,
            'user_id' => 1,
            'heading' => 'My Test Heading 1',
            'slug' => 'my-test-heading-1',
            'content' => 'This is my content',
            'created_at' => date(now()),
            'updated_at' => date(now())
        ))
);

Role model:

class Role extends Model
{
    use HasFactory;

    /**
     * @Protected_variables
     */

    protected $table = 'roles';

    protected $guarded = ['id'];

    /**
     * @Public_variables
     */

    /**
     * @Relationships
     */

    /**
     * @Attributes
     */

    /**
     * @Custom_functions
     */

    public static function getIdByName($name)
    {
        return Cache::rememberForever('getRoleIDByName.'.$name, function() use ($name)
        {
            return self::where('name', $name)->first()->id;
        });
    }
}

User model:

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{

    protected $table = 'users';

    protected $guarded = ['id'];

    protected $hidden = [
        'password', 'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function role()
    {
        return $this->belongsTo('App\Models\Role', 'role_id', 'id');
    }

    public function hasRole($roleNames)
    {
        //dd($this->role->id);
        $role = $this->refresh()->role->name;

        if(is_array($roleNames)){
            return in_array($role, $roleNames);
        }

        if(is_string($roleNames)){
            return $role == $roleNames;
        }

        return false;
    }

    public function isAdmin($authorized = array('admin', 'superadmin'))
    {
        return $this->hasRole($authorized);
    }
}

Page model

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Page extends Model
{
    protected $table = 'pages';

    protected $guarded = ['id'];

    protected $with = ['comments'];

    protected $casts = [
        'publish_at' => 'datetime',
        'created_at' => 'datetime',
        'updated_at' => 'datetime'
    ];

    public function test(){
        return $this->belongsTo('App\Models\User');
    }

    public function comments()
    {
        $user = User::find(2);

        if($user->isAdmin()){
            dd("I am FALSELY admin!");
            dump($user->role->name . "        <- This is wrong!");
            dump(json_encode($user->isAdmin()) . "              <- This is wrong!");
        }else{
            return $this->belongsTo('App\Models\User');
        }
    }

    public function user()
    {
        return $this->belongsTo('App\Models\User');
    }
}

Kind regards

0 likes
7 replies
s4muel's avatar
s4muel
Best Answer
Level 50

weird indeed, i hope someone more experienced answers the reason, why that happens. something to do with eager loading withing another eager loading?

but, if you update the hasRole() method in User model like so:

    public function hasRole($roleNames)
    {
        $this->load('role'); //<-- lazy load the role here
        $role = $this->role->name;

        if (is_array($roleNames)){
            return in_array($role, $roleNames);
        }

        if (is_string($roleNames)){
            return $role == $roleNames;
        }

        return false;
    }

since this is in the context of eager loading from page ~> comments ~> (user)->isAdmin() it lazy loads the role instead and it does (should) work.

anyway i think it could be better to separate the comments method on Page model to publishedComments and pendingComments (with respective constraint) and decide which to show on the outer level (outside the eager loading)

2 likes
JeromeFitzpatrick's avatar

Hey @frazecolder,

TL;DR

Loading the $user->role relation (via the $user->isAdmin method) in the comment relation method is mucking up the $user->role relation query.

If you eager load the role relation on the User model using the $with property, then it should work with the authenticated user using auth()->user()->isAdmin() or even with your current test code, but this is flakey. You should probably consider a different solution.

Reason why this is happening:

The reason you are getting the super admin each time is because while in the context of the comments relation, relation constraints are disabled statically on src/Illuminate/Database/Eloquent/Relations/Relation::class so when trying to do $user->isAdmin, the query that loads the $user->role relation is run without constraints, meaning it just finds the first role in the table (without constraining the query to the user).

So basically, you get select * from roles limit 1 instead of select * from roles where roles.id = 3 limit 1

2 likes
FrazeColder's avatar

Thanks for the answer @jeromefitzpatrick! But you are saying "You should probably consider a different solution.". Do you have any other solution or idea how I can solve this?

FrazeColder's avatar

@s4muel yes, thank you as well! I have not tested it yet. But you are also writing "anyway i think it could be better to separate it". But I will test it ASAP!

1 like
s4muel's avatar

we both are, there will probably be something to it;) but i proposed (in the first place) an "other solution how you can solve this"

1 like
FrazeColder's avatar

Thank you both. The solution from @s4muel is working!

But if anyone has a better solution, please feel free to comment!

Please or to participate in this conversation.