Weave Code
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 Query Builder Laravel Package

spatie/laravel-query-builder

Safely build Eloquent queries from incoming API requests. Allowlist filters, sorts, includes, and fields; supports partial/exact and custom filters, nested relationships, relation counts, and default values. Works with existing queries for clean, consistent endpoints.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Installation:

    composer require spatie/laravel-query-builder
    

    Publish the config (optional):

    php artisan vendor:publish --provider="Spatie\QueryBuilder\QueryBuilderServiceProvider"
    
  2. First Use Case:

    use Spatie\QueryBuilder\QueryBuilder;
    
    // Basic filtering
    $users = QueryBuilder::for(\App\Models\User::class)
        ->allowedFilters('name', 'email')
        ->get();
    

    Access via API: /users?filter[name]=John

  3. Where to Look First:


Implementation Patterns

Core Workflows

  1. API Resource Filtering:

    // In a controller
    public function index(Request $request)
    {
        $users = QueryBuilder::for(\App\Models\User::class)
            ->allowedFilters([
                'name', // partial match by default
                AllowedFilter::exact('email'),
                AllowedFilter::operator('age', FilterOperator::GREATER_THAN),
            ])
            ->allowedSorts('name', 'created_at')
            ->allowedFields('id', 'name', 'email')
            ->get();
    
        return UserResource::collection($users);
    }
    

    API Request: /users?filter[name]=John&filter[age]=30&sort=-created_at&fields[users]=id,name

  2. Integration with Existing Queries:

    $baseQuery = \App\Models\User::where('active', true);
    
    $users = QueryBuilder::for($baseQuery)
        ->allowedIncludes('posts')
        ->get();
    
  3. Dynamic Filtering in Middleware:

    public function handle(Request $request, Closure $next)
    {
        $request->merge([
            'filter[active]' => 'true',
        ]);
        return $next($request);
    }
    
  4. Reusable Query Builder Classes:

    class UserQueryBuilder extends QueryBuilder
    {
        public function __construct()
        {
            parent::__construct(\App\Models\User::class);
            $this->allowedFilters([
                'name',
                AllowedFilter::exact('id'),
                AllowedFilter::scope('is_admin'),
            ]);
            $this->allowedSorts('name', 'created_at');
        }
    }
    
    // Usage
    $users = (new UserQueryBuilder())->get();
    

Advanced Patterns

  1. Custom Filter Logic:

    QueryBuilder::for(\App\Models\Post::class)
        ->allowedFilters([
            AllowedFilter::custom('published', function ($query, $value) {
                return $query->where('published_at', '<=', $value)
                    ->orWhere('published_at', null);
            }),
        ])
        ->get();
    
  2. Nested Relationship Includes:

    QueryBuilder::for(\App\Models\User::class)
        ->allowedIncludes('posts.author')
        ->get();
    

    API Request: /users?include=posts.author

  3. Conditional Query Building:

    $query = QueryBuilder::for(\App\Models\User::class);
    
    if ($request->has('advanced')) {
        $query->allowedFilters([
            AllowedFilter::belongsTo('role'),
            AllowedFilter::operator('score', FilterOperator::DYNAMIC),
        ]);
    }
    
    $users = $query->get();
    
  4. Pagination and API Resources:

    use Spatie\QueryBuilder\QueryBuilder;
    use Illuminate\Pagination\LengthAwarePaginator;
    
    $users = QueryBuilder::for(\App\Models\User::class)
        ->allowedFilters('name')
        ->paginate(15)
        ->appends(request()->query());
    
    return new UserCollection($users);
    

Gotchas and Tips

