10

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…

Matt Komarnicki
  • 5,198
  • 7
  • 40
  • 92
  • 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 Jun 07 '19 at 04:54

4 Answers4

12

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.

mpyw
  • 5,526
  • 4
  • 30
  • 36
  • 1
    This is fantastic, I was just about to move this logic to controllers, but persevered with 'laravel MUST have an answer for this'. – digout Apr 14 '20 at 10:03
2

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
        });
    });
}
Raed Yakoubi
  • 332
  • 3
  • 8
0

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']);
}
mrhn
  • 17,961
  • 4
  • 27
  • 46
  • It's not often i write db::table logic it could be off a little, but i guess you get the point. – mrhn Jun 08 '19 at 18:05
  • What you're saying makes absolute sense to me, unfortunately Auth::id() also throws "Maximum function nesting level of '512' reached, aborting!" even in dd(). I thought about putting data for scope into session variable but I'm not convince how solid that would be. – Matt Komarnicki Jun 10 '19 at 02:32
  • I will reproduce it on my local environtment at some point and figure out a solution when time is for it. – mrhn Jun 10 '19 at 22:13
  • Thanks Martin, let me know if you've found something. For now as a workaround I went with sessions, but I'm I don't find this solution a stable one. :( – Matt Komarnicki Jun 11 '19 at 23:44
0

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;
Mike
  • 493
  • 5
  • 14
  • Thanks for your feedback Mike. Utilizing session to solve my problem was my initial plan, but I don't want to rely on sessions at all in this case as it doesn't give me a feeling that this is a solid solution against the problem… Thank you anyway. – Matt Komarnicki Aug 12 '20 at 07:10