PHPackages                             rawnoq/laravel-query-api - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. rawnoq/laravel-query-api

ActiveLibrary[HTTP &amp; Networking](/categories/http)

rawnoq/laravel-query-api
========================

A powerful Laravel package that brings GraphQL-like flexibility to REST APIs with elegant query management

1.2.1(5mo ago)063MITPHPPHP ^8.2CI failing

Since Nov 22Pushed 5mo agoCompare

[ Source](https://github.com/rawnoq/laravel-query-api)[ Packagist](https://packagist.org/packages/rawnoq/laravel-query-api)[ Docs](https://github.com/rawnoq/laravel-query-api)[ RSS](/packages/rawnoq-laravel-query-api/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (2)Dependencies (6)Versions (5)Used By (0)

Laravel Query API
=================

[](#laravel-query-api)

[![Latest Version](https://camo.githubusercontent.com/928a8e1e02b4cf6f6a0fe999e2baf3bba3ef022884eb128d81c72bac1985e7f8/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7261776e6f712f6c61726176656c2d71756572792d6170692e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/rawnoq/laravel-query-api)[![Total Downloads](https://camo.githubusercontent.com/45e0e92ec7212aefb21f1e8aec56c73203cf868be378c09d60ce5125eeb7dccb/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7261776e6f712f6c61726176656c2d71756572792d6170692e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/rawnoq/laravel-query-api)[![License](https://camo.githubusercontent.com/7c07293ae54bc05e941e2c01ef96f2ab2eef55cc5ade84569c97d64129a700b1/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f7261776e6f712f6c61726176656c2d71756572792d6170692e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/rawnoq/laravel-query-api)

A powerful and elegant Laravel package that brings GraphQL-like flexibility to your REST APIs. Built on top of [Spatie Laravel Query Builder](https://github.com/spatie/laravel-query-builder), it provides a fluent API for managing complex queries with ease.

Features
--------

[](#features)

- ✅ **Sorting** - Sort results by multiple fields with ascending/descending order
- ✅ **Default Sorting** - Define default sorting behavior
- ✅ **Sparse Fieldsets** - Request only the fields you need
- ✅ **Virtual Fields** - Support for computed/accessor fields that aren't database columns
- ✅ **Filters** - Support for exact, partial, scope, callback, operator, exclusion, and custom filters
- ✅ **Includes (Relationships)** - Load relationships with count, exists, and custom includes
- ✅ **Pagination** - Built-in pagination support
- ✅ **Config Classes** - Organize query configurations in dedicated classes
- ✅ **Fluent API** - Beautiful and intuitive API with method chaining
- ✅ **HMVC Support** - Full compatibility with rawnoq/laravel-hmvc package
- ✅ **Artisan Command** - Generate QueryAPI config classes easily
- ✅ **Helper Methods** - Built-in helpers for checking requested fields and includes in Resources
- ✅ **Security** - Whitelist-based access control for fields, filters, and relationships

Requirements
------------

[](#requirements)

- PHP &gt;= 8.2
- Laravel &gt;= 12.0
- Spatie Laravel Query Builder &gt;= 6.0

Installation
------------

[](#installation)

Install the package via Composer:

```
composer require rawnoq/laravel-query-api
```

The package will automatically register itself via Laravel's package discovery.

Quick Start
-----------

[](#quick-start)

### Generate QueryAPI Config Class

[](#generate-queryapi-config-class)

The easiest way to get started is to generate a QueryAPI config class using the Artisan command:

```
# Generate a QueryAPI config for a model
php artisan make:query-api UserQueryAPI --model=User

# Or let it guess the model from the class name
php artisan make:query-api UserQueryAPI

# For HMVC modules (requires rawnoq/laravel-hmvc)
php artisan make:query-api SettingQueryAPI --model=Setting --module=Settings
```

This will create a file at `app/QueryAPI/UserQueryAPI.php` (or `modules/{Module}/App/QueryAPI/` for HMVC) with all the basic methods stubbed out.

### Basic Usage with Helper Function

[](#basic-usage-with-helper-function)

```
use App\Models\User;

// Simple query
$users = query_api(User::class)->get();

// With sorting and pagination
$users = query_api(User::class)
    ->sort(['name', 'created_at'])
    ->paginate(20);
```

### Using Config Classes (Recommended)

[](#using-config-classes-recommended)

Create a config class to organize your query settings:

```
namespace App\QueryAPI;

use Rawnoq\QueryAPI\QueryAPIConfig;

class UserQueryAPI extends QueryAPIConfig
{
    public static function fields(): array
    {
        return ['id', 'name', 'email', 'created_at'];
    }

    public static function sorts(): array
    {
        return ['id', 'name', 'created_at'];
    }

    public static function defaultSort(): string
    {
        return '-created_at'; // Descending by created_at
    }

    public static function filters(): array
    {
        return [
            self::filter()->exact('id'),
            self::filter()->partial('name'),
            self::filter()->partial('email'),
        ];
    }

    public static function includes(): array
    {
        return [
            self::include()->relationship('posts'),
            self::include()->count('postsCount', 'posts'),
        ];
    }
}
```

Use the config class in your controller:

```
use App\QueryAPI\UserQueryAPI;

// Direct static call
$users = UserQueryAPI::get();
$users = UserQueryAPI::paginate(20);

// Flexible pagination methods
$users = UserQueryAPI::getOrPaginate();      // Default: get all, use ?paginate=1 for pagination
$users = UserQueryAPI::paginateOrGet();      // Default: paginate, use ?get=1 for all

// With custom query
$users = UserQueryAPI::for(User::where('is_active', true))->get();
```

Query Parameters
----------------

[](#query-parameters)

### Sorting

[](#sorting)

```
GET /api/users?sort=name                    # Ascending
GET /api/users?sort=-created_at             # Descending
GET /api/users?sort=name,-created_at        # Multiple
```

### Fields (Sparse Fieldsets)

[](#fields-sparse-fieldsets)

```
GET /api/users?fields[users]=id,name,email
GET /api/users?fields[users]=id,name&fields[posts]=id,title
```

### Filters

[](#filters)

**Partial Filter (LIKE):**

```
GET /api/users?filter[name]=john
```

**Exact Filter:**

```
GET /api/users?filter[id]=1
GET /api/users?filter[status]=active
```

**Operator Filters:**

```
GET /api/users?filter[age]=>25              # Greater than
GET /api/users?filter[price]=50            # Dynamic operator
```

**Scope Filters:**

```
GET /api/users?filter[active]=1
```

**Exclusion Filters:**

```
GET /api/users?filter[exclude_status]=deleted        # WHERE status != 'deleted'
GET /api/users?filter[exclude_id]=1,2,3              # WHERE id NOT IN (1,2,3)
```

### Includes (Relationships)

[](#includes-relationships)

```
GET /api/users?include=posts                # Single relationship
GET /api/users?include=posts,roles          # Multiple relationships
GET /api/users?include=posts.comments       # Nested relationships
GET /api/users?include=postsCount           # Relationship count
GET /api/users?include=postsExists          # Relationship exists
```

### Pagination

[](#pagination)

```
GET /api/users?page=2&per_page=15
```

**Flexible Pagination Methods:**

```
# getOrPaginate() - Default: get all, use ?paginate=1 for pagination
GET /api/users                    # Returns all users (Collection)
GET /api/users?paginate=1        # Returns paginated (LengthAwarePaginator)
GET /api/users?paginate=1&per_page=50  # Returns paginated with custom per_page

# paginateOrGet() - Default: paginate, use ?get=1 for all
GET /api/users                    # Returns paginated (LengthAwarePaginator)
GET /api/users?get=1              # Returns all users (Collection)
GET /api/users?per_page=50        # Returns paginated with custom per_page
```

### Complete Example

[](#complete-example)

```
GET /api/users?include=posts&fields[users]=id,name,email&fields[posts]=id,title&filter[name]=john&filter[status]=active&sort=-created_at&page=1&per_page=20
```

Available Methods
-----------------

[](#available-methods)

### Core Methods

[](#core-methods)

```
// Set target model or query
->for(User::class)
->for(User::where('active', true))

// Configure allowed fields
->fields(['id', 'name', 'email'])

// Configure allowed sorting
->sort(['id', 'name', 'created_at'])
->defaultSort('-created_at')

// Configure allowed filters
->filters(['name', 'email'])

// Configure allowed includes
->includes(['posts', 'roles'])

// Set config class
->config(UserQueryAPI::class)

// Execute query
->get()
->paginate(20)  // or paginate() to use defaultPerPage() from config

// Flexible pagination methods
->getOrPaginate()    // Default: get all, use ?paginate=1 for pagination
->paginateOrGet()    // Default: paginate, use ?get=1 for all
```

Filter Types
------------

[](#filter-types)

### Exact Filter

[](#exact-filter)

```
self::filter()->exact('id')
self::filter()->exact('status')
```

### Partial Filter (LIKE %value%)

[](#partial-filter-like-value)

```
self::filter()->partial('name')
self::filter()->partial('email')
```

### Begins With Filter (LIKE value%)

[](#begins-with-filter-like-value)

```
self::filter()->beginsWith('email')
```

### Ends With Filter (LIKE %value)

[](#ends-with-filter-like-value)

```
self::filter()->endsWith('domain')
```

### Scope Filter

[](#scope-filter)

```
self::filter()->scope('active')
self::filter()->scope('published')
```

### Callback Filter

[](#callback-filter)

```
self::filter()->callback('has_posts', function ($query) {
    $query->whereHas('posts');
})
```

### Operator Filter

[](#operator-filter)

```
use Rawnoq\QueryAPI\Enums\FilterOperator;

self::filter()->operator('age', FilterOperator::GREATER_THAN)
self::filter()->operator('price', FilterOperator::LESS_THAN)
self::filter()->operator('salary', FilterOperator::DYNAMIC) // Allows: >3000, trashed()
```

### Exclusion Filters

[](#exclusion-filters)

```
// Exclude single value (WHERE field != value)
self::filter()->exclude('status', 'internal_status')

// Exclude multiple values (WHERE field NOT IN [...])
self::filter()->excludeIn('id', 'internal_id')

// WHERE NOT with operator
self::filter()->whereNot('status', '!=', 'internal_status')
```

Include Types
-------------

[](#include-types)

### Relationship Include

[](#relationship-include)

```
self::include()->relationship('posts')
self::include()->relationship('profile', 'userProfile') // With alias
```

### Count Include

[](#count-include)

```
self::include()->count('postsCount', 'posts')
```

### Exists Include

[](#exists-include)

```
self::include()->exists('postsExists', 'posts')
```

### Callback Include

[](#callback-include)

```
self::include()->callback('latest_post', function ($query) {
    $query->latestOfMany();
})
```

### Custom Include

[](#custom-include)

```
self::include()->custom('comments_sum_votes', new AggregateInclude('votes', 'sum'), 'comments')
```

Pagination Configuration
------------------------

[](#pagination-configuration)

You can configure pagination defaults and limits per model:

```
class UserQueryAPI extends QueryAPIConfig
{
    /**
     * Default items per page
     */
    public static function defaultPerPage(): int
    {
        return 15; // Default: 20
    }

    /**
     * Maximum items per page
     */
    public static function maxPerPage(): int
    {
        return 100; // Default: 100
    }

    /**
     * Minimum items per page
     */
    public static function minPerPage(): int
    {
        return 1; // Default: 1
    }
}
```

**Usage:**

```
// Uses defaultPerPage() if per_page not in request
$users = UserQueryAPI::paginate();

// Reads per_page from request, applies min/max limits
$users = UserQueryAPI::paginate(); // Request: ?per_page=50

// Flexible methods
$users = UserQueryAPI::getOrPaginate();  // ?paginate=1&per_page=50
$users = UserQueryAPI::paginateOrGet();  // ?per_page=50 or ?get=1
```

Advanced Usage
--------------

[](#advanced-usage)

### Using with Eloquent Query

[](#using-with-eloquent-query)

```
$query = User::where('is_active', true)
    ->where('role', 'admin');

$users = UserQueryAPI::for($query)->get();
```

### Manual Configuration

[](#manual-configuration)

```
$users = query_api(User::class)
    ->fields(['id', 'name', 'email'])
    ->filters([
        filter_exact('id'),
        filter_partial('name'),
    ])
    ->includes([
        'posts',
        'roles',
    ])
    ->sort(['name', 'created_at'])
    ->defaultSort('-created_at')
    ->paginate(20);
```

### Using Facade

[](#using-facade)

```
use Rawnoq\QueryAPI\Facades\QueryAPI;

$users = QueryAPI::for(User::class)
    ->fields(['id', 'name'])
    ->sort(['name'])
    ->get();
```

Virtual Fields
--------------

[](#virtual-fields)

Virtual fields are computed fields or accessors that aren't actual database columns. They can be requested in API calls but won't cause SQL errors.

### Defining Virtual Fields

[](#defining-virtual-fields)

```
class UserQueryAPI extends QueryAPIConfig
{
    public static function fields(): array
    {
        return ['id', 'name', 'email', 'full_name']; // full_name is virtual
    }

    public static function virtualFields(): array
    {
        return ['full_name']; // Declare as virtual
    }
}
```

### Using in Resources

[](#using-in-resources)

```
use App\QueryAPI\UserQueryAPI;

class UserResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->when(
                UserQueryAPI::isFieldRequested('id'),
                $this->id
            ),
            'name' => $this->when(
                UserQueryAPI::isFieldRequested('name'),
                $this->name
            ),
            'email' => $this->when(
                UserQueryAPI::isFieldRequested('email'),
                $this->email
            ),
            'full_name' => $this->when(
                UserQueryAPI::isFieldRequested('full_name'),
                $this->first_name . ' ' . $this->last_name
            ),
            'posts' => $this->when(
                UserQueryAPI::isIncludeRequested('posts'),
                fn () => PostResource::collection($this->posts)
            ),
        ];
    }
}
```

### Helper Methods

[](#helper-methods)

The package provides helper methods for checking requested fields and includes:

```
// Check if a field is requested
UserQueryAPI::isFieldRequested('email', $request);

// Get all requested fields
$fields = UserQueryAPI::getRequestedFields($request);

// Check if an include is requested
UserQueryAPI::isIncludeRequested('posts', $request);

// Get all requested includes
$includes = UserQueryAPI::getRequestedIncludes($request);
```

Performance
-----------

[](#performance)

The package includes several performance optimizations:

- **Model Table Caching**: Table names are cached to avoid repeated model instantiation
- **Efficient Field Parsing**: Optimized parsing of field formats
- **Lazy Loading**: Relationships are only loaded when explicitly requested

To clear the model table cache (useful for testing):

```
use Rawnoq\QueryAPI\QueryAPI;

QueryAPI::clearModelTableCache();
```

Security
--------

[](#security)

The package implements a whitelist-based security model:

1. **Fields** - Only explicitly allowed fields can be selected
2. **Virtual Fields** - Computed fields that are validated but not queried from database
3. **Filters** - Only explicitly allowed filters can be applied
4. **Includes** - Only explicitly allowed relationships can be loaded
5. **Sorts** - Only explicitly allowed fields can be sorted

Any unauthorized request will be silently ignored or throw an exception based on Spatie Query Builder configuration.

Artisan Commands
----------------

[](#artisan-commands)

### make:query-api

[](#makequery-api)

Generate a new QueryAPI configuration class:

```
php artisan make:query-api {name} --model={ModelName}
```

**Arguments:**

- `name` - The name of the QueryAPI config class (e.g., UserQueryAPI)

**Options:**

- `--model, -m` - The model that this QueryAPI config is for
- `--module` - The module that this QueryAPI config belongs to (for HMVC structure)
- `--force, -f` - Create the class even if it already exists

**Examples:**

```
# Generate with explicit model
php artisan make:query-api UserQueryAPI --model=User

# Generate and let it auto-detect the model
php artisan make:query-api PostQueryAPI

# Generate for a specific module (HMVC)
php artisan make:query-api SettingQueryAPI --model=Setting --module=Settings

# Force overwrite existing file
php artisan make:query-api UserQueryAPI --model=User --force
```

### Publishing Stubs

[](#publishing-stubs)

You can publish the command stub for customization:

```
php artisan vendor:publish --tag=query-api-stubs
```

This will copy the stub file to `stubs/query-api.stub` in your project root where you can customize it.

Advanced Examples
-----------------

[](#advanced-examples)

### Edge Cases

[](#edge-cases)

**Handling Empty Results:**

```
$users = UserQueryAPI::for(User::where('deleted', true))->get();
if ($users->isEmpty()) {
    return response()->json(['message' => 'No users found'], 404);
}
```

**Custom Query with Filters:**

```
$activeUsers = UserQueryAPI::for(
    User::where('status', 'active')
        ->where('verified', true)
)->paginate(10);
```

**Multiple Field Formats:**

```
# All these formats work:
GET /api/users?fields=id,name
GET /api/users?fields[users]=id,name
GET /api/users?fields[_]=id,name
```

**Virtual Fields with Nested Resources:**

```
class UserResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->when(
                UserQueryAPI::isFieldRequested('id'),
                $this->id
            ),
            'full_name' => $this->when(
                UserQueryAPI::isFieldRequested('full_name'),
                "{$this->first_name} {$this->last_name}"
            ),
            'posts' => $this->when(
                UserQueryAPI::isIncludeRequested('posts'),
                fn() => PostResource::collection($this->posts)
            ),
        ];
    }
}
```

**Complex Filtering:**

```
// In your QueryAPIConfig
public static function filters(): array
{
    return [
        // Multiple filters on same field
        self::filter()->exact('status'),
        self::filter()->partial('name'),

        // Exclusion filters
        self::filter()->exclude('exclude_status', 'status'),
        self::filter()->excludeIn('exclude_ids', 'id'),

        // Operator filters
        self::filter()->operator('age', FilterOperator::GREATER_THAN),
        self::filter()->operator('price', FilterOperator::DYNAMIC), // Allows >, =, callback('has_recent_posts', function ($query, $value) {
            if ($value) {
                $query->whereHas('posts', function ($q) {
                    $q->where('created_at', '>', now()->subDays(7));
                });
            }
        }),
    ];
}
```

Troubleshooting
---------------

[](#troubleshooting)

### Common Issues

[](#common-issues)

**Issue: "Requested field(s) are not allowed"**

```
// Make sure the field is in your fields() method
public static function fields(): array
{
    return ['id', 'name', 'email']; // Add missing field here
}
```

**Issue: Virtual field causing SQL errors**

```
// Make sure to declare it in virtualFields()
public static function virtualFields(): array
{
    return ['full_name', 'value']; // Add virtual field here
}
```

**Issue: Includes not loading**

```
// Check if the include is allowed
public static function includes(): array
{
    return [
        self::include()->relationship('posts'), // Make sure it's here
    ];
}

// And check if it's requested in the Resource
'posts' => $this->when(
    UserQueryAPI::isIncludeRequested('posts'),
    fn() => $this->posts
)
```

**Issue: Model class not found**

```
// Make sure model() method returns correct class
public static function model(): string
{
    return User::class; // Use full namespace if needed: \App\Models\User::class
}
```

**Issue: Performance with large datasets**

```
// Use pagination and limit fields
$users = UserQueryAPI::paginate(20); // Instead of get()

// Request only needed fields
GET /api/users?fields[users]=id,name&per_page=20
```

**Issue: Filter not working**

```
// Check filter type matches your use case
// For exact match:
self::filter()->exact('status') // ?filter[status]=active

// For partial match:
self::filter()->partial('name') // ?filter[name]=john (matches "john", "johnny", etc.)

// For exclusion:
self::filter()->exclude('exclude_status', 'status') // ?filter[exclude_status]=deleted
```

Testing
-------

[](#testing)

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on recent changes.

Contributing
------------

[](#contributing)

Contributions are welcome! Please feel free to submit a Pull Request.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

If you discover any security-related issues, please email  instead of using the issue tracker.

Credits
-------

[](#credits)

- [Rawnoq](https://github.com/rawnoq)
- [Spatie](https://spatie.be) for the amazing [Laravel Query Builder](https://github.com/spatie/laravel-query-builder)
- All [contributors](https://github.com/rawnoq/laravel-query-api/contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE) for more information.

###  Health Score

36

—

LowBetter than 82% of packages

Maintenance71

Regular maintenance activity

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

4

Last Release

164d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/fef8f5b623783c112ea9b179c610918e020a18fe0e921a48dc45c351cb7b5115?d=identicon)[HassanHassanKerdash](/maintainers/HassanHassanKerdash)

---

Top Contributors

[![HKerdash](https://avatars.githubusercontent.com/u/52297419?v=4)](https://github.com/HKerdash "HKerdash (3 commits)")

---

Tags

spatieapilaravelrestgraphqlpaginationqueryquery builderfilterssortingincludessparse-fieldsets

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/rawnoq-laravel-query-api/health.svg)

```
[![Health](https://phpackages.com/badges/rawnoq-laravel-query-api/health.svg)](https://phpackages.com/packages/rawnoq-laravel-query-api)
```

###  Alternatives

[api-platform/laravel

API Platform support for Laravel

59126.4k6](/packages/api-platform-laravel)[essa/api-tool-kit

set of tools to build an api with laravel

52680.5k](/packages/essa-api-tool-kit)[marcelgwerder/laravel-api-handler

Package providing helper functions for a Laravel REST-API

16092.6k](/packages/marcelgwerder-laravel-api-handler)[bjerke/api-query-builder

A query builder for Laravel that parses the request and uses Eloquent ORM to query database

267.7k1](/packages/bjerke-api-query-builder)[illuminatech/data-provider

Allows easy build for DB queries from API requests

4413.3k](/packages/illuminatech-data-provider)[bjerke/laravel-bread

A boilerplate package for BREAD operations through REST API in Laravel

115.2k](/packages/bjerke-laravel-bread)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
