jonquihote/pluggable

This package allows developers to plug their custom code into Action/Filter hooks, similar to WordPress

Maintainers

Package info

github.com/jonquihote/pluggable

pkg:composer/jonquihote/pluggable

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-08-08 13:33 UTC

This package is auto-updated.

Last update: 2026-08-08 13:35:04 UTC


README

Pluggable

Packagist PHP from Packagist Laravel versions GitHub Workflow Status (main) Total Downloads

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.

Installation

You can install the package via Composer:

composer require jonquihote/pluggable

Laravel discovers the package service provider and facade automatically. The package has no configuration, migrations, routes, views, or assets to publish.

Laravel Boost

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:update

Select the pluggable-development skill when prompted. For a new Boost setup, php artisan boost:install --skills discovers the skill during installation.

Actions

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

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();

Registering pluggables and listeners

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.

Introspection

List registered actions, filters, and listener counts:

php artisan pluggable:list
php artisan pluggable:list --type=filter --module=app

Inspect one pluggable and its listeners:

php artisan pluggable:inspect 'App\Pluggables\OrderPlaced'

Registry data is also available through pluggable()->registry() or the Pluggable facade.

When to use Pluggable

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.

Contributing

Thank you for considering contributing to Pluggable! Please review our contributing guide to get started.

Credits

License

Pluggable is open-sourced software licensed under the MIT license.