artisanpack-ui/hooks
WordPress-style actions and filters for Laravel with helper functions, Action and Filter facades, and Blade directives. Register callbacks on named hooks, run them in priority order, and remove them when needed.
Install via Composer; Laravel package discovery registers the service providers and Action/Filter facade aliases automatically, so there is nothing to add to config/app.php.
composer require artisanpack-ui/hooks
First place to look: the README "Quick start" section, then resources/boost/guidelines/core.blade.php if you use Laravel Boost (v1.2.0+) — it contains AI-assistable examples of every API. The classes live under ArtisanPackUI\Hooks\ (Action, Filter) with helpers addAction, doAction, addFilter, applyFilters, plus the removal functions added in 1.1.0.
First use case — pick the hook style based on whether you need a return value:
order.placed, user.registered, view.rendering).use function addAction;
use function doAction;
use function addFilter;
use function applyFilters;
addAction('order.placed', fn ($order) => logger("Order {$order->id} placed"), 5);
doAction('order.placed', $order);
addFilter('price.display', fn (string $price, string $currency) => "{$currency} {$price}");
$display = applyFilters('price.display', '49.00', 'USD');
Facades and Blade directives are drop-in equivalents if that fits your codebase better:
use ArtisanPackUI\Hooks\Facades\Action;
use ArtisanPackUI\Hooks\Facades\Filter;
Action::add('user.registered', fn ($user) => \Log::info('Registered: '.$user->id));
Action::do('user.registered', $user);
$summary = Filter::apply('content.summary', $text);
@action('view.rendering', $post)
@filter('title.display', $post->title)
Define hooks at extension points, register callbacks in providers. Treat doAction() / applyFilters() calls as your package's public API. Core code dispatches; consumers (app code, add-on packages) subscribe. This is the pattern that makes the package genuinely useful — it's how you get WordPress-style pluggability without forking core.
// In core code — the extension point
public function placeOrder(Order $order): void
{
doAction('order.placed', $order);
$order->total = applyFilters('order.total', $order->total, $order);
}
Priority-based layering. Lower numbers run first (default 10). Use this semantically rather than arbitrarily: 5 for normalization/prep, 10 for the main behavior, 20+ for presentation or reporting. Same-priority callbacks run in registration order, which makes ordering fragile — assign distinct priorities when sequence matters.
Per-package hook namespacing. Prefer dotted, owned prefixes (acme.reports.export.before) over generic names (export.before). With only a global registry, a namespace collision between two packages silently chains unrelated callbacks together. Register callbacks in each package's service provider boot() method to keep the registration site discoverable.
Filters as pipeline stages. Because each callback receives the previous callback's return value, compose small single-purpose filters instead of one monolith:
addFilter('content.summary', fn ($t) => trim(strip_tags($t)), 5);
addFilter('content.summary', fn ($t) => str($t)->limit(120)->toString(), 10);
Removal for conditional behavior. The 1.1.0 removal APIs are the day-to-day tool for environment- or tenant-specific customization — e.g. strip a third-party callback in testing, or swap behavior per tenant:
if (app()->environment('testing')) {
removeAllFilters('content.summary', 20); // one priority only
removeAllActions('mail.send'); // entire hook
}
$removed = removeAction('order.placed', $callback); // returns bool
Note that remove* for a single callback needs the same callable instance and the same priority it was added with. Store the closure in a variable or use an invokable class so you keep the reference.
Testing pattern. Since callbacks are global state on singletons, clean up between tests with removeAllActions($hook) / removeAllFilters($hook) in tearDown, or assert effects rather than registry contents. Pest is what the package itself uses.
Blade integration. Use @filter only when you want the echoed result; for side effects prefer @action. For anything more than a single call, move the logic into a view composer or provider and keep Blade declarative.
Filters must return. A filter callback that returns null (a bare fn ($v) => logger($v)) silently destroys the value for every downstream callback. There is no exception — the chain just continues with null. This is the single most common bug with this style of package.
applyFilters argument order is fixed. The value is the second parameter; everything after it is passed verbatim to every callback. Callbacks must accept the same trailing signature, so add trailing args carefully — it's a contract change for all subscribers.
Removal requires matching the callable and priority. removeAction('hook', $fn) defaults to priority 10; if you registered at 20, the call is a no-op returning false. Pass the priority explicitly. Closures created inline cannot be removed later — always capture them.
removeAll($hook, $priority) uses int|false. Omit the second argument (or pass false) to clear the whole hook; passing 0 is not a reset flag, it targets priority zero.
Global singleton state and Octane/queue workers. The registry is a container singleton and lives for the request lifecycle. Under long-running workers (Octane, queue daemons) registrations made at runtime persist across jobs/requests. Register in service providers during boot, and be careful with dynamic registration inside controllers — or flush explicitly.
No debug/introspection surface documented. There's no hasAction, didAction, or "list registered hooks" API in the README. When debugging a hook that "doesn't fire," check the spelling of the dotted name and whether the dispatch actually runs — you'll likely add a temporary addAction('*')-style logger or a wrapper around doAction yourself.
Blade directive syntax is positional-only. @action('hook', $args...) and @filter('hook', $value, $args...) spread their arguments; you cannot pass a named array and have it splatted. Build the array and spread it in PHP if you need dynamic argument lists.
Version floor. Requires PHP 8.2+; the changelog's CI is standardized on PHP 8.4. On older PHP 8.0/8.1 apps this package will not install — relevant if you're backporting into a legacy Laravel 10 codebase.
Extension point for tooling: if you add your own conventions (e.g. auto-discovery of hook registrations), the Action and Filter singletons are resolvable from the container — wrap them rather than reaching for the global helpers, so you keep testability and can swap implementations.
Maturity caveat: 0 stars and 0 dependents means the ecosystem is thin — no community-documented pitfalls, so pin the version in composer.json and cover your own hook contracts with tests, since a breaking change upstream would be quiet.
How can I help you explore Laravel packages today?