Skip to content

Commit 15c179b

Browse files
committed
docs: rewrite documentation with real-world examples
- Rewrite README with quick example and feature summary - Rewrite usage docs with practical controller examples - Rewrite strategies docs with complete real-world strategy - Rewrite clauses docs with clause resolution explanation - Rewrite transmutes docs with custom transmute ideas - Rewrite configuration docs with custom parameter names
1 parent 7b1fbaa commit 15c179b

6 files changed

Lines changed: 454 additions & 252 deletions

File tree

README.md

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Laravel Query Strategies
22

3-
A package to help build queries with Eloquent Builder from URL parameters in a request.
3+
Build safe, flexible API endpoints by turning URL query parameters into Eloquent queries. Define a strategy that controls exactly which filters, sorts, fields, and relationships your API consumers can use.
44

55
[![Latest Stable Version](https://poser.pugx.org/myerscode/laravel-query-strategies/v/stable)](https://packagist.org/packages/myerscode/laravel-query-strategies)
66
[![Total Downloads](https://poser.pugx.org/myerscode/laravel-query-strategies/downloads)](https://packagist.org/packages/myerscode/laravel-query-strategies)
@@ -14,31 +14,68 @@ A package to help build queries with Eloquent Builder from URL parameters in a r
1414
- PHP ^8.5
1515
- Laravel ^13.0
1616

17-
## Why this package is helpful?
17+
## Quick Example
1818

19-
If you want to apply query clauses to Eloquent Models using parameters passed by the user, then this package will allow you to create strategies that will enable them to be applied automatically.
19+
Given a `ProductStrategy` that defines which parameters are allowed:
2020

21-
Using query strategies you can define what properties a user can have access to offering a safer way for them interact with your data schemas.
21+
```php
22+
class ProductStrategy extends Strategy
23+
{
24+
protected array $canOrderBy = ['name', 'price', 'created_at'];
25+
protected array $canWith = ['category', 'reviews'];
26+
protected array $allowedFields = ['id', 'name', 'price', 'category_id'];
27+
protected array $allowedAppends = ['discount_price'];
2228

23-
Strategies can obfuscate the real column names, add aliases to them and enable/disable the query clauses that can be applied to the model.
29+
protected array $config = [
30+
'name' => ['filter' => ContainsClause::class],
31+
'price' => ['column' => 'unit_price'],
32+
'category' => ['column' => 'category_id', 'explode' => true],
33+
];
34+
}
35+
```
2436

25-
You can work the builder before and after applying a strategy, so it can be easily integrated with existing code and queries.
37+
Your API consumers can now query like this:
2638

27-
## Installation
39+
```
40+
GET /products?name=laptop&price=500&category=1,2,3&order=price&sort=desc&limit=10&with=reviews&fields=id,name,price&append=discount_price
41+
```
2842

29-
You can install the package via composer:
43+
And in your controller:
44+
45+
```php
46+
use function Myerscode\Laravel\QueryStrategies\filter;
47+
48+
public function index()
49+
{
50+
return filter(Product::class)->with(ProductStrategy::class)->apply();
51+
}
52+
```
53+
54+
That single line handles filtering, sorting, field selection, eager loading, accessor appending, limiting, and pagination — all controlled by the strategy.
55+
56+
## Why Use This?
57+
58+
- **Safe by default** — only parameters defined in your strategy are applied. Unknown parameters are ignored (or rejected in strict mode).
59+
- **Column obfuscation** — map public parameter names to real database columns so your schema stays private.
60+
- **Flexible clauses** — 17 built-in filter clauses (equals, contains, between, greater than, etc.) with operator overrides via URL.
61+
- **Relationship support** — filter through relationships with dot notation, sort by relationship columns, and control eager loading.
62+
- **Composable** — chain individual methods (`filter()`, `order()`, `fields()`, etc.) or call `apply()` to run everything at once.
63+
64+
## Installation
3065

3166
```bash
3267
composer require myerscode/laravel-query-strategies
3368
```
3469

70+
The package auto-discovers its service provider. No manual registration needed.
71+
3572
## Documentation
3673

37-
- [Usage](docs/usage.md) — Getting started, filter methods, query parameter syntax, and pagination
38-
- [Configuration](docs/configuration.md)Publishing and customising the config file
39-
- [Strategies](docs/strategies.md)Defining strategies, parameter config options, ordering, limiting, and eager loads
40-
- [Clauses](docs/clauses.md)Built-in clauses, aliases, and creating custom clauses
41-
- [Transmutes](docs/transmutes.md)Value transformation with built-in and custom transmutes
74+
- [Usage](docs/usage.md) — Getting started, creating filters, query parameter syntax, and pagination
75+
- [Strategies](docs/strategies.md)Defining strategies, parameter options, ordering, limiting, eager loads, and default filters
76+
- [Clauses](docs/clauses.md)Built-in clauses, scope and trashed filtering, callbacks, and custom clauses
77+
- [Transmutes](docs/transmutes.md)Transforming values before filtering
78+
- [Configuration](docs/configuration.md)Customising parameter names and strict mode
4279

4380
## License
4481

docs/clauses.md

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Clauses
22

3-
Clauses define how a filter value is applied to the Eloquent query builder. Each clause maps to a SQL WHERE condition.
3+
Clauses define how a filter value translates into a SQL condition. When a user sends `?name=John`, the clause determines whether that becomes `WHERE name = 'John'`, `WHERE name LIKE '%John%'`, or something else entirely.
44

55
## Built-in Clauses
66

@@ -20,86 +20,100 @@ Clauses define how a filter value is applied to the Eloquent query builder. Each
2020
| `IsNotInClause` | `WHERE column NOT IN (...)` | `notIn`, `!in` |
2121
| `IsNullClause` | `WHERE column IS NULL` | `isNull`, `null` |
2222
| `IsNotNullClause` | `WHERE column IS NOT NULL` | `isNotNull`, `!null`, `notNull` |
23-
| `TrashedClause` | Soft delete filtering (`with`, `only`) ||
2423
| `OrEqualsClause` | `OR WHERE column = value` | `or`, `\|\|` |
2524
| `ScopeClause` | Calls a model scope ||
25+
| `TrashedClause` | Soft delete filtering ||
2626

27-
Aliases are used in operator overrides. For example, `?name--contains=John` uses the `ContainsClause`.
27+
## How Clauses Are Resolved
28+
29+
When a request comes in, the package resolves which clause to use in this order:
30+
31+
1. If the parameter has a `callback` defined, that runs instead of any clause.
32+
2. If the user specifies an operator override (e.g. `?name--contains=John`), the alias is looked up in the parameter's methods and the strategy's default methods.
33+
3. If the parameter has a `filter` or `default` clause class set, that's used.
34+
4. If multiple values are provided, the `multi` clause is used (defaults to `IsInClause`).
35+
5. Otherwise, the global default `EqualsClause` is used.
36+
37+
## Operator Overrides
38+
39+
API consumers can override the default clause for any parameter by appending `--` and a clause alias:
40+
41+
```
42+
?name--contains=lap → WHERE name LIKE '%lap%'
43+
?price--gte=100 → WHERE price >= 100
44+
?price--between=10,500 → WHERE price BETWEEN 10 AND 500
45+
?email--beginsWith=admin → WHERE email LIKE 'admin%'
46+
?status--isNull → WHERE status IS NULL
47+
```
48+
49+
This gives consumers flexibility without you needing to define separate parameters for each operation.
2850

2951
## Scope Clause
3052

31-
The `ScopeClause` calls an Eloquent local scope on the model instead of applying a direct WHERE condition. The parameter name is converted to camelCase to match the scope method name.
53+
The `ScopeClause` calls an Eloquent local scope instead of applying a direct WHERE condition. The parameter name is converted to camelCase to match the scope method.
54+
55+
Say your model has:
3256

3357
```php
34-
// Model scope
3558
public function scopeStartsBefore(Builder $query, string $date): Builder
3659
{
3760
return $query->where('starts_at', '<=', $date);
3861
}
62+
```
63+
64+
Configure it in your strategy:
3965

40-
// Strategy config
66+
```php
4167
protected array $config = [
4268
'starts_before' => [
43-
'default' => ScopeClause::class,
69+
'filter' => ScopeClause::class,
4470
],
4571
];
4672
```
4773

48-
Usage: `?starts_before=2024-01-01`
74+
Then `?starts_before=2024-01-01` calls `scopeStartsBefore($query, '2024-01-01')`.
4975

50-
Multiple parameters can be passed to a scope using comma-separated values:
76+
Scopes that accept multiple arguments work with comma-separated values:
5177

5278
```
5379
?created_between=2024-01-01,2024-12-31
80+
→ scopeCreatedBetween($query, '2024-01-01', '2024-12-31')
5481
```
5582

56-
This calls `scopeCreatedBetween($query, '2024-01-01', '2024-12-31')`.
57-
5883
Empty values are ignored — the scope won't be called.
5984

6085
## Trashed Clause
6186

62-
The `TrashedClause` provides built-in soft delete filtering for models using Laravel's `SoftDeletes` trait.
87+
For models using Laravel's `SoftDeletes` trait, the `TrashedClause` provides built-in soft delete control:
6388

6489
```php
6590
protected array $config = [
6691
'trashed' => [
67-
'default' => TrashedClause::class,
92+
'filter' => TrashedClause::class,
6893
],
6994
];
7095
```
7196

72-
Accepted values:
73-
7497
| Value | Behaviour |
7598
|---|---|
76-
| `with` | Include soft-deleted records in results |
77-
| `only` | Return only soft-deleted records |
99+
| `?trashed=with` | Include soft-deleted records alongside active ones |
100+
| `?trashed=only` | Return only soft-deleted records |
78101
| Any other value | Default behaviour (exclude soft-deleted) |
79102

80-
Usage:
81-
82-
```
83-
?trashed=with → Include trashed records
84-
?trashed=only → Only trashed records
85-
```
86-
87103
## Callback Filters
88104

89-
For one-off filters that don't warrant a full clause class, use the `callback` config option:
105+
For one-off filters that don't warrant a full clause class, use a closure:
90106

91107
```php
92108
protected array $config = [
93-
'has_posts' => [
94-
'callback' => static function ($builder, $value, $column): void {
95-
$builder->whereHas('posts');
96-
},
109+
// Simple boolean check
110+
'has_reviews' => [
111+
'callback' => fn ($builder, $value, $column) => $builder->whereHas('reviews'),
97112
],
113+
114+
// Value-dependent filter
98115
'min_score' => [
99-
'callback' => static function ($builder, $value, $column): void {
100-
$val = is_array($value) ? $value[0] : $value;
101-
$builder->where('score', '>=', $val);
102-
},
116+
'callback' => fn ($builder, $value, $column) => $builder->where('score', '>=', is_array($value) ? $value[0] : $value),
103117
],
104118
];
105119
```
@@ -108,13 +122,13 @@ The closure receives `(Builder $builder, mixed $value, string $column)`. When a
108122

109123
## Creating a Custom Clause
110124

111-
Generate a clause using the artisan command:
125+
Generate one with artisan:
112126

113127
```bash
114128
php artisan make:clause ILikeClause
115129
```
116130

117-
This creates a class in `app/Queries/Clause/`. Implement the `filter` method:
131+
This creates `app/Queries/Clause/ILikeClause.php`. Implement the `filter` method:
118132

119133
```php
120134
use Illuminate\Database\Eloquent\Builder;
@@ -141,16 +155,16 @@ class ILikeClause extends AbstractClause
141155
}
142156
```
143157

144-
### Using a Custom Clause
145-
146-
Register it in your strategy's `$config`:
158+
Use it in your strategy:
147159

148160
```php
149161
protected array $config = [
162+
// As the default clause for a parameter
150163
'name' => [
151-
'default' => ILikeClause::class,
164+
'filter' => ILikeClause::class,
152165
],
153-
// Or as a named method alias
166+
167+
// As a named method alias (usable via operator override)
154168
'email' => [
155169
'methods' => [
156170
'ilike' => ILikeClause::class,
@@ -159,11 +173,12 @@ protected array $config = [
159173
];
160174
```
161175

162-
You can also add it to the strategy's `$defaultMethods` to make it available to all parameters:
176+
With the methods approach, users can opt in: `?email--ilike=admin`.
177+
178+
You can also register a custom clause globally by overriding `$defaultMethods` in your strategy, making it available to all parameters:
163179

164180
```php
165181
protected array $defaultMethods = [
166-
// Include parent defaults
167182
...parent::$defaultMethods,
168183
ILikeClause::class => ['ilike', '~'],
169184
];

docs/configuration.md

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22

33
## Publishing the Config
44

5-
Publish the configuration file to customise query parameter names:
6-
75
```bash
86
php artisan vendor:publish --tag=config --provider="Myerscode\Laravel\QueryStrategies\ServiceProvider"
97
```
108

11-
This creates `config/query-strategies.php`.
9+
This creates `config/query-strategies.php`. If you don't publish it, the package uses sensible defaults.
1210

1311
## Options
1412

@@ -27,31 +25,39 @@ return [
2725
];
2826
```
2927

30-
### Strict Mode
28+
## Strict Mode
3129

32-
| Key | Default | Description |
33-
|---|---|---|
34-
| `strict` | `false` | When `true`, throws `InvalidFilterException` if a query parameter is not a recognised filter, system key, or operator override |
30+
When `strict` is `true`, any query parameter that isn't a recognised filter, system key, or operator override throws an `InvalidFilterException`. This is useful for APIs where you want to catch typos or prevent consumers from guessing at filter names.
31+
32+
```php
33+
'strict' => true,
34+
```
35+
36+
When `strict` is `false` (the default), unknown parameters are silently ignored.
37+
38+
You can also enable strict mode per-request by passing it in the config array:
3539

36-
When strict mode is enabled, any unknown query parameter will throw an exception with a message listing the allowed filters. This is useful for APIs where you want to prevent typos or unauthorised filter attempts from silently being ignored.
40+
```php
41+
$filter = new Filter($builder, $strategy, $request->query->all(), ['strict' => true]);
42+
```
3743

38-
### Parameter Keys
44+
## Parameter Names
3945

40-
Each key maps an internal concept to the query parameter name your API exposes:
46+
Each key maps an internal concept to the query parameter name your API exposes. Change these if your API conventions differ from the defaults:
4147

42-
| Key | Default | Description |
48+
| Key | Default | What it controls |
4349
|---|---|---|
44-
| `order` | `order` | Parameter for specifying which column to order results by |
45-
| `sort` | `sort` | Parameter for specifying sort direction (`asc` or `desc`) |
46-
| `limit` | `limit` | Parameter for limiting the number of results returned |
47-
| `page` | `page` | Parameter for pagination page number |
48-
| `with` | `with` | Parameter for specifying relationships to eager load |
49-
| `fields` | `fields` | Parameter for selecting specific columns |
50-
| `append` | `append` | Reserved for future use |
50+
| `order` | `order` | Column to sort by |
51+
| `sort` | `sort` | Sort direction (`asc` / `desc`) |
52+
| `limit` | `limit` | Results per page |
53+
| `page` | `page` | Page number |
54+
| `with` | `with` | Relationships to eager load |
55+
| `fields` | `fields` | Columns to select |
56+
| `append` | `append` | Model accessors to append |
5157

52-
## Example
58+
## Example: Custom Parameter Names
5359

54-
If your API uses `sort_by` instead of `order` and `per_page` instead of `limit`:
60+
If your API uses different conventions:
5561

5662
```php
5763
return [
@@ -61,10 +67,16 @@ return [
6167
'limit' => 'per_page',
6268
'page' => 'page',
6369
'with' => 'include',
64-
'fields' => 'fields',
70+
'fields' => 'select',
6571
'append' => 'append',
6672
],
6773
];
6874
```
6975

70-
Users would then query with `?sort_by=name&direction=desc&per_page=25&include=owner`.
76+
Consumers would then query with:
77+
78+
```
79+
GET /products?sort_by=name&direction=desc&per_page=25&include=category&select=id,name
80+
```
81+
82+
The strategy and filter logic stays the same — only the URL parameter names change.

0 commit comments

Comments
 (0)