WP Manifestindependent plugin directory
manifest / developer / event-dispatcher

Event Dispatcher

Provides tools that allow your application components to communicate with each other by dispatching events and listening to them

★ 1stars
v1.0.5latest · 19d ago

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/sympress/event-dispatcher/archive/refs/heads/main.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

Standalone WordPress MU-plugin for Symfony-style class-based events, listeners, and subscribers on top of native WordPress hooks and filters.

Requirements

  • WordPress 6.9
  • PHP 8.5

Design Goals

  • Symfony-like listener and subscriber API
  • PSR-14 compliant dispatcher and listener provider contracts
  • Class-based WordPress action and filter events
  • Immutable event value objects
  • Native WordPress hook bridge with one callback registration per event class
  • Container-friendly bootstrap for SymPress\Kernel\App
  • Enterprise-focused QA with PHPUnit, PHPStan, PHPCS, and PHP CS Fixer

Installation

  1. Place the package in wp-content/mu-plugins/event-dispatcher/.
  2. Ensure Composer autoloading is available.
  3. Add a root MU loader file because WordPress does not autoload subdirectories.
<?php

declare(strict_types=1);

require WPMU_PLUGIN_DIR . '/event-dispatcher/event-dispatcher.php';

Registering Subscribers

use SymPress\EventDispatcher\Application\EventSystem;
use SymPress\EventDispatcher\Contract\EventSubscriberInterface;
use SymPress\EventDispatcher\Event\AbstractFilterEvent;

final readonly class UploadMimesEvent extends AbstractFilterEvent
{
    public function __construct(
        array $mimes,
        public int $userId,
    )
    {
        parent::__construct($mimes, [$mimes, $userId]);
    }

    public static function hookName(): string
    {
        return 'upload_mimes';
    }

    public static function acceptedArgs(): int
    {
        return 2;
    }

    public static function fromHookArguments(array $arguments): static
    {
        return new self(
            (array) ($arguments[0] ?? []),
            (int) ($arguments[1] ?? 0),
        );
    }

    /**
     * @return array<string, string>
     */
    public function mimes(): array
    {
        /** @var array<string, string> $mimes */
        $mimes = $this->value();

        return $mimes;
    }

    public function withAllowed(string $extension, string $mimeType): self
    {
        $mimes = $this->mimes();
        $mimes[$extension] = $mimeType;

        return new self($mimes, $this->userId);
    }
}

final class UploadSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            UploadMimesEvent::class => ['allowSvg', 100],
        ];
    }

    public function allowSvg(UploadMimesEvent $event): UploadMimesEvent
    {
        return $event->withAllowed('svg', 'image/svg+xml');
    }
}

add_action(
    EventSystem::REGISTER_HOOK,
    static function ($dispatcher): void {
        $dispatcher->addSubscriber(new UploadSubscriber());
    },
);

Filter Strategy

Filter sind bereits implementiert.

  • Das erste native Filter-Argument ist der Payload des Events.
  • Filter-Events sind immutable Value Objects.
  • Listener mutieren nie in-place, sondern geben bei Änderungen eine neue Event-Instanz zurück.
  • Der letzte Event-Stand wird über toHookResult() zurück in den nativen WordPress-Filter geschrieben.

Kurz gesagt: apply_filters() bleibt nach außen WordPress-kompatibel, intern arbeitest du aber mit sauberen immutable Events.

Registering Attribute-Based Listeners

use SymPress\EventDispatcher\Application\EventSystem;
use SymPress\EventDispatcher\Attribute\AsEventListener;
use SymPress\EventDispatcher\Attribute\AsEventSubscriber;

#[AsEventSubscriber(event: UploadMimesEvent::class, method: 'allowSvg', priority: 100)]
final class UploadListener
{
    public function allowSvg(UploadMimesEvent $event): UploadMimesEvent
    {
        return $event->withAllowed('svg', 'image/svg+xml');
    }

    #[AsEventListener(priority: 0)]
    public function allowWebp(UploadMimesEvent $event): UploadMimesEvent
    {
        return $event->withAllowed('webp', 'image/webp');
    }
}

add_action(
    EventSystem::REGISTER_HOOK,
    static function ($dispatcher): void {
        $dispatcher->register(new UploadListener());
    },
);

Use Cases

1. WordPress Filter: MIME Types erweitern

use SymPress\EventDispatcher\Application\EventSystem;
use SymPress\EventDispatcher\Event\AbstractFilterEvent;

final readonly class UploadMimesEvent extends AbstractFilterEvent
{
    /**
     * @param array<string, string> $mimes
     */
    public function __construct(
        array $mimes,
        public int $userId,
    ) {
        parent::__construct($mimes, [$mimes, $userId]);
    }

    public static function hookName(): string
    {
        return 'upload_mimes';
    }

    public static function acceptedArgs(): int
    {
        return 2;
    }

    public static function fromHookArguments(array $arguments): static
    {
        return new self(
            (array) ($arguments[0] ?? []),
            (int) ($arguments[1] ?? 0),
        );
    }

    /**
     * @return array<string, string>
     */
    public function mimes(): array
    {
        /** @var array<string, string> $mimes */
        $mimes = $this->value();

        return $mimes;
    }

    public function withAllowed(string $extension, string $mimeType): self
    {
        $mimes = $this->mimes();
        $mimes[$extension] = $mimeType;

        return new self($mimes, $this->userId);
    }
}

