PHPackages                             aponahmed/laravel-miniql - 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. aponahmed/laravel-miniql

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

aponahmed/laravel-miniql
========================

A Laravel GraphQL-lite internal query engine — single endpoint, schema-driven, N+1-safe, mutation-ready.

1.1.0(3mo ago)221MITPHPPHP ^8.2CI passing

Since Apr 22Pushed 2mo agoCompare

[ Source](https://github.com/AponAhmed/laravel-miniql)[ Packagist](https://packagist.org/packages/aponahmed/laravel-miniql)[ Docs](https://github.com/AponAhmed/laravel-miniql)[ RSS](/packages/aponahmed-laravel-miniql/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (8)Versions (2)Used By (0)

MiniQL — Laravel GraphQL-Lite Query Engine
==========================================

[](#miniql--laravel-graphql-lite-query-engine)

[![PHP](https://camo.githubusercontent.com/83dd395020c37276225039739320f6c8e7e99963ab21ee3d09282cb48dad2a60/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e312532422d626c7565)](https://www.php.net)[![Laravel](https://camo.githubusercontent.com/add0a2daaf51dd84233459fc84033647bab96087f8cefa4e7b8e3a56e1f52451/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d313025324225323025374325323031312532422d726564)](https://laravel.com)[![License](https://camo.githubusercontent.com/5caa455d8debc46fb23abbadb45a733a937f3910a73fc875c2f7820468e1bb54/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e)](LICENSE)

A **production-grade, single-endpoint internal query engine** for Laravel.
Think GraphQL — without the schema language, without the AST parser, with full Laravel idioms.

---

Table of Contents
-----------------

[](#table-of-contents)

1. [What is MiniQL?](#what-is-miniql)
2. [Installation](#installation)
3. [Quick Start](#quick-start)
4. [Query Syntax Reference](#query-syntax-reference)
5. [Mutation Syntax Reference](#mutation-syntax-reference)
6. [Schema Config Reference](#schema-config-reference)
7. [Resolvers](#resolvers)
8. [Mutation Handlers](#mutation-handlers)
9. [Hooks](#hooks)
10. [Fluent PHP API](#fluent-php-api)
11. [Caching](#caching)
12. [Security](#security)
13. [Introspection](#introspection)
14. [Artisan Commands](#artisan-commands)
15. [Architecture Overview](#architecture-overview)
16. [Upgrading / Roadmap](#roadmap)

---

What is MiniQL?
---------------

[](#what-is-miniql)

MiniQL lets your frontend (or internal services) send expressive queries to a **single POST endpoint** instead of building dozens of REST routes.

```
POST /api/miniql

```

A request looks like this:

```
{
  "query": {
    "users": {
      "where":    { "active": true },
      "fields":   ["id", "name", "email"],
      "relations": {
        "posts": {
          "fields":  ["id", "title"],
          "orderBy": { "column": "created_at", "direction": "desc" },
          "limit":   5
        }
      },
      "orderBy": { "column": "name", "direction": "asc" },
      "page":    1,
      "perPage": 20
    }
  }
}
```

**Everything you send is validated against a whitelist schema. No raw SQL exposure. No N+1.**

---

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

[](#installation)

```
composer require aponahmed/laravel-miniql
```

Publish the config:

```
php artisan vendor:publish --tag=miniql-config
```

---

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

[](#quick-start)

### 1. Register your models in `config/miniql.php`

[](#1-register-your-models-in-configminiqlphp)

```
'models' => [

    'users' => [
        'model'     => App\Models\User::class,
        'fields'    => ['id', 'name', 'email', 'created_at'],
        'relations' => ['posts'],
        'mutations' => [
            'createUser' => App\MiniQL\Mutations\CreateUserMutation::class,
            'updateUser' => App\MiniQL\Mutations\UpdateUserMutation::class,
        ],
    ],

    'posts' => [
        'model'     => App\Models\Post::class,
        'fields'    => ['id', 'title', 'body', 'user_id'],
        'relations' => ['user'],
        'mutations' => [
            'createPost' => App\MiniQL\Mutations\CreatePostMutation::class,
        ],
    ],

],
```

### 2. Send a query

[](#2-send-a-query)

```
curl -X POST http://yourapp.test/api/miniql \
  -H "Content-Type: application/json" \
  -d '{
    "query": {
      "users": {
        "fields": ["id", "name"],
        "limit": 5
      }
    }
  }'
```

**Response:**

```
{
  "data": {
    "users": [
      { "id": 1, "name": "Alice" },
      { "id": 2, "name": "Bob" }
    ]
  },
  "errors": []
}
```

---

Query Syntax Reference
----------------------

[](#query-syntax-reference)

Every query node supports:

KeyTypeDescription`fields``string[]`Fields to return (whitelist enforced)`where``object`Simple equality OR advanced `{op, value}` filter`whereIn``object`Column → array of values`whereNull``string[]`Columns that must be NULL`whereNotNull``string[]`Columns that must NOT be NULL`search``object`Full-text LIKE search `{term, fields[]}``orderBy``string|object`String column, or `{column, direction}``limit``int`Max rows (hard-capped by `security.max_results`)`page``int`Enable pagination (returns `{data, meta}`)`perPage``int`Rows per page (default from config)`relations``object`Eager-load nested relations (recursively supported)### Advanced `where` example

[](#advanced-where-example)

```
{
  "query": {
    "users": {
      "where": {
        "age":    { "op": ">=", "value": 18 },
        "status": "active"
      },
      "whereIn":     { "role": ["admin", "editor"] },
      "whereNotNull": ["email_verified_at"],
      "search": { "term": "alice", "fields": ["name", "email"] }
    }
  }
}
```

### Pagination example

[](#pagination-example)

```
{
  "query": {
    "posts": {
      "fields":  ["id", "title", "created_at"],
      "orderBy": { "column": "created_at", "direction": "desc" },
      "page":    2,
      "perPage": 10
    }
  }
}
```

**Response shape with pagination:**

```
{
  "data": {
    "posts": {
      "data": [ ... ],
      "meta": {
        "current_page": 2,
        "per_page": 10,
        "total": 87,
        "last_page": 9,
        "from": 11,
        "to": 20
      }
    }
  },
  "errors": []
}
```

### Multi-type query (batch)

[](#multi-type-query-batch)

```
{
  "query": {
    "users": { "fields": ["id", "name"], "limit": 3 },
    "posts": { "fields": ["id", "title"], "limit": 3 }
  }
}
```

Both are resolved in a single request. No N+1. Each type runs its own eager-load strategy.

---

Mutation Syntax Reference
-------------------------

[](#mutation-syntax-reference)

```
{
  "mutation": {
    "createUser": {
      "data": {
        "name":     "Apon",
        "email":    "apon@example.com",
        "password": "secret123"
      }
    }
  }
}
```

- All mutations run inside a **database transaction**. If any mutation fails, all are rolled back.
- Multiple mutations can be sent in one request.

```
{
  "mutation": {
    "createUser": { "data": { "name": "Bob", "email": "bob@x.com", "password": "pass1234" } },
    "createPost": { "data": { "title": "Hello World", "user_id": 1 } }
  }
}
```

### Query + Mutation in one request

[](#query--mutation-in-one-request)

```
{
  "mutation": {
    "updateUser": { "id": 3, "data": { "name": "Updated" } }
  },
  "query": {
    "users": { "where": { "id": 3 }, "fields": ["id", "name"] }
  }
}
```

Mutations run first, then queries — so the query returns the updated state.

---

Schema Config Reference
-----------------------

[](#schema-config-reference)

Full annotated config (published to `config/miniql.php`):

```
'models' => [
    'users' => [

        // ✅ Required: Eloquent model class
        'model' => App\Models\User::class,

        // ✅ Required: whitelisted selectable fields
        'fields' => ['id', 'name', 'email', 'created_at'],

        // Whitelisted eager-loadable relation names
        'relations' => ['posts', 'profile'],

        // Custom resolver (overrides default query builder)
        'resolver' => App\MiniQL\Resolvers\UserResolver::class,

        // Mutation handlers, keyed by mutation name
        'mutations' => [
            'createUser' => App\MiniQL\Mutations\CreateUserMutation::class,
            'updateUser' => App\MiniQL\Mutations\UpdateUserMutation::class,
            'deleteUser' => App\MiniQL\Mutations\DeleteUserMutation::class,
        ],

        // Before/after hooks for query and mutation lifecycle
        'hooks' => [
            'before_query'    => App\MiniQL\Hooks\UserBeforeQueryHook::class,
            'after_query'     => App\MiniQL\Hooks\UserAfterQueryHook::class,
            'before_mutation' => null,
            'after_mutation'  => null,
        ],

        // Global Eloquent scopes applied to every query on this type
        // e.g. 'active' calls ->active() on the query builder
        'scopes' => ['active'],

        // Free-form metadata (documentation, versioning, etc.)
        'meta' => ['description' => 'Registered users'],
    ],
],
```

---

Resolvers
---------

[](#resolvers)

A Resolver replaces the default query builder for a specific type. Use it for:

- Multi-tenant scoping
- Auth-based filtering
- Complex joins that can't be expressed in `where`

### Generate a resolver

[](#generate-a-resolver)

```
php artisan miniql:make-resolver UserResolver
```

### Implement it

[](#implement-it)

```
// app/MiniQL/Resolvers/UserResolver.php

namespace App\MiniQL\Resolvers;

use MiniQL\Resolvers\BaseResolver;
use Illuminate\Database\Eloquent\Builder;

class UserResolver extends BaseResolver
{
    protected function model(): string
    {
        return \App\Models\User::class;
    }

    public function query(array $node): Builder
    {
        $q = parent::query($node); // applies basic where filters

        // Scope to current tenant
        $q->where('tenant_id', auth()->user()->tenant_id);

        return $q;
    }
}
```

### Register it

[](#register-it)

```
// config/miniql.php
'users' => [
    'resolver' => App\MiniQL\Resolvers\UserResolver::class,
    ...
],
```

---

Mutation Handlers
-----------------

[](#mutation-handlers)

### Generate a mutation

[](#generate-a-mutation)

```
php artisan miniql:make-mutation CreateUser
```

### Implement it

[](#implement-it-1)

```
// app/MiniQL/Mutations/CreateUserMutation.php

namespace App\MiniQL\Mutations;

use MiniQL\Mutations\BaseMutation;
use App\Models\User;
use Illuminate\Support\Facades\Hash;

class CreateUserMutation extends BaseMutation
{
    protected function rules(): array
    {
        return [
            'data.name'     => 'required|string|max:255',
            'data.email'    => 'required|email|unique:users,email',
            'data.password' => 'required|string|min:8',
        ];
    }

    public function handle(array $node): mixed
    {
        $data = $this->validate($node); // throws ValidationException on failure

        return User::create([
            'name'     => $data['name'],
            'email'    => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }
}
```

`$this->validate($node)` runs Laravel's Validator against `rules()`. On failure it throws a `ValidationException` that is caught by the controller and returned in the `errors` key.

---

Hooks
-----

[](#hooks)

Hooks let you tap into the query/mutation lifecycle without modifying the engine.

HookReceivesCalled`before_query``&$node` (array)Before the query executes`after_query``&$result` (mixed)After query result is built`before_mutation``&$node` (array)Before mutation handler runs`after_mutation``&$result` (mixed)After mutation handler returns```
// app/MiniQL/Hooks/AuditAfterMutationHook.php

namespace App\MiniQL\Hooks;

use MiniQL\Contracts\HookInterface;

class AuditAfterMutationHook implements HookInterface
{
    public function handle(mixed &$context): void
    {
        logger()->info('[Audit] Mutation completed.', ['result' => $context]);
    }
}
```

Register in config:

```
'hooks' => [
    'after_mutation' => App\MiniQL\Hooks\AuditAfterMutationHook::class,
],
```

---

Fluent PHP API
--------------

[](#fluent-php-api)

Use MiniQL directly in PHP (controllers, jobs, services) without HTTP:

```
use MiniQL\Facades\MiniQL;

// Simple
$users = MiniQL::query('users')
    ->fields(['id', 'name', 'email'])
    ->where('active', true)
    ->whereIn('role', ['admin', 'editor'])
    ->orderBy('name', 'asc')
    ->limit(50)
    ->get();

// Paginated
$result = MiniQL::query('posts')
    ->fields(['id', 'title', 'created_at'])
    ->with('user', ['fields' => ['id', 'name']])
    ->orderBy('created_at', 'desc')
    ->paginate(15, 1)
    ->get();
// $result['data'], $result['meta']

// Full payload
$result = MiniQL::execute([
    'query' => [
        'users' => ['fields' => ['id', 'name'], 'limit' => 5],
    ],
    'mutation' => [
        'createUser' => ['data' => ['name' => 'Eve', 'email' => 'eve@x.com', 'password' => 'pass1234']],
    ],
]);
```

---

Caching
-------

[](#caching)

Enable Redis-backed query caching in `.env`:

```
MINIQL_CACHE=true
MINIQL_CACHE_TTL=120
MINIQL_CACHE_STORE=redis

```

Cache keys are derived from `type + md5(node)` — identical queries return cached results instantly.

Cache is **automatically invalidated** per-type when a mutation runs (if `cache.auto_invalidate = true`).

---

Security
--------

[](#security)

All security options live in `config/miniql.php` under `security`:

OptionDefaultDescription`max_depth``5`Max relation nesting depth`max_query_nodes``10`Max top-level query types per request`max_results``1000`Hard cap on rows returned`require_auth``false`Set to `true` to require authentication globally`rate_limit``60`Requests per minute per IP (0 = disabled)**Field &amp; relation whitelisting** is always enforced — there is no way to query a field not listed in `config('miniql.models.*.fields')`.

**Protect the endpoint** by adding `auth:sanctum` (or any Laravel middleware) to the route middleware:

```
// config/miniql.php
'route' => [
    'middleware' => ['api', 'auth:sanctum'],
],
```

---

Introspection
-------------

[](#introspection)

```
GET /api/miniql/schema
```

Returns the full registered schema as JSON (useful for frontend tooling):

```
{
  "schema": {
    "users": {
      "fields":    ["id", "name", "email", "created_at"],
      "relations": ["posts"],
      "mutations": ["createUser", "updateUser", "deleteUser"],
      "meta":      {}
    }
  },
  "version": "1.0"
}
```

Disable in production:

```
MINIQL_INTROSPECTION=false
```

Or protect with middleware:

```
'introspection' => [
    'enabled'    => true,
    'middleware' => ['auth:sanctum'],
],
```

---

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

[](#artisan-commands)

CommandDescription`miniql:make-resolver Name`Scaffold a resolver class`miniql:make-mutation Name`Scaffold a mutation handler class`miniql:schema-dump`Pretty-print the registered schema`miniql:schema-dump --json`Output schema as raw JSON`miniql:schema-validate`Verify all model/resolver/mutation classes exist on disk---

Architecture Overview
---------------------

[](#architecture-overview)

```
POST /api/miniql
       │
       ▼
MiniQLController
       │
       ├─ SchemaValidator       ← whitelist check (fields, relations, depth)
       │
       ├─ QueryEngine
       │      ├─ ResolverInterface (custom) OR default Eloquent builder
       │      ├─ Eager loading  ← N+1 prevention via ->with()
       │      ├─ QueryCache     ← Redis-backed result cache
       │      └─ Hooks          ← before/after lifecycle
       │
       └─ MutationEngine
              ├─ DB::transaction ← atomic multi-mutation
              ├─ BaseMutation    ← Laravel validation built-in
              └─ Hooks           ← before/after lifecycle

```

---

Roadmap
-------

[](#roadmap)

Possible future upgrades:

- **Query complexity scoring** (prevent expensive queries by weight)
- **String DSL parser** — accept `users { id name posts { title } }` syntax
- **WebSocket subscriptions** — real-time updates on mutations
- **Persisted queries** — hash-indexed pre-registered queries
- **Automatic SQL optimization** — detect and merge redundant joins
- **OpenAPI export** — generate an OpenAPI 3 spec from the schema config

---

License
-------

[](#license)

MIT © Muhiminul Haque

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance83

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 86.7% 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

Unknown

Total

1

Last Release

93d ago

### Community

Maintainers

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

---

Top Contributors

[![AponAhmed](https://avatars.githubusercontent.com/u/24431220?v=4)](https://github.com/AponAhmed "AponAhmed (13 commits)")[![anikrahman0](https://avatars.githubusercontent.com/u/52888800?v=4)](https://github.com/anikrahman0 "anikrahman0 (1 commits)")[![JbcMuhiminul](https://avatars.githubusercontent.com/u/223445703?v=4)](https://github.com/JbcMuhiminul "JbcMuhiminul (1 commits)")

---

Tags

apilaravelrestgraphqlquery-engine

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/aponahmed-laravel-miniql/health.svg)

```
[![Health](https://phpackages.com/badges/aponahmed-laravel-miniql/health.svg)](https://phpackages.com/packages/aponahmed-laravel-miniql)
```

###  Alternatives

[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M136](/packages/laravel-pulse)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9762.4M133](/packages/roots-acorn)[mike-bronner/laravel-model-caching

Automatic caching for Eloquent models.

2.4k96.5k1](/packages/mike-bronner-laravel-model-caching)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

255.2k](/packages/aedart-athenaeum)

PHPackages © 2026

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