Repository files navigation

Filterable

About Filterable

Latest Version on PackagistTestsLintCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Filterable is a Laravel package for turning HTTP request parameters into rich, composable Eloquent query filters. The base Filter class exposes a stateful pipeline that you can extend, toggle, and compose with traits to add validation, caching, logging, rate limiting, memory management, and more. Everything is opt-in, so you enable only the behaviour you need while keeping type-safe, testable filters.

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Laravel 11.x, 12.x, or 13.x components (illuminate/cache, illuminate/contracts, illuminate/database, illuminate/http, illuminate/support)
  • A configured cache store when you enable caching features
  • A PSR-3 logger when you enable logging (optional)

Installation & Setup

composer require jerome/filterable

Package auto-discovery registers the FilterableServiceProvider, which contextual-binds the current Request into resolved filters and exposes the make:filter Artisan command. Publish the configuration to set global feature defaults, cache behaviour, or runtime options:

php artisan vendor:publish --tag=filterable-config

Stubs live under src/Filterable/Console/stubs/ and can be overridden by placing copies in your application's stubs directory.

Highlights

  • Publishable configuration (config/filterable.php) to set default feature bundles, runtime options, and cache TTLs that the base filter reads during construction.
  • Stateful lifecycle with apply, get, runQuery, reset, rich debug output via getDebugInfo(), lifecycle events (FilterApplying, FilterApplied, FilterFailed), and configurable exception handling.
  • Opt-in concerns for validation, permissions, rate limiting, caching (with heuristics), logging, performance metrics, query optimisation, memory management, value transformation, and fluent filter chaining.
  • Drop-in Filterable Eloquent scope trait so any model can accept a filter instance.
  • Smart caching that builds deterministic cache keys, supports tags, memoises counts, and can decide automatically when to cache complex queries.
  • Contextual binding in FilterableServiceProvider makes sure container-resolved filters receive the current HTTP Request; injecting a cache repository or PSR-3 logger auto-enables the relevant features.
  • Memory-friendly helpers (lazy, stream, streamGenerator, lazyEach, cursor, chunk, map, filter, reduce) when the memoryManagement feature is enabled.
  • First-party Artisan generator with --basic, --model, and --force options to rapidly scaffold filters.

Repository Layout

  • src/Filterable/Filter.php – abstract base class orchestrating the filter lifecycle and feature toggles.
  • src/Filterable/Concerns/ – traits implementing discrete behaviour (filter discovery, validation, caching, logging, performance, optimisation, rate limiting, etc.).
  • src/Filterable/Contracts/ – interfaces for the filter pipeline and the Eloquent scope signature.
  • src/Filterable/Traits/Filterable.php – model scope that forwards to a Filter instance.
  • src/Filterable/Console/MakeFilterCommand.php & src/Filterable/Console/stubs/ – Artisan generator and overrideable stub templates.
  • src/Filterable/Providers/FilterableServiceProvider.php – registers the package and console command via spatie/laravel-package-tools.
  • bin/ – executable scripts executed by the Composer lint, fix, and test commands.
  • tests/ – Orchestra Testbench suite with concern-focused tests and reusable fixtures in tests/Fixtures/.
  • assets/ – shared media used in documentation.
  • config/filterable.php – publishable defaults for feature toggles, cache TTL, and runtime options.
  • database/factories/ – reserved for additional factories should you extend the package.

Quick Start

1. Generate a filter

php artisan make:filter PostFilter --model=Post

--model wires the stub to your Eloquent model. Use --basic for an empty shell or --force to overwrite an existing class.

2. Implement filtering logic

<?phpnamespaceApp\Filters;
useFilterable\Filter;
useIlluminate\Database\Eloquent\Builder;
useIlluminate\Http\Request;
useIlluminate\Support\Carbon;
useIlluminate\Validation\Rule;
class PostFilter extends Filter
{
/** * Request keys that map straight to filter methods. * * Methods follow camelCased versions of the keys (e.g. published_after β†’ publishedAfter). */protectedarray$filters = ['status', 'published_after', 'q'];
publicfunction__construct(Request$request)
{
parent::__construct($request);
$this->enableFeatures([
'validation',
'optimization',
'filterChaining',
'valueTransformation',
]);
$this->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'published_after' => ['nullable', 'date'],
]);
$this->registerTransformer('published_after', fn ($value) => Carbon::parse($value));
$this->registerPreFilters(fn (Builder$query) => $query->where('is_visible', true));
$this->select(['id', 'title', 'status', 'published_at'])->with('author');
}
protectedfunctionstatus(string$value): void
{
$this->getBuilder()->where('status', $value);
}
protectedfunctionpublishedAfter(Carbon$date): void
{
$this->getBuilder()->whereDate('published_at', '>=', $date);
}
protectedfunctionq(string$term): void
{
$this->getBuilder()->where(function (Builder$query) use ($term) {
$query->where('title', 'like', "%{$term}%")
->orWhere('body', 'like', "%{$term}%");
});
}
}

Define protected array $filterMethodMap when you need to alias request keys to method names. Programmatic filters can be appended with appendFilterable('key', $value) before apply() runs. Supplying an Illuminate\Contracts\Cache\Repository or Psr\Log\LoggerInterface to the constructor immediately enables the caching and logging features.

3. Attach the scope to a model

<?phpnamespaceApp\Models;
useFilterable\Traits\Filterable;
useIlluminate\Database\Eloquent\Model;
class Post extends Model
{
use Filterable;
}

4. Run the filter pipeline

<?phpnamespaceApp\Http\Controllers;
useApp\Filters\PostFilter;
useApp\Http\Resources\PostResource;
useApp\Models\Post;
useIlluminate\Http\Request;
class PostController
{
publicfunctionindex(Request$request, PostFilter$filter)
{
$posts = Post::query()
->filter(
$filter
->forUser($request->user())
->enableFeature('caching')
->setOptions(['chunk_size' => 500])
)
->get();
return PostResource::collection($posts);
}
}

apply() may only be called once per instance; call reset() if you need to reuse a filter. Because the Filter base class uses Laravel's Conditionable trait, you can use helpers such as $filter->when($request->boolean('validate'), fn ($filter) => $filter->enableFeature('validation'));.

Lifecycle & Core API

  • apply(Builder $builder, ?array $options = []) binds the filter to a query, merges options, runs enabled concerns, and transitions the state from initialized β†’ applying β†’ applied. Re-applying without reset() raises a RuntimeException.
  • get() returns an Illuminate\Support\Collection of results, delegating to caching or memory-managed helpers when those features are active. runQuery() is a convenience wrapper for apply() + get().
  • count() respects smart caching (including tagged caches and memoised counts when enabled). toSql() exposes the raw SQL for debugging.
  • enableFeature(), enableFeatures(), disableFeature(), hasFeature() toggle concerns per instance; defaults may be set in config/filterable.php and are applied in the constructor.
  • setOption(), setOptions() persist runtime flags (for example chunk_size, use_chunking) that concerns such as OptimizesQueries and ManagesMemory consume.
  • reset() returns the filter to the initialized state so it can be applied again. getDebugInfo() surfaces state, filters applied, options, SQL/bindings, and metrics.

Feature Guides & API

Validation & Value Transformation

  • Enable with enableFeature('validation') and configure with setValidationRules(), addValidationRule(), and setValidationMessages(). Only active filters are validated and ValidationException is rethrown.
  • Enable valueTransformation to normalise inputs before filter methods execute. Register per-key transformers with registerTransformer() or bulk-array transforms with transformArray().
$filter->enableFeatures(['validation', 'valueTransformation'])
->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'tags' => ['array'],
])
->registerTransformer('tags', fn ($value) => array_map('intval', (array) $value));

Permissions & User Scope

  • forUser($user) scopes queries to the authenticated identifier and folds that identifier into cache keys automatically.
  • Enable permissions and declare requirements with setFilterPermissions(). Override userHasPermission() in your filter to plug into your authorisation layer; disallowed filters are dropped (and optionally logged) before execution.
$filter->enableFeature('permissions')
->forUser($request->user())
->setFilterPermissions(['email' => 'view-sensitive-fields']);

Rate Limiting

  • Enable with enableFeature('rateLimit'). Defaults allow 10 filters, a complexity budget of 100, and 60 attempts within a 60-second window; decay is ceil(complexity/10) seconds.
  • Tune guardrails with setMaxFilters(), setMaxComplexity(), and setFilterComplexity() (array-valued filters multiply complexity). Override resolveRateLimitMaxAttempts(), resolveRateLimitWindowSeconds(), or resolveRateLimitDecaySeconds() for finer control.
$filter->enableFeature('rateLimit')
->setMaxFilters(5)
->setMaxComplexity(25)
->setFilterComplexity(['tags' => 3, 'q' => 2]);

Caching & SmartCaching

  • Inject an Illuminate\Contracts\Cache\Repository or call enableFeature('caching') to activate caching. TTL defaults to 5 minutes or config('filterable.defaults.cache.ttl'); override per instance with setCacheExpiration().
  • Opt into result or count caching via cacheResults() / cacheCount(), and scope invalidation with cacheTags(), clearCache(), and clearRelatedCaches(). Cache keys include sanitised filter values and optional user identifiers from forUser().
  • SmartCaching will automatically cache more complex queries (multiple where clauses, joins, select statements) when caching is enabled, while skipping trivial single-clause lookups.
$filter->enableFeature('caching')
->cacheTags(['posts'])
->cacheResults()
->cacheCount()
->setCacheExpiration(15);
$posts = Post::query()->filter($filter)->get();
$total = $filter->count();

Logging & Performance Metrics

  • Inject a PSR-3 logger or call setLogger() + enableFeature('logging') to emit structured lifecycle logs. Hooks such as applyFilterable and cache-building log automatically when logging is active.
  • Enable performance to measure execution time, memory usage, and filter count; extend with addMetric() and read via getMetrics() / getExecutionTime().

Query Optimisation & Filter Chaining

  • Enable optimization to apply select(), with(), and chunkSize() before filters run; useIndex() can hint MySQL indexes when appropriate.
  • Enable filterChaining to queue fluent additions after request-driven filters: where(), whereIn(), whereNotIn(), whereBetween(), and orderBy() are supported.
$filter->enableFeatures(['optimization', 'filterChaining'])
->select(['id', 'title', 'status'])
->with(['author', 'tags'])
->chunkSize(500)
->where('status', 'published')
->orderBy('published_at', 'desc');

Memory Management

  • Enable memoryManagement for streaming helpers that avoid loading whole result sets into memory: lazy(), lazyEach(), cursor(), stream(), streamGenerator(), chunk(), map(), filter(), reduce().
  • executeQueryWithMemoryManagement() underpins get() when chunk_size is set; resolveChunkSize() honours chunk_size options or provided arguments. Call apply() before streaming helpers; misuse raises a RuntimeException.
$filter->enableFeature('memoryManagement')
->setOption('chunk_size', 250);
$filter->apply(Post::query());
$filter->lazyEach(fn ($post) => /* ... */, 250);

Pre-Filters & Manual Filters

  • Register global constraints with registerPreFilters(); they run before request-driven filters and are logged when logging is enabled.
  • Add programmatic filter values with appendFilterable(), or alias request keys to method names via protected array $filterMethodMap on your filter class. asCollectionFilter() returns a callable compatible with collection pipelines when you want to reuse filterables outside of Eloquent.

Debugging & Events

  • getDebugInfo() returns state, enabled features, options, SQL, bindings, and (when performance is enabled) metrics. Override handleFilteringException() to decide whether to swallow or rethrow non-validation errors.
  • Listen for FilterApplying, FilterApplied, and FilterFailed events around apply() to hook telemetry, notifications, or side effects.

Configuration

The publishable config/filterable.php controls defaults applied during filter construction:

return [
'defaults' => [
'features' => [
'validation' => false,
'permissions' => false,
'rateLimit' => false,
'caching' => false,
'logging' => false,
'performance' => false,
'optimization' => false,
'memoryManagement' => false,
'filterChaining' => false,
'valueTransformation' => false,
],
'options' => [/* runtime options seeded here */],
'cache' => ['ttl' => null],
],
];

Per-filter overrides always winβ€”call enableFeature(), disableFeature(), setOption(), or setCacheExpiration() inside individual filters when you need different defaults.

Artisan Generator & Stubs

php artisan make:filter scaffolds a filter class under App\Filters by default:

  • --basic emits a minimal filter without feature toggles.
  • --model=User imports the model and pre-fills a typed constructor parameter.
  • --force overwrites an existing class.

Publish customised stubs by copying src/Filterable/Console/stubs/ into your application's stubs/ directory; the command prefers application stubs when present.

Tooling & Scripts

Package maintenance scripts live in bin/ and are surfaced through Composer:

composer lint # Runs Tighten Duster lint mode + PHP syntax checks
composer fix # Formats with Duster and writes a timestamped log
composer test# Executes PHPUnit via bin/test.sh

./bin/test.sh accepts --filter=ClassName, --test=tests/FeatureTest.php, --coverage, and --parallel. ./bin/lint.sh --strict exits non-zero when any issue is detected.

Testing

The PHPUnit suite runs on Orchestra Testbench (phpunit.xml.dist). tests/TestCase.php provisions an in-memory sqlite schema (mocks table) and aliases factories under tests/Fixtures/. Each concern has a dedicated test file (for example CachingTest.php, ManagesMemoryTest.php) with partial mocks and fixtures such as MockFilterable, MockFilterableFactory, and TestFilter. End-to-end behaviour is exercised in tests/Integration/, which boots the full filter pipeline (feature defaults, caching, streaming, lifecycle events) against the in-memory database.

Run targeted subsets with:

./bin/test.sh --filter=SupportsFilterChainingTest
./bin/test.sh --test=tests/HandlesRateLimitingTest.php

Add new integration doubles under tests/Fixtures/ to stay aligned with the existing autoloading.

Frontend Usage

Send filter parameters as query strings from your clients:

awaitfetch('/posts?status=active&category_id=2');awaitfetch('/posts?tags[]=laravel&tags[]=performance&sort_by=created_at:desc');

Contributing

Please review AGENTS.md for contributor expectations around structure, tooling, and workflow. When ready:

  1. Fork the repository and create a feature branch (git checkout -b feature/my-change).
  2. Run composer lint and composer test (or ./bin/test.sh --coverage) before opening a PR.
  3. Describe the capabilities touched, newly exposed options, and verification commands in the pull request body.

License

This project is open-sourced under the MIT license. See LICENSE for the full text.

Authors

See contributors for the full list of collaborators.

Acknowledgements

Inspired by the flexibility of spatie/laravel-query-builder and Tighten's duster tooling.

About

πŸ” Enhance Laravel queries with adaptable, customisable filters and intelligent caching to improve both performance and functionality.

Topics

Resources

Security policy

Stars

195 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Filterable

