Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Request Scope Laravel Package

submtd/laravel-request-scope

Laravel Request Scope adds request-scoped service container bindings, so services are resolved once per request and reset automatically between requests. Useful for request-specific state, tenant context, and long-running workers such as Laravel Octane.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and confirm the API surface first. The package is essentially undocumented in the wild (no stars, no dependents), so before writing any code:

    composer require submtd/laravel-request-scope
    composer show submtd/laravel-request-scope      # confirm version + Laravel constraint
    ls vendor/submtd/laravel-request-scope/src     # confirm namespaces/traits before copying snippets below
    
  2. Publish any config (if the package registers one):

    php artisan vendor:publish --tag=request-scope-config
    

    Treat the published file as the contract: it usually holds the allow-lists for sortable/filterable/includable attributes and the default per_page.

  3. Attach the trait to one model you already expose over an API — not all of them. Typical shape:

    use Submtd\RequestScope\Concerns\HasRequestScope;
    
    class Post extends Model
    {
        use HasRequestScope;
    
        protected array $sortable   = ['created_at', 'title', 'views'];
        protected array $filterable = ['status', 'author_id'];
        protected array $includable = ['author', 'comments'];
    }
    
  4. First use case: a list endpoint where the frontend controls ordering without new endpoints.

    // routes/api.php
    Route::get('/posts', [PostController::class, 'index']);
    
    // PostController
    public function index()
    {
        return PostResource::collection(
            Post::requestScope()->paginate()
        );
    }
    
    GET /api/posts?sort=-created_at&filter[status]=published&include=author&per_page=25
    
  5. Where to look first: the model trait and the query-parameter parser. Everything else (config, service provider) is thin. Read those two files end-to-end before trusting the package with public traffic — you need to know exactly how it validates attribute names.

Implementation Patterns

Controller-thin, model/scope-declared. Keep the allow-lists on the model and leave controllers as one-liners. This is the pattern the package is designed for, and it keeps the "what can the client ask for" question in one place.

public function index(Request $request)
{
    $posts = Post::query()
        ->requestScope()          // apply sort/filter/include/fields
        ->paginate();             // or ->simplePaginate()

    return PostResource::collection($posts);
}

Compose with authorization, not instead of it. requestScope() widens a query; it must never widen visibility. Apply tenant/ownership constraints first:

$posts = Post::query()
    ->where('team_id', $request->user()->team_id)  // hard constraint
    ->requestScope()                               // client-controlled narrowing
    ->paginate();

If the package internally calls select(), with(), or orderBy(), ordering of calls matters — constraints added before requestScope() can be clobbered by an aggressive select. Verify with ->toSql().

Allow-list aggressively; the package's real job is parsing. The value is ?filter[status]=draft,published → whereIn, ?sort=-views → orderByDesc, ?include=author.comments → nested with. Your job is to cap the surface:

protected array $sortable   = ['created_at', 'title', 'views']; // never expose 'email' or 'password_hash'
protected array $filterable = ['status', 'author_id'];
protected array $includable = ['author', 'comments'];

Default eager loads plus client includes. Declare protected $with = ['author'] on the model and let include= add more. That way a client that sends nothing still avoids N+1 on the common case.

Custom filters for anything with an operator. Equality filters get you 60% of the way; date ranges, LIKE, and whereHas need an escape hatch. Expose named filters rather than raw columns:

// ?filter[search]=laravel  -> where('title','like','%laravel%')
// ?filter[created_after]=2024-01-01 -> where('created_at','>=',...)

Implement these either as closures/classes registered in the config (if supported) or as a local scope applied before requestScope().

Validate the query string explicitly. Treat it as input, because it is:

$request->validate([
    'sort'        => ['nullable', 'string', 'regex:/^-?[a-z_]+$/'],
    'filter'      => ['nullable', 'array'],
    'filter.*'    => ['nullable'],
    'include'     => ['nullable', 'string'],
    'per_page'    => ['nullable', 'integer', 'min:1', 'max:100'],
]);

