blessing/filter
blessing/filter is a Laravel package for organizing and applying query filters in a clean, reusable way. Define filters once, compose them with Eloquent queries, and keep controllers and models lean. Ideal for APIs and admin lists that need flexible, maintainable search. Simple to extend.
blessing/filter is a small, dependency-light PHP library that gives you a WordPress-style filter/hook registry so a host application can expose extension points and plugins can attach to them without touching core code. It has effectively zero public adoption (0 stars, 0 dependents), so treat the repository itself as the documentation.
Install and pin
composer require blessing/filter
Because the package is unmaintained-by-popularity, commit your composer.lock and pin a caret version only after reading src/. There are no downstream dependents to inherit compatibility guarantees from.
Where to look first, in order
composer.json — check the required PHP version and whether it pulls in illuminate/support (it may be Laravel-flavored) or is framework-agnostic.src/ — usually one or two classes. Read every public method; the entire API surface is probably under 10 methods (add, apply, remove, has, and maybe a priority argument).tests/ — if present, this is your real documentation. The test cases show the exact argument order and return-value contract.README.md — often stale or empty for packages like this; trust the source over the README.First use case: expose an extension point
The canonical flow is declare a hook name in core, let plugins register callbacks, apply the filter at the point of use:
use Blessing\Filter;
// 1. Core declares and later applies the filter
$title = $filter->apply('page.title', $title, [$page]);
// 2. A plugin registers against it
$filter->add('page.title', function ($title, $page) {
return $title . ' — ' . $page->owner;
});
Get this round-trip working (register → apply → see the transformed value) before building any plugin-loading machinery on top of it.
Namespace your hook names aggressively. Since the registry is a flat string map, collisions between plugins are the most common real-world failure. Use dot-separated, package-prefixed names: acme.shop.cart.total, not cart_total. Establish the convention in core before the first plugin ships — retrofitting is painful.
Always return the value from a filter callback. A filter is a transformation pipeline, not a notification. A callback that forgets return will inject null into the chain and silently break downstream logic. This is the single biggest source of "why is my value empty" bugs.
Use priority arguments for ordering, but don't over-rely on them. Most filter implementations accept an integer priority (lower runs first, WordPress-style) with registration order as a tiebreaker. Reserve distinct priority bands per concern (e.g. 10 = defaults, 20 = user config, 30 = overrides) rather than hand-tuning integers per plugin.
Wrap the registry in your own service. Do not scatter $filter->apply('...') strings across the codebase. Create a thin domain layer so hook names are discoverable and tests can mock one object:
final class Hooks
{
public function __construct(private Filter $filter) {}
public function pageTitle(string $title, Page $page): string
{
return $this->filter->apply('cms.page.title', $title, [$page]);
}
}
This also gives you one place to add logging, timing, or a "strict mode" that throws on unknown hook names.
Load plugins through a bootstrap contract. Give plugins a single entry point (register(Filter $filter): void) and require them to register everything there. Loading order then becomes the only variable, and you can snapshot registered hooks per plugin for diagnostics.
Pass a context array as the last argument, not extra positional parameters. A [$model, $request] context array lets you add data later without breaking existing callbacks — the same reason Laravel's event payloads are associative.
Testing pattern: in tests, register stubs against the same hook names core uses, assert the transformed output, and call the removal method in tearDown() so state does not leak between tests. If the package uses a static/global registry (common in WordPress-likes), you must reset it — otherwise test order becomes load-bearing.
Laravel integration: bind the registry as a singleton in a service provider, register core filters during boot(), then load plugins after. The package almost certainly has no auto-discovery, so this wiring is yours to write.
null chain break. Mentioned twice because it is the #1 bug. Consider a debug mode that wraps callbacks and throws if a non-void hook returns null.remove() with a closure literal rarely works unless the implementation compares by reference. Store the closure in a property ($this->handler = fn(...)) so you can pass the identical instance back for removal, and always remove in tearDown/unregister.callable loosely. A plugin returning an int where core expects string will not be caught until it reaches the view layer. Validate the return value at the apply() call site in core.apply() on its own hook name will loop until the stack blows. Guard with a re-entrancy flag in your wrapper if plugins can plausibly do this.usort with priority only), equal priorities may reorder between PHP versions. Add an incrementing sequence number as a secondary sort key if you need deterministic output.Blessing\Filter and whether the package ships a facade/service provider. With zero dependents and a low opportunity score, do not assume Laravel auto-discovery — verify extra.laravel in composer.json.How can I help you explore Laravel packages today?