About Filterable

Latest Version on PackagistTestsLintCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Filterable is a Laravel package for turning HTTP request parameters into rich, composable Eloquent query filters. The base Filter class exposes a stateful pipeline that you can extend, toggle, and compose with traits to add validation, caching, logging, rate limiting, memory management, and more. Everything is opt-in, so you enable only the behaviour you need while keeping type-safe, testable filters.

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Laravel 11.x, 12.x, or 13.x components (illuminate/cache, illuminate/contracts, illuminate/database, illuminate/http, illuminate/support)
  • A configured cache store when you enable caching features
  • A PSR-3 logger when you enable logging (optional)

Installation & Setup

composer require jerome/filterable

Package auto-discovery registers the FilterableServiceProvider, which contextual-binds the current Request into resolved filters and exposes the make:filter Artisan command. Publish the configuration to set global feature defaults, cache behaviour, or runtime options:

php artisan vendor:publish --tag=filterable-config

Stubs live under src/Filterable/Console/stubs/ and can be overridden by placing copies in your application's stubs directory.

Highlights

  • Publishable configuration (config/filterable.php) to set default feature bundles, runtime options, and cache TTLs that the base filter reads during construction.
  • Stateful lifecycle with apply, get, runQuery, reset, rich debug output via getDebugInfo(), lifecycle events (FilterApplying, FilterApplied, FilterFailed), and configurable exception handling.
  • Opt-in concerns for validation, permissions, rate limiting, caching (with heuristics), logging, performance metrics, query optimisation, memory management, value transformation, and fluent filter chaining.
  • Drop-in Filterable Eloquent scope trait so any model can accept a filter instance.
  • Smart caching that builds deterministic cache keys, supports tags, memoises counts, and can decide automatically when to cache complex queries.
  • Contextual binding in FilterableServiceProvider makes sure container-resolved filters receive the current HTTP Request; injecting a cache repository or PSR-3 logger auto-enables the relevant features.
  • Memory-friendly helpers (lazy, stream, streamGenerator, lazyEach, cursor, chunk, map, filter, reduce) when the memoryManagement feature is enabled.
  • First-party Artisan generator with --basic, --model, and --force options to rapidly scaffold filters.

Repository Layout

  • src/Filterable/Filter.php – abstract base class orchestrating the filter lifecycle and feature toggles.
  • src/Filterable/Concerns/ – traits implementing discrete behaviour (filter discovery, validation, caching, logging, performance, optimisation, rate limiting, etc.).
  • src/Filterable/Contracts/ – interfaces for the filter pipeline and the Eloquent scope signature.
  • src/Filterable/Traits/Filterable.php – model scope that forwards to a Filter instance.
  • src/Filterable/Console/MakeFilterCommand.php & src/Filterable/Console/stubs/ – Artisan generator and overrideable stub templates.
  • src/Filterable/Providers/FilterableServiceProvider.php – registers the package and console command via spatie/laravel-package-tools.
  • bin/ – executable scripts executed by the Composer lint, fix, and test commands.
  • tests/ – Orchestra Testbench suite with concern-focused tests and reusable fixtures in tests/Fixtures/.
  • assets/ – shared media used in documentation.
  • config/filterable.php – publishable defaults for feature toggles, cache TTL, and runtime options.
  • database/factories/ – reserved for additional factories should you extend the package.

Quick Start

1. Generate a filter

php artisan make:filter PostFilter --model=Post

--model wires the stub to your Eloquent model. Use --basic for an empty shell or --force to overwrite an existing class.

2. Implement filtering logic

<?phpnamespaceApp\Filters;
useFilterable\Filter;
useIlluminate\Database\Eloquent\Builder;
useIlluminate\Http\Request;
useIlluminate\Support\Carbon;
useIlluminate\Validation\Rule;
class PostFilter extends Filter
{
/** * Request keys that map straight to filter methods. * * Methods follow camelCased versions of the keys (e.g. published_after β†’ publishedAfter). */protectedarray$filters = ['status', 'published_after', 'q'];
publicfunction__construct(Request$request)
{
parent::__construct($request);
$this->enableFeatures([
'validation',
'optimization',
'filterChaining',
'valueTransformation',
]);
$this->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'published_after' => ['nullable', 'date'],
]);
$this->registerTransformer('published_after', fn ($value) => Carbon::parse($value));
$this->registerPreFilters(fn (Builder$query) => $query->where('is_visible', true));
$this->select(['id', 'title', 'status', 'published_at'])->with('author');
}
protectedfunctionstatus(string$value): void
{
$this->getBuilder()->where('status', $value);
}
protectedfunctionpublishedAfter(Carbon$date): void
{
$this->getBuilder()->whereDate('published_at', '>=', $date);
}
protectedfunctionq(string$term): void
{
$this->getBuilder()->where(function (Builder$query) use ($term) {
$query->where('title', 'like', "%{$term}%")
->orWhere('body', 'like', "%{$term}%");
});
}
}

Define protected array $filterMethodMap when you need to alias request keys to method names. Programmatic filters can be appended with appendFilterable('key', $value) before apply() runs. Supplying an Illuminate\Contracts\Cache\Repository or Psr\Log\LoggerInterface to the constructor immediately enables the caching and logging features.

3. Attach the scope to a model

<?phpnamespaceApp\Models;
useFilterable\Traits\Filterable;
useIlluminate\Database\Eloquent\Model;
class Post extends Model
{
use Filterable;
}

4. Run the filter pipeline

<?phpnamespaceApp\Http\Controllers;
useApp\Filters\PostFilter;
useApp\Http\Resources\PostResource;
useApp\Models\Post;
useIlluminate\Http\Request;
class PostController
{
publicfunctionindex(Request$request, PostFilter$filter)
{
$posts = Post::query()
->filter(
$filter
->forUser($request->user())
->enableFeature('caching')
->setOptions(['chunk_size' => 500])
)
->get();
return PostResource::collection($posts);
}
}

apply() may only be called once per instance; call reset() if you need to reuse a filter. Because the Filter base class uses Laravel's Conditionable trait, you can use helpers such as $filter->when($request->boolean('validate'), fn ($filter) => $filter->enableFeature('validation'));.

Lifecycle & Core API

  • apply(Builder $builder, ?array $options = []) binds the filter to a query, merges options, runs enabled concerns, and transitions the state from initialized β†’ applying β†’ applied. Re-applying without reset() raises a RuntimeException.
  • get() returns an Illuminate\Support\Collection of results, delegating to caching or memory-managed helpers when those features are active. runQuery() is a convenience wrapper for apply() + get().
  • count() respects smart caching (including tagged caches and memoised counts when enabled). toSql() exposes the raw SQL for debugging.
  • enableFeature(), enableFeatures(), disableFeature(), hasFeature() toggle concerns per instance; defaults may be set in config/filterable.php and are applied in the constructor.
  • setOption(), setOptions() persist runtime flags (for example chunk_size, use_chunking) that concerns such as OptimizesQueries and ManagesMemory consume.
  • reset() returns the filter to the initialized state so it can be applied again. getDebugInfo() surfaces state, filters applied, options, SQL/bindings, and metrics.

Feature Guides & API

Validation & Value Transformation

  • Enable with enableFeature('validation') and configure with setValidationRules(), addValidationRule(), and setValidationMessages(). Only active filters are validated and ValidationException is rethrown.
  • Enable valueTransformation to normalise inputs before filter methods execute. Register per-key transformers with registerTransformer() or bulk-array transforms with transformArray().
$filter->enableFeatures(['validation', 'valueTransformation'])
->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'tags' => ['array'],
])
->registerTransformer('tags', fn ($value) => array_map('intval', (array) $value));

Permissions & User Scope

  • forUser($user) scopes queries to the authenticated identifier and folds that identifier into cache keys automatically.
  • Enable permissions and declare requirements with setFilterPermissions(). Override userHasPermission() in your filter to plug into your authorisation layer; disallowed filters are dropped (and optionally logged) before execution.
$filter->enableFeature('permissions')
->forUser($request->user())
->setFilterPermissions(['email' => 'view-sensitive-fields']);

Rate Limiting

  • Enable with enableFeature('rateLimit'). Defaults allow 10 filters, a complexity budget of 100, and 60 attempts within a 60-second window; decay is ceil(complexity/10) seconds.
  • Tune guardrails with setMaxFilters(), setMaxComplexity(), and setFilterComplexity() (array-valued filters multiply complexity). Override resolveRateLimitMaxAttempts(), resolveRateLimitWindowSeconds(), or resolveRateLimitDecaySeconds() for finer control.
$filter->enableFeature('rateLimit')
->setMaxFilters(5)
->setMaxComplexity(25)
->setFilterComplexity(['tags' => 3, 'q' => 2]);

Caching & SmartCaching

  • Inject an Illuminate\Contracts\Cache\Repository or call enableFeature('caching') to activate caching. TTL defaults to 5 minutes or config('filterable.defaults.cache.ttl'); override per instance with setCacheExpiration().
  • Opt into result or count caching via cacheResults() / cacheCount(), and scope invalidation with cacheTags(), clearCache(), and clearRelatedCaches(). Cache keys include sanitised filter values and optional user identifiers from forUser().
  • SmartCaching will automatically cache more complex queries (multiple where clauses, joins, select statements) when caching is enabled, while skipping trivial single-clause lookups.
$filter->enableFeature('caching')
->cacheTags(['posts'])
->cacheResults()
->cacheCount()
->setCacheExpiration(15);
$posts = Post::query()->filter($filter)->get();
$total = $filter->count();

Logging & Performance Metrics

  • Inject a PSR-3 logger or call setLogger() + enableFeature('logging') to emit structured lifecycle logs. Hooks such as applyFilterable and cache-building log automatically when logging is active.
  • Enable performance to measure execution time, memory usage, and filter count; extend with addMetric() and read via getMetrics() / getExecutionTime().

Query Optimisation & Filter Chaining

  • Enable optimization to apply select(), with(), and chunkSize() before filters run; useIndex() can hint MySQL indexes when appropriate.
  • Enable filterChaining to queue fluent additions after request-driven filters: where(), whereIn(), whereNotIn(), whereBetween(), and orderBy() are supported.
$filter->enableFeatures(['optimization', 'filterChaining'])
->select(['id', 'title', 'status'])
->with(['author', 'tags'])
->chunkSize(500)
->where('status', 'published')
->orderBy('published_at', 'desc');

Memory Management

  • Enable memoryManagement for streaming helpers that avoid loading whole result sets into memory: lazy(), lazyEach(), cursor(), stream(), streamGenerator(), chunk(), map(), filter(), reduce().
  • executeQueryWithMemoryManagement() underpins get() when chunk_size is set; resolveChunkSize() honours chunk_size options or provided arguments. Call apply() before streaming helpers; misuse raises a RuntimeException.
$filter->enableFeature('memoryManagement')
->setOption('chunk_size', 250);
$filter->apply(Post::query());
$filter->lazyEach(fn ($post) => /* ... */, 250);

Pre-Filters & Manual Filters

  • Register global constraints with registerPreFilters(); they run before request-driven filters and are logged when logging is enabled.
  • Add programmatic filter values with appendFilterable(), or alias request keys to method names via protected array $filterMethodMap on your filter class. asCollectionFilter() returns a callable compatible with collection pipelines when you want to reuse filterables outside of Eloquent.

Debugging & Events

  • getDebugInfo() returns state, enabled features, options, SQL, bindings, and (when performance is enabled) metrics. Override handleFilteringException() to decide whether to swallow or rethrow non-validation errors.
  • Listen for FilterApplying, FilterApplied, and FilterFailed events around apply() to hook telemetry, notifications, or side effects.

Configuration

The publishable config/filterable.php controls defaults applied during filter construction:

return [
'defaults' => [
'features' => [
'validation' => false,
'permissions' => false,
'rateLimit' => false,
'caching' => false,
'logging' => false,
'performance' => false,
'optimization' => false,
'memoryManagement' => false,
'filterChaining' => false,
'valueTransformation' => false,
],
'options' => [/* runtime options seeded here */],
'cache' => ['ttl' => null],
],
];

Per-filter overrides always winβ€”call enableFeature(), disableFeature(), setOption(), or setCacheExpiration() inside individual filters when you need different defaults.

Artisan Generator & Stubs

php artisan make:filter scaffolds a filter class under App\Filters by default:

  • --basic emits a minimal filter without feature toggles.
  • --model=User imports the model and pre-fills a typed constructor parameter.
  • --force overwrites an existing class.

Publish customised stubs by copying src/Filterable/Console/stubs/ into your application's stubs/ directory; the command prefers application stubs when present.

Tooling & Scripts

Package maintenance scripts live in bin/ and are surfaced through Composer:

composer lint # Runs Tighten Duster lint mode + PHP syntax checks
composer fix # Formats with Duster and writes a timestamped log
composer test# Executes PHPUnit via bin/test.sh

./bin/test.sh accepts --filter=ClassName, --test=tests/FeatureTest.php, --coverage, and --parallel. ./bin/lint.sh --strict exits non-zero when any issue is detected.

Testing

The PHPUnit suite runs on Orchestra Testbench (phpunit.xml.dist). tests/TestCase.php provisions an in-memory sqlite schema (mocks table) and aliases factories under tests/Fixtures/. Each concern has a dedicated test file (for example CachingTest.php, ManagesMemoryTest.php) with partial mocks and fixtures such as MockFilterable, MockFilterableFactory, and TestFilter. End-to-end behaviour is exercised in tests/Integration/, which boots the full filter pipeline (feature defaults, caching, streaming, lifecycle events) against the in-memory database.

Run targeted subsets with:

./bin/test.sh --filter=SupportsFilterChainingTest
./bin/test.sh --test=tests/HandlesRateLimitingTest.php

Add new integration doubles under tests/Fixtures/ to stay aligned with the existing autoloading.

Frontend Usage

Send filter parameters as query strings from your clients:

awaitfetch('/posts?status=active&category_id=2');awaitfetch('/posts?tags[]=laravel&tags[]=performance&sort_by=created_at:desc');

Contributing

Please review AGENTS.md for contributor expectations around structure, tooling, and workflow. When ready:

  1. Fork the repository and create a feature branch (git checkout -b feature/my-change).
  2. Run composer lint and composer test (or ./bin/test.sh --coverage) before opening a PR.
  3. Describe the capabilities touched, newly exposed options, and verification commands in the pull request body.

License

This project is open-sourced under the MIT license. See LICENSE for the full text.

Authors

See contributors for the full list of collaborators.

Acknowledgements

Inspired by the flexibility of spatie/laravel-query-builder and Tighten's duster tooling.

About

πŸ” Enhance Laravel queries with adaptable, customisable filters and intelligent caching to improve both performance and functionality.

Topics

Resources

Security policy

Stars

195 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Filterable

About Filterable

