catchadmin/plugin-hook
A lightweight hook system for CatchAdmin that lets plugins register and listen for events, so you can extend core behaviour without patching framework code. Useful for inserting custom logic at defined extension points in a Laravel-based admin panel.
composer require catchadmin/plugin-hook
The package is small and Laravel-native: on Laravel 9+/10 it should be picked up by package auto-discovery through extra.laravel.providers. On older skeletons, or if you've vendored it as a path repository, add the service provider to config/app.php manually. Check for a publishable config with php artisan vendor:publish --provider="CatchAdmin\PluginHook\...ServiceProvider" — if nothing is published, the defaults are fine.
Where to look first. This package carries almost no documentation and zero dependents, so the source is the manual. Open, in order:
composer.json — PHP and illuminate/* constraints, so you don't fight your framework version.src/ — the service provider (what it binds into the container) and the hook manager class (the entire public API: add/listen, fire/apply, remove, priority).config/ — the default registry, if one ships.Expect the API surface to be a WordPress-shaped dispatcher: register a callback against a named string, fire it later, optionally pass a value through and take the return value back. Confirm the exact method names before writing production code — the assessment below uses Hook::addFilter / Hook::applyFilters / Hook::addAction / Hook::doAction as the conventional shape.
First use case. Don't retrofit the whole app. Pick one seam where core code currently hard-codes a list, and invert it.
// app/Http/Controllers/DashboardController.php
$widgets = Hook::applyFilters('dashboard.widgets', $defaultWidgets);
return view('dashboard', compact('widgets'));
Then let a plugin — or just another module in your own app — contribute:
// packages/acme/analytics/src/Providers/AnalyticsServiceProvider.php
public function boot(): void
{
Hook::addFilter('dashboard.widgets', function (array $widgets) {
$widgets[] = new AnalyticsWidget();
return $widgets;
}, priority: 20);
}
That's the whole value proposition: downstream code extends upstream code without a composer.json fork.
Separate actions from filters, rigorously. Actions are fire-and-forget side effects (Hook::doAction('order.paid', $order)). Filters transform and must return (Hook::applyFilters('invoice.number', $number)). Mixing them is how hook systems rot — a listener that mutates by side effect inside a filter chain is invisible to everyone downstream.
Namespace every hook you define. dashboard.widgets collides the moment a second plugin uses the same obvious name. Use vendor.package.point (acme.analytics.dashboard.widgets). You own the namespace for hooks you emit; plugins own theirs for hooks they consume.
Keep hook registration declarative in boot(). Register the callback, don't do work. A closure that queries the database at registration time will run on every request, including route:list and queue workers booting.
Use priority deliberately, and document it. Filters resolve in priority order (typically low number = earlier). If two plugins both append to a widget list and order matters, that ordering is now load-bearing production behavior. Write it down next to the hook declaration.
Wrap the emission point, not the call sites. Emit hooks from a single service or base controller, so you can add caching, logging, or a try/catch in one place later.
Model plugin lifecycle as hooks too. plugin.enabled, plugin.installed, plugin.uninstalling give you a place to purge cache, drop tables, or migrate data — much cleaner than reaching into plugin classes from your admin controller.
Test the seam directly. In feature tests, register a spy listener, hit the route, assert the value arrived. Give yourself a Hook::flush()-style reset (or rebuild the application) between tests, otherwise callbacks leak across test cases and produce order-dependent failures.
White-labeling is the killer app. One codebase, N clients: filters on brand name, menu entries, PDF templates, and notification channels. Far cheaper than branches.
Know when to stop. Hooks are synchronous, in-process, and stringly-typed. For domain events that need queues, retries, or multiple subscribers across services, use Laravel's native events. Reach for this package when you specifically need ordered, value-transforming, synchronous extension inside one request.
composer.json order.return yields null, and the next listener receives null. This fails silently. Consider a debug mode where the dispatcher asserts a non-null return when the input was non-null.php artisan config:cache in deploy will freeze them. Run config:clear in your deploy script or avoid config-driven registration.remove/forget method. Without one, disabling a plugin at runtime requires unregistering at boot. If it's missing, keep a per-hook enable flag in your own config and check it inside your callback."catchadmin/plugin-hook": "1.x.y"), and read the diff before every bump. This is a package extracted from CatchAdmin for internal use; the public API may not be treated as stable.Hook::getListeners('your.hook') dump (or whatever introspection the manager exposes) at the emission site, and log the resolved name at both ends.src/ end to end and write your own one-page README with the method signatures you actually use. Future-you and every teammate will thank you.How can I help you explore Laravel packages today?