Typed, synchronous WordPress-style actions and filters for Laravel. Pluggable lets packages and application modules react to events or contribute values without depending directly on each other. Dispatch uses the PSR-14 standard through Tukio.
You can install the package via Composer:
composer require jonquihote/pluggableLaravel discovers the package service provider and facade automatically. The package has no configuration, migrations, routes, views, or assets to publish.
Pluggable ships a Boost skill that teaches supported AI coding agents how to define, register, dispatch, and inspect actions and filters. After installing this package in an application that uses Laravel Boost, run:
php artisan boost:updateSelect the pluggable-development skill when prompted. For a new Boost setup,
php artisan boost:install --skills discovers the skill during installation.
Actions announce that something happened. Listeners perform side effects while the action object can carry results back to its caller.
<?php
namespace App\Pluggables;
use Pluggable\Pluggable\PluggableAction;
final class OrderPlaced extends PluggableAction
{
public function __construct(public readonly int $orderId) {}
}Dispatching is synchronous. Every listener finishes before dispatch() returns:
$action = OrderPlaced::dispatch($order->id);Filters pass a typed value through listeners. Each listener can replace that value, making filters useful for transformations and cross-module aggregation.
<?php
namespace App\Pluggables;
use Illuminate\Support\Collection;
use Pluggable\Pluggable\PluggableFilter;
/** @extends PluggableFilter<Collection<int, string>> */
final class NavigationItems extends PluggableFilter
{
/** @param Collection<int, string> $items */
public function __construct(Collection $items = new Collection)
{
parent::__construct($items);
}
}
$items = NavigationItems::dispatch()->getValue();Create one provider for each logical package or application module. $module is
introspection metadata only; it does not affect dispatch.
<?php
namespace App\Providers;
use App\Listeners\AddAccountNavigation;
use App\Listeners\SendOrderReceipt;
use App\Pluggables\NavigationItems;
use App\Pluggables\OrderPlaced;
use Pluggable\Pluggable\Providers\PluggableServiceProvider;
final class AppPluggableServiceProvider extends PluggableServiceProvider
{
protected string $module = 'app';
protected array $actions = [
OrderPlaced::class,
];
protected array $filters = [
NavigationItems::class,
];
protected array $listen = [
OrderPlaced::class => [
SendOrderReceipt::class,
],
NavigationItems::class => [
[AddAccountNavigation::class, 'handle'],
],
];
}Register this consumer provider in bootstrap/providers.php:
return [
App\Providers\AppServiceProvider::class,
App\Providers\AppPluggableServiceProvider::class,
];Listener classes are resolved lazily from Laravel's container, so constructor
injection works normally. A class entry invokes __invoke(); an array entry calls
the named method.
final readonly class SendOrderReceipt
{
public function __construct(private ReceiptSender $receipts) {}
public function __invoke(OrderPlaced $action): void
{
$this->receipts->sendForOrder($action->orderId);
}
}Override bootModuleEvents() when a module needs to bridge native Laravel events
into a pluggable action or filter. Keep framework-owned event subscriptions next
to module's pluggable declarations:
protected function bootModuleEvents(): void
{
Order::created(
fn (Order $order) => OrderPlaced::dispatch($order->getKey()),
);
}For ad-hoc closure listeners or tests, use the manager directly. Higher priority listeners run first:
pluggable()->listen(
NavigationItems::class,
fn (NavigationItems $filter) => $filter->getValue()->push('Account'),
priority: 20,
);Call $action->stopPropagation() or $filter->stopPropagation() to prevent later
listeners from running.
List registered actions, filters, and listener counts:
php artisan pluggable:list
php artisan pluggable:list --type=filter --module=appInspect one pluggable and its listeners:
php artisan pluggable:inspect 'App\Pluggables\OrderPlaced'Registry data is also available through pluggable()->registry() or the
Pluggable facade.
Use actions when one module owns a signal and another owns the reaction. Use filters when several modules contribute to or transform one value. Prefer direct calls or normal Laravel events inside one module. Dispatch is synchronous and in-process; listeners should enqueue jobs when work belongs in the background.
Thank you for considering contributing to Pluggable! Please review our contributing guide to get started.
Pluggable is open-sourced software licensed under the MIT license.