0

When attempting to eager load roles with their assigned users from Spatie's laravel-permissions library like this

use Spatie\Permission\Models\Role;

Role::with('users')->get();

This error occurs

Error: Class name must be a valid object or a string in file vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php on line 791

The code above works just fine in the Laravel's PsySH powered Repl Tinker, see this StackOverflow post

So I thought if this only happens in HTTP requests, it must be due to a middleware issue

Am using Laravel Sanctum for API authentication and thus the route is under the middleware auth:sanctum

in routes/api.php

Route::middleware('auth:sanctum')->group(function () {
  Route::get('/roles', [RolesController::class, 'index']);
});

It also works if I move the route out of the middleware but I shouldn't, since only authenticated users should be able to access that endpoint

Am guessing this has something to do with the fact that roles get created with the guard_name as web in the database by default, but am not sure how to fix it

Salim Djerbouh
  • 10,719
  • 6
  • 29
  • 61

1 Answers1

0

TL;DR?

in config/auth.php add this to the guards array

'sanctum' => [
  'driver' => 'sanctum',
  'provider' => 'users'
],

Explanation

The problem is that Sanctum's service provider sets an authentication guard with a null value for the provider, see source code here

class SanctumServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        config([
            'auth.guards.sanctum' => array_merge([
                'driver' => 'sanctum',
                'provider' => null, // <=== This is the issue
            ], config('auth.guards.sanctum', [])),
        ]);

        if (! app()->configurationIsCached()) {
            $this->mergeConfigFrom(__DIR__.'/../config/sanctum.php', 'sanctum');
        }
    }

This causes Laravel-Permission's getModelForGuard() helper function here

function getModelForGuard(string $guard)
{
    return collect(config('auth.guards'))
        ->map(fn ($guard) => isset($guard['provider']) ? config("auth.providers.{$guard['provider']}.model") : null)
        ->get($guard);
}

to return nothing when middleware requires the Sanctum guard to be authenticated, hence the eager load fails here

/**
 * A role belongs to some users of the model associated with its guard.
 */
public function users(): BelongsToMany
{
    return $this->morphedByMany(
        // Error occurs here
        getModelForGuard($this->attributes['guard_name'] ?? config('auth.defaults.guard')),
        'model',
        config('permission.table_names.model_has_roles'),
        app(PermissionRegistrar::class)->pivotRole,
        config('permission.column_names.model_morph_key')
    );
}

After manually inserting the guard in auth/config.php with a proper provider, eager loading works again

'guards' => [
    ...

    'sanctum' => [
        'driver' => 'sanctum',
        'provider' => 'users'
    ]
],
Salim Djerbouh
  • 10,719
  • 6
  • 29
  • 61