Latest Version on PackagistTestsLintCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Filterable is a Laravel package for turning HTTP request parameters into rich, composable Eloquent query filters. The base Filter class exposes a stateful pipeline that you can extend, toggle, and compose with traits to add validation, caching, logging, rate limiting, memory management, and more. Everything is opt-in, so you enable only the behaviour you need while keeping type-safe, testable filters.

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Laravel 11.x, 12.x, or 13.x components (illuminate/cache, illuminate/contracts, illuminate/database, illuminate/http, illuminate/support)
  • A configured cache store when you enable caching features
  • A PSR-3 logger when you enable logging (optional)

Installation & Setup

composer require jerome/filterable

Package auto-discovery registers the FilterableServiceProvider, which contextual-binds the current Request into resolved filters and exposes the make:filter Artisan command. Publish the configuration to set global feature defaults, cache behaviour, or runtime options:

php artisan vendor:publish --tag=filterable-config

Stubs live under src/Filterable/Console/stubs/ and can be overridden by placing copies in your application's stubs directory.

Highlights

  • Publishable configuration (config/filterable.php) to set default feature bundles, runtime options, and cache TTLs that the base filter reads during construction.
  • Stateful lifecycle with apply, get, runQuery, reset, rich debug output via getDebugInfo(), lifecycle events (FilterApplying, FilterApplied, FilterFailed), and configurable exception handling.
  • Opt-in concerns for validation, permissions, rate limiting, caching (with heuristics), logging, performance metrics, query optimisation, memory management, value transformation, and fluent filter chaining.
  • Drop-in Filterable Eloquent scope trait so any model can accept a filter instance.
  • Smart caching that builds deterministic cache keys, supports tags, memoises counts, and can decide automatically when to cache complex queries.
  • Contextual binding in FilterableServiceProvider makes sure container-resolved filters receive the current HTTP Request; injecting a cache repository or PSR-3 logger auto-enables the relevant features.
  • Memory-friendly helpers (lazy, stream, streamGenerator, lazyEach, cursor, chunk, map, filter, reduce) when the memoryManagement feature is enabled.
  • First-party Artisan generator with --basic, --model, and --force options to rapidly scaffold filters.

Repository Layout

  • src/Filterable/Filter.php – abstract base class orchestrating the filter lifecycle and feature toggles.
  • src/Filterable/Concerns/ – traits implementing discrete behaviour (filter discovery, validation, caching, logging, performance, optimisation, rate limiting, etc.).
  • src/Filterable/Contracts/ – interfaces for the filter pipeline and the Eloquent scope signature.
  • src/Filterable/Traits/Filterable.php – model scope that forwards to a Filter instance.
  • src/Filterable/Console/MakeFilterCommand.php & src/Filterable/Console/stubs/ – Artisan generator and overrideable stub templates.
  • src/Filterable/Providers/FilterableServiceProvider.php – registers the package and console command via spatie/laravel-package-tools.
  • bin/ – executable scripts executed by the Composer lint, fix, and test commands.
  • tests/ – Orchestra Testbench suite with concern-focused tests and reusable fixtures in tests/Fixtures/.
  • assets/ – shared media used in documentation.
  • config/filterable.php – publishable defaults for feature toggles, cache TTL, and runtime options.
  • database/factories/ – reserved for additional factories should you extend the package.

Quick Start

1. Generate a filter

php artisan make:filter PostFilter --model=Post

--model wires the stub to your Eloquent model. Use --basic for an empty shell or --force to overwrite an existing class.

2. Implement filtering logic

<?phpnamespaceApp\Filters;
useFilterable\Filter;
useIlluminate\Database\Eloquent\Builder;
useIlluminate\Http\Request;
useIlluminate\Support\Carbon;
useIlluminate\Validation\Rule;
class PostFilter extends Filter
{
/** * Request keys that map straight to filter methods. * * Methods follow camelCased versions of the keys (e.g. published_after β†’ publishedAfter). */protectedarray$filters = ['status', 'published_after', 'q'];
publicfunction__construct(Request$request)
{
parent::__construct($request);
$this->enableFeatures([
'validation',
'optimization',
'filterChaining',
'valueTransformation',
]);
$this->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'published_after' => ['nullable', 'date'],
]);
$this->registerTransformer('published_after', fn ($value) => Carbon::parse($value));
$this->registerPreFilters(fn (Builder$query) => $query->where('is_visible', true));
$this->select(['id', 'title', 'status', 'published_at'])->with('author');
}
protectedfunctionstatus(string$value): void
{
$this->getBuilder()->where('status', $value);
}
protectedfunctionpublishedAfter(Carbon$date): void
{
$this->getBuilder()->whereDate('published_at', '>=', $date);
}
protectedfunctionq(string$term): void
{
$this->getBuilder()->where(function (Builder$query) use ($term) {
$query->where('title', 'like', "%{$term}%")
->orWhere('body', 'like', "%{$term}%");
});
}
}

Define protected array $filterMethodMap when you need to alias request keys to method names. Programmatic filters can be appended with appendFilterable('key', $value) before apply() runs. Supplying an Illuminate\Contracts\Cache\Repository or Psr\Log\LoggerInterface to the constructor immediately enables the caching and logging features.

3. Attach the scope to a model

<?phpnamespaceApp\Models;
useFilterable\Traits\Filterable;
useIlluminate\Database\Eloquent\Model;
class Post extends Model
{
use Filterable;
}

4. Run the filter pipeline

<?phpnamespaceApp\Http\Controllers;
useApp\Filters\PostFilter;
useApp\Http\Resources\PostResource;
useApp\Models\Post;
useIlluminate\Http\Request;
class PostController
{
publicfunctionindex(Request$request, PostFilter$filter)
{
$posts = Post::query()
->filter(
$filter
->forUser($request->user())
->enableFeature('caching')
->setOptions(['chunk_size' => 500])
)
->get();
return PostResource::collection($posts);
}
}

apply() may only be called once per instance; call reset() if you need to reuse a filter. Because the Filter base class uses Laravel's Conditionable trait, you can use helpers such as $filter->when($request->boolean('validate'), fn ($filter) => $filter->enableFeature('validation'));.

Lifecycle & Core API

  • apply(Builder $builder, ?array $options = []) binds the filter to a query, merges options, runs enabled concerns, and transitions the state from initialized β†’ applying β†’ applied. Re-applying without reset() raises a RuntimeException.
  • get() returns an Illuminate\Support\Collection of results, delegating to caching or memory-managed helpers when those features are active. runQuery() is a convenience wrapper for apply() + get().
  • count() respects smart caching (including tagged caches and memoised counts when enabled). toSql() exposes the raw SQL for debugging.
  • enableFeature(), enableFeatures(), disableFeature(), hasFeature() toggle concerns per instance; defaults may be set in config/filterable.php and are applied in the constructor.
  • setOption(), setOptions() persist runtime flags (for example chunk_size, use_chunking) that concerns such as OptimizesQueries and ManagesMemory consume.
  • reset() returns the filter to the initialized state so it can be applied again. getDebugInfo() surfaces state, filters applied, options, SQL/bindings, and metrics.

Feature Guides & API

Validation & Value Transformation

  • Enable with enableFeature('validation') and configure with setValidationRules(), addValidationRule(), and setValidationMessages(). Only active filters are validated and ValidationException is rethrown.
  • Enable valueTransformation to normalise inputs before filter methods execute. Register per-key transformers with registerTransformer() or bulk-array transforms with transformArray().
$filter->enableFeatures(['validation', 'valueTransformation'])
->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'tags' => ['array'],
])
->registerTransformer('tags', fn ($value) => array_map('intval', (array) $value));

Permissions & User Scope

  • forUser($user) scopes queries to the authenticated identifier and folds that identifier into cache keys automatically.
  • Enable permissions and declare requirements with setFilterPermissions(). Override userHasPermission() in your filter to plug into your authorisation layer; disallowed filters are dropped (and optionally logged) before execution.
$filter->enableFeature('permissions')
->forUser($request->user())
->setFilterPermissions(['email' => 'view-sensitive-fields']);

Rate Limiting

  • Enable with enableFeature('rateLimit'). Defaults allow 10 filters, a complexity budget of 100, and 60 attempts within a 60-second window; decay is ceil(complexity/10) seconds.
  • Tune guardrails with setMaxFilters(), setMaxComplexity(), and setFilterComplexity() (array-valued filters multiply complexity). Override resolveRateLimitMaxAttempts(), resolveRateLimitWindowSeconds(), or resolveRateLimitDecaySeconds() for finer control.
$filter->enableFeature('rateLimit')
->setMaxFilters(5)
->setMaxComplexity(25)
->setFilterComplexity(['tags' => 3, 'q' => 2]);

Caching & SmartCaching

  • Inject an Illuminate\Contracts\Cache\Repository or call enableFeature('caching') to activate caching. TTL defaults to 5 minutes or config('filterable.defaults.cache.ttl'); override per instance with setCacheExpiration().
  • Opt into result or count caching via cacheResults() / cacheCount(), and scope invalidation with cacheTags(), clearCache(), and clearRelatedCaches(). Cache keys include sanitised filter values and optional user identifiers from forUser().
  • SmartCaching will automatically cache more complex queries (multiple where clauses, joins, select statements) when caching is enabled, while skipping trivial single-clause lookups.
$filter->enableFeature('caching')
->cacheTags(['posts'])
->cacheResults()
->cacheCount()
->setCacheExpiration(15);
$posts = Post::query()->filter($filter)->get();
$total = $filter->count();

Logging & Performance Metrics

  • Inject a PSR-3 logger or call setLogger() + enableFeature('logging') to emit structured lifecycle logs. Hooks such as applyFilterable and cache-building log automatically when logging is active.
  • Enable performance to measure execution time, memory usage, and filter count; extend with addMetric() and read via getMetrics() / getExecutionTime().

Query Optimisation & Filter Chaining

  • Enable optimization to apply select(), with(), and chunkSize() before filters run; useIndex() can hint MySQL indexes when appropriate.
  • Enable filterChaining to queue fluent additions after request-driven filters: where(), whereIn(), whereNotIn(), whereBetween(), and orderBy() are supported.
$filter->enableFeatures(['optimization', 'filterChaining'])
->select(['id', 'title', 'status'])
->with(['author', 'tags'])
->chunkSize(500)
->where('status', 'published')
->orderBy('published_at', 'desc');

Memory Management

  • Enable memoryManagement for streaming helpers that avoid loading whole result sets into memory: lazy(), lazyEach(), cursor(), stream(), streamGenerator(), chunk(), map(), filter(), reduce().
  • executeQueryWithMemoryManagement() underpins get() when chunk_size is set; resolveChunkSize() honours chunk_size options or provided arguments. Call apply() before streaming helpers; misuse raises a RuntimeException.
$filter->enableFeature('memoryManagement')
->setOption('chunk_size', 250);
$filter->apply(Post::query());
$filter->lazyEach(fn ($post) => /* ... */, 250);

Pre-Filters & Manual Filters

  • Register global constraints with registerPreFilters(); they run before request-driven filters and are logged when logging is enabled.
  • Add programmatic filter values with appendFilterable(), or alias request keys to method names via protected array $filterMethodMap on your filter class. asCollectionFilter() returns a callable compatible with collection pipelines when you want to reuse filterables outside of Eloquent.

Debugging & Events

  • getDebugInfo() returns state, enabled features, options, SQL, bindings, and (when performance is enabled) metrics. Override handleFilteringException() to decide whether to swallow or rethrow non-validation errors.
  • Listen for FilterApplying, FilterApplied, and FilterFailed events around apply() to hook telemetry, notifications, or side effects.

Configuration

The publishable config/filterable.php controls defaults applied during filter construction:

return [
'defaults' => [
'features' => [
'validation' => false,
'permissions' => false,
'rateLimit' => false,
'caching' => false,
'logging' => false,
'performance' => false,
'optimization' => false,
'memoryManagement' => false,
'filterChaining' => false,
'valueTransformation' => false,
],
'options' => [/* runtime options seeded here */],
'cache' => ['ttl' => null],
],
];

Per-filter overrides always winβ€”call enableFeature(), disableFeature(), setOption(), or setCacheExpiration() inside individual filters when you need different defaults.

Artisan Generator & Stubs

php artisan make:filter scaffolds a filter class under App\Filters by default:

  • --basic emits a minimal filter without feature toggles.
  • --model=User imports the model and pre-fills a typed constructor parameter.
  • --force overwrites an existing class.

Publish customised stubs by copying src/Filterable/Console/stubs/ into your application's stubs/ directory; the command prefers application stubs when present.

Tooling & Scripts

Package maintenance scripts live in bin/ and are surfaced through Composer:

composer lint # Runs Tighten Duster lint mode + PHP syntax checks
composer fix # Formats with Duster and writes a timestamped log
composer test# Executes PHPUnit via bin/test.sh

./bin/test.sh accepts --filter=ClassName, --test=tests/FeatureTest.php, --coverage, and --parallel. ./bin/lint.sh --strict exits non-zero when any issue is detected.

Testing

The PHPUnit suite runs on Orchestra Testbench (phpunit.xml.dist). tests/TestCase.php provisions an in-memory sqlite schema (mocks table) and aliases factories under tests/Fixtures/. Each concern has a dedicated test file (for example CachingTest.php, ManagesMemoryTest.php) with partial mocks and fixtures such as MockFilterable, MockFilterableFactory, and TestFilter. End-to-end behaviour is exercised in tests/Integration/, which boots the full filter pipeline (feature defaults, caching, streaming, lifecycle events) against the in-memory database.

Run targeted subsets with:

./bin/test.sh --filter=SupportsFilterChainingTest
./bin/test.sh --test=tests/HandlesRateLimitingTest.php

Add new integration doubles under tests/Fixtures/ to stay aligned with the existing autoloading.

Frontend Usage

Send filter parameters as query strings from your clients:

awaitfetch('/posts?status=active&category_id=2');awaitfetch('/posts?tags[]=laravel&tags[]=performance&sort_by=created_at:desc');

Contributing

Please review AGENTS.md for contributor expectations around structure, tooling, and workflow. When ready:

  1. Fork the repository and create a feature branch (git checkout -b feature/my-change).
  2. Run composer lint and composer test (or ./bin/test.sh --coverage) before opening a PR.
  3. Describe the capabilities touched, newly exposed options, and verification commands in the pull request body.

License

This project is open-sourced under the MIT license. See LICENSE for the full text.

Authors

See contributors for the full list of collaborators.

Acknowledgements

Inspired by the flexibility of spatie/laravel-query-builder and Tighten's duster tooling.

About

πŸ” Enhance Laravel queries with adaptable, customisable filters and intelligent caching to improve both performance and functionality.

Topics

Resources

Security policy

Stars

195 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Filterable

About Filterable