Cache keys must include the full query string. Once list endpoints become filterable, a naive Cache::remember("posts.{$page}") returns the wrong page to the wrong user.

$key = 'posts.'.md5($request->getQueryString().'|'.$request->user()->team_id);

Test the parameter matrix, not just the happy path. A handful of feature tests covers more than reading the source:

$this->getJson('/api/posts?sort=-views')->assertOk()->assertJsonPath('data.0.id', 3);
$this->getJson('/api/posts?sort=password')->assertStatus(422); // or 400, whichever you decide
$this->getJson('/api/posts?filter[status]=published')->assertJsonCount(2, 'data');
$this->getJson('/api/posts?include=author')->assertJsonStructure(['data' => [['author']]]);

Gotchas and Tips

  • Attribute injection is the main risk. If sort=, filter[...], or fields[...] are passed to orderBy/select without an allow-list, a client can sort by an unindexed or sensitive column, or trigger a 500 on a non-existent one. Always declare allow-lists; never fall back to $request->input('sort') directly.

  • Return 4xx, not 500, for rejected parameters. Out of the box, most of these packages throw a generic exception. Wrap the call or validate up front so unknown sort/filter keys produce a clean 422 with the offending key in the message — frontend teams will thank you.

  • per_page is unbounded unless you bound it. ?per_page=100000 will happily try to serialize your whole table. Cap it in validation or in config.

  • Boolean and empty values get stringy. ?filter[active]=false arrives as the string "false", which is truthy in MySQL. Normalize booleans and drop empty filter arrays before they hit the query.

  • Sorting on joined/aliased columns breaks. sort=author_name won't work if author_name comes from a join; you need a select alias or a custom sort mapping. Decide per-endpoint.

  • Watch for select() side effects. If the package narrows columns for fields[...], later where()/orderBy() on excluded columns silently behave differently, and $post->author may be null because author_id wasn't selected. Always keep the primary key and FK in any field selection.

  • Global scopes and soft deletes still apply — good — but withoutGlobalScopes() in a controller plus requestScope() is an easy way to accidentally expose archived rows. Grep for it.

  • Debugging: the fastest loop is ->toSql() + ->getBindings(), or ->ddRawSql() in a scratch route:

    dd(Post::requestScope()->toRawSql());
    

    Also Log::info($request->fullUrl()) in the controller for a day — you'll see exactly which parameter shapes your clients actually send, which is usually different from what you documented.

  • Extension point: wrap it. Put requestScope() behind your own repository method or a ListPosts action so the package is one use statement away from being replaced. Given zero dependents and no release history, assume you may need to swap to spatie/laravel-query-builder or a 60-line hand-rolled parser.

  • Pin the version and check the Laravel constraint in composer.json. An unmaintained package that hard-requires illuminate/support ^10 will block your next major upgrade, and the fix is a fork, not a composer update.

  • Where it's a good fit: internal admin panels and early-stage APIs where you want filtering/sorting without writing ten controller methods. Where it isn't: public, security-sensitive endpoints, or anywhere you'd need documented, stable behavior for external consumers — build the allow-list + parser yourself in that case; it's a small amount of code and you own the failure modes.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
quimgc/tasks
sistem-atc/bank-sdk
core45/laravel-cookieconsent
projectsend/community-modules
ovac/laravel-hubtel-payment
capimail/capimail-php
shiroamada/green-api-laravel-notification
shiroamada/ultrasms-laravel-notification
esazykin/laravel-clickhouse
nbrabant/laraadmin
tobya/saloonfire
kjmtrue/laravel-debugbar
hexincorp/debugbar
razorbacks/laravel-shibboleth
adilazhari/laravel-idempotency
mamitech/laravel-slow-query-log
mamitech/laravel-http-logger
medeirosrafael/docxmustache
webdevlabs/phreak
vis/log-envelope