diff --git a/README.md b/README.md index 7287be9..5760c1d 100644 --- a/README.md +++ b/README.md @@ -6,47 +6,351 @@ Total Downloads License

-## ✨ Overview +## Why Filterable? -**Filterable** lets you build highly customizable filtering logic for Laravel's Eloquent queries without messy conditions. With support for multiple engines like: +Most filtering packages give you one approach and expect you to fit your problem around it. Filterable works the other way — you pick the **engine** that matches how your frontend sends data, and the package handles the rest. -- Ruleset Engine -- Invokable Engine -- Expression Engine -- Tree Engine +It ships with four production-ready engines, a full caching system, per-filter authorization, validation, sanitization, sorting, a CLI, and an event system — all while keeping your controllers clean and your filter logic organized and testable. -...you can structure your filter logic however you like — from simple lists to deeply nested conditional trees with relationship support. +--- -## ⚙️ Key Features +## Installation -- **Multiple Filtering Engines** -- **Chainable & Nested Filter Logic** -- **Relation & Nested Relation Filtering** -- **Custom Operators & Sanitization** -- **SOLID & Extensible Design** -- **Zero-Config Optional Defaults** +```bash +composer require kettasoft/filterable +``` +```bash +php artisan vendor:publish --provider="Kettasoft\Filterable\Providers\FilterableServiceProvider" --tag="config" +``` -## 📚 Documentation +Add the following line to the providers array in config/app.php or bootstrap/providers.php: -For full documentation, installation, and usage examples, visit: **[https://kettasoft.github.io/filterable](https://kettasoft.github.io/filterable)** +```php +'providers' => [ + ... + + Kettasoft\Filterable\Providers\FilterableServiceProvider::class, +]; +``` --- -## ✅ Quick Start +## Quick Start + +**1. Create a filter class** ```bash -composer require kettasoft/filterable +php artisan filterable:make-filter PostFilter --filters=title,status +``` + +**2. Define your filters** + +```php +namespace App\Http\Filters; + +use Kettasoft\Filterable\Filterable; +use Kettasoft\Filterable\Support\Payload; + +class PostFilter extends Filterable +{ + protected $filters = ['status', 'title']; + + protected function title(Payload $payload) + { + return $this->builder->where('title', 'like', $payload->asLike('both')); + } + + protected function status(Payload $payload) + { + return $this->builder->where('status', $payload->value); + } +} ``` -Use it in your controller: +**3. Apply in your controller** ```php -$posts = Post::filter(new PostFilter)->paginate(); +$posts = Post::filter(PostFilter::class)->paginate(); +``` + +**4. Or bind the filter directly to the model** + +```php +class Post extends Model +{ + use HasFilterable; + + protected $filterable = PostFilter::class; +} + +// Now just: +$posts = Post::filter()->paginate(); ``` -Create your PostFilter using your preferred engine. +--- + +## Choosing an Engine + +Each engine is designed for a different filtering style. Pick the one that fits your use case — or mix and match across different models. + +| Engine | Best For | Example Request | +| -------------- | -------------------------------------------------- | ----------------------------------------------------- | +| **Invokable** | Custom logic per field, method-per-filter pattern | `?status=active&title=laravel` | +| **Ruleset** | Clean key/operator/value API queries | `?filter[title][like]=laravel&filter[views][gte]=100` | +| **Expression** | Ruleset-style + filtering through nested relations | `?filter[author.profile.name][like]=ahmed` | +| **Tree** | Complex AND/OR nested logic sent as JSON | `{ "and": [{ "field": "status", ... }] }` | + +### Invokable Engine + +Map request keys to methods automatically. Add PHP 8 annotations for per-method sanitization, casting, validation, and authorization with zero boilerplate. + +```php +class PostFilter extends Filterable +{ + protected $filters = ['status', 'created_at']; + + #[Cast('integer')] + #[DefaultValue(1)] + protected function status(Payload $payload) { ... } + + #[SkipIf('auth()->guest()')] + #[Between(min: '2020-01-01', max: 'now')] + protected function created_at(Payload $payload) { ... } +} +``` + +Available annotations: `#[Authorize]` `#[SkipIf]` `#[Cast]` `#[Sanitize]` `#[Trim]` `#[DefaultValue]` `#[MapValue]` `#[Explode]` `#[Required]` `#[In]` `#[Between]` `#[Regex]` `#[Scope]` + +### Ruleset Engine + +Flat field-operator-value format, ideal for REST APIs where the frontend controls which operator to use. + +``` +GET /posts?filter[status]=published +GET /posts?filter[title][like]=%laravel% +GET /posts?filter[views][gte]=100 +GET /posts?filter[id][in][]=1&filter[id][in][]=2 +``` + +Supported operators: `eq` `neq` `gt` `gte` `lt` `lte` `like` `nlike` `in` `between` + +### Expression Engine + +Everything Ruleset does, plus filtering through deep Eloquent relationships using dot notation. + +``` +GET /posts?filter[author.profile.name][like]=ahmed +``` + +```php +Filterable::create() + ->useEngine('expression') + ->allowedFields(['status', 'title']) + ->allowRelations(['author.profile' => ['name']]) + ->paginate(); +``` + +### Tree Engine + +Send a nested AND/OR JSON tree — the engine recursively translates it into Eloquent `where` / `orWhere` groups. + +```json +{ + "filter": { + "and": [ + { "field": "status", "operator": "eq", "value": "active" }, + { + "or": [ + { "field": "age", "operator": "gt", "value": 25 }, + { "field": "city", "operator": "eq", "value": "Cairo" } + ] + } + ] + } +} +``` + +Supports depth limiting, strict operator whitelisting, and normalized field keys. + +--- + +## Features + +### Caching + +A complete caching system built into the filter pipeline — not bolted on after the fact. + +```php +// Cache for 1 hour +Post::filter()->cache(3600)->get(); + +// User-scoped cache (each user gets their own) +Post::filter()->cache(1800)->scopeByUser()->get(); + +// Tenant-isolated cache +Product::filter()->cache(3600)->scopeByTenant(tenant()->id)->get(); + +// Conditional cache +Model::filter()->cacheWhen(!auth()->user()->isAdmin(), 3600)->get(); + +// Tagged cache with easy invalidation +Post::filter()->cache(3600)->cacheTags(['posts', 'content'])->get(); +Post::flushCacheByTagsStatic(['posts']); + +// Reusable profiles defined in config +Report::filter()->cacheProfile('heavy_reports')->get(); +``` + +Auto-invalidation: configure models and tags in `config/filterable.php` and caches are cleared automatically on create/update/delete. + +### Authorization + +Protect entire filter classes based on roles or permissions. + +```php +class AdminFilter extends Filterable +{ + public function authorize(): bool + { + return auth()->user()?->isSuperAdmin() ?? false; + } +} +``` + +Per-method authorization is also available via the `#[Authorize]` annotation in the Invokable engine. + +### Validation & Sanitization + +Validation rules and sanitizers are defined directly on the filter class — +input is cleaned and validated before any filtering logic runs. + +**Validation** uses Laravel's native rules format via a `$rules` property: + +```php +class PostFilter extends Filterable +{ + protected $rules = [ + 'status' => ['required', 'string', 'in:active,pending,archived'], + 'title' => ['sometimes', 'string', 'max:32'], + ]; +} +``` + +If validation fails, a `ValidationException` is thrown automatically — +no extra handling needed in your controller. + +**Sanitization** runs _before_ validation, via dedicated sanitizer classes: + +```php +class PostFilter extends Filterable +{ + protected $sanitizers = [ + TrimSanitizer::class, // global — applies to all fields + 'title' => [ + StripTagsSanitizer::class, + CapitalizeSanitizer::class, + ], + ]; +} +``` + +A sanitizer is a simple class implementing the `Sanitizable` interface: + +```php +class TrimSanitizer implements Sanitizable +{ + public function sanitize(mixed $value): mixed + { + return is_string($value) ? trim($value) : $value; + } +} +``` + +The execution order is always: **sanitize → validate → filter**. + +### Sorting + +Built-in sorting support with allowed-field whitelisting. + +```php +class PostFilter extends Filterable +{ + protected $sortable = ['created_at', 'views', 'title']; +} + +// GET /posts?sort=-created_at (descending) +// GET /posts?sort=views (ascending) +``` + +### Event System + +Hook into the filter lifecycle to add logging, metrics, or custom behavior. + +```php +// Fired before filters are applied +Event::listen(FilterApplying::class, fn($e) => Log::info('Filtering '.$e->model)); + +// Fired after filters are applied +Event::listen(FilterApplied::class, fn($e) => $metrics->record($e)); +``` + +### Profile Management & Profiler + +Save and reuse filter configurations, and inspect exactly what queries each filter generates. + +### Lifecycle Hooks + +`initially()` and `finally()` hooks let you modify the query builder before or after filtering runs. + +--- + +## CLI + +```bash +# Generate a new filter class with interactive setup +php artisan filterable:setup PostFilter + +# Discover and auto-register all filter classes in your app +php artisan filterable:discover + +# List all registered filters +php artisan filterable:list + +# Test a filter class with a sample data string (key=value pairs) +php artisan filterable:test {filter} --model=User --data="status=active,age=30" + +# Inspect a filter class (engines, fields, rules, etc.) +php artisan filterable:inspect PostFilter +``` + +--- + +## Requirements + +- PHP 8.1+ +- Laravel 10.x or higher +- Redis or Memcached recommended for tagged caching + +--- + +## 📚 Documentation + +For full documentation, installation, and usage examples, visit: **[kettasoft.github.io/filterable](https://kettasoft.github.io/filterable)** + +- [Introduction](https://kettasoft.github.io/filterable/introduction.html) +- [Engines Overview](https://kettasoft.github.io/filterable/engines/invokable/) +- [Caching System](https://kettasoft.github.io/filterable/caching/overview.html) +- [Authorization](https://kettasoft.github.io/filterable/authorization.html) +- [CLI Reference](https://kettasoft.github.io/filterable/cli/setup.html) +- [API Reference](https://kettasoft.github.io/filterable/api/filterable.html) + +--- + +## Contributing + +Found a bug or want to add an engine? PRs are welcome — please open an issue first to discuss. -License +## License -MIT © 2024-present Kettasoft +MIT © 2024-present [Kettasoft](https://github.com/kettasoft)