Latest Version on PackagistTestsLintCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Filterable is a Laravel package for turning HTTP request parameters into rich, composable Eloquent query filters. The base Filter class exposes a stateful pipeline that you can extend, toggle, and compose with traits to add validation, caching, logging, rate limiting, memory management, and more. Everything is opt-in, so you enable only the behaviour you need while keeping type-safe, testable filters.

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Laravel 11.x, 12.x, or 13.x components (illuminate/cache, illuminate/contracts, illuminate/database, illuminate/http, illuminate/support)
  • A configured cache store when you enable caching features
  • A PSR-3 logger when you enable logging (optional)

Installation & Setup

composer require jerome/filterable

Package auto-discovery registers the FilterableServiceProvider, which contextual-binds the current Request into resolved filters and exposes the make:filter Artisan command. Publish the configuration to set global feature defaults, cache behaviour, or runtime options:

php artisan vendor:publish --tag=filterable-config

Stubs live under src/Filterable/Console/stubs/ and can be overridden by placing copies in your application's stubs directory.

Highlights

  • Publishable configuration (config/filterable.php) to set default feature bundles, runtime options, and cache TTLs that the base filter reads during construction.
  • Stateful lifecycle with apply, get, runQuery, reset, rich debug output via getDebugInfo(), lifecycle events (FilterApplying, FilterApplied, FilterFailed), and configurable exception handling.
  • Opt-in concerns for validation, permissions, rate limiting, caching (with heuristics), logging, performance metrics, query optimisation, memory management, value transformation, and fluent filter chaining.
  • Drop-in Filterable Eloquent scope trait so any model can accept a filter instance.
  • Smart caching that builds deterministic cache keys, supports tags, memoises counts, and can decide automatically when to cache complex queries.
  • Contextual binding in FilterableServiceProvider makes sure container-resolved filters receive the current HTTP Request; injecting a cache repository or PSR-3 logger auto-enables the relevant features.
  • Memory-friendly helpers (lazy, stream, streamGenerator, lazyEach, cursor, chunk, map, filter, reduce) when the memoryManagement feature is enabled.
  • First-party Artisan generator with --basic, --model, and --force options to rapidly scaffold filters.

Repository Layout

  • src/Filterable/Filter.php – abstract base class orchestrating the filter lifecycle and feature toggles.
  • src/Filterable/Concerns/ – traits implementing discrete behaviour (filter discovery, validation, caching, logging, performance, optimisation, rate limiting, etc.).
  • src/Filterable/Contracts/ – interfaces for the filter pipeline and the Eloquent scope signature.
  • src/Filterable/Traits/Filterable.php – model scope that forwards to a Filter instance.
  • src/Filterable/Console/MakeFilterCommand.php & src/Filterable/Console/stubs/ – Artisan generator and overrideable stub templates.
  • src/Filterable/Providers/FilterableServiceProvider.php – registers the package and console command via spatie/laravel-package-tools.
  • bin/ – executable scripts executed by the Composer lint, fix, and test commands.
  • tests/ – Orchestra Testbench suite with concern-focused tests and reusable fixtures in tests/Fixtures/.
  • assets/ – shared media used in documentation.
  • config/filterable.php – publishable defaults for feature toggles, cache TTL, and runtime options.
  • database/factories/ – reserved for additional factories should you extend the package.

Quick Start

1. Generate a filter

php artisan make:filter PostFilter --model=Post

--model wires the stub to your Eloquent model. Use --basic for an empty shell or --force to overwrite an existing class.

2. Implement filtering logic

<?phpnamespaceApp\Filters;
useFilterable\Filter;
useIlluminate\Database\Eloquent\Builder;
useIlluminate\Http\Request;
useIlluminate\Support\Carbon;
useIlluminate\Validation\Rule;
class PostFilter extends Filter
{
/** * Request keys that map straight to filter methods. * * Methods follow camelCased versions of the keys (e.g. published_after β†’ publishedAfter). */protectedarray$filters = ['status', 'published_after', 'q'];
publicfunction__construct(Request$request)
{
parent::__construct($request);
$this->enableFeatures([
'validation',
'optimization',
'filterChaining',
'valueTransformation',
]);
$this->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'published_after' => ['nullable', 'date'],
]);
$this->registerTransformer('published_after', fn ($value) => Carbon::parse($value));
$this->registerPreFilters(fn (Builder$query) => $query->where('is_visible', true));
$this->select(['id', 'title', 'status', 'published_at'])->with('author');
}
protectedfunctionstatus(string$value): void
{
$this->getBuilder()->where('status', $value);
}
protectedfunctionpublishedAfter(Carbon$date): void
{
$this->getBuilder()->whereDate('published_at', '>=', $date);
}
protectedfunctionq(string$term): void
{
$this->getBuilder()->where(function (Builder$query) use ($term) {
$query->where('title', 'like', "%{$term}%")
->orWhere('body', 'like', "%{$term}%");
});
}
}

Define protected array $filterMethodMap when you need to alias request keys to method names. Programmatic filters can be appended with appendFilterable('key', $value) before apply() runs. Supplying an Illuminate\Contracts\Cache\Repository or Psr\Log\LoggerInterface to the constructor immediately enables the caching and logging features.

3. Attach the scope to a model

<?phpnamespaceApp\Models;
useFilterable\Traits\Filterable;
useIlluminate\Database\Eloquent\Model;
class Post extends Model
{
use Filterable;
}

4. Run the filter pipeline

<?phpnamespaceApp\Http\Controllers;
useApp\Filters\PostFilter;
useApp\Http\Resources\PostResource;
useApp\Models\Post;
useIlluminate\Http\Request;
class PostController
{
publicfunctionindex(Request$request, PostFilter$filter)
{
$posts = Post::query()
->filter(
$filter
->forUser($request->user())
->enableFeature('caching')
->setOptions(['chunk_size' => 500])
)
->get();
return PostResource::collection($posts);
}
}

apply() may only be called once per instance; call reset() if you need to reuse a filter. Because the Filter base class uses Laravel's Conditionable trait, you can use helpers such as $filter->when($request->boolean('validate'), fn ($filter) => $filter->enableFeature('validation'));.

Lifecycle & Core API

  • apply(Builder $builder, ?array $options = []) binds the filter to a query, merges options, runs enabled concerns, and transitions the state from initialized β†’ applying β†’ applied. Re-applying without reset() raises a RuntimeException.
  • get() returns an Illuminate\Support\Collection of results, delegating to caching or memory-managed helpers when those features are active. runQuery() is a convenience wrapper for apply() + get().
  • count() respects smart caching (including tagged caches and memoised counts when enabled). toSql() exposes the raw SQL for debugging.
  • enableFeature(), enableFeatures(), disableFeature(), hasFeature() toggle concerns per instance; defaults may be set in config/filterable.php and are applied in the constructor.
  • setOption(), setOptions() persist runtime flags (for example chunk_size, use_chunking) that concerns such as OptimizesQueries and ManagesMemory consume.
  • reset() returns the filter to the initialized state so it can be applied again. getDebugInfo() surfaces state, filters applied, options, SQL/bindings, and metrics.

Feature Guides & API

Validation & Value Transformation

  • Enable with enableFeature('validation') and configure with setValidationRules(), addValidationRule(), and setValidationMessages(). Only active filters are validated and ValidationException is rethrown.
  • Enable valueTransformation to normalise inputs before filter methods execute. Register per-key transformers with registerTransformer() or bulk-array transforms with transformArray().
$filter->enableFeatures(['validation', 'valueTransformation'])
->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'tags' => ['array'],
])
->registerTransformer('tags', fn ($value) => array_map('intval', (array) $value));

Permissions & User Scope

  • forUser($user) scopes queries to the authenticated identifier and folds that identifier into cache keys automatically.
  • Enable permissions and declare requirements with setFilterPermissions(). Override userHasPermission() in your filter to plug into your authorisation layer; disallowed filters are dropped (and optionally logged) before execution.
$filter->enableFeature('permissions')
->forUser($request->user())
->setFilterPermissions(['email' => 'view-sensitive-fields']);

Rate Limiting

  • Enable with enableFeature('rateLimit'). Defaults allow 10 filters, a complexity budget of 100, and 60 attempts within a 60-second window; decay is ceil(complexity/10) seconds.
  • Tune guardrails with setMaxFilters(), setMaxComplexity(), and setFilterComplexity() (array-valued filters multiply complexity). Override resolveRateLimitMaxAttempts(), resolveRateLimitWindowSeconds(), or resolveRateLimitDecaySeconds() for finer control.
$filter->enableFeature('rateLimit')
->setMaxFilters(5)
->setMaxComplexity(25)
->setFilterComplexity(['tags' => 3, 'q' => 2]);

Caching & SmartCaching

  • Inject an Illuminate\Contracts\Cache\Repository or call enableFeature('caching') to activate caching. TTL defaults to 5 minutes or config('filterable.defaults.cache.ttl'); override per instance with setCacheExpiration().
  • Opt into result or count caching via cacheResults() / cacheCount(), and scope invalidation with cacheTags(), clearCache(), and clearRelatedCaches(). Cache keys include sanitised filter values and optional user identifiers from forUser().
  • SmartCaching will automatically cache more complex queries (multiple where clauses, joins, select statements) when caching is enabled, while skipping trivial single-clause lookups.
$filter->enableFeature('caching')
->cacheTags(['posts'])
->cacheResults()
->cacheCount()
->setCacheExpiration(15);
$posts = Post::query()->filter($filter)->get();
$total = $filter->count();

Logging & Performance Metrics

  • Inject a PSR-3 logger or call setLogger() + enableFeature('logging') to emit structured lifecycle logs. Hooks such as applyFilterable and cache-building log automatically when logging is active.
  • Enable performance to measure execution time, memory usage, and filter count; extend with addMetric() and read via getMetrics() / getExecutionTime().

Query Optimisation & Filter Chaining

  • Enable optimization to apply select(), with(), and chunkSize() before filters run; useIndex() can hint MySQL indexes when appropriate.
  • Enable filterChaining to queue fluent additions after request-driven filters: where(), whereIn(), whereNotIn(), whereBetween(), and orderBy() are supported.
$filter->enableFeatures(['optimization', 'filterChaining'])
->select(['id', 'title', 'status'])
->with(['author', 'tags'])
->chunkSize(500)
->where('status', 'published')
->orderBy('published_at', 'desc');

Memory Management

  • Enable memoryManagement for streaming helpers that avoid loading whole result sets into memory: lazy(), lazyEach(), cursor(), stream(), streamGenerator(), chunk(), map(), filter(), reduce().
  • executeQueryWithMemoryManagement() underpins get() when chunk_size is set; resolveChunkSize() honours chunk_size options or provided arguments. Call apply() before streaming helpers; misuse raises a RuntimeException.
$filter->enableFeature('memoryManagement')
->setOption('chunk_size', 250);
$filter->apply(Post::query());
$filter->lazyEach(fn ($post) => /* ... */, 250);

Pre-Filters & Manual Filters

  • Register global constraints with registerPreFilters(); they run before request-driven filters and are logged when logging is enabled.
  • Add programmatic filter values with appendFilterable(), or alias request keys to method names via protected array $filterMethodMap on your filter class. asCollectionFilter() returns a callable compatible with collection pipelines when you want to reuse filterables outside of Eloquent.

Debugging & Events

  • getDebugInfo() returns state, enabled features, options, SQL, bindings, and (when performance is enabled) metrics. Override handleFilteringException() to decide whether to swallow or rethrow non-validation errors.
  • Listen for FilterApplying, FilterApplied, and FilterFailed events around apply() to hook telemetry, notifications, or side effects.

Configuration

The publishable config/filterable.php controls defaults applied during filter construction:

return [
'defaults' => [
'features' => [
'validation' => false,
'permissions' => false,
'rateLimit' => false,
'caching' => false,
'logging' => false,
'performance' => false,
'optimization' => false,
'memoryManagement' => false,
'filterChaining' => false,
'valueTransformation' => false,
],
'options' => [/* runtime options seeded here */],
'cache' => ['ttl' => null],
],
];

Per-filter overrides always winβ€”call enableFeature(), disableFeature(), setOption(), or setCacheExpiration() inside individual filters when you need different defaults.

Artisan Generator & Stubs

php artisan make:filter scaffolds a filter class under App\Filters by default:

  • --basic emits a minimal filter without feature toggles.
  • --model=User imports the model and pre-fills a typed constructor parameter.
  • --force overwrites an existing class.

Publish customised stubs by copying src/Filterable/Console/stubs/ into your application's stubs/ directory; the command prefers application stubs when present.

Tooling & Scripts

Package maintenance scripts live in bin/ and are surfaced through Composer:

composer lint # Runs Tighten Duster lint mode + PHP syntax checks
composer fix # Formats with Duster and writes a timestamped log
composer test# Executes PHPUnit via bin/test.sh

./bin/test.sh accepts --filter=ClassName, --test=tests/FeatureTest.php, --coverage, and --parallel. ./bin/lint.sh --strict exits non-zero when any issue is detected.

Testing

The PHPUnit suite runs on Orchestra Testbench (phpunit.xml.dist). tests/TestCase.php provisions an in-memory sqlite schema (mocks table) and aliases factories under tests/Fixtures/. Each concern has a dedicated test file (for example CachingTest.php, ManagesMemoryTest.php) with partial mocks and fixtures such as MockFilterable, MockFilterableFactory, and TestFilter. End-to-end behaviour is exercised in tests/Integration/, which boots the full filter pipeline (feature defaults, caching, streaming, lifecycle events) against the in-memory database.

Run targeted subsets with:

./bin/test.sh --filter=SupportsFilterChainingTest
./bin/test.sh --test=tests/HandlesRateLimitingTest.php

Add new integration doubles under tests/Fixtures/ to stay aligned with the existing autoloading.

Frontend Usage

Send filter parameters as query strings from your clients:

awaitfetch('/posts?status=active&category_id=2');awaitfetch('/posts?tags[]=laravel&tags[]=performance&sort_by=created_at:desc');

Contributing

Please review AGENTS.md for contributor expectations around structure, tooling, and workflow. When ready:

  1. Fork the repository and create a feature branch (git checkout -b feature/my-change).
  2. Run composer lint and composer test (or ./bin/test.sh --coverage) before opening a PR.
  3. Describe the capabilities touched, newly exposed options, and verification commands in the pull request body.

License

This project is open-sourced under the MIT license. See LICENSE for the full text.

Authors

See contributors for the full list of collaborators.

Acknowledgements

Inspired by the flexibility of spatie/laravel-query-builder and Tighten's duster tooling.

About

πŸ” Enhance Laravel queries with adaptable, customisable filters and intelligent caching to improve both performance and functionality.

Topics

Resources

Security policy

Stars

195 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Filterable

About Filterable

