Search by

romanfedorskij / message-bus

wolfcharaa

Attribute-driven PHP message bus with flows, compiled registry, queue transport contracts, and immutable envelopes

Package info

github.com/wolfcharaa/message-bus

pkg:composer/romanfedorskij/message-bus

Statistics

Installs: 336

Dependents: 2

Suggesters: 0

Stars: 1

Open Issues: 0

v6.0.0 2026-09-10 22:59 UTC

README

Attribute-driven PHP message bus для command/query/event сценариев, async очередей, worker-ов, cache result и понятного runtime control.

Библиотека помогает вынести правила выполнения сообщений из application code в явный registry: какие сообщения есть, какие handlers их обрабатывают, какие flows используются, что выполняется sync, что уходит в queue, как это сериализуется, кешируется, ретраится и контролируется в production.

Зачем это ставить

MessageBus полезен, когда в приложении появляются такие проблемы:

  • controller/service начинает напрямую знать слишком много handlers;
  • sync command/query и async events смешаны в одном application code;
  • события надо отправлять в очередь и потом показывать frontend статус выполнения;
  • нужны стабильные message aliases и handler binding ids, чтобы refactoring PHP classes не ломал очередь;
  • нужны retry, delay, priority, cancellation и polling задач;
  • long-running workers надо ставить на pause, drain, restart или emergency kill;
  • нужен один подход для Symfony, Laravel, Spiral, Yii, Mezzio, Slim или standalone PHP.

Что предоставляет библиотека

Возможность Для чего нужна Подробности
dispatch() Выполнить sync query с result или sync command без result Quick start
publish() Опубликовать event в один или несколько handlers Event guide
Contextless handlers Выполнить небольшой handler без доступа к nested dispatch/publish Contextless handlers
Flows Разделить sync, async, queue, middleware и execution strategy Core concepts
Compiled registry Получить стабильную карту messages/handlers/aliases/bindings Core concepts
Registry diagnostics Проверить bindings, signatures, flows и project rules в CLI/CI Registry compilation
Payload serialization Выбрать JSON, PHP serialize, protobuf или custom payload Payload serialization
PostgreSQL queue Поставить async jobs в БД и запускать workers Async queue
Queue status/control Вернуть frontend queueMessageId, polling status и cancel Queue and worker
Worker control plane Управлять long-running workers через pause/resume/drain/stop/kill/restart Worker control plane
Cache result Кешировать результат query handler-а Cache result
PSR-11 integration Подключить handlers и infrastructure через container Container contract
Framework integration Подключить библиотеку в популярные frameworks Framework integration

Общая модель

MessageBus строится вокруг простой цепочки:

message -> envelope -> registry -> flow -> handler -> result / queue job

Что делает каждая часть:

Часть Простыми словами Зачем нужна
Message DTO с намерением или фактом Отделить business request/event от framework/controller кода
Handler Service, который выполняет работу Держать business logic в явной точке обработки
Registry Скомпилированная карта messages, handlers, aliases и bindings Не искать handlers в runtime магией и не держать wiring в голове
Envelope Message плюс metadata Передавать correlationId, causationId, headers, flow и bindingId
Flow Правило “как выполнять” Разделить sync, async, middleware, queue и strategy
Queue job SerializedEnvelope в transport Выполнить handler позже, в worker-е, с retry/status/cancel
Worker Runtime для queue jobs Надёжно брать задачи, выполнять handlers и обновлять lifecycle
Control plane Команды управления workers Pause, resume, drain, stop, kill, restart и status для production

Главная идея: application code публикует messages, а библиотека по registry и flow решает, какой handler выполнить сейчас, какой поставить в queue, как сохранить metadata, как вернуть результат и как дать backend/frontend наблюдать состояние.

Какие проблемы закрывает

1. Controller не должен знать все handlers

Без message bus controller часто напрямую вызывает services, events, queues и side effects.

С MessageBus controller отправляет один message:

$bus->dispatch(new CreateUserCommand($email, $name));

Дальше registry определяет primary command handler и flow. Если приложению нужен результат, используйте query message и QueryHandler.

2. Events должны быть fan-out, а не цепочкой ручных вызовов

Один event может иметь несколько subscribers:

$bus->publish(new UserCreatedEvent($userId));

Каждый subscriber получает свой bindingId, поэтому email, audit, webhook и analytics jobs становятся независимыми. Если один subscriber упал, остальные не обязаны падать вместе с ним.

3. Async job должен быть наблюдаемым

publish() возвращает PublishResult. Из него можно получить queueMessageId и вернуть его frontend.

Frontend может polling-ом спрашивать backend:

$status = $runtime->queueStatus()?->get($queueMessageId);

Это закрывает обычный UX: “мы приняли задачу, она выполняется, вот её статус”.

4. Долгие jobs должны уметь отменяться

