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

Indemnity83's avatar

User current team during API calls

I'm wondering if anybody has come up with an elegant solution to team switching in API calls.

I've currently got a global scope that I apply which will limit results to the user's current team

<?php

namespace App\Scopes;

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

class TeamScope 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)
    {
        /** @var User $user */
        $user = auth()->user();

        $builder->where(
            'team_id', '=', $user->currentTeam()->id
        );
    }
}

This works great, particularly when consuming my API on the front-end. Team switching etc is all great.

But its really confusing for external consumers because the results are scoped to a specific team, but there is nothing in the request which defines the team (its whatever team the user was last using on the web). It's even worse if the users are active on the front-end as its possible for two calls to the same API endpoint to return different results if the user changes teams between the calls.

Basically, the call to $user->currentTeam() prevents the API call from being (stateless)[https://restfulapi.net/rest-architectural-constraints/#stateless]

So obviously I need to get the team id or slug into the request; I can imagine a couple of ways to do this, but each has trade-offs (note, I've used team_id below, but team_slug could be substituted too).

  • Accept a ?team_id= query parameter which, if provided is used instead of the state. This is pretty straight forward, if a bit a-typical, and although its technically optional its practically required on all API requests for any third-party consumer: 'team_id', '=', request('team_id', $user->currentTeam()->id)
  • Require the team_id in the URI so that URI's end up looking like /{team_id}/api/posts. Then use the URI param to scope results. This is probably the best solution from the perspective of a third-party consumer, but makes my use of a global scope less practical, since every API controller method is going to accept the team and filter inside the controller method. Maybe there is a way to pop a parameter off of the request I'm not aware of? It will also require that I adust all my front-end requests, which isn't hard or bad but since my front-end can be stateful it seems handy.
  • Add team_id as a header to requests. This is ugly, it is really no different than the query parameter except that now client endpoint caching is hosed and building a request is awkward, I'm really only listing it here for completeness.
0 likes
2 replies
Indemnity83's avatar
Indemnity83
OP
Best Answer
Level 22

For anybody who comes across this thread, here is what I ended up doing. In the end, requiring the team to be identified in the URI was the only real choice and I've built a convention now that at least works for me.

My TeamScope looks like this now:

<?php

namespace App\Scopes;

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

class TeamScope 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)
    {
        $team = request()->route('team');

        $builder->where(
            'team_id', '=', $team->id
        );
    }
}

So I'm now grabbing the team directly from the request (if you're not using Spark, you'll also need to set up explicit route model binding to resolve team from the URL)

My original concern with this method was now how do I tweak all the routes so the URI has the {team_id} in it without completely duplicating my frameworks route list. For this, I ended being more explicit about my route registration in the framework service provider:

<?php


namespace TechnicPack\SolderFramework;

use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;

class SolderFrameworkServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     */
    public function boot()
    {
        // Register framework resources
        $this->registerRoutes();
    }

    /**
     * Register Solder's routes.
     */
    private function registerRoutes()
    {
        // If the routes have not been cached, we will include them in a route group
        // so that all of the routes will be conveniently registered to the given
        // controller namespace. After that we will load the Solder routes file.
        if (! $this->app->routesAreCached()) {
            Route::name('api.')
                ->namespace('TechnicPack\SolderFramework\Http\Controllers')
                ->prefix(Solder::$apiRoutePrefix)
                ->group(function ($router) {
                    require __DIR__.'/Http/routes.php';
                });
        }
    }
}

The key thing here is that the prefix call points to a static class which contains a default prefix of '/api' but can easily be overridden (you could do this with a config parameter too if you wanted).

<?php

namespace TechnicPack\SolderFramework;

class Solder
{
    /**
     * The string prefix for all solder api routes.
     *
     * @var string
     */
    public static $apiRoutePrefix = 'api';

    /**
     * Set the uri to prefix api routes with.
     *
     * @param $prefix
     *
     * @return Solder
     */
    public static function prefixApiRoutesWith($prefix)
    {
        self::$apiRoutePrefix = $prefix;

        return new static();
    }
}

Now, in my Spark application service provider I can override the prefix for all of my framework routes and inject the {team} into the URI:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use TechnicPack\SolderFramework\Solder;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register()
    {
        Solder::prefixApiRoutesWith('{team}/api');
    }
}
1 like
steve_laracasts's avatar

Oh yes!!

Thank you so much @indemnity83 :D

I was working on this and I had no idea about scopes until I read your posts here - you probably just saved me a huge headache and lots of time - this is great!!

I am completely okay with the first version and don't need to pass the team as I am outputting the team name in the list of resources so it's really clear what's going on.

For anyone else trying to do this here are the steps required (I had to work these out for myself):

  1. create a new folder in your App directory called Scopes (the hint is in the namespace in the code above)
  2. create a file called TeamScope.php and copy and paste the code from the first post from @indemnity83
  3. In your model files add this code to add the Scope:
/**
 * The "booting" method of the model.
 *
 * @return void
 */
    protected static function boot()
    {
        parent::boot();
     
        static::addGlobalScope(new TeamScope);
}

and finally add this to the top of the model file:

use App\Scopes\TeamScope;

My only question is why this isn't in the Spark docs!? It seems like something that everyone using Spark is going to want to do - no?

If I have one general complaint about Spark so far it is that the guidance is erm... limited to say the least. As I am working things out the docs do become clearer so it's partly down to my 'newbie' status - but heck! What's going on - this is not a cheap product - sparse docs, out of date demo, hardly anything on Laracasts? Are we really alone out there - I feel like a space cadet! :D

Please or to participate in this conversation.