Latest Version on PackagistTestsLintCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Filterable is a Laravel package for turning HTTP request parameters into rich, composable Eloquent query filters. The base Filter class exposes a stateful pipeline that you can extend, toggle, and compose with traits to add validation, caching, logging, rate limiting, memory management, and more. Everything is opt-in, so you enable only the behaviour you need while keeping type-safe, testable filters.

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Laravel 11.x, 12.x, or 13.x components (illuminate/cache, illuminate/contracts, illuminate/database, illuminate/http, illuminate/support)
  • A configured cache store when you enable caching features
  • A PSR-3 logger when you enable logging (optional)

Installation & Setup

composer require jerome/filterable

Package auto-discovery registers the FilterableServiceProvider, which contextual-binds the current Request into resolved filters and exposes the make:filter Artisan command. Publish the configuration to set global feature defaults, cache behaviour, or runtime options:

php artisan vendor:publish --tag=filterable-config

Stubs live under src/Filterable/Console/stubs/ and can be overridden by placing copies in your application's stubs directory.

Highlights

  • Publishable configuration (config/filterable.php) to set default feature bundles, runtime options, and cache TTLs that the base filter reads during construction.
  • Stateful lifecycle with apply, get, runQuery, reset, rich debug output via getDebugInfo(), lifecycle events (FilterApplying, FilterApplied, FilterFailed), and configurable exception handling.
  • Opt-in concerns for validation, permissions, rate limiting, caching (with heuristics), logging, performance metrics, query optimisation, memory management, value transformation, and fluent filter chaining.
  • Drop-in Filterable Eloquent scope trait so any model can accept a filter instance.
  • Smart caching that builds deterministic cache keys, supports tags, memoises counts, and can decide automatically when to cache complex queries.
  • Contextual binding in FilterableServiceProvider makes sure container-resolved filters receive the current HTTP Request; injecting a cache repository or PSR-3 logger auto-enables the relevant features.
  • Memory-friendly helpers (lazy, stream, streamGenerator, lazyEach, cursor, chunk, map, filter, reduce) when the memoryManagement feature is enabled.
  • First-party Artisan generator with --basic, --model, and --force options to rapidly scaffold filters.

Repository Layout

  • src/Filterable/Filter.php – abstract base class orchestrating the filter lifecycle and feature toggles.
  • src/Filterable/Concerns/ – traits implementing discrete behaviour (filter discovery, validation, caching, logging, performance, optimisation, rate limiting, etc.).
  • src/Filterable/Contracts/ – interfaces for the filter pipeline and the Eloquent scope signature.
  • src/Filterable/Traits/Filterable.php – model scope that forwards to a Filter instance.
  • src/Filterable/Console/MakeFilterCommand.php & src/Filterable/Console/stubs/ – Artisan generator and overrideable stub templates.
  • src/Filterable/Providers/FilterableServiceProvider.php – registers the package and console command via spatie/laravel-package-tools.
  • bin/ – executable scripts executed by the Composer lint, fix, and test commands.
  • tests/ – Orchestra Testbench suite with concern-focused tests and reusable fixtures in tests/Fixtures/.
  • assets/ – shared media used in documentation.
  • config/filterable.php – publishable defaults for feature toggles, cache TTL, and runtime options.
  • database/factories/ – reserved for additional factories should you extend the package.

Quick Start

1. Generate a filter

php artisan make:filter PostFilter --model=Post

--model wires the stub to your Eloquent model. Use --basic for an empty shell or --force to overwrite an existing class.

2. Implement filtering logic

<?phpnamespaceApp\Filters;
useFilterable\Filter;
useIlluminate\Database\Eloquent\Builder;
useIlluminate\Http\Request;
useIlluminate\Support\Carbon;
useIlluminate\Validation\Rule;
class PostFilter extends Filter
{
/** * Request keys that map straight to filter methods. * * Methods follow camelCased versions of the keys (e.g. published_after β†’ publishedAfter). */protectedarray$filters = ['status', 'published_after', 'q'];
publicfunction__construct(Request$request)
{
parent::__construct($request);
$this->enableFeatures([
'validation',
'optimization',
'filterChaining',
'valueTransformation',
]);
$this->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'published_after' => ['nullable', 'date'],
]);
$this->registerTransformer('published_after', fn ($value) => Carbon::parse($value));
$this->registerPreFilters(fn (Builder$query) => $query->where('is_visible', true));
$this->select(['id', 'title', 'status', 'published_at'])->with('author');
}
protectedfunctionstatus(string$value): void
{
$this->getBuilder()->where('status', $value);
}
protectedfunctionpublishedAfter(Carbon$date): void
{
$this->getBuilder()->whereDate('published_at', '>=', $date);
}
protectedfunctionq(string$term): void
{
$this->getBuilder()->where(function (Builder$query) use ($term) {
$query->where('title', 'like', "%{$term}%")
->orWhere('body', 'like', "%{$term}%");
});
}
}

Define protected array $filterMethodMap when you need to alias request keys to method names. Programmatic filters can be appended with appendFilterable('key', $value) before apply() runs. Supplying an Illuminate\Contracts\Cache\Repository or Psr\Log\LoggerInterface to the constructor immediately enables the caching and logging features.

3. Attach the scope to a model

<?phpnamespaceApp\Models;
useFilterable\Traits\Filterable;
useIlluminate\Database\Eloquent\Model;
class Post extends Model
{
use Filterable;
}

4. Run the filter pipeline

<?phpnamespaceApp\Http\Controllers;
useApp\Filters\PostFilter;
useApp\Http\Resources\PostResource;
useApp\Models\Post;
useIlluminate\Http\Request;
class PostController
{
publicfunctionindex(Request$request, PostFilter$filter)
{
$posts = Post::query()
->filter(
$filter
->forUser($request->user())
->enableFeature('caching')
->setOptions(['chunk_size' => 500])
)
->get();
return PostResource::collection($posts);
}
}

apply() may only be called once per instance; call reset() if you need to reuse a filter. Because the Filter base class uses Laravel's Conditionable trait, you can use helpers such as $filter->when($request->boolean('validate'), fn ($filter) => $filter->enableFeature('validation'));.

Lifecycle & Core API

  • apply(Builder $builder, ?array $options = []) binds the filter to a query, merges options, runs enabled concerns, and transitions the state from initialized β†’ applying β†’ applied. Re-applying without reset() raises a RuntimeException.
  • get() returns an Illuminate\Support\Collection of results, delegating to caching or memory-managed helpers when those features are active. runQuery() is a convenience wrapper for apply() + get().
  • count() respects smart caching (including tagged caches and memoised counts when enabled). toSql() exposes the raw SQL for debugging.
  • enableFeature(), enableFeatures(), disableFeature(), hasFeature() toggle concerns per instance; defaults may be set in config/filterable.php and are applied in the constructor.
  • setOption(), setOptions() persist runtime flags (for example chunk_size, use_chunking) that concerns such as OptimizesQueries and ManagesMemory consume.
  • reset() returns the filter to the initialized state so it can be applied again. getDebugInfo() surfaces state, filters applied, options, SQL/bindings, and metrics.

Feature Guides & API

Validation & Value Transformation

  • Enable with enableFeature('validation') and configure with setValidationRules(), addValidationRule(), and setValidationMessages(). Only active filters are validated and ValidationException is rethrown.
  • Enable valueTransformation to normalise inputs before filter methods execute. Register per-key transformers with registerTransformer() or bulk-array transforms with transformArray().
$filter->enableFeatures(['validation', 'valueTransformation'])
->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'tags' => ['array'],
])
->registerTransformer('tags', fn ($value) => array_map('intval', (array) $value));

Permissions & User Scope

  • forUser($user) scopes queries to the authenticated identifier and folds that identifier into cache keys automatically.
  • Enable permissions and declare requirements with setFilterPermissions(). Override userHasPermission() in your filter to plug into your authorisation layer; disallowed filters are dropped (and optionally logged) before execution.
$filter->enableFeature('permissions')
->forUser($request->user())
->setFilterPermissions(['email' => 'view-sensitive-fields']);

Rate Limiting

  • Enable with enableFeature('rateLimit'). Defaults allow 10 filters, a complexity budget of 100, and 60 attempts within a 60-second window; decay is ceil(complexity/10) seconds.
  • Tune guardrails with setMaxFilters(), setMaxComplexity(), and setFilterComplexity() (array-valued filters multiply complexity). Override resolveRateLimitMaxAttempts(), resolveRateLimitWindowSeconds(), or resolveRateLimitDecaySeconds() for finer control.
$filter->enableFeature('rateLimit')
->setMaxFilters(5)
->setMaxComplexity(25)
->setFilterComplexity(['tags' => 3, 'q' => 2]);

Caching & SmartCaching

  • Inject an Illuminate\Contracts\Cache\Repository or call enableFeature('caching') to activate caching. TTL defaults to 5 minutes or config('filterable.defaults.cache.ttl'); override per instance with setCacheExpiration().
  • Opt into result or count caching via cacheResults() / cacheCount(), and scope invalidation with cacheTags(), clearCache(), and clearRelatedCaches(). Cache keys include sanitised filter values and optional user identifiers from forUser().
  • SmartCaching will automatically cache more complex queries (multiple where clauses, joins, select statements) when caching is enabled, while skipping trivial single-clause lookups.
$filter->enableFeature('caching')
->cacheTags(['posts'])
->cacheResults()
->cacheCount()
->setCacheExpiration(15);
$posts = Post::query()->filter($filter)->get();
$total = $filter->count();

Logging & Performance Metrics

  • Inject a PSR-3 logger or call setLogger() + enableFeature('logging') to emit structured lifecycle logs. Hooks such as applyFilterable and cache-building log automatically when logging is active.
  • Enable performance to measure execution time, memory usage, and filter count; extend with addMetric() and read via getMetrics() / getExecutionTime().

Query Optimisation & Filter Chaining

  • Enable optimization to apply select(), with(), and chunkSize() before filters run; useIndex() can hint MySQL indexes when appropriate.
  • Enable filterChaining to queue fluent additions after request-driven filters: where(), whereIn(), whereNotIn(), whereBetween(), and orderBy() are supported.
$filter->enableFeatures(['optimization', 'filterChaining'])
->select(['id', 'title', 'status'])
->with(['author', 'tags'])
->chunkSize(500)
->where('status', 'published')
->orderBy('published_at', 'desc');

Memory Management

  • Enable memoryManagement for streaming helpers that avoid loading whole result sets into memory: lazy(), lazyEach(), cursor(), stream(), streamGenerator(), chunk(), map(), filter(), reduce().
  • executeQueryWithMemoryManagement() underpins get() when chunk_size is set; resolveChunkSize() honours chunk_size options or provided arguments. Call apply() before streaming helpers; misuse raises a RuntimeException.
$filter->enableFeature('memoryManagement')
->setOption('chunk_size', 250);
$filter->apply(Post::query());
$filter->lazyEach(fn ($post) => /* ... */, 250);

Pre-Filters & Manual Filters

  • Register global constraints with registerPreFilters(); they run before request-driven filters and are logged when logging is enabled.
  • Add programmatic filter values with appendFilterable(), or alias request keys to method names via protected array $filterMethodMap on your filter class. asCollectionFilter() returns a callable compatible with collection pipelines when you want to reuse filterables outside of Eloquent.

Debugging & Events

  • getDebugInfo() returns state, enabled features, options, SQL, bindings, and (when performance is enabled) metrics. Override handleFilteringException() to decide whether to swallow or rethrow non-validation errors.
  • Listen for FilterApplying, FilterApplied, and FilterFailed events around apply() to hook telemetry, notifications, or side effects.

Configuration

The publishable config/filterable.php controls defaults applied during filter construction:

return [
'defaults' => [
'features' => [
'validation' => false,
'permissions' => false,
'rateLimit' => false,
'caching' => false,
'logging' => false,
'performance' => false,
'optimization' => false,
'memoryManagement' => false,
'filterChaining' => false,
'valueTransformation' => false,
],
'options' => [/* runtime options seeded here */],
'cache' => ['ttl' => null],
],
];

Per-filter overrides always winβ€”call enableFeature(), disableFeature(), setOption(), or setCacheExpiration() inside individual filters when you need different defaults.

Artisan Generator & Stubs

php artisan make:filter scaffolds a filter class under App\Filters by default:

  • --basic emits a minimal filter without feature toggles.
  • --model=User imports the model and pre-fills a typed constructor parameter.
  • --force overwrites an existing class.

Publish customised stubs by copying src/Filterable/Console/stubs/ into your application's stubs/ directory; the command prefers application stubs when present.

Tooling & Scripts

Package maintenance scripts live in bin/ and are surfaced through Composer:

composer lint # Runs Tighten Duster lint mode + PHP syntax checks
composer fix # Formats with Duster and writes a timestamped log
composer test# Executes PHPUnit via bin/test.sh

./bin/test.sh accepts --filter=ClassName, --test=tests/FeatureTest.php, --coverage, and --parallel. ./bin/lint.sh --strict exits non-zero when any issue is detected.

Testing

The PHPUnit suite runs on Orchestra Testbench (phpunit.xml.dist). tests/TestCase.php provisions an in-memory sqlite schema (mocks table) and aliases factories under tests/Fixtures/. Each concern has a dedicated test file (for example CachingTest.php, ManagesMemoryTest.php) with partial mocks and fixtures such as MockFilterable, MockFilterableFactory, and TestFilter. End-to-end behaviour is exercised in tests/Integration/, which boots the full filter pipeline (feature defaults, caching, streaming, lifecycle events) against the in-memory database.

Run targeted subsets with:

./bin/test.sh --filter=SupportsFilterChainingTest
./bin/test.sh --test=tests/HandlesRateLimitingTest.php

Add new integration doubles under tests/Fixtures/ to stay aligned with the existing autoloading.

Frontend Usage

Send filter parameters as query strings from your clients:

awaitfetch('/posts?status=active&category_id=2');awaitfetch('/posts?tags[]=laravel&tags[]=performance&sort_by=created_at:desc');

Contributing

Please review AGENTS.md for contributor expectations around structure, tooling, and workflow. When ready:

  1. Fork the repository and create a feature branch (git checkout -b feature/my-change).
  2. Run composer lint and composer test (or ./bin/test.sh --coverage) before opening a PR.
  3. Describe the capabilities touched, newly exposed options, and verification commands in the pull request body.

License

This project is open-sourced under the MIT license. See LICENSE for the full text.

Authors

See contributors for the full list of collaborators.

Acknowledgements

Inspired by the flexibility of spatie/laravel-query-builder and Tighten's duster tooling.

About

πŸ” Enhance Laravel queries with adaptable, customisable filters and intelligent caching to improve both performance and functionality.

Topics

Resources

Security policy

Stars

195 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Filterable

About Filterable

