spatie/laravel-permission

Call to undefined method App\\Models\\User::hasRole()

ACannuniRP opened this issue · 2 comments

I know, this is the nth issue with same title but I tried all the methods and I haven't found a solution.
I recently upgraded a Laravel app that I developed and have been in production for long time from Laravel 7.26.0 to Laravel 10.31.0. The User model (app/User.php) is using Spatie Laravel Permissions:

<?php

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use Notifiable;
    use HasRoles;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'username', 'password','api_token'
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token', 'roles'
    ];

    protected $appends = ['is_admin'];

    /**
     * Get calls owned by the user.
     */
    public function calls()
    {
        return $this->hasMany('App\Call');
    }

    /**
     * Check if a user is an admin.
     *
     * @return bool
     */
    public function getIsAdminAttribute()
    {
        return $this->hasRole('admin');
    }
}

The User model has a relationship with the Call model. The Call model (app/Call.php) is:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Kyslik\ColumnSortable\Sortable;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;

class Call extends Model
{
	use Sortable;

    protected $fillable = [
    	'endpoint_dc',
        'request_headers',
        'request_payload',
        'response_headers',
        'response_body',
        'user_id'
    ];

    protected $hidden = ['test_results', 'user'];

    protected $appends = ['created_at_formatted', 'response_time_formatted', 'test_results_count', 'user_name'];

    /**
     * Get the test results for the call.
     */
    public function test_results()
    {
        $user = Auth::user();
        if ($user->hasRole('admin')) {
            return $this->hasMany('App\TestResult')->withoutGlobalScopes();
        } else {
            return $this->hasMany('App\TestResult');
        }
    }

    /**
     * Get the user owning the call.
     */
    public function user()
    {
        return $this->belongsTo('App\User');
    }

    /**
     * Get the count of the test results.
     *
     * @return integer
     */
    public function getTestResultsCountAttribute()
    {
        return $this->test_results->count();
    }

    /**
     * Get the creation date/time formatted as only date (no time).
     *
     * @return null | string
     */
    public function getCreatedAtFormattedAttribute()
    {
        return Carbon::parse($this->created_at)->format('d-m-Y H:i:s') ." GMT";
    }

    /**
     * Get the response time formatted as only two decimals.
     *
     * @return null | string
     */
    public function getResponseTimeFormattedAttribute()
    {
        return number_format($this->response_time, 2);
    }

    /**
     * Get the account that sent the request.
     *
     * @return string
     */
    public function getUserNameAttribute()
    {
        return $this->user->name;
    }

    /**
     * Anonymous Global Scopes for restrict the results to the one related to the authenticated user.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('user_id', function (Builder $builder) {
            $builder->where('user_id', '=', Auth::id());
        });
    }

    /**
     * Scope a query to only include not validated calls.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeNotValidated($query)
    {
        return $query->where('is_request_validated', 0);
    }
}

When calling the Call model I use the API routes (routes/api.php):

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\V1\UserController;
use App\Http\Controllers\Api\V1\ExchangeController;
use App\Http\Controllers\Api\V1\CallController;
use App\Http\Controllers\Api\V1\TestResultController;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

Route::middleware('auth:api')->get('/api/v1/user', [UserController::class, 'show']);

Route::post('/a/api/exchange.json', [ExchangeController::class, 'show']);

Route::middleware('auth:api')->get('/api/v1/calls', [CallController::class, 'index']);
Route::middleware('auth:api')->get('/api/v1/calls/{id}', [CallController::class, 'show']);
Route::middleware('auth:api')->get('/api/v1/calls/{id}/test-results', [TestResultController::class, 'indexCall']);

Route::middleware('auth:api')->get('/api/v1/test-results', [TestResultController::class, 'index']);

The Call model is erroring in the line:

        if ($user->hasRole('admin')) {

and displaying the error:

{
    "message": "Call to undefined method App\\Models\\User::hasRole()",
    "exception": "BadMethodCallException",
    "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Support/Traits/ForwardsCalls.php",
    "line": 67,
    "trace": [
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Support/Traits/ForwardsCalls.php",
            "line": 36,
            "function": "throwBadMethodCallException",
            "class": "Illuminate\\Database\\Eloquent\\Model",
            "type": "::"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php",
            "line": 2334,
            "function": "forwardCallTo",
            "class": "Illuminate\\Database\\Eloquent\\Model",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/app/Http/Controllers/Api/V1/CallController.php",
            "line": 23,
            "function": "__call",
            "class": "Illuminate\\Database\\Eloquent\\Model",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Controller.php",
            "line": 54,
            "function": "index",
            "class": "App\\Http\\Controllers\\Api\\V1\\CallController",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php",
            "line": 43,
            "function": "callAction",
            "class": "Illuminate\\Routing\\Controller",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Route.php",
            "line": 259,
            "function": "dispatch",
            "class": "Illuminate\\Routing\\ControllerDispatcher",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Route.php",
            "line": 205,
            "function": "runController",
            "class": "Illuminate\\Routing\\Route",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
            "line": 799,
            "function": "run",
            "class": "Illuminate\\Routing\\Route",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 141,
            "function": "Illuminate\\Routing\\{closure}",
            "class": "Illuminate\\Routing\\Router",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php",
            "line": 50,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Routing\\Middleware\\SubstituteBindings",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php",
            "line": 159,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php",
            "line": 125,
            "function": "handleRequest",
            "class": "Illuminate\\Routing\\Middleware\\ThrottleRequests",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php",
            "line": 87,
            "function": "handleRequestUsingNamedLimiter",
            "class": "Illuminate\\Routing\\Middleware\\ThrottleRequests",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Routing\\Middleware\\ThrottleRequests",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php",
            "line": 57,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Auth\\Middleware\\Authenticate",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 116,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
            "line": 798,
            "function": "then",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
            "line": 777,
            "function": "runRouteWithinStack",
            "class": "Illuminate\\Routing\\Router",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
            "line": 741,
            "function": "runRoute",
            "class": "Illuminate\\Routing\\Router",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
            "line": 730,
            "function": "dispatchToRoute",
            "class": "Illuminate\\Routing\\Router",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
            "line": 200,
            "function": "dispatch",
            "class": "Illuminate\\Routing\\Router",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 141,
            "function": "Illuminate\\Foundation\\Http\\{closure}",
            "class": "Illuminate\\Foundation\\Http\\Kernel",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php",
            "line": 21,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php",
            "line": 31,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php",
            "line": 21,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php",
            "line": 40,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Middleware\\TrimStrings",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php",
            "line": 27,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php",
            "line": 99,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php",
            "line": 62,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Http\\Middleware\\HandleCors",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php",
            "line": 39,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Http\\Middleware\\TrustProxies",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 116,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
            "line": 175,
            "function": "then",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
            "line": 144,
            "function": "sendRequestThroughRouter",
            "class": "Illuminate\\Foundation\\Http\\Kernel",
            "type": "->"
        },
        {
            "file": "/home/vagrant/code/xapi-interceptor-laravel-10/public/index.php",
            "line": 51,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Kernel",
            "type": "->"
        }
    ]
}

As you can see I already included the required trait in the User model:

use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles; //this line

    // ...
}

I used spatie/laravel-permission version 4.0.1 when using Laravel 7 and now I'm using spatie/laravel-permission version 6.1.0 with Laravel 10.
This issue is not happening with Laravel 6 but only with Laravel 10.
I also upgraded spatie/laravel-permission to version 5.11.1 for Laravel 7 and I can confirm that Laravel 7 works.
I also tested the application with php artisan tinker on Laravel 10 and I'm able to see the roles relationship in Eloquent results:

> $users = \App\User::where('id', 16)->with('roles')->first();
= App\User {#7348
    id: 16,
    name: "FirstName LastName",
    username: "flast@example.com",
    #password: "password",
    api_token: "token",
    #remember_token: "token",
    created_at: "2021-05-04 21:20:05",
    updated_at: "2021-05-04 21:20:05",
    #roles: Illuminate\Database\Eloquent\Collection {#7326
      all: [
        Spatie\Permission\Models\Role {#7375
          id: 1,
          name: "admin",
          guard_name: "web",
          created_at: "2021-05-04 21:21:31",
          updated_at: "2021-05-04 21:21:31",
          pivot: Illuminate\Database\Eloquent\Relations\MorphPivot {#7087
            model_type: "App\User",
            model_id: 16,
            role_id: 1,
          },
        },
      ],
    },
    +is_admin: true,
  }

> $users->roles;
= Illuminate\Database\Eloquent\Collection {#7326
    all: [
      Spatie\Permission\Models\Role {#7375
        id: 1,
        name: "admin",
        guard_name: "web",
        created_at: "2021-05-04 21:21:31",
        updated_at: "2021-05-04 21:21:31",
        pivot: Illuminate\Database\Eloquent\Relations\MorphPivot {#7087
          model_type: "App\User",
          model_id: 16,
          role_id: 1,
        },
      },
    ],
  }

> $users->hasRole('admin');
= true

I believe that must be some change in Laravel 8+ or in spatie/laravel-permission version 6 regarding the relationships between models (in my case User and Call).

       {
           "file": "/home/vagrant/code/xapi-interceptor-laravel-10/vendor/laravel/framework/src/Illuminate/Routing/Controller.php",
           "line": 54,
           "function": "index",
           "class": "App\\Http\\Controllers\\Api\\V1\\CallController",
           "type": "->"
       },

Please post the code of CallController.php, from the top down to at least the end of index(), including the constructor. I'm especially interested in the constructor and any additional middleware it's calling; as well as which namespace it's referencing for the User.

Actually, I think this is the source of your problem: Your User model is in \App\User namespace, and the error message from your controller says it is looking for \App\Models\User namespace.

Perhaps you haven't completely updated all the namespaces in your app?