PHPackages                             zeusi/asyncapi-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. zeusi/asyncapi-bundle

ActiveSymfony-bundle[API Development](/categories/api)

zeusi/asyncapi-bundle
=====================

Symfony bundle to generate AsyncAPI 3.x documentation from your message DTOs, always in sync with the code

0.2.0(1mo ago)00MITPHP &gt;=8.1

Since Jun 21Compare

[ Source](https://github.com/antonioturdo/asyncapi-bundle)[ Packagist](https://packagist.org/packages/zeusi/asyncapi-bundle)[ RSS](/packages/zeusi-asyncapi-bundle/feed)WikiDiscussions Synced 2w ago

READMEChangelog (2)Dependencies (38)Versions (3)Used By (0)

AsyncAPI Bundle
===============

[](#asyncapi-bundle)

[![Packagist Version](https://camo.githubusercontent.com/46b84104f95317eac67e89f5ce54f1928cc8b4a26a93f90a107fcabe5b499280/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7a657573692f6173796e636170692d62756e646c652e737667)](https://packagist.org/packages/zeusi/asyncapi-bundle)[![PHP](https://camo.githubusercontent.com/7af7d4dfef5d061a6b3cbf5834f0e449e6cef793f26372034dbc815ae3f3de73/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253345253344382e312d3737376262342e737667)](https://www.php.net/)[![CI](https://github.com/antonioturdo/asyncapi-bundle/actions/workflows/ci.yml/badge.svg)](https://github.com/antonioturdo/asyncapi-bundle/actions/workflows/ci.yml)[![PHPStan](https://camo.githubusercontent.com/6e33b77b51a771e6655e2b9229dca1682109fdbaae399ff6b2d6d7a4d5e35db7/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068707374616e2d6c6576656c253230382d627269676874677265656e2e737667)](https://phpstan.org/)[![Coverage](https://camo.githubusercontent.com/08c4c00f930f7fc15847f3d54d9676cf4b14692a96216220663788d6567a1aa4/68747470733a2f2f636f6465636f762e696f2f67682f616e746f6e696f747572646f2f6173796e636170692d62756e646c652f67726170682f62616467652e737667)](https://codecov.io/gh/antonioturdo/asyncapi-bundle)[![License](https://camo.githubusercontent.com/4d639f68d04da98b1da00976cb8b781707a1b09f5f7053653e7abc15d5ac325a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f7a657573692f6173796e636170692d62756e646c652e737667)](LICENSE)

> Symfony bundle to generate AsyncAPI 3.x documentation from your message DTOs, always in sync with the code.

Unlike REST API tooling, which can enumerate controllers and routes, an event-driven app offers no natural anchor for discovering its messages — and AsyncAPI doesn't impose one. So the bundle takes the simplest pragmatic route: each message is declared with an attribute on its DTO. Its payload schema is derived from the PHP class (via [`zeusi/json-schema-extractor`](https://github.com/antonioturdo/json-schema-extractor)) and assembled into a valid [AsyncAPI](https://www.asyncapi.com) 3.x document. Because the schema comes from the code, changing a DTO changes the docs.

Features
--------

[](#features)

- **Code-derivable AsyncAPI 3.x** — messages, channels, operations and reusable `components.messages`, assembled from your DTOs.
- **Payload schemas from your PHP** — derived from native types, PHPDoc, Symfony Validator constraints and Serializer metadata (via [`zeusi/json-schema-extractor`](https://github.com/antonioturdo/json-schema-extractor)).
- **AsyncAPI 3.x** — supports the 3.0.0 and 3.1.0 versions; defaults to 3.1.0.
- **Two endpoints** — `GET /asyncapi.json` serves the document as JSON, `GET /asyncapi` renders it as an HTML UI.
- **Symfony Messenger integration** — derives servers, content types and channel↔server links from your transports, and can discover messages straight from your routing (opt-in).
- **Extensible by design** — pluggable message sources and a processor pipeline to add or refine anything in the document.
- **Bring your own extraction** — swap the payload extractor, and supply a per-message extraction context (e.g. serialization groups).

Requirements
------------

[](#requirements)

- PHP 8.1+
- Symfony 6.4, 7.x, or 8.x

Installation
------------

[](#installation)

```
composer require zeusi/asyncapi-bundle
```

If you don't use Symfony Flex, register the bundle in `config/bundles.php`:

```
return [
    // ...
    Zeusi\AsyncApiBundle\AsyncApiBundle::class => ['all' => true],
];
```

Minimal configuration — just your document's `info`:

```
# config/packages/asyncapi.yaml
asyncapi:
  document:
    info:
      title: 'Wanderlust Events'
      version: '1.0.0'
```

Declare a message
-----------------

[](#declare-a-message)

Mark each message DTO with `#[AsyncApiMessage]`:

```
use Zeusi\AsyncApiBundle\Attribute\AsyncApiMessage;

#[AsyncApiMessage]
final class TripBooked
{
    public function __construct(
        public readonly string $bookingId,
        public readonly int $travelers,
    ) {
    }
}
```

The bundle scans your code for `#[AsyncApiMessage]`, derives each payload schema from the DTO, and assembles a valid AsyncAPI 3.x document. You'll typically add a `channel` (and other metadata) — see the [attribute reference](#attribute-reference).

Expose the document and UI
--------------------------

[](#expose-the-document-and-ui)

Import the bundle's controller routes:

```
# config/routes/asyncapi.yaml
asyncapi:
  resource: '@AsyncApiBundle/Controller/AsyncApiController.php'
  type: attribute
```

You now have:

- `GET /asyncapi.json` — the generated AsyncAPI document as JSON. Always available.
- `GET /asyncapi` — an HTML UI rendered with the AsyncAPI web component. This one needs Twig: install `symfony/twig-bundle` to enable it.

Configuration reference
-----------------------

[](#configuration-reference)

```
asyncapi:
  # Static AsyncAPI fragment merged into the generated document. Put any static
  # parts here — info, servers, security, tags, externalDocs…: modelled fields are
  # folded into the typed model, anything else is kept verbatim. The common ones:
  document:
    # Spec version. Optional; defaults to the latest supported (3.1.0).
    # Supported: 3.0.0, 3.1.0. An unsupported value is rejected.
    asyncapi: '3.1.0'
    info:
      title: 'Wanderlust Events'
      version: '1.0.0'
      description: 'Events Wanderlust publishes to its shared broker.'
    servers:
      production:
        host: 'broker.wanderlust.example:5672'
        protocol: 'amqp'

  # Message sources and integrations.
  providers:
    # The built-in source: scans for #[AsyncApiMessage] classes.
    # Paths default to the project's PSR-4 roots when omitted.
    attribute:
      paths:
        - '%kernel.project_dir%/src/Message'

    # Symfony Messenger integration (needs symfony/messenger). Two opt-in
    # capabilities, both off by default. See the "Symfony Messenger" section below.
    messenger:
      # Allowlist of transport names the integration considers; empty means all.
      # Failure/retry transports are always excluded. Scopes both capabilities.
      transports: ['events']
      # Enrich documented messages from your transports: servers, content types,
      # channel↔server links. Enable when your Messenger transports are how you publish.
      enrichment: true
      # Discover messages from the routing map (not just from attributes): the
      # FQCN keys you already wrote, plus interface/parent/namespace-wildcard
      # matches. Scoped by `transports`.
      discovery: false

  # Payload schema derivation (see "Payload schemas"). Shorthand: a bare string
  # `payload_schema_extractor: 'app.my_extractor'` sets just the service id.
  payload_schema_extractor:
    # Service id of the SchemaExtractor. Defaults to a built-in one
    # (Symfony Serializer when available, else json_encode).
    service: 'app.my_schema_extractor'
    # Optional service implementing Payload\ExtractionContextFactory: builds the
    # ExtractionContext (e.g. serialization groups) passed to the extractor per
    # message. Omit it (the default) to call the extractor without a context.
    context_factory: 'app.my_context_factory'

  # Options forwarded to the AsyncAPI web component (the HTML UI).
  ui:
    config:
      show:
        sidebar: true
        errors: true
    css_import_path: 'https://unpkg.com/@asyncapi/react-component@3.1.3/styles/default.min.css'
```

Attribute reference
-------------------

[](#attribute-reference)

`#[AsyncApiMessage]` carries the message-level documentation and its placement.

ArgumentTypeDefaultPurpose`channel``?string``null`Grouping key; `null` gives the message a channel of its own.`action``OperationAction``Send``Send` (the app publishes) or `Receive` (the app consumes).`name``?string`class short nameMessage name / components key.`title``?string`the message nameHuman title (shown in the UI).`summary``?string``null`One-line summary.`description``?string``null`Longer description (CommonMark).`contentType``?string``null` → `application/json`Payload content type. Left `null`, a routed Messenger transport's serializer can set it; otherwise it renders as `application/json`.`tags``list``[]`Free-form tags; a plain string is shorthand for `new Tag(name: …)`.`externalDocs``?ExternalDocumentation``null`Link to external documentation for this message.`correlationId``?CorrelationId``null`Where the correlation id lives in the message (a runtime expression).> The message name defaults to the class short name. For a stable public contract, set an explicit `name` so renaming the PHP class doesn't change it (a duplicate name across two classes is reported as a warning).

The richer arguments take value objects, constructed inline in the attribute:

```
use Zeusi\AsyncApiBundle\Attribute\AsyncApiMessage;
use Zeusi\AsyncApiBundle\Document\CorrelationId;
use Zeusi\AsyncApiBundle\Document\ExternalDocumentation;
use Zeusi\AsyncApiBundle\Document\Tag;

#[AsyncApiMessage(
    channel: 'bookings',
    summary: 'A trip was booked',
    tags: ['travel', new Tag(name: 'public', description: 'Part of the public contract')],
    externalDocs: new ExternalDocumentation(url: 'https://docs.wanderlust.example/events/trip-booked'),
    correlationId: new CorrelationId(location: '$message.header#/bookingId'),
)]
final class TripBooked
{
    // ...
}
```

What it produces
----------------

[](#what-it-produces)

The generation is an opinionated, code-derivable subset of AsyncAPI (the model can express more — see [Discovery and extending](#discovery-and-extending)):

- Each DTO marked with `#[AsyncApiMessage]` becomes a **message** placed in `components.messages` and referenced via `$ref`.
- Messages are grouped into **channels** by their `channel`; a message with no `channel` gets a channel of its own (keyed by the message `name`).
- Each `(message, action)` yields one **operation**.
- Static parts from `document` (`info`, `servers`, …) are merged underneath.

Payload schemas
---------------

[](#payload-schemas)

Payloads are derived from your DTOs by [`zeusi/json-schema-extractor`](https://github.com/antonioturdo/json-schema-extractor). The bundle wires a default `SchemaExtractor` for you — no setup required:

- it uses the **Symfony Serializer** strategy when the Serializer is available, so serialization groups, `#[SerializedName]`, name converters and discriminators are reflected (the shape that actually goes over the wire), falling back to `json_encode` otherwise;
- PHPDoc and Symfony Validator enrichers are added when their packages are present.

To use your own, register it as a service and set the `payload_schema_extractor.service` config key to its id.

The serialization context is decided at extraction time, not baked into the DTO: *which* serialization groups (or other `ExtractionContext` capabilities) apply is a per-message choice that must reach the extractor. Supply it by implementing `Payload\ExtractionContextFactory` and wiring it via `payload_schema_extractor.context_factory`; its `create(ExtractionTarget, DocumentContext)` returns the `ExtractionContext` passed to the extractor (or `null` for none). This keeps extractor-specific knowledge (e.g. which Symfony Serializer groups select a payload's shape) in your code, not in the bundle.

Symfony Messenger
-----------------

[](#symfony-messenger)

AsyncAPI describes events flowing over a broker, but Symfony has no first-class notion of "a published event" to hang that on. The closest thing is Messenger — it's where transports, routing and serialization are already declared — which makes it the one sensible anchor in the ecosystem. How widely teams use Messenger for *public* events (rather than internal async work) is honestly unclear, so both capabilities are **off by default**: turn them on only where your Messenger transports really are your publication channel. A message can also pass through an internal Messenger queue and be published by some *other* mechanism — which the bundle can't see — so enabling this is your assertion that, for the documented transports, Messenger is how the event goes out.

When `symfony/messenger` is installed, the bundle reads that setup to fill in the parts of the document that live in your infrastructure rather than your DTOs. It reuses Messenger's own routing semantics — the same type matching Messenger uses to pick a transport — so what it documents matches where your messages actually go.

With `providers.messenger.enrichment: true`, for each documented message routed to a transport it adds:

- **Servers** — derived from the transport DSN (host + protocol). Internal transports (`sync://`, `in-memory://`, `doctrine://`) and failure/retry transports are left out.
- **Content type** — taken from the transport's serializer (the Symfony Serializer yields `application/json`), unless the message declares one explicitly.
- **Channel ↔ server links** — when more than one server exists, each channel is pinned to the servers its messages are routed to.

`transports` is an allowlist scoping which transports the integration considers (empty = all; failure transports always excluded) — use it to keep internal transports out of the document.

### Discovering messages from routing

[](#discovering-messages-from-routing)

Optionally, the routing map itself becomes a message source — so classes you've already routed don't need the attribute. Turn it on with `providers.messenger.discovery: true`:

- the **FQCN keys** in your `framework.messenger.routing` become messages, plus classes matched by **interface, parent, or namespace-wildcard** rules;
- the `*` catch-all is never a source (it can't be enumerated), and classes carrying `#[AsyncApiMessage]` stay owned by the attribute — its metadata wins;
- discovery is **scoped by `transports`**: only messages routed to documented transports are picked up.

A routing-discovered message has no human metadata (its name is the class short name and its placement is `send`), but its payload is still derived from the class. Use the attribute when you want titles, summaries, tags or an explicit channel; let routing-discovery cover the rest.

Discovery and extending
-----------------------

[](#discovery-and-extending)

You've seen two message sources — the `#[AsyncApiMessage]` attribute and Messenger routing. Neither is the boundary: the document is assembled by an extensible pipeline, so you can add sources of your own and refine anything in the output before it's served.

**The general extension point is the processor.** Implement `Zeusi\AsyncApiBundle\Processor\AsyncApiProcessorInterface` (services tagged `asyncapi.processor` — autoconfigured — ordered by priority) to add or refine *anything* in the document: channels, operations, messages, top-level fields. It receives the whole document plus a context, so it can build straight from operations, a channel registry, or any source that isn't message-shaped. The built-in behaviour is itself just a series of these processors, and yours slot in by priority.

Higher priority runs first. The built-in pipeline, for reference — pick a priority relative to these to slot yours in:

ProcessorPriorityDoes`ConfigProcessor``1000`merges the static `document` config into the model`DiscoveryProcessor``500`assembles channels/operations/messages from the providers`MessengerServerProcessor``400`adds servers, content types and channel↔server links from Messenger transports — only when `providers.messenger.enrichment` is on`PayloadProcessor``-100`derives each message's payload schema`CanonicalizeMessagesProcessor``-500`hoists channel messages into `components.messages``VersionValidationProcessor``-1000`rejects an unsupported `asyncapi` version (runs last)A processor left at the **default priority `0`** runs after discovery (the channels/operations/messages exist, messages still inline) and before payload derivation — a sensible default for refining the discovered document.

**For message-shaped sources specifically**, there's a narrower convenience seam: implement `Zeusi\AsyncApiBundle\Discovery\MessageProviderInterface` to yield messages from somewhere other than attributes (a registry, say — Messenger routing is one such provider the bundle ships), and the built-in discovery processor assembles the channels and operations for you. Reach for this when your source really is a flat list of messages; otherwise, write a processor.

Where it pays off most
----------------------

[](#where-it-pays-off-most)

AsyncAPI shines as an event contract **between services**: a **shared broker**(AMQP / Kafka / SQS / Pub-Sub) with heterogeneous consumers, where the JSON payload is a public contract others depend on.

If your events stay inside a single app — **internal queues** (`doctrine://`, `redis://`) with no external consumer — there's no cross-service contract to publish. The documentation can still earn its keep, though: a schema catalog for onboarding and for understanding the event flow.

License
-------

[](#license)

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

###  Health Score

33

—

LowBetter than 72% of packages

Maintenance92

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity33

Early-stage or recently created project

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~8 days

Total

2

Last Release

37d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/573faa69bb56ed78be26666625a82f4f98475884f941ff8a1b4aa33ece878d24?d=identicon)[antonio.turdo](/maintainers/antonio.turdo)

---

Tags

symfonyjson-schemadocumentationevent-drivendtoSymfony Bundleasyncapiasync api

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/zeusi-asyncapi-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/zeusi-asyncapi-bundle/health.svg)](https://phpackages.com/packages/zeusi-asyncapi-bundle)
```

###  Alternatives

[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

595.8M647](/packages/shopware-core)[symfony/framework-bundle

Provides a tight integration between Symfony components and the Symfony full-stack framework

3.6k257.3M12.1k](/packages/symfony-framework-bundle)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k6.0M764](/packages/sylius-sylius)[drupal/core

Drupal is an open source content management platform powering millions of websites and applications.

21667.3M1.9k](/packages/drupal-core)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M222](/packages/sulu-sulu)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