Latest Version on PackagistTestsLintCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Filterable is a Laravel package for turning HTTP request parameters into rich, composable Eloquent query filters. The base Filter class exposes a stateful pipeline that you can extend, toggle, and compose with traits to add validation, caching, logging, rate limiting, memory management, and more. Everything is opt-in, so you enable only the behaviour you need while keeping type-safe, testable filters.

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Laravel 11.x, 12.x, or 13.x components (illuminate/cache, illuminate/contracts, illuminate/database, illuminate/http, illuminate/support)
  • A configured cache store when you enable caching features
  • A PSR-3 logger when you enable logging (optional)

Installation & Setup

composer require jerome/filterable

Package auto-discovery registers the FilterableServiceProvider, which contextual-binds the current Request into resolved filters and exposes the make:filter Artisan command. Publish the configuration to set global feature defaults, cache behaviour, or runtime options:

php artisan vendor:publish --tag=filterable-config

Stubs live under src/Filterable/Console/stubs/ and can be overridden by placing copies in your application's stubs directory.

Highlights

  • Publishable configuration (config/filterable.php) to set default feature bundles, runtime options, and cache TTLs that the base filter reads during construction.
  • Stateful lifecycle with apply, get, runQuery, reset, rich debug output via getDebugInfo(), lifecycle events (FilterApplying, FilterApplied, FilterFailed), and configurable exception handling.
  • Opt-in concerns for validation, permissions, rate limiting, caching (with heuristics), logging, performance metrics, query optimisation, memory management, value transformation, and fluent filter chaining.
  • Drop-in Filterable Eloquent scope trait so any model can accept a filter instance.
  • Smart caching that builds deterministic cache keys, supports tags, memoises counts, and can decide automatically when to cache complex queries.
  • Contextual binding in FilterableServiceProvider makes sure container-resolved filters receive the current HTTP Request; injecting a cache repository or PSR-3 logger auto-enables the relevant features.
  • Memory-friendly helpers (lazy, stream, streamGenerator, lazyEach, cursor, chunk, map, filter, reduce) when the memoryManagement feature is enabled.
  • First-party Artisan generator with --basic, --model, and --force options to rapidly scaffold filters.

Repository Layout

  • src/Filterable/Filter.php – abstract base class orchestrating the filter lifecycle and feature toggles.
  • src/Filterable/Concerns/ – traits implementing discrete behaviour (filter discovery, validation, caching, logging, performance, optimisation, rate limiting, etc.).
  • src/Filterable/Contracts/ – interfaces for the filter pipeline and the Eloquent scope signature.
  • src/Filterable/Traits/Filterable.php – model scope that forwards to a Filter instance.
  • src/Filterable/Console/MakeFilterCommand.php & src/Filterable/Console/stubs/ – Artisan generator and overrideable stub templates.
  • src/Filterable/Providers/FilterableServiceProvider.php – registers the package and console command via spatie/laravel-package-tools.
  • bin/ – executable scripts executed by the Composer lint, fix, and test commands.
  • tests/ – Orchestra Testbench suite with concern-focused tests and reusable fixtures in tests/Fixtures/.
  • assets/ – shared media used in documentation.
  • config/filterable.php – publishable defaults for feature toggles, cache TTL, and runtime options.
  • database/factories/ – reserved for additional factories should you extend the package.

Quick Start

1. Generate a filter

php artisan make:filter PostFilter --model=Post

--model wires the stub to your Eloquent model. Use --basic for an empty shell or --force to overwrite an existing class.

2. Implement filtering logic

<?phpnamespaceApp\Filters;
useFilterable\Filter;
useIlluminate\Database\Eloquent\Builder;
useIlluminate\Http\Request;
useIlluminate\Support\Carbon;
useIlluminate\Validation\Rule;
class PostFilter extends Filter
{
/** * Request keys that map straight to filter methods. * * Methods follow camelCased versions of the keys (e.g. published_after β†’ publishedAfter). */protectedarray$filters = ['status', 'published_after', 'q'];
publicfunction__construct(Request$request)
{
parent::__construct($request);
$this->enableFeatures([
'validation',
'optimization',
'filterChaining',
'valueTransformation',
]);
$this->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'published_after' => ['nullable', 'date'],
]);
$this->registerTransformer('published_after', fn ($value) => Carbon::parse($value));
$this->registerPreFilters(fn (Builder$query) => $query->where('is_visible', true));
$this->select(['id', 'title', 'status', 'published_at'])->with('author');
}
protectedfunctionstatus(string$value): void
{
$this->getBuilder()->where('status', $value);
}
protectedfunctionpublishedAfter(Carbon$date): void
{
$this->getBuilder()->whereDate('published_at', '>=', $date);
}
protectedfunctionq(string$term): void
{
$this->getBuilder()->where(function (Builder$query) use ($term) {
$query->where('title', 'like', "%{$term}%")
->orWhere('body', 'like', "%{$term}%");
});
}
}

Define protected array $filterMethodMap when you need to alias request keys to method names. Programmatic filters can be appended with appendFilterable('key', $value) before apply() runs. Supplying an Illuminate\Contracts\Cache\Repository or Psr\Log\LoggerInterface to the constructor immediately enables the caching and logging features.

3. Attach the scope to a model

<?phpnamespaceApp\Models;
useFilterable\Traits\Filterable;
useIlluminate\Database\Eloquent\Model;
class Post extends Model
{
use Filterable;
}

4. Run the filter pipeline

<?phpnamespaceApp\Http\Controllers;
useApp\Filters\PostFilter;
useApp\Http\Resources\PostResource;
useApp\Models\Post;
useIlluminate\Http\Request;
class PostController
{
publicfunctionindex(Request$request, PostFilter$filter)
{
$posts = Post::query()
->filter(
$filter
->forUser($request->user())
->enableFeature('caching')
->setOptions(['chunk_size' => 500])
)
->get();
return PostResource::collection($posts);
}
}

apply() may only be called once per instance; call reset() if you need to reuse a filter. Because the Filter base class uses Laravel's Conditionable trait, you can use helpers such as $filter->when($request->boolean('validate'), fn ($filter) => $filter->enableFeature('validation'));.

Lifecycle & Core API

  • apply(Builder $builder, ?array $options = []) binds the filter to a query, merges options, runs enabled concerns, and transitions the state from initialized β†’ applying β†’ applied. Re-applying without reset() raises a RuntimeException.
  • get() returns an Illuminate\Support\Collection of results, delegating to caching or memory-managed helpers when those features are active. runQuery() is a convenience wrapper for apply() + get().
  • count() respects smart caching (including tagged caches and memoised counts when enabled). toSql() exposes the raw SQL for debugging.
  • enableFeature(), enableFeatures(), disableFeature(), hasFeature() toggle concerns per instance; defaults may be set in config/filterable.php and are applied in the constructor.
  • setOption(), setOptions() persist runtime flags (for example chunk_size, use_chunking) that concerns such as OptimizesQueries and ManagesMemory consume.
  • reset() returns the filter to the initialized state so it can be applied again. getDebugInfo() surfaces state, filters applied, options, SQL/bindings, and metrics.

Feature Guides & API

Validation & Value Transformation

  • Enable with enableFeature('validation') and configure with setValidationRules(), addValidationRule(), and setValidationMessages(). Only active filters are validated and ValidationException is rethrown.
  • Enable valueTransformation to normalise inputs before filter methods execute. Register per-key transformers with registerTransformer() or bulk-array transforms with transformArray().
$filter->enableFeatures(['validation', 'valueTransformation'])
->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'tags' => ['array'],
])
->registerTransformer('tags', fn ($value) => array_map('intval', (array) $value));

Permissions & User Scope

  • forUser($user) scopes queries to the authenticated identifier and folds that identifier into cache keys automatically.
  • Enable permissions and declare requirements with setFilterPermissions(). Override userHasPermission() in your filter to plug into your authorisation layer; disallowed filters are dropped (and optionally logged) before execution.
$filter->enableFeature('permissions')
->forUser($request->user())
->setFilterPermissions(['email' => 'view-sensitive-fields']);

Rate Limiting

  • Enable with enableFeature('rateLimit'). Defaults allow 10 filters, a complexity budget of 100, and 60 attempts within a 60-second window; decay is ceil(complexity/10) seconds.
  • Tune guardrails with setMaxFilters(), setMaxComplexity(), and setFilterComplexity() (array-valued filters multiply complexity). Override resolveRateLimitMaxAttempts(), resolveRateLimitWindowSeconds(), or resolveRateLimitDecaySeconds() for finer control.
$filter->enableFeature('rateLimit')
->setMaxFilters(5)
->setMaxComplexity(25)
->setFilterComplexity(['tags' => 3, 'q' => 2]);

Caching & SmartCaching

  • Inject an Illuminate\Contracts\Cache\Repository or call enableFeature('caching') to activate caching. TTL defaults to 5 minutes or config('filterable.defaults.cache.ttl'); override per instance with setCacheExpiration().
  • Opt into result or count caching via cacheResults() / cacheCount(), and scope invalidation with cacheTags(), clearCache(), and clearRelatedCaches(). Cache keys include sanitised filter values and optional user identifiers from forUser().
  • SmartCaching will automatically cache more complex queries (multiple where clauses, joins, select statements) when caching is enabled, while skipping trivial single-clause lookups.
$filter->enableFeature('caching')
->cacheTags(['posts'])
->cacheResults()
->cacheCount()
->setCacheExpiration(15);
$posts = Post::query()->filter($filter)->get();
$total = $filter->count();

Logging & Performance Metrics

  • Inject a PSR-3 logger or call setLogger() + enableFeature('logging') to emit structured lifecycle logs. Hooks such as applyFilterable and cache-building log automatically when logging is active.
  • Enable performance to measure execution time, memory usage, and filter count; extend with addMetric() and read via getMetrics() / getExecutionTime().

Query Optimisation & Filter Chaining

  • Enable optimization to apply select(), with(), and chunkSize() before filters run; useIndex() can hint MySQL indexes when appropriate.
  • Enable filterChaining to queue fluent additions after request-driven filters: where(), whereIn(), whereNotIn(), whereBetween(), and orderBy() are supported.
$filter->enableFeatures(['optimization', 'filterChaining'])
->select(['id', 'title', 'status'])
->with(['author', 'tags'])
->chunkSize(500)
->where('status', 'published')
->orderBy('published_at', 'desc');

Memory Management

  • Enable memoryManagement for streaming helpers that avoid loading whole result sets into memory: lazy(), lazyEach(), cursor(), stream(), streamGenerator(), chunk(), map(), filter(), reduce().
  • executeQueryWithMemoryManagement() underpins get() when chunk_size is set; resolveChunkSize() honours chunk_size options or provided arguments. Call apply() before streaming helpers; misuse raises a RuntimeException.
$filter->enableFeature('memoryManagement')
->setOption('chunk_size', 250);
$filter->apply(Post::query());
$filter->lazyEach(fn ($post) => /* ... */, 250);

Pre-Filters & Manual Filters

  • Register global constraints with registerPreFilters(); they run before request-driven filters and are logged when logging is enabled.
  • Add programmatic filter values with appendFilterable(), or alias request keys to method names via protected array $filterMethodMap on your filter class. asCollectionFilter() returns a callable compatible with collection pipelines when you want to reuse filterables outside of Eloquent.

Debugging & Events

  • getDebugInfo() returns state, enabled features, options, SQL, bindings, and (when performance is enabled) metrics. Override handleFilteringException() to decide whether to swallow or rethrow non-validation errors.
  • Listen for FilterApplying, FilterApplied, and FilterFailed events around apply() to hook telemetry, notifications, or side effects.

Configuration

The publishable config/filterable.php controls defaults applied during filter construction:

return [
'defaults' => [
'features' => [
'validation' => false,
'permissions' => false,
'rateLimit' => false,
'caching' => false,
'logging' => false,
'performance' => false,
'optimization' => false,
'memoryManagement' => false,
'filterChaining' => false,
'valueTransformation' => false,
],
'options' => [/* runtime options seeded here */],
'cache' => ['ttl' => null],
],
];

Per-filter overrides always winβ€”call enableFeature(), disableFeature(), setOption(), or setCacheExpiration() inside individual filters when you need different defaults.

Artisan Generator & Stubs

php artisan make:filter scaffolds a filter class under App\Filters by default:

  • --basic emits a minimal filter without feature toggles.
  • --model=User imports the model and pre-fills a typed constructor parameter.
  • --force overwrites an existing class.

Publish customised stubs by copying src/Filterable/Console/stubs/ into your application's stubs/ directory; the command prefers application stubs when present.

Tooling & Scripts

Package maintenance scripts live in bin/ and are surfaced through Composer:

composer lint # Runs Tighten Duster lint mode + PHP syntax checks
composer fix # Formats with Duster and writes a timestamped log
composer test# Executes PHPUnit via bin/test.sh

./bin/test.sh accepts --filter=ClassName, --test=tests/FeatureTest.php, --coverage, and --parallel. ./bin/lint.sh --strict exits non-zero when any issue is detected.

Testing

The PHPUnit suite runs on Orchestra Testbench (phpunit.xml.dist). tests/TestCase.php provisions an in-memory sqlite schema (mocks table) and aliases factories under tests/Fixtures/. Each concern has a dedicated test file (for example CachingTest.php, ManagesMemoryTest.php) with partial mocks and fixtures such as MockFilterable, MockFilterableFactory, and TestFilter. End-to-end behaviour is exercised in tests/Integration/, which boots the full filter pipeline (feature defaults, caching, streaming, lifecycle events) against the in-memory database.

Run targeted subsets with:

./bin/test.sh --filter=SupportsFilterChainingTest
./bin/test.sh --test=tests/HandlesRateLimitingTest.php

Add new integration doubles under tests/Fixtures/ to stay aligned with the existing autoloading.

Frontend Usage

Send filter parameters as query strings from your clients:

awaitfetch('/posts?status=active&category_id=2');awaitfetch('/posts?tags[]=laravel&tags[]=performance&sort_by=created_at:desc');

Contributing

Please review AGENTS.md for contributor expectations around structure, tooling, and workflow. When ready:

  1. Fork the repository and create a feature branch (git checkout -b feature/my-change).
  2. Run composer lint and composer test (or ./bin/test.sh --coverage) before opening a PR.
  3. Describe the capabilities touched, newly exposed options, and verification commands in the pull request body.

License

This project is open-sourced under the MIT license. See LICENSE for the full text.

Authors

See contributors for the full list of collaborators.

Acknowledgements

Inspired by the flexibility of spatie/laravel-query-builder and Tighten's duster tooling.

About

πŸ” Enhance Laravel queries with adaptable, customisable filters and intelligent caching to improve both performance and functionality.

Topics

Resources

Security policy

Stars

195 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Filterable

About Filterable

