PHPackages                             chamber-orchestra/openapi-doc-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. [HTTP &amp; Networking](/categories/http)
4. /
5. chamber-orchestra/openapi-doc-bundle

ActiveSymfony-bundle[HTTP &amp; Networking](/categories/http)

chamber-orchestra/openapi-doc-bundle
====================================

Symfony bundle that auto-generates OpenAPI 3.0.1 documentation for Action-Domain-Responder (ADR) pattern applications by scanning #\[Operation\] and #\[Route\] attributes.

v8.0.2(1mo ago)01.6k↓78.3%MITPHPPHP ^8.5CI passing

Since Mar 10Pushed 1mo agoCompare

[ Source](https://github.com/chamber-orchestra/openapi-doc-bundle)[ Packagist](https://packagist.org/packages/chamber-orchestra/openapi-doc-bundle)[ RSS](/packages/chamber-orchestra-openapi-doc-bundle/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (24)Versions (5)Used By (0)

OpenAPI ADR Bundle
==================

[](#openapi-adr-bundle)

Symfony bundle that **auto-generates OpenAPI 3.0.1 documentation** for applications built on the [Action-Domain-Responder](https://en.wikipedia.org/wiki/Action%E2%80%93domain%E2%80%93responder) (ADR) pattern. Works by scanning PHP source files for action classes annotated with `#[Operation]` and `#[Route]` attributes — no YAML configuration required.

Features
--------

[](#features)

- Zero-config documentation generation from PHP attributes
- Automatic schema inference from Symfony Form types (including validation constraints → OpenAPI constraints)
- Automatic schema inference from View classes (`ViewInterface`, `IterableView`)
- Automatic schema inference from plain DTO classes
- BackedEnum properties → `enum` values in schemas
- `Uuid`/`Ulid` → `{ type: string, format: uuid }`
- `DateTime`/`DateTimeImmutable` → `{ type: string, format: date-time }`
- GET/DELETE/HEAD requests: form fields automatically expanded as query parameters
- Recursive schema detection (cycle guard)
- Security via `#[IsGranted]` — no extra annotation needed

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

[](#requirements)

- PHP 8.5+
- Symfony 8.x
- `chamber-orchestra/view-bundle`

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

[](#installation)

```
composer require chamber-orchestra/openapi-doc-bundle
```

Register the bundle in `config/bundles.php`:

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

Configuration
-------------

[](#configuration)

### proto.yaml

[](#protoyaml)

Create a `proto.yaml` file in your project root. This file is merged into the final output and **must contain `securitySchemes`** for security annotations to appear in the generated documentation:

```
# proto.yaml
components:
    securitySchemes:
        BearerAuth:
            type: http
            scheme: bearer
            bearerFormat: JWT

    # Shared response references (used as string refs in #[Operation])
    responses:
        Unauthorized:
            description: Unauthorized
        NotFound:
            description: Not found
        Forbidden:
            description: Forbidden

    # Additional schemas not generated from code
    schemas:
        Error:
            type: object
            properties:
                message:
                    type: string
```

> **Note:** Without `securitySchemes` in proto.yaml, `#[IsGranted]` annotations are silently ignored and operations appear as public in the generated documentation.

Usage
-----

[](#usage)

### 1. Annotate action classes

[](#1-annotate-action-classes)

Each invokable action class needs `#[Route]` and `#[Operation]`:

```
use ChamberOrchestra\OpenApiDocBundle\Attribute\Operation;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[Route('/users/{id}', methods: ['GET'])]
#[IsGranted('ROLE_USER')]
#[Operation(
    description: 'Get a user by ID',
    responses: [UserView::class],
)]
class GetUserAction
{
    public function __invoke(): UserView
    {
        // ...
    }
}
```

### 2. POST/PUT/PATCH — request body from a Form

[](#2-postputpatch--request-body-from-a-form)

```
#[Route('/users', methods: ['POST'])]
#[IsGranted('ROLE_ADMIN')]
#[Operation(
    description: 'Create a new user',
    request: CreateUserForm::class,
    responses: [UserView::class],
)]
class CreateUserAction
{
    public function __invoke(): UserView { /* ... */ }
}
```

The form fields are read at generation time and become the `requestBody` schema. Symfony validation constraints are mapped to OpenAPI constraints:

ConstraintOpenAPI`NotBlank``required: [field]` at schema level`Length(min, max)``minLength`, `maxLength``Range(min, max)``minimum`, `maximum``Positive``minimum: 1``GreaterThanOrEqual(n)``minimum: n``Count(min, max)``minItems`, `maxItems` (array fields)`Email``format: email``Url``format: uri``ChoiceType(choices)``enum` values#### Form example

[](#form-example)

```
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints as Assert;

class CreateUserForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, [
                'constraints' => [
                    new Assert\NotBlank(),
                    new Assert\Length(min: 2, max: 100),
                ],
            ])
            ->add('email', EmailType::class, [
                'constraints' => [new Assert\NotBlank(), new Assert\Email()],
            ])
            ->add('age', IntegerType::class, [
                'required' => false,
                'constraints' => [new Assert\Range(min: 18, max: 120)],
            ])
            ->add('role', ChoiceType::class, [
                'choices' => ['User' => 'user', 'Admin' => 'admin', 'Moderator' => 'moderator'],
                'constraints' => [new Assert\NotBlank()],
            ]);
    }
}
```

This generates the following OpenAPI schema:

```
CreateUserForm:
    type: object
    required: [name, email, role]
    properties:
        name:
            type: string
            minLength: 2
            maxLength: 100
        email:
            type: string
            format: email
        age:
            type: integer
            minimum: 18
            maximum: 120
        role:
            type: string
            enum: [user, admin, moderator]
```

#### Nested form (sub-form as object)

[](#nested-form-sub-form-as-object)

```
class AddressForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('street', TextType::class, ['constraints' => [new Assert\NotBlank()]])
            ->add('city', TextType::class, ['constraints' => [new Assert\NotBlank()]])
            ->add('zip', TextType::class);
    }
}

class CreateOrderForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('title', TextType::class, ['constraints' => [new Assert\NotBlank()]])
            ->add('address', AddressForm::class);  // nested form → $ref
    }
}
```

Generated schema:

```
CreateOrderForm:
    type: object
    required: [title]
    properties:
        title:
            type: string
        address:
            $ref: '#/components/schemas/AddressForm'

AddressForm:
    type: object
    required: [street, city]
    properties:
        street:
            type: string
        city:
            type: string
        zip:
            type: string
```

#### Collection of sub-forms

[](#collection-of-sub-forms)

```
use Symfony\Component\Form\Extension\Core\Type\CollectionType;

class CreateInvoiceForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('number', TextType::class, ['constraints' => [new Assert\NotBlank()]])
            ->add('lines', CollectionType::class, [
                'entry_type' => InvoiceLineForm::class,
                'constraints' => [new Assert\Count(min: 1)],
            ]);
    }
}
```

Generated schema:

```
CreateInvoiceForm:
    type: object
    required: [number]
    properties:
        number:
            type: string
        lines:
            type: array
            minItems: 1
            items:
                $ref: '#/components/schemas/InvoiceLineForm'
```

### 3. GET — form fields become query parameters

[](#3-get--form-fields-become-query-parameters)

GET/DELETE/HEAD actions automatically expand form fields into query parameters instead of a request body:

```
#[Route('/users', methods: ['GET'])]
#[IsGranted('ROLE_USER')]
#[Operation(
    description: 'Search users',
    request: SearchUsersForm::class,
    responses: [UserListView::class],
)]
class SearchUsersAction
{
    public function __invoke(): UserListView { /* ... */ }
}
```

```
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;

class SearchUsersForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('query', TextType::class, ['required' => false])
            ->add('role', ChoiceType::class, [
                'required' => false,
                'choices'  => ['User' => 'user', 'Admin' => 'admin'],
            ])
            ->add('page', IntegerType::class, ['required' => false]);
    }
}
```

Each field becomes an individual query parameter:

```
parameters:
    - name: query
      in: query
      schema:
          type: string
    - name: role
      in: query
      schema:
          type: string
          enum: [user, admin]
    - name: page
      in: query
      schema:
          type: integer
```

### 4. Multiple responses (including shared references from proto.yaml)

[](#4-multiple-responses-including-shared-references-from-protoyaml)

```
#[Operation(
    description: 'Update user',
    request: UpdateUserForm::class,
    responses: [
        UserView::class,           // Described as a component schema
        '404' => 'NotFound',       // String ref → #/components/responses/NotFound
        '403' => 'Forbidden',      // String ref → #/components/responses/Forbidden
    ],
)]
```

### 5. Custom security

[](#5-custom-security)

Override security for a specific operation (e.g., API key instead of the default Bearer):

```
#[Operation(
    description: 'Webhook endpoint',
    security: ['ApiKeyAuth' => []],
)]
```

Disable security for a public endpoint:

```
#[Operation(
    description: 'Public endpoint',
    security: [],
)]
```

### 6. Annotating DTO properties

[](#6-annotating-dto-properties)

Use `#[Property]` to mark a property as required or add arbitrary OpenAPI attributes:

```
use ChamberOrchestra\OpenApiDocBundle\Attribute\Property;

class UserView
{
    public Uuid $id;
    public string $name;

    #[Property(required: true, attr: ['example' => 'user@example.com'])]
    public ?string $email = null;  // nullable but explicitly required

    #[Property(attr: ['minLength' => 3, 'maxLength' => 50])]
    public string $username;
}
```

### 7. Iterable views (lists)

[](#7-iterable-views-lists)

Use `#[Type]` from `chamber-orchestra/view-bundle` to specify the item type:

```
use ChamberOrchestra\ViewBundle\Attribute\Type;
use ChamberOrchestra\ViewBundle\View\IterableView;

class UserListView extends IterableView
{
    #[Type(UserView::class)]
    protected array $entries = [];
}
```

Generates:

```
UserListView:
    type: array
    items:
        $ref: '#/components/schemas/UserView'
```

Generating documentation
------------------------

[](#generating-documentation)

```
php bin/console openapi-doc:generate
```

Options:

```
--src         Source directory to scan (default: /src)
--output      Output file path      (default: /doc.yaml)
--proto       Proto YAML file path  (default: /proto.yaml)
--title       API title             (default: "API Documentation")
--doc-version API version string    (default: "1.0.0")

```

Example:

```
php bin/console openapi-doc:generate \
  --src src/Api \
  --output public/openapi.yaml \
  --proto proto.yaml \
  --title "My API" \
  --doc-version "2.1.0"
```

Type mapping reference
----------------------

[](#type-mapping-reference)

### PHP types → OpenAPI

[](#php-types--openapi)

PHP typeOpenAPI`string``type: string``int``type: integer``float``type: number``bool``type: boolean``array` / `iterable``type: array``BackedEnum``type: string|integer` + `enum: [...]``Uuid` / `Ulid``type: string, format: uuid``DateTime` / `DateTimeImmutable``type: string, format: date-time`Custom class`$ref: '#/components/schemas/ClassName'`### Form field types → OpenAPI

[](#form-field-types--openapi)

Symfony typeOpenAPI`TextType``type: string``IntegerType``type: integer``NumberType``type: number``CheckboxType``type: boolean``EmailType``type: string, format: email``UrlType``type: string, format: uri``DateType``type: string, format: date``DateTimeType``type: string, format: date-time``ChoiceType``type: string` + `enum: [...]``ChoiceType(multiple: true)``type: array, items: { enum: [...] }``CollectionType``type: array, items: { ... }``RepeatedType``type: object` with sub-propertiesCustom `FormTypeInterface``$ref: '#/components/schemas/FormName'`Architecture
------------

[](#architecture)

```
Action class
  └─ Locator           scans src/ for #[Operation] + #[Route]
  └─ OperationDescriber
       └─ SecurityParser    #[IsGranted] → security placeholder
       └─ RouteParser       #[Route]     → path / method / operationId
       └─ OperationParser   #[Operation] → description / request / responses
       └─ ResponseParser    __invoke return type → ComponentDescriber
  └─ ComponentDescriber (lazy, per class)
       └─ FormParser    FormTypeInterface → schema from form fields + constraints
       └─ ViewParser    ViewInterface     → schema from public properties
       └─ ObjectParser  plain DTO         → schema from public properties
  └─ DocumentBuilder
       └─ merge proto.yaml
       └─ resolve 'default' security → first securityScheme
       └─ emit OpenAPI 3.0.1 YAML

```

Running tests
-------------

[](#running-tests)

```
composer test           # all tests
composer test:unit      # unit tests only
composer test:integration  # integration tests only
```

License
-------

[](#license)

MIT

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance89

Actively maintained with recent releases

Popularity21

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 75% of commits — single point of failure

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 ~16 days

Total

4

Last Release

57d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/44037eb1c8dc2c4fa9871ac213653f33e22a9348dcec7132df07cc71933f2a2e?d=identicon)[wtorsi](/maintainers/wtorsi)

---

Top Contributors

[![baldrys-ed](https://avatars.githubusercontent.com/u/60212508?v=4)](https://github.com/baldrys-ed "baldrys-ed (3 commits)")[![wtorsi](https://avatars.githubusercontent.com/u/2115840?v=4)](https://github.com/wtorsi "wtorsi (1 commits)")

---

Tags

apisymfonybundlerestdocumentationswaggeropenapigeneratorattributesphp85adraction domain responder

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/chamber-orchestra-openapi-doc-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/chamber-orchestra-openapi-doc-bundle/health.svg)](https://phpackages.com/packages/chamber-orchestra-openapi-doc-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.5M373](/packages/easycorp-easyadmin-bundle)[sulu/sulu

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

1.3k1.4M196](/packages/sulu-sulu)[chameleon-system/chameleon-base

The Chameleon System core.

1027.9k4](/packages/chameleon-system-chameleon-base)[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

9317.2k55](/packages/open-dxp-opendxp)

PHPackages © 2026

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