add_action(
    EventSystem::REGISTER_HOOK,
    static function ($dispatcher): void {
        $dispatcher->addListener(
            UploadMimesEvent::class,
            static fn (UploadMimesEvent $event): UploadMimesEvent => $event->withAllowed(
                'svg',
                'image/svg+xml',
            ),
            100,
        );
    },
);

2. WordPress Action: Typed save_post

use SymPress\EventDispatcher\Application\EventSystem;
use SymPress\EventDispatcher\Event\AbstractActionEvent;

final readonly class SavePostEvent extends AbstractActionEvent
{
    public function __construct(
        public int $postId,
        public bool $update,
    ) {
        parent::__construct([$postId, $update]);
    }

    public static function hookName(): string
    {
        return 'save_post';
    }

    public static function acceptedArgs(): int
    {
        return 2;
    }

    public static function fromHookArguments(array $arguments): static
    {
        return new self(
            (int) ($arguments[0] ?? 0),
            (bool) ($arguments[1] ?? false),
        );
    }
}

add_action(
    EventSystem::REGISTER_HOOK,
    static function ($dispatcher): void {
        $dispatcher->addListener(
            SavePostEvent::class,
            static function (SavePostEvent $event): void {
                if (!$event->update) {
                    return;
                }

                error_log('Updated post: ' . $event->postId);
            },
        );
    },
);

3. Attribute-Based Hook Service

use SymPress\EventDispatcher\Application\EventSystem;
use SymPress\EventDispatcher\Attribute\AsEventListener;
use SymPress\EventDispatcher\Attribute\AsEventSubscriber;

#[AsEventSubscriber(event: SavePostEvent::class, method: 'onSavePost')]
final class EditorialWorkflowListener
{
    public function onSavePost(SavePostEvent $event): void
    {
        if (!$event->update) {
            return;
        }

        error_log('Editorial workflow for post ' . $event->postId);
    }

    #[AsEventListener(priority: 100)]
    public function allowSvg(UploadMimesEvent $event): UploadMimesEvent
    {
        return $event->withAllowed('svg', 'image/svg+xml');
    }
}

add_action(
    EventSystem::REGISTER_HOOK,
    static function ($dispatcher): void {
        $dispatcher->register(new EditorialWorkflowListener());
    },
);

4. Pure Domain Event Without WordPress Hook

use SymPress\EventDispatcher\Application\EventSystem;
use SymPress\EventDispatcher\Event\AbstractEvent;

final readonly class OrderPaidEvent extends AbstractEvent
{
    public function __construct(
        public int $orderId,
        public string $paymentReference,
    ) {
    }
}

$dispatcher = EventSystem::getInstance()->getDispatcher();

$dispatcher->addListener(
    OrderPaidEvent::class,
    static function (OrderPaidEvent $event): void {
        error_log(
            sprintf(
                'Order %d paid with reference %s',
                $event->orderId,
                $event->paymentReference,
            ),
        );
    },
);

$dispatcher->dispatch(new OrderPaidEvent(42, 'PAY-123'));

5. Invokable Attribute Listener for Domain Events

use SymPress\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: OrderPaidEvent::class, priority: 50)]
final class SendInvoiceListener
{
    public function __invoke(OrderPaidEvent $event): void
    {
        error_log('Send invoice for order ' . $event->orderId);
    }
}

add_action(
    EventSystem::READY_HOOK,
    static function ($dispatcher): void {
        $dispatcher->register(new SendInvoiceListener());
    },
);

Runtime Model

  • EventSystem owns the singleton bootstrap lifecycle.
  • HookEventDispatcher implements PSR-14 dispatching and listener resolution plus the WordPress hook bridge.
  • HookEventDispatcher::register() resolves AsEventListener and AsEventSubscriber on classes and methods.
  • AbstractActionEvent maps native actions to typed event objects.
  • AbstractFilterEvent maps native filters to immutable event value objects; listeners return a new event instance when they change data.
  • Filter listeners should return the same event unchanged or a new event instance of the same class.
  • Subscribers follow the Symfony event subscriber format.

Development

composer qa

License

This package is licensed under GPL-2.0-or-later.

Compiled listener lifecycle

Autoconfigured AsEventListener/AsEventSubscriber services are resolved once during container compilation and registered through a lazy service locator before the EventSystem ready hook. Keep listeners private; their constructor runs on first matching dispatch. The manual register API remains supported and caches class attribute definitions rather than reflecting on every dispatch.

Removing the last effective listener removes exactly the dispatcher's native WP callback and priority, preserving unrelated callbacks and inherited/interface listeners. Re-registration installs one callback. Already-started/completed bootstrap hooks (muplugins_loaded, plugins_loaded, setup_theme, after_setup_theme, init, wp_loaded) reject late listeners. Repeated actions/filters report missed invocations through _doing_it_wrong and observe future invocations only. Register bootstrap listeners before the relevant WordPress lifecycle boundary.

Releases

6 releases.

Tag
Published
v1.0.5 latest
Oct 3, 2026 19d ago
Oct 3, 2026 19d ago
Oct 2, 2026 20d ago
Oct 2, 2026 20d ago
Jun 19, 2026 4mo ago
Jun 14, 2026 4mo ago

These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.