Latest Version on PackagistTestsLintCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Filterable is a Laravel package for turning HTTP request parameters into rich, composable Eloquent query filters. The base Filter class exposes a stateful pipeline that you can extend, toggle, and compose with traits to add validation, caching, logging, rate limiting, memory management, and more. Everything is opt-in, so you enable only the behaviour you need while keeping type-safe, testable filters.

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Laravel 11.x, 12.x, or 13.x components (illuminate/cache, illuminate/contracts, illuminate/database, illuminate/http, illuminate/support)
  • A configured cache store when you enable caching features
  • A PSR-3 logger when you enable logging (optional)

Installation & Setup

composer require jerome/filterable

Package auto-discovery registers the FilterableServiceProvider, which contextual-binds the current Request into resolved filters and exposes the make:filter Artisan command. Publish the configuration to set global feature defaults, cache behaviour, or runtime options:

php artisan vendor:publish --tag=filterable-config

Stubs live under src/Filterable/Console/stubs/ and can be overridden by placing copies in your application's stubs directory.

Highlights

  • Publishable configuration (config/filterable.php) to set default feature bundles, runtime options, and cache TTLs that the base filter reads during construction.
  • Stateful lifecycle with apply, get, runQuery, reset, rich debug output via getDebugInfo(), lifecycle events (FilterApplying, FilterApplied, FilterFailed), and configurable exception handling.
  • Opt-in concerns for validation, permissions, rate limiting, caching (with heuristics), logging, performance metrics, query optimisation, memory management, value transformation, and fluent filter chaining.
  • Drop-in Filterable Eloquent scope trait so any model can accept a filter instance.
  • Smart caching that builds deterministic cache keys, supports tags, memoises counts, and can decide automatically when to cache complex queries.
  • Contextual binding in FilterableServiceProvider makes sure container-resolved filters receive the current HTTP Request; injecting a cache repository or PSR-3 logger auto-enables the relevant features.
  • Memory-friendly helpers (lazy, stream, streamGenerator, lazyEach, cursor, chunk, map, filter, reduce) when the memoryManagement feature is enabled.
  • First-party Artisan generator with --basic, --model, and --force options to rapidly scaffold filters.

Repository Layout

  • src/Filterable/Filter.php – abstract base class orchestrating the filter lifecycle and feature toggles.
  • src/Filterable/Concerns/ – traits implementing discrete behaviour (filter discovery, validation, caching, logging, performance, optimisation, rate limiting, etc.).
  • src/Filterable/Contracts/ – interfaces for the filter pipeline and the Eloquent scope signature.
  • src/Filterable/Traits/Filterable.php – model scope that forwards to a Filter instance.
  • src/Filterable/Console/MakeFilterCommand.php & src/Filterable/Console/stubs/ – Artisan generator and overrideable stub templates.
  • src/Filterable/Providers/FilterableServiceProvider.php – registers the package and console command via spatie/laravel-package-tools.
  • bin/ – executable scripts executed by the Composer lint, fix, and test commands.
  • tests/ – Orchestra Testbench suite with concern-focused tests and reusable fixtures in tests/Fixtures/.
  • assets/ – shared media used in documentation.
  • config/filterable.php – publishable defaults for feature toggles, cache TTL, and runtime options.
  • database/factories/ – reserved for additional factories should you extend the package.

Quick Start

1. Generate a filter

php artisan make:filter PostFilter --model=Post

--model wires the stub to your Eloquent model. Use --basic for an empty shell or --force to overwrite an existing class.

2. Implement filtering logic

<?phpnamespaceApp\Filters;
useFilterable\Filter;
useIlluminate\Database\Eloquent\Builder;
useIlluminate\Http\Request;
useIlluminate\Support\Carbon;
useIlluminate\Validation\Rule;
class PostFilter extends Filter
{
/** * Request keys that map straight to filter methods. * * Methods follow camelCased versions of the keys (e.g. published_after β†’ publishedAfter). */protectedarray$filters = ['status', 'published_after', 'q'];
publicfunction__construct(Request$request)
{
parent::__construct($request);
$this->enableFeatures([
'validation',
'optimization',
'filterChaining',
'valueTransformation',
]);
$this->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'published_after' => ['nullable', 'date'],
]);
$this->registerTransformer('published_after', fn ($value) => Carbon::parse($value));
$this->registerPreFilters(fn (Builder$query) => $query->where('is_visible', true));
$this->select(['id', 'title', 'status', 'published_at'])->with('author');
}
protectedfunctionstatus(string$value): void
{
$this->getBuilder()->where('status', $value);
}
protectedfunctionpublishedAfter(Carbon$date): void
{
$this->getBuilder()->whereDate('published_at', '>=', $date);
}
protectedfunctionq(string$term): void
{
$this->getBuilder()->where(function (Builder$query) use ($term) {
$query->where('title', 'like', "%{$term}%")
->orWhere('body', 'like', "%{$term}%");
});
}
}

Define protected array $filterMethodMap when you need to alias request keys to method names. Programmatic filters can be appended with appendFilterable('key', $value) before apply() runs. Supplying an Illuminate\Contracts\Cache\Repository or Psr\Log\LoggerInterface to the constructor immediately enables the caching and logging features.

3. Attach the scope to a model

<?phpnamespaceApp\Models;
useFilterable\Traits\Filterable;
useIlluminate\Database\Eloquent\Model;
class Post extends Model
{
use Filterable;
}

4. Run the filter pipeline

<?phpnamespaceApp\Http\Controllers;
useApp\Filters\PostFilter;
useApp\Http\Resources\PostResource;
useApp\Models\Post;
useIlluminate\Http\Request;
class PostController
{
publicfunctionindex(Request$request, PostFilter$filter)
{
$posts = Post::query()
->filter(
$filter
->forUser($request->user())
->enableFeature('caching')
->setOptions(['chunk_size' => 500])
)
->get();
return PostResource::collection($posts);
}
}

apply() may only be called once per instance; call reset() if you need to reuse a filter. Because the Filter base class uses Laravel's Conditionable trait, you can use helpers such as $filter->when($request->boolean('validate'), fn ($filter) => $filter->enableFeature('validation'));.

Lifecycle & Core API

  • apply(Builder $builder, ?array $options = []) binds the filter to a query, merges options, runs enabled concerns, and transitions the state from initialized β†’ applying β†’ applied. Re-applying without reset() raises a RuntimeException.
  • get() returns an Illuminate\Support\Collection of results, delegating to caching or memory-managed helpers when those features are active. runQuery() is a convenience wrapper for apply() + get().
  • count() respects smart caching (including tagged caches and memoised counts when enabled). toSql() exposes the raw SQL for debugging.
  • enableFeature(), enableFeatures(), disableFeature(), hasFeature() toggle concerns per instance; defaults may be set in config/filterable.php and are applied in the constructor.
  • setOption(), setOptions() persist runtime flags (for example chunk_size, use_chunking) that concerns such as OptimizesQueries and ManagesMemory consume.
  • reset() returns the filter to the initialized state so it can be applied again. getDebugInfo() surfaces state, filters applied, options, SQL/bindings, and metrics.

Feature Guides & API

Validation & Value Transformation

  • Enable with enableFeature('validation') and configure with setValidationRules(), addValidationRule(), and setValidationMessages(). Only active filters are validated and ValidationException is rethrown.
  • Enable valueTransformation to normalise inputs before filter methods execute. Register per-key transformers with registerTransformer() or bulk-array transforms with transformArray().
$filter->enableFeatures(['validation', 'valueTransformation'])
->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'tags' => ['array'],
])
->registerTransformer('tags', fn ($value) => array_map('intval', (array) $value));

Permissions & User Scope

  • forUser($user) scopes queries to the authenticated identifier and folds that identifier into cache keys automatically.
  • Enable permissions and declare requirements with setFilterPermissions(). Override userHasPermission() in your filter to plug into your authorisation layer; disallowed filters are dropped (and optionally logged) before execution.
$filter->enableFeature('permissions')
->forUser($request->user())
->setFilterPermissions(['email' => 'view-sensitive-fields']);

Rate Limiting

  • Enable with enableFeature('rateLimit'). Defaults allow 10 filters, a complexity budget of 100, and 60 attempts within a 60-second window; decay is ceil(complexity/10) seconds.
  • Tune guardrails with setMaxFilters(), setMaxComplexity(), and setFilterComplexity() (array-valued filters multiply complexity). Override resolveRateLimitMaxAttempts(), resolveRateLimitWindowSeconds(), or resolveRateLimitDecaySeconds() for finer control.
$filter->enableFeature('rateLimit')
->setMaxFilters(5)
->setMaxComplexity(25)
->setFilterComplexity(['tags' => 3, 'q' => 2]);

Caching & SmartCaching

  • Inject an Illuminate\Contracts\Cache\Repository or call enableFeature('caching') to activate caching. TTL defaults to 5 minutes or config('filterable.defaults.cache.ttl'); override per instance with setCacheExpiration().
  • Opt into result or count caching via cacheResults() / cacheCount(), and scope invalidation with cacheTags(), clearCache(), and clearRelatedCaches(). Cache keys include sanitised filter values and optional user identifiers from forUser().
  • SmartCaching will automatically cache more complex queries (multiple where clauses, joins, select statements) when caching is enabled, while skipping trivial single-clause lookups.
$filter->enableFeature('caching')
->cacheTags(['posts'])
->cacheResults()
->cacheCount()
->setCacheExpiration(15);
$posts = Post::query()->filter($filter)->get();
$total = $filter->count();

Logging & Performance Metrics

  • Inject a PSR-3 logger or call setLogger() + enableFeature('logging') to emit structured lifecycle logs. Hooks such as applyFilterable and cache-building log automatically when logging is active.
  • Enable performance to measure execution time, memory usage, and filter count; extend with addMetric() and read via getMetrics() / getExecutionTime().

Query Optimisation & Filter Chaining

  • Enable optimization to apply select(), with(), and chunkSize() before filters run; useIndex() can hint MySQL indexes when appropriate.
  • Enable filterChaining to queue fluent additions after request-driven filters: where(), whereIn(), whereNotIn(), whereBetween(), and orderBy() are supported.
$filter->enableFeatures(['optimization', 'filterChaining'])
->select(['id', 'title', 'status'])
->with(['author', 'tags'])
->chunkSize(500)
->where('status', 'published')
->orderBy('published_at', 'desc');

Memory Management

  • Enable memoryManagement for streaming helpers that avoid loading whole result sets into memory: lazy(), lazyEach(), cursor(), stream(), streamGenerator(), chunk(), map(), filter(), reduce().
  • executeQueryWithMemoryManagement() underpins get() when chunk_size is set; resolveChunkSize() honours chunk_size options or provided arguments. Call apply() before streaming helpers; misuse raises a RuntimeException.
$filter->enableFeature('memoryManagement')
->setOption('chunk_size', 250);
$filter->apply(Post::query());
$filter->lazyEach(fn ($post) => /* ... */, 250);

Pre-Filters & Manual Filters

  • Register global constraints with registerPreFilters(); they run before request-driven filters and are logged when logging is enabled.
  • Add programmatic filter values with appendFilterable(), or alias request keys to method names via protected array $filterMethodMap on your filter class. asCollectionFilter() returns a callable compatible with collection pipelines when you want to reuse filterables outside of Eloquent.

Debugging & Events

  • getDebugInfo() returns state, enabled features, options, SQL, bindings, and (when performance is enabled) metrics. Override handleFilteringException() to decide whether to swallow or rethrow non-validation errors.
  • Listen for FilterApplying, FilterApplied, and FilterFailed events around apply() to hook telemetry, notifications, or side effects.

Configuration

The publishable config/filterable.php controls defaults applied during filter construction:

return [
'defaults' => [
'features' => [
'validation' => false,
'permissions' => false,
'rateLimit' => false,
'caching' => false,
'logging' => false,
'performance' => false,
'optimization' => false,
'memoryManagement' => false,
'filterChaining' => false,
'valueTransformation' => false,
],
'options' => [/* runtime options seeded here */],
'cache' => ['ttl' => null],
],
];

Per-filter overrides always winβ€”call enableFeature(), disableFeature(), setOption(), or setCacheExpiration() inside individual filters when you need different defaults.

Artisan Generator & Stubs

php artisan make:filter scaffolds a filter class under App\Filters by default:

  • --basic emits a minimal filter without feature toggles.
  • --model=User imports the model and pre-fills a typed constructor parameter.
  • --force overwrites an existing class.

Publish customised stubs by copying src/Filterable/Console/stubs/ into your application's stubs/ directory; the command prefers application stubs when present.

Tooling & Scripts

Package maintenance scripts live in bin/ and are surfaced through Composer:

composer lint # Runs Tighten Duster lint mode + PHP syntax checks
composer fix # Formats with Duster and writes a timestamped log
composer test# Executes PHPUnit via bin/test.sh

./bin/test.sh accepts --filter=ClassName, --test=tests/FeatureTest.php, --coverage, and --parallel. ./bin/lint.sh --strict exits non-zero when any issue is detected.

Testing

The PHPUnit suite runs on Orchestra Testbench (phpunit.xml.dist). tests/TestCase.php provisions an in-memory sqlite schema (mocks table) and aliases factories under tests/Fixtures/. Each concern has a dedicated test file (for example CachingTest.php, ManagesMemoryTest.php) with partial mocks and fixtures such as MockFilterable, MockFilterableFactory, and TestFilter. End-to-end behaviour is exercised in tests/Integration/, which boots the full filter pipeline (feature defaults, caching, streaming, lifecycle events) against the in-memory database.

Run targeted subsets with:

./bin/test.sh --filter=SupportsFilterChainingTest
./bin/test.sh --test=tests/HandlesRateLimitingTest.php

Add new integration doubles under tests/Fixtures/ to stay aligned with the existing autoloading.

Frontend Usage

Send filter parameters as query strings from your clients:

awaitfetch('/posts?status=active&category_id=2');awaitfetch('/posts?tags[]=laravel&tags[]=performance&sort_by=created_at:desc');

Contributing

Please review AGENTS.md for contributor expectations around structure, tooling, and workflow. When ready:

  1. Fork the repository and create a feature branch (git checkout -b feature/my-change).
  2. Run composer lint and composer test (or ./bin/test.sh --coverage) before opening a PR.
  3. Describe the capabilities touched, newly exposed options, and verification commands in the pull request body.

License

This project is open-sourced under the MIT license. See LICENSE for the full text.

Authors

See contributors for the full list of collaborators.

Acknowledgements

Inspired by the flexibility of spatie/laravel-query-builder and Tighten's duster tooling.

About

πŸ” Enhance Laravel queries with adaptable, customisable filters and intelligent caching to improve both performance and functionality.

Topics

Resources

Security policy

Stars

195 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Filterable

About Filterable

Latest Version on PackagistTestsLintCodeQLPHPStanPHP VersionLicenseTotal DownloadsGitHub Stars