Для running job можно запросить cancellation:

$runtime->queueControl()?->requestCancellation($queueMessageId);

Handler проверяет отмену кооперативно:

$context->throwIfCancellationRequested();

Так задача завершается контролируемо, а runner переводит её в cancelled.

5. Workers должны управляться в production

Long-running workers нельзя просто “запустить и забыть”. Им нужны диагностика и управляющие команды.

vendor/bin/message-bus worker:status --bootstrap=config/message_bus_runtime.php --children
vendor/bin/message-bus worker:pause --bootstrap=config/message_bus_runtime.php --group=emails
vendor/bin/message-bus worker:drain --bootstrap=config/message_bus_runtime.php --group=emails --reason="deploy"
vendor/bin/message-bus worker:restart --bootstrap=config/message_bus_runtime.php --worker-name=emails-worker

Это позволяет безопасно делать deploy, maintenance, emergency stop и restart через supervisor/docker/systemd.

6. Queue payload не должен зависеть от PHP class name

Для async сообщений используется MessageAlias, а для handler job - bindingId.

#[MessageAlias('user.created')]
final class UserCreatedEvent {}

#[EventSubscriber(
    message: UserCreatedEvent::class,
    flow: 'async',
    bindingId: 'user.created.send_welcome_email',
)]
final class SendWelcomeEmail {}

Если PHP class переименуют, старые queue jobs всё ещё можно восстановить по alias и binding id.

7. Библиотека не заменяет container и framework

MessageBus не пытается быть DI container, framework queue или application kernel.

Она ожидает PSR-11 container и использует его для:

  • handlers;
  • middleware;
  • context factories;
  • execution strategies;
  • queue/runtime infrastructure.

Это делает интеграцию одинаковой для Symfony, Laravel, Spiral, Yii, Mezzio, Slim и standalone PHP.

Что остаётся на стороне приложения

Библиотека предоставляет runtime и contracts, но не забирает у приложения business decisions.

Приложение отвечает за:

  • какие messages существуют;
  • какие handlers выполняют business logic;
  • какие dependencies нужны handlers;
  • какой container использовать;
  • какие flows и queues нужны для нагрузки;
  • как frontend показывает status/progress;
  • как supervisor/docker/systemd перезапускает workers;
  • какую serialization strategy выбрать для конкретного проекта.

MessageBus отвечает за:

  • dispatch/publish API;
  • envelope metadata;
  • handler registry;
  • sync/async execution flows;
  • queue job lifecycle;
  • retry/cancel/status contracts;
  • worker runtime;
  • worker control plane;
  • serializer contracts.

Как читать документацию

Если вы впервые открыли библиотеку, читайте в таком порядке:

  1. README до конца, чтобы понять общую модель.
  2. Quick start, чтобы собрать первый sync command.
  3. Contextless handlers, если handler не должен получать MessageBus context.
  4. Event guide, если нужны events и fan-out.
  5. Async queue, если нужны queue jobs и workers.
  6. Worker control plane, если workers будут жить в production.
  7. Migration v4 to v5, если обновляетесь с предыдущей версии.

Install

composer require romanfedorskij/message-bus

Requirements

Обязательно:

  • PHP ^8.3;
  • psr/container;
  • psr/clock;
  • psr/simple-cache;
  • psr/log;
  • symfony/console;
  • symfony/var-exporter;
  • ext-json.

Опционально:

  • ext-pdo_pgsql - для PostgreSQL queue transport;
  • ext-pcntl - для worker:run --mode=auto;
  • ext-posix - для process liveness checks и signals в worker:run --mode=auto;
  • ext-pgsql - для PostgreSQL diagnostics/native support.

Container не входит в библиотеку намеренно. Используйте любой PSR-11 compatible container, например:

Quick start

Минимальный sync query состоит из message, handler, container, registry и MessageBus.

1. Message

final class CreateUserQuery
{
    public function __construct(
        public readonly string $email,
        public readonly string $name,
    ) {
    }
}

Message - это DTO. Он описывает намерение или факт и не содержит business logic, database connection или framework request.

2. Handler

use Wolfcharaa\MessageBus\Attribute\QueryHandler;
use Wolfcharaa\MessageBus\Context\MessageContextInterface;

#[QueryHandler(message: CreateUserQuery::class)]
final class CreateUserHandler
{
    public function __invoke(CreateUserQuery $message, MessageContextInterface $context): string
    {
        return 'created:' . $message->email;
    }
}

Handler должен быть service в PSR-11 container. Dependencies передавайте через constructor, а не через message.

3. Container

use Wolfcharaa\MessageBus\Context\DefaultMessageContextFactory;
use Wolfcharaa\MessageBus\Execution\SequentialExecutionStrategy;

$container->set(CreateUserHandler::class, fn () => new CreateUserHandler());
$container->set(DefaultMessageContextFactory::class, fn () => new DefaultMessageContextFactory());
$container->set(SequentialExecutionStrategy::class, fn () => new SequentialExecutionStrategy());

4. Registry

use Wolfcharaa\MessageBus\Discovery\ClassListProvider;
use Wolfcharaa\MessageBus\Flow\FlowRegistry;
use Wolfcharaa\MessageBus\Registry\CompiledMessageRegistry;
use Wolfcharaa\MessageBus\Registry\MessageRegistryCompiler;

$definition = (new MessageRegistryCompiler())->compile(
    new ClassListProvider([
        CreateUserQuery::class,
        CreateUserHandler::class,
    ]),
    new FlowRegistry(),
    '6.0.0',
);

$registry = new CompiledMessageRegistry($definition);

Registry отвечает на вопросы: какие messages есть, какие handlers к ним привязаны, какие flows используются, какой binding является primary.

5. MessageBus

use Wolfcharaa\MessageBus\MessageBus;

$bus = new MessageBus(
    registry: $registry,
    flows: $registry->definition()->flows,
    container: $container,
);

$result = $bus->dispatch(new CreateUserQuery('user@example.com', 'Roman'));

dispatch() возвращает business result только для sync query. Sync command выполняется тем же методом, но command handler обязан возвращать void.

Подробный разбор quick start: docs/guides/quick-start.md.

Event quick start

Для events используйте MessageAlias и стабильный bindingId.

use Wolfcharaa\MessageBus\Attribute\EventSubscriber;
use Wolfcharaa\MessageBus\Attribute\MessageAlias;

#[MessageAlias('user.created')]
final class UserCreatedEvent
{
    public function __construct(public readonly string $userId) {}
}

#[EventSubscriber(
    message: UserCreatedEvent::class,
    flow: 'async',
    bindingId: 'user.created.send_welcome_email',
)]
final class SendWelcomeEmail
{
    public function __invoke(UserCreatedEvent $event): void
    {
    }
}

MessageAlias нужен для стабильного serialized name сообщения. bindingId нужен для стабильной identity конкретной handler job в queue.

Подробности: docs/guides/events.md.

Async queue и workers

Встроенный PostgreSQL runtime закрывает producer, queue storage, consumer, worker и status/control repositories.

use Wolfcharaa\MessageBus\Runtime\MessageBusRuntime;
use Wolfcharaa\MessageBus\Postgres\CallbackPdoConnectionProvider;
use Wolfcharaa\MessageBus\Postgres\PostgresRetryConfig;
use PDO;

$runtime = MessageBusRuntime::postgres(
    pdo: new CallbackPdoConnectionProvider(static fn (): PDO => new PDO($dsn, $user, $password)),
    registry: $registry,
    container: $container,
    flows: $flows,
    postgresRetryConfig: PostgresRetryConfig::default(),
);

Для production workers лучше передавать reconnect-capable provider, например CallbackPdoConnectionProvider. Если передать готовый PDO, runtime обернет его в StaticPdoConnectionProvider: обычные запросы будут работать, но reconnect невозможен, и при transient disconnect библиотека упадет с явной ошибкой.

Создать schema:

vendor/bin/message-bus schema:postgres --with=all

Для production migration можно использовать SQL templates из resources/postgres/schema/5.1.

Проверить schema:

vendor/bin/message-bus message-bus:postgres:schema:validate \
  --dsn='pgsql:host=127.0.0.1;port=5432;dbname=app' \
  --user='app' \
  --password='secret'

Запустить single worker:

vendor/bin/message-bus worker:run --bootstrap=config/message_bus_runtime.php

Запустить auto worker с child processes:

vendor/bin/message-bus worker:run \
  --bootstrap=config/message_bus_runtime.php \
  --mode=auto \
  --workers=4 \
  --worker-name=emails-worker \
  --worker-group=emails \
  --output-verbosity=normal \
  --output-format=text \
  --storage-failure-backoff=1000 \
  --max-heartbeat-failures=3

--output-verbosity управляет stdout/stderr событиями worker-а: quiet, normal, debug, trace. --output-format может быть text для Docker logs или json для log collectors. --storage-failure-backoff и --max-heartbeat-failures задают базовую hybrid failure policy после exhausted retry.

normal - безопасный default для long-running worker-а: он пишет lifecycle/control/job/storage события, но не пишет heartbeat каждую секунду в idle режиме. Для диагностики живости процесса включайте --output-verbosity=debug: heartbeat появится в stdout без изменения storage heartbeat механики.

Если worker запускается в Docker с log driver json-file, настройте ротацию логов на стороне приложения/deployment слоя:

logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "5"

Подробности: docs/guides/async-queue.md и docs/reference/queue-and-worker.md.

Worker control plane

Worker control plane нужен для эксплуатации long-running workers.

Самые частые команды:

vendor/bin/message-bus worker:status --bootstrap=config/message_bus_runtime.php --children
vendor/bin/message-bus worker:pause --bootstrap=config/message_bus_runtime.php --group=emails --reason="maintenance"
vendor/bin/message-bus worker:resume --bootstrap=config/message_bus_runtime.php --group=emails
vendor/bin/message-bus worker:drain --bootstrap=config/message_bus_runtime.php --group=emails --reason="deploy"
vendor/bin/message-bus worker:restart --bootstrap=config/message_bus_runtime.php --worker-name=emails-worker --reason="config reload"
vendor/bin/message-bus worker:kill --bootstrap=config/message_bus_runtime.php --worker-instance-id=emails-app-01-1 --reason="stuck child"

Коротко:

  • status - посмотреть живые workers и children;
  • pause - временно не брать новые jobs;
  • resume - вернуть paused workers в работу;
  • drain - перестать брать jobs, дождаться running children и выйти;
  • stop - штатно остановить worker;
  • kill - аварийно завершить children через signals;
  • restart - graceful drain и exit code для supervisor/docker/systemd.

Подробности и сценарии: docs/reference/worker-control-plane.md.

Payload serialization

По умолчанию используется JSON payload. Для PHP-only проектов можно использовать PHP serialize. Для protobuf/binary форматов используйте custom serializer с явным contentType.

Подробности: docs/guides/payload-serialization.md.

Миграция с v4 на v5

v5 не сохраняет совместимость registry/schema с v4.

Минимальный safe path:

  • остановить или drain-нуть v4 producers/workers;
  • дать v4 workers завершить старые jobs;
  • применить v5 schema;
  • пересобрать compiled registry cache;
  • задеплоить v5 producers/workers вместе;
  • запустить v5 workers.

Подробная инструкция: docs/migration/v4-to-v5.md.

Миграция с v5.0 на v5.1

v5.1 расширяет PostgreSQL schema для worker control-plane и добавляет schema validation.

Минимальный safe path:

  • остановить или drain-нуть v5.0 workers;
  • применить SQL из resources/postgres/schema/5.1/all.sql;
  • запустить message-bus:postgres:schema:validate;
  • перезапустить workers;
  • проверить worker:status.

Подробная инструкция: docs/migration/v5.0-to-v5.1.md.

Миграция с v5.2 на v6

v6 уточняет contract message bus: QueryHandler является единственным источником business result, CommandHandler обязан возвращать void, а contextless-вызов задаётся параметром contextAware: false на обычном handler attribute. DomainHandler, deprecated middleware pipeline и --deprecations удалены.

Подробная инструкция: docs/migration/v5.2-to-v6.md.

Framework integration

Библиотека не навязывает framework. Основной контракт - PSR-11 container.

Подключение для Generic PSR-11, Symfony, Laravel, Spiral и Yii3 вынесено в docs/guides/framework-integration.md.

Документация по разделам

Раздел Документ
Подробный быстрый старт docs/guides/quick-start.md
Handler без MessageBus context docs/guides/contextless-handlers.md
Компиляция registry и CLI diagnostics docs/guides/registry-compilation.md
События, MessageAlias и bindingId docs/guides/events.md
Async очередь и запуск worker-а docs/guides/async-queue.md
Сериализация payload docs/guides/payload-serialization.md
Миграция с v4 на v5 docs/migration/v4-to-v5.md
Миграция с v5.0 на v5.1 docs/migration/v5.0-to-v5.1.md
Миграция с v5.1 на v5.2 docs/migration/v5.1-to-v5.2.md
Миграция с v5.2 на v6 docs/migration/v5.2-to-v6.md
Основные концепции docs/reference/core-concepts.md
Контракт контейнера docs/reference/container-contract.md
Контракты очереди и worker-а docs/reference/queue-and-worker.md
Управление worker-ами docs/reference/worker-control-plane.md
Кеширование результата docs/guides/cache-result.md
Логирование через middleware docs/guides/logging.md
Подключение к frameworks docs/guides/framework-integration.md
Готовые примеры docs/examples

Tests

composer test

Default suite не запускает внешние integration tests.

PostgreSQL integration profile:

docker compose -f docker-compose.integration.yml up -d --wait
vendor/bin/phpunit -c phpunit.integration.xml.dist

Process/pcntl integration profile:

vendor/bin/phpunit -c phpunit.process.xml.dist

Если PostgreSQL уже поднят отдельно, можно передать DSN явно:

MESSAGE_BUS_TEST_PGSQL_DSN='pgsql:host=127.0.0.1;port=5432;dbname=messagebus' \
MESSAGE_BUS_TEST_PGSQL_USER='messagebus' \
MESSAGE_BUS_TEST_PGSQL_PASSWORD='messagebus' \
vendor/bin/phpunit -c phpunit.integration.xml.dist