alizharb/laravel-hooks
Laravel Hooks provides a simple action/filter system for Laravel apps, letting you register and run hooks to extend packages and application code without modifying the original source. Ideal for modular, maintainable customizations.
Laravel Hooks is an extensibility engine for Laravel that lets you attach, discover, and execute behavior around events, models, and Blade views without touching package source. With v1.2.0 it now spans hooks, Eloquent model extensions, Blade fallbacks, and DTO-typed payloads.
Minimal setup:
composer require alizharb/laravel-hooks then php artisan vendor:publish --tag=hooks-config.Hook::on('name', Listener::class).php artisan hooks:clear (or your configured cache command) — this is the most common "why isn't my hook firing" fix.First use case to reach for: wrapping an existing feature with a hook so downstream apps can override behavior. E.g. drop a Hook::on('order.created') call in your service, then let other packages or app code subscribe. Run php artisan hooks:list (or the debug command) to confirm discovery.
If you're coming from v1.1.x, audit any hooks on hot paths before deploying — the Circuit Breaker in v1.2.0 can silence listeners automatically (see Gotchas).
Attribute-driven Eloquent extensions. For day-to-day use, the killer feature is #[DynamicRelation], #[DynamicCast], and #[DynamicScope] on listener classes. You can add a latestOrder relation or a isActive scope to a vendor model (e.g. User) without a migration or fork:
#[DynamicRelation(model: User::class, name: 'latestOrder')]
public function latestOrder(User $user) {
return $user->hasOne(Order::class)->latestOfMany();
}
Prefer this for additive behavior on models you don't own. Reach for the fluent API (Hook::model(User::class)->addRelation(...)) inside a service provider when you need conditional logic (e.g. only in non-production or behind a feature flag).
HasDynamicHooks trait. Reserve this for models where you genuinely need interception — dynamic methods and attribute filtering. It's powerful but adds magic; use it on your own models, not as a default on every model.
Type-safe DTOs. New in 1.2.0 — declare a hook signature with a DTO, and the package validates the payload automatically:
Hook::on('order.created', function (OrderCreatedDTO $dto) { ... });
This is the cleanest way to evolve an internal hook contract. Define the DTO in your app (or a shared package), version it, and let the framework reject malformed payloads at dispatch instead of deep in a listener.
Circuit breaker for integrations. Wrap third-party calls (webhooks, mail providers, external APIs) in a hook and let the breaker mute flaky listeners after N failures. Configure threshold + cooldown in config/hooks.php. Use it for anything non-critical; do not put critical business logic behind a hook you're willing to auto-silence.
Blade fallbacks. @hook('sidebar.widgets', 'fallbacks.sidebar') avoids the "empty UI in a fresh install" problem. Ship a sensible fallback view alongside every optional widget hook.
Modular integration. Zero-config with laravel-modular via modules_path() discovery. If you're already on that stack, no extra registration needed — hooks in module listeners get picked up.
null/skips silently by default. In staging, log breaker trips loudly and consider disabling the breaker (threshold 0 or off flag) so you catch regressions that would otherwise vanish in production.#[DynamicRelation] or changing DTO signatures, run the cache clear. Symptom: "my hook works in tinker but not in the app."@hook calls, but unusual formatting or concatenated view names may still be missed. Keep @hook calls on clean, single-name lines when documentation matters.#[DynamicRelation] with the same name on the same model will conflict; pick namespaced relation names (e.g. billing_latestOrder).@phpstan-* hints on dynamic method calls; the trait's magic isn't statically resolved.How can I help you explore Laravel packages today?