Filterable is a Laravel package for turning HTTP request parameters into rich, composable Eloquent query filters. The base Filter class exposes a stateful pipeline that you can extend, toggle, and compose with traits to add validation, caching, logging, rate limiting, memory management, and more. Everything is opt-in, so you enable only the behaviour you need while keeping type-safe, testable filters.

Requirements

  • PHP 8.3, 8.4, or 8.5
  • Laravel 11.x, 12.x, or 13.x components (illuminate/cache, illuminate/contracts, illuminate/database, illuminate/http, illuminate/support)
  • A configured cache store when you enable caching features
  • A PSR-3 logger when you enable logging (optional)

Installation & Setup

composer require jerome/filterable

Package auto-discovery registers the FilterableServiceProvider, which contextual-binds the current Request into resolved filters and exposes the make:filter Artisan command. Publish the configuration to set global feature defaults, cache behaviour, or runtime options:

php artisan vendor:publish --tag=filterable-config

Stubs live under src/Filterable/Console/stubs/ and can be overridden by placing copies in your application's stubs directory.

Highlights

  • Publishable configuration (config/filterable.php) to set default feature bundles, runtime options, and cache TTLs that the base filter reads during construction.
  • Stateful lifecycle with apply, get, runQuery, reset, rich debug output via getDebugInfo(), lifecycle events (FilterApplying, FilterApplied, FilterFailed), and configurable exception handling.
  • Opt-in concerns for validation, permissions, rate limiting, caching (with heuristics), logging, performance metrics, query optimisation, memory management, value transformation, and fluent filter chaining.
  • Drop-in Filterable Eloquent scope trait so any model can accept a filter instance.
  • Smart caching that builds deterministic cache keys, supports tags, memoises counts, and can decide automatically when to cache complex queries.
  • Contextual binding in FilterableServiceProvider makes sure container-resolved filters receive the current HTTP Request; injecting a cache repository or PSR-3 logger auto-enables the relevant features.
  • Memory-friendly helpers (lazy, stream, streamGenerator, lazyEach, cursor, chunk, map, filter, reduce) when the memoryManagement feature is enabled.
  • First-party Artisan generator with --basic, --model, and --force options to rapidly scaffold filters.

Repository Layout

  • src/Filterable/Filter.php – abstract base class orchestrating the filter lifecycle and feature toggles.
  • src/Filterable/Concerns/ – traits implementing discrete behaviour (filter discovery, validation, caching, logging, performance, optimisation, rate limiting, etc.).
  • src/Filterable/Contracts/ – interfaces for the filter pipeline and the Eloquent scope signature.
  • src/Filterable/Traits/Filterable.php – model scope that forwards to a Filter instance.
  • src/Filterable/Console/MakeFilterCommand.php & src/Filterable/Console/stubs/ – Artisan generator and overrideable stub templates.
  • src/Filterable/Providers/FilterableServiceProvider.php – registers the package and console command via spatie/laravel-package-tools.
  • bin/ – executable scripts executed by the Composer lint, fix, and test commands.
  • tests/ – Orchestra Testbench suite with concern-focused tests and reusable fixtures in tests/Fixtures/.
  • assets/ – shared media used in documentation.
  • config/filterable.php – publishable defaults for feature toggles, cache TTL, and runtime options.
  • database/factories/ – reserved for additional factories should you extend the package.

Quick Start

1. Generate a filter

php artisan make:filter PostFilter --model=Post

--model wires the stub to your Eloquent model. Use --basic for an empty shell or --force to overwrite an existing class.

2. Implement filtering logic

<?phpnamespaceApp\Filters;
useFilterable\Filter;
useIlluminate\Database\Eloquent\Builder;
useIlluminate\Http\Request;
useIlluminate\Support\Carbon;
useIlluminate\Validation\Rule;
class PostFilter extends Filter
{
/** * Request keys that map straight to filter methods. * * Methods follow camelCased versions of the keys (e.g. published_after β†’ publishedAfter). */protectedarray$filters = ['status', 'published_after', 'q'];
publicfunction__construct(Request$request)
{
parent::__construct($request);
$this->enableFeatures([
'validation',
'optimization',
'filterChaining',
'valueTransformation',
]);
$this->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'published_after' => ['nullable', 'date'],
]);
$this->registerTransformer('published_after', fn ($value) => Carbon::parse($value));
$this->registerPreFilters(fn (Builder$query) => $query->where('is_visible', true));
$this->select(['id', 'title', 'status', 'published_at'])->with('author');
}
protectedfunctionstatus(string$value): void
{
$this->getBuilder()->where('status', $value);
}
protectedfunctionpublishedAfter(Carbon$date): void
{
$this->getBuilder()->whereDate('published_at', '>=', $date);
}
protectedfunctionq(string$term): void
{
$this->getBuilder()->where(function (Builder$query) use ($term) {
$query->where('title', 'like', "%{$term}%")
->orWhere('body', 'like', "%{$term}%");
});
}
}

Define protected array $filterMethodMap when you need to alias request keys to method names. Programmatic filters can be appended with appendFilterable('key', $value) before apply() runs. Supplying an Illuminate\Contracts\Cache\Repository or Psr\Log\LoggerInterface to the constructor immediately enables the caching and logging features.

3. Attach the scope to a model

<?phpnamespaceApp\Models;
useFilterable\Traits\Filterable;
useIlluminate\Database\Eloquent\Model;
class Post extends Model
{
use Filterable;
}

4. Run the filter pipeline

<?phpnamespaceApp\Http\Controllers;
useApp\Filters\PostFilter;
useApp\Http\Resources\PostResource;
useApp\Models\Post;
useIlluminate\Http\Request;
class PostController
{
publicfunctionindex(Request$request, PostFilter$filter)
{
$posts = Post::query()
->filter(
$filter
->forUser($request->user())
->enableFeature('caching')
->setOptions(['chunk_size' => 500])
)
->get();
return PostResource::collection($posts);
}
}

apply() may only be called once per instance; call reset() if you need to reuse a filter. Because the Filter base class uses Laravel's Conditionable trait, you can use helpers such as $filter->when($request->boolean('validate'), fn ($filter) => $filter->enableFeature('validation'));.

Lifecycle & Core API

  • apply(Builder $builder, ?array $options = []) binds the filter to a query, merges options, runs enabled concerns, and transitions the state from initialized β†’ applying β†’ applied. Re-applying without reset() raises a RuntimeException.
  • get() returns an Illuminate\Support\Collection of results, delegating to caching or memory-managed helpers when those features are active. runQuery() is a convenience wrapper for apply() + get().
  • count() respects smart caching (including tagged caches and memoised counts when enabled). toSql() exposes the raw SQL for debugging.
  • enableFeature(), enableFeatures(), disableFeature(), hasFeature() toggle concerns per instance; defaults may be set in config/filterable.php and are applied in the constructor.
  • setOption(), setOptions() persist runtime flags (for example chunk_size, use_chunking) that concerns such as OptimizesQueries and ManagesMemory consume.
  • reset() returns the filter to the initialized state so it can be applied again. getDebugInfo() surfaces state, filters applied, options, SQL/bindings, and metrics.

Feature Guides & API

Validation & Value Transformation

  • Enable with enableFeature('validation') and configure with setValidationRules(), addValidationRule(), and setValidationMessages(). Only active filters are validated and ValidationException is rethrown.
  • Enable valueTransformation to normalise inputs before filter methods execute. Register per-key transformers with registerTransformer() or bulk-array transforms with transformArray().
$filter->enableFeatures(['validation', 'valueTransformation'])
->setValidationRules([
'status' => ['nullable', Rule::in(['draft', 'published'])],
'tags' => ['array'],
])
->registerTransformer('tags', fn ($value) => array_map('intval', (array) $value));

Permissions & User Scope

  • forUser($user) scopes queries to the authenticated identifier and folds that identifier into cache keys automatically.
  • Enable permissions and declare requirements with setFilterPermissions(). Override userHasPermission() in your filter to plug into your authorisation layer; disallowed filters are dropped (and optionally logged) before execution.
$filter->enableFeature('permissions')
->forUser($request->user())
->setFilterPermissions(['email' => 'view-sensitive-fields']);

Rate Limiting

  • Enable with enableFeature('rateLimit'). Defaults allow 10 filters, a complexity budget of 100, and 60 attempts within a 60-second window; decay is ceil(complexity/10) seconds.
  • Tune guardrails with setMaxFilters(), setMaxComplexity(), and setFilterComplexity() (array-valued filters multiply complexity). Override resolveRateLimitMaxAttempts(), resolveRateLimitWindowSeconds(), or resolveRateLimitDecaySeconds() for finer control.
$filter->enableFeature('rateLimit')
->setMaxFilters(5)
->setMaxComplexity(25)
->setFilterComplexity(['tags' => 3, 'q' => 2]);

Caching & SmartCaching

  • Inject an Illuminate\Contracts\Cache\Repository or call enableFeature('caching') to activate caching. TTL defaults to 5 minutes or config('filterable.defaults.cache.ttl'); override per instance with setCacheExpiration().
  • Opt into result or count caching via cacheResults() / cacheCount(), and scope invalidation with cacheTags(), clearCache(), and clearRelatedCaches(). Cache keys include sanitised filter values and optional user identifiers from forUser().
  • SmartCaching will automatically cache more complex queries (multiple where clauses, joins, select statements) when caching is enabled, while skipping trivial single-clause lookups.
$filter->enableFeature('caching')
->cacheTags(['posts'])
->cacheResults()
->cacheCount()
->setCacheExpiration(15);
$posts = Post::query()->filter($filter)->get();
$total = $filter->count();

Logging & Performance Metrics

  • Inject a PSR-3 logger or call setLogger() + enableFeature('logging') to emit structured lifecycle logs. Hooks such as applyFilterable and cache-building log automatically when logging is active.
  • Enable performance to measure execution time, memory usage, and filter count; extend with addMetric() and read via getMetrics() / getExecutionTime().

Query Optimisation & Filter Chaining

  • Enable optimization to apply select(), with(), and chunkSize() before filters run; useIndex() can hint MySQL indexes when appropriate.
  • Enable filterChaining to queue fluent additions after request-driven filters: where(), whereIn(), whereNotIn(), whereBetween(), and orderBy() are supported.
$filter->enableFeatures(['optimization', 'filterChaining'])
->select(['id', 'title', 'status'])
->with(['author', 'tags'])
->chunkSize(500)
->where('status', 'published')
->orderBy('published_at', 'desc');

Memory Management

  • Enable memoryManagement for streaming helpers that avoid loading whole result sets into memory: lazy(), lazyEach(), cursor(), stream(), streamGenerator(), chunk(), map(), filter(), reduce().
  • executeQueryWithMemoryManagement() underpins get() when chunk_size is set; resolveChunkSize() honours chunk_size options or provided arguments. Call apply() before streaming helpers; misuse raises a RuntimeException.
$filter->enableFeature('memoryManagement')
->setOption('chunk_size', 250);
$filter->apply(Post::query());
$filter->lazyEach(fn ($post) => /* ... */, 250);

Pre-Filters & Manual Filters

  • Register global constraints with registerPreFilters(); they run before request-driven filters and are logged when logging is enabled.
  • Add programmatic filter values with appendFilterable(), or alias request keys to method names via protected array $filterMethodMap on your filter class. asCollectionFilter() returns a callable compatible with collection pipelines when you want to reuse filterables outside of Eloquent.

Debugging & Events

  • getDebugInfo() returns state, enabled features, options, SQL, bindings, and (when performance is enabled) metrics. Override handleFilteringException() to decide whether to swallow or rethrow non-validation errors.
  • Listen for FilterApplying, FilterApplied, and FilterFailed events around apply() to hook telemetry, notifications, or side effects.

Configuration

The publishable config/filterable.php controls defaults applied during filter construction:

return [
'defaults' => [
'features' => [
'validation' => false,
'permissions' => false,
'rateLimit' => false,
'caching' => false,
'logging' => false,
'performance' => false,
'optimization' => false,
'memoryManagement' => false,
'filterChaining' => false,
'valueTransformation' => false,
],
'options' => [/* runtime options seeded here */],
'cache' => ['ttl' => null],
],
];

Per-filter overrides always winβ€”call enableFeature(), disableFeature(), setOption(), or setCacheExpiration() inside individual filters when you need different defaults.

Artisan Generator & Stubs

php artisan make:filter scaffolds a filter class under App\Filters by default:

  • --basic emits a minimal filter without feature toggles.
  • --model=User imports the model and pre-fills a typed constructor parameter.
  • --force overwrites an existing class.

Publish customised stubs by copying src/Filterable/Console/stubs/ into your application's stubs/ directory; the command prefers application stubs when present.

Tooling & Scripts

Package maintenance scripts live in bin/ and are surfaced through Composer:

composer lint # Runs Tighten Duster lint mode + PHP syntax checks
composer fix # Formats with Duster and writes a timestamped log
composer test# Executes PHPUnit via bin/test.sh

./bin/test.sh accepts --filter=ClassName, --test=tests/FeatureTest.php, --coverage, and --parallel. ./bin/lint.sh --strict exits non-zero when any issue is detected.

Testing

The PHPUnit suite runs on Orchestra Testbench (phpunit.xml.dist). tests/TestCase.php provisions an in-memory sqlite schema (mocks table) and aliases factories under tests/Fixtures/. Each concern has a dedicated test file (for example CachingTest.php, ManagesMemoryTest.php) with partial mocks and fixtures such as MockFilterable, MockFilterableFactory, and TestFilter. End-to-end behaviour is exercised in tests/Integration/, which boots the full filter pipeline (feature defaults, caching, streaming, lifecycle events) against the in-memory database.

Run targeted subsets with:

./bin/test.sh --filter=SupportsFilterChainingTest
./bin/test.sh --test=tests/HandlesRateLimitingTest.php

Add new integration doubles under tests/Fixtures/ to stay aligned with the existing autoloading.

Frontend Usage

Send filter parameters as query strings from your clients:

awaitfetch('/posts?status=active&category_id=2');awaitfetch('/posts?tags[]=laravel&tags[]=performance&sort_by=created_at:desc');

Contributing

Please review AGENTS.md for contributor expectations around structure, tooling, and workflow. When ready:

  1. Fork the repository and create a feature branch (git checkout -b feature/my-change).
  2. Run composer lint and composer test (or ./bin/test.sh --coverage) before opening a PR.
  3. Describe the capabilities touched, newly exposed options, and verification commands in the pull request body.

License

This project is open-sourced under the MIT license. See LICENSE for the full text.

Authors

See contributors for the full list of collaborators.

Acknowledgements

Inspired by the flexibility of spatie/laravel-query-builder and Tighten's duster tooling.

About

πŸ” Enhance Laravel queries with adaptable, customisable filters and intelligent caching to improve both performance and functionality.

Topics

Resources

Security policy

Stars

195 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages