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

JerryBels's avatar

Issue with tests, session and global scopes

Hello guys,

So I'm in a cat-chasing-tail situation. I'm defining global scopes depending on the content of a session variable :

    protected static function booted()
    {
        if(session()->has('organization')) {
            static::addGlobalScope(new OrganizationScope);
        }
    }

This session value is set using login events (thanks @kreierson). So, in tests, I will have to use $this->actingAs($user)->withSession(['organization' => $organization]); where relevant, right? Sure, this works.

But now, when creating my data for tests with factories, the scope is applied. Moreover, as I'm connected as a specific user, other things are automatically set up that I don't want. So I want to prepare my date before calling actingAs. That's what I did.

The issue is, when I do that, the booted function of the model is called when creating the data and never again. That means the if statement checking if there is an organization variable in the session is always false, even after I called the actingAs...withSession.

So, a couple question:

1/ Is it possible to have those booted, booting etc... Be reinintialized on demand?

2/ Is it bad practice to have conditions in there? I could put the condition in the OrganizationScope code, of course, but then I would add the scope to every models even for people not needing it (and the scope wouldn't change anything then of course)... Not clean, I think.

3/ Do you have a better approach to recommend?

Thanks ahead!

0 likes
8 replies
lbecket's avatar

No, it's not possible to reinitialize the booted function on demand. The booted function only runs once when the model is loaded into memory. Having conditions in the booted function is not necessarily a bad practice, but it may cause issues like the one you are encountering if the condition depends on a changing value.

A better approach to consider would be to use Model Observers instead of Global Scopes. Model Observers allow you to observe changes to a model and perform actions based on those changes. In your case, you could observe the login event and dynamically add or remove the OrganizationScope based on the session variable, avoiding the issues you are encountering with the booted function.

1 like
JerryBels's avatar

@lbecket thanks for that, it was interesting! Although I don't really understand, the login event would be fired when the user is logged in, and the scopes applied, but what about subsequent navigations? Are login events fired every time?

Also, what about the tests then? actingAs doesn't fire any events. Should I overwrite it to apply scopes accordingly?

lbecket's avatar

No, login events are typically only fired once when the user logs in and the session is established. Subsequent navigations wouldn't fire the login event again unless the user logs out and logs back in.

As for tests, you can manually trigger the observer to apply the scopes in your tests. You can create a helper function to set the session variable and then call the observer to apply the scopes. This way, you can have control over the scopes being applied and can ensure they are applied as needed in your tests.

JerryBels's avatar

@lbecket okay, that's what I thought, so I'm sorry but I don't understand your recommendation for a better approach by using Model observers then. How can I conditionally apply the scope in there?

lbecket's avatar

You can use the Model Observers to conditionally apply the scope by observing changes to the session variable and dynamically adding or removing the scope based on the value of the session variable.

So, that might look something like:

class LoginObserver
{
    public function updated(User $user)
    {
        if (session()->has('organization')) {
            $user->addGlobalScope(new OrganizationScope);
        } else {
            $user->removeGlobalScope(OrganizationScope::class);
        }
    }
}

LoginObserver is observing the User model and listening for changes to the user. When the user is updated, the observer checks the value of the session variable and either adds or removes the OrganizationScope based on its value.

You can register the observer in the AppServiceProvider boot method:

User::observe(LoginObserver::class);

This way, the observer will automatically be triggered whenever the user is updated, allowing you to dynamically apply the scope based on the value of the session variable.

JerryBels's avatar

@lbecket okay, for updates it's fine, but this won't apply for select statements then. If I want for global scopes to be applied to all select queries, that's because a user belonging to an organization shouldn't be able to see users from other organizations. So it's really for all operations conducted by the currently authenticated user. I don't think model observers are working in that case.

lbecket's avatar

@JerryBels In that case, using Global Scopes might be the most appropriate solution. If you want to apply the scope based on the value of a session variable, you can refactor your code to conditionally apply the scope after the user has been authenticated and the session has been established.

class UserController extends Controller
{
    public function index()
    {
        if (session()->has('organization')) {
            User::addGlobalScope(new OrganizationScope);
        }
        $users = User::all();
        User::removeGlobalScope(OrganizationScope::class);

        return view('users.index', compact('users'));
    }
}

In this example, the scope is added before the select statement is executed and removed after the data has been retrieved. This way, the scope will only be applied for the current request and will not affect subsequent requests.

JerryBels's avatar
JerryBels
OP
Best Answer
Level 1

I ended applying the condition in the scope. It's not ideal as some instances that don't require the scope have an empty one added, but it's working.

Please or to participate in this conversation.