Pitfalls

  1. Filter Validation:

    • Forgetting to allow filters explicitly will throw InvalidFilterQuery (unless disable_invalid_filter_query_exception is true in config).
    • Fix: Always define allowed filters, even if empty:
      ->allowedFilters([]) // Explicitly disallow all filters
      
  2. Case Sensitivity in Partial Filters:

    • Partial filters use LIKE LOWER(%value%) by default, which may not match case-sensitive databases.
    • Fix: Use AllowedFilter::partial('name', null, false, true) to preserve case sensitivity.
  3. Performance with LIKE Queries:

    • Partial filters (LIKE) can ignore database indexes.
    • Fix: Use beginsWith or endsWith for indexed columns:
      ->allowedFilters(AllowedFilter::beginsWith('email'))
      
  4. Relationship Constraints:

    • Dot-notation filters (e.g., posts.title) may not work as expected with complex joins.
    • Fix: Disable relation constraints:
      AllowedFilter::exact('posts.title', null, false)
      
  5. Scope Parameter Handling:

    • Scopes with multiple parameters may not parse correctly if the URL encoding is off.
    • Fix: Ensure comma-separated values are properly URL-encoded:
      // Correct: filter[tags]=tag1,tag2
      // Incorrect: filter[tags]=tag1%2Ctag2 (may cause issues)
      
  6. Caching Queries:

    • QueryBuilder does not cache queries by default. Repeated identical requests may hit the database multiple times.
    • Fix: Cache the results in your application layer:
      $cacheKey = 'users_'.md5(request()->getQueryString());
      return Cache::remember($cacheKey, now()->addMinutes(5), function () {
          return QueryBuilder::for(\App\Models\User::class)->allowedFilters('name')->get();
      });
      

Debugging Tips

  1. Inspect the Final Query:

    • Use Laravel's query logging or toSql() to debug:
      $query = QueryBuilder::for(\App\Models\User::class)->allowedFilters('name')->getQuery();
      \Log::info($query->toSql(), $query->getBindings());
      
  2. Validate Request Input:

    • Ensure the request contains expected parameters:
      \Log::info('Request query:', request()->query());
      
  3. Test Edge Cases:

    • Test with empty strings, special characters, and malformed input:
      // Test with empty filter
      $this->get('/users?filter[name]=')->assertOk();
      
  4. Check for Typos:

    • Typos in filter names (e.g., namee instead of name) will silently fail unless disable_invalid_filter_query_exception is false.

Configuration Quirks

  1. disable_invalid_filter_query_exception:

    • Setting this to true hides invalid filter errors but does not allow arbitrary filters. Useful for APIs where you want to ignore unknown filters gracefully.
  2. Default Values:

    • Set default filter values in the config:
      'default_filter_values' => [
          'active' => 'true',
      ],
      
    • Override per query:
      ->defaultFilterValues(['active' => 'false'])
      
  3. Global Allowed Filters:

    • Define global defaults in the config:
      'global' => [
          'allowed_filters' => ['name', 'email'],
          'allowed_sorts' => ['name', 'created_at'],
      ],
      
    • Override per query as needed.

Extension Points

  1. Custom Filter Classes:

    • Extend Spatie\QueryBuilder\Filters\Filter to create reusable filters:
      class PublishedAtFilter extends Filter
      {
          protected $name = 'published_at';
      
          public function apply($query, $value)
          {
              return $query->where('published_at', '<=', $value);
          }
      }
      
    • Register in allowedFilters:
      ->allowedFilters(new PublishedAtFilter())
      
  2. Middleware for QueryBuilder:

    • Create middleware to apply QueryBuilder to all requests:
      public function handle($request, Closure $next)
      {
          if ($request->expectsJson()) {
              $response = $next($request);
              return $response;
          }
      
          $query = QueryBuilder::for(\App\Models\User::class)
              ->allowedFilters('name')
              ->allowedSorts('name');
      
          // Replace the route's controller logic with QueryBuilder
          return response()->json($query->get());
      }
      
  3. Dynamic Model Binding:

    • Bind dynamic models based on request parameters:
      $modelClass = \request('model', \App\Models\User::class);
      $query = QueryBuilder::
      
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.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
christhompsontldr/phpsdk
enqueue/dsn
bunny/bunny
enqueue/test
enqueue/null
enqueue/amqp-tools
milesj/emojibase
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport