9
votes

I need to apply specific global scope only if authenticated user's role is equal to something. So that user with certain role will only be able to execute queries on a given subset of records.

I can easily deal with User model (of currently logged in user) but not inside scope's apply method.

https://github.com/laravel/framework/issues/22316#issuecomment-349548374

The scope constructor executes very early, before the auth middleware has run. Resolve the user within the apply method, not in the constructor.

OK, so I'm inside the apply method:

<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class UserScopeForSalesContactAdmin implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        dd('within apply', auth()->user());
        dd(auth()->user()); // won't work

        $builder->where('foo', '=', 'something from currently logged in user');
    }
}

Obviously second dd gives:

Maximum function nesting level of '512' reached, aborting!

How to resolve this? I imagine I should call IOC container via app()->make() but then?

Thanks for any hints.

edit: I think I see what's causing the infinite loop (https://github.com/laravel/framework/issues/26113) but still I need to find a best way to obtain the User…

4
Have you tried the Auth::user()? If you can get the request into the scope, you could give a try to $request->user() too. Don't know about what is causing this, but maybe there is a conflict under the hood and using a workaround could make it. I'm not sure, but I think Auth::user() do a query to the db every time you use it. - Elie Morin

4 Answers

10
votes

The method what you are looking for is exactly Auth::hasUser(). The method was added in Laravel 5.6 through my PR.

[5.6] Add ability to determine if the current user is ALREADY authenticated without triggering side effects by mpyw · Pull Request #24518 · laravel/framework

<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class UserScopeForSalesContactAdmin implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        if (Auth::hasUser() && Auth::user()->role === 'something') {
            $builder->where('foo', '=', 'something from currently logged in user');
        }
    }
}

Just call Auth::hasUser() to prevent Auth::user() from causing side effects.

2
votes

Introduction

Finally found a solution for this, I have been trying to fix it and looking for solutions for over an hour. The problem with what I have is that I am using a trait to apply the global scope on certain models and not all models. The list is big so I had to find a way to use auth()->check() in a global scope.

Solution

So in your model class or trait that is being used by your model class, you can listen to the RouteMatched event, which assures that the session provider is booted. Then add the global scope within the the event listener's closure/callable.

Code

use Illuminate\Routing\Events\RouteMatched;

/**
 * The "boot" method of the model.
 *
 * @return void
 */
protected static function boot()
{
    Event::listen(RouteMatched::class, function () {
        static::addGlobalScope('company', function ($query) {
            // auth()->check() will now return the correct value
            // Logic here
        });
    });
}
0
votes

The problem here is you are defining a scope for User, while retrieving the user with Auth::user() it will utilize the scope again before it has been resolved the first method call and there from we have an StackOverflow. I think there is a clever approach, overwriting guards, and saving an user model on a class you can fetch out from there, but am also not a fan of tweaking with built in Laravel features.

An simple approach could be to utilise Auth::id and fetching out the info you need with DB::table, it's not pretty but i think it is a straight forward approach to solving the problem.

public function apply(Builder $builder, Model $model)
{
    // this will not trigger the scope when you fetch it out again, and avoiding the stackoverflow
    $user = DB::table('users')->where('id', Auth::id())->get()->first();

    $builder->where('foo', '=', $user['the_answer_to_life']);
}
0
votes

I solved that problem by simply adding the user id to the session, right after successful login (Controllers/Auth/LoginController.php, attemptLogin()) :

$request->session()->put('id_user', $user->id);

...And then fetching it within the global scope like:

$id_user = (request()->session()->get('id_user')) ?? null;