Search by

stixx / openapi-command-bundle

jellevo

Create Command-Bus APIs with auto-generated OpenAPI docs and schema-driven request validation

Package info

github.com/stixx/openapi-command-bundle

Type:symfony-bundle

pkg:composer/stixx/openapi-command-bundle

Statistics

Installs: 4 269

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

0.15.0 2026-10-03 14:37 UTC

README

Latest Version on Packagist Total Downloads License

The OpenAPI Command Bundle is a Symfony bundle that allows you to build HTTP APIs around Command Bus messages (DTOs) without the need for manual controller creation or Symfony #[Route] attributes on your commands.

By using standard OpenAPI operation attributes (from zircote/swagger-php) directly on your command DTOs, this bundle automatically generates Symfony routes and handles the entire request-to-command lifecycle: deserialization, validation, dispatching to the messenger bus, and responding.

Key Features

  • OpenAPI-Driven Routing: Define your API endpoints directly on your command DTOs using #[OA\Post], #[OA\Get], #[OA\Put], etc.
  • No Manual Controllers: A single CommandController handles all generated routes by default.
  • Automatic Deserialization: Automatically maps JSON request bodies, route parameters, and query parameters to your command DTOs.
  • Two-Layer Validation: Each request is checked against the OpenAPI schema (headers, query parameters, body shape via league/openapi-psr7-validator) and the deserialized command is validated with Symfony Validator constraints before it reaches your handler.
  • Messenger Integration: Dispatches your commands directly to the Symfony Messenger bus.
  • Auto-Generated Documentation: Seamlessly integrates with NelmioApiDocBundle to include your command-based routes in your OpenAPI/Swagger documentation.
  • Problem Details Support: Returns RFC 7807 compliant error responses for validation and mapping errors.

Installation

1. Install via Composer

composer require stixx/openapi-command-bundle

2. Enable the Bundle

If you are using Symfony Flex, the bundle will be automatically enabled. Otherwise, add it to your config/bundles.php:

return [
    // ...
    Stixx\OpenApiCommandBundle\StixxOpenApiCommandBundle::class => ['all' => true],
];

3. Configure NelmioApiDocBundle

This bundle reads its routes from NelmioApiDocBundle's areas, so Nelmio must be registered and configured — installing it via Composer is not enough. Its Flex recipe normally does this, but if the recipe was skipped you have to do it by hand. Add it to config/bundles.php:

return [
    // ...
    Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
];

And define at least one area in config/packages/nelmio_api_doc.yaml:

nelmio_api_doc:
    areas:
        default:
            path_patterns: ['^/api']

Without this, the container fails to compile and cache:clear reports which part is missing — the config/bundles.php entry or the package config — along with the configuration to add.

4. Tell the bundle where your commands live

Every project keeps its command DTOs somewhere else, so the bundle scans only the directories you list in config/packages/stixx_openapi_command.yaml. Glob patterns are allowed (* for one directory level, ** for any depth):

stixx_openapi_command:
    command_paths:
        - '%kernel.project_dir%/src/Command'                  # a single directory
        # - '%kernel.project_dir%/src/*/Application/Command'  # DDD: one directory per context

The key is required: without it the container fails to compile. Set it to [] to turn discovery off and import command routes from your routing config instead. See Command Routing.

Usage

1. Create a Command DTO

Annotate your command with OpenAPI attributes. No Symfony #[Route] is needed.

namespace App\Command;

use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;

#[OA\Post(
    path: '/api/projects',
    operationId: 'create_project',
    summary: 'Create a new project'
)]
final class CreateProjectCommand
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Length(min: 3, max: 50)]
        public string $name,

        #[Assert\Length(max: 255)]
        public ?string $description = null,
    ) {}
}

2. Create a Message Handler

Implement a standard Symfony Messenger handler for your command.

namespace App\Handler;

use App\Command\CreateProjectCommand;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final class CreateProjectHandler
{
    public function __invoke(CreateProjectCommand $command): array
    {
        // Your business logic here (e.g., persist to database)
        
        return [
            'id' => '123',
            'name' => $command->name,
        ];
    }
}

3. Call the API

The bundle automatically registers the route /api/projects (POST).

curl -X POST http://localhost:3000/api/projects \
     -H "Content-Type: application/json" \
     -d '{"name": "New Project", "description": "This is a project description"}'

The bundle will:

  1. Detect the route and map it to CreateProjectCommand.
  2. Deserialize the JSON body into the command object.
  3. Validate the command using Symfony Validator.
  4. Dispatch the command to the Messenger bus.
  5. Return the handler's result as a JSON response with an appropriate status code (e.g., 201 Created).

Configuration

You can customize the bundle's behavior in config/packages/stixx_openapi_command.yaml:

stixx_openapi_command:
    validation:
        enabled: true
        groups: ['Default']
    cache_control: 'no-store' # Any valid Cache-Control directives, or null to disable
    command_paths:            # Required. Directories or glob patterns holding command DTOs; [] disables discovery
        - '%kernel.project_dir%/src/Command'
    openapi:
        problem_details: true  # Enable RFC 7807 problem details for errors
        warm_up: false         # Generate the OpenAPI documents during cache:warmup instead of on the first API request

Documentation

For more detailed information, please refer to the following documentation:

Stability & supported API

The bundle distinguishes a small public API surface from its internal implementation. Public surface is the contract you can safely depend on; internals can change in any release.

Public (@api) — covered by the backward-compatibility policy:

Type What it is
Stixx\OpenApiCommandBundle\StixxOpenApiCommandBundle Bundle entry point
Stixx\OpenApiCommandBundle\Attribute\CommandObject Marker attribute for command arguments
Stixx\OpenApiCommandBundle\Exception\ApiProblemException Throw this from your code to express RFC 7807 problems
Stixx\OpenApiCommandBundle\Exception\ExceptionToApiProblemTransformerInterface Replace to customize how exceptions become problem responses
Stixx\OpenApiCommandBundle\Responder\ResponderInterface Implement to handle custom result shapes (CSV, XML, …)
Stixx\OpenApiCommandBundle\Response\StatusResolverInterface Replace to customize status code resolution
Stixx\OpenApiCommandBundle\Validator\RequestValidatorInterface Implement to add custom request-level validation
Stixx\OpenApiCommandBundle\Model\ProblemDetails OpenAPI schema model — reference from your annotations
Stixx\OpenApiCommandBundle\Model\ProblemDetailsInvalidRequestBody OpenAPI schema model — reference from your annotations
Stixx\OpenApiCommandBundle\Model\Violation OpenAPI schema model — reference from your annotations

Internal (@internal) — everything else, including default implementations like JsonResponder, DefaultExceptionToApiProblemTransformer, RequestValidator, the controllers, DI extension, compiler passes, route loaders, and event subscribers. Do not extend or rely on these directly; replace them through the @api interfaces above.

The bundle's configuration schema (the keys under stixx_openapi_command:) is also part of the supported API.

See Extension Points for a worked example of each extension interface.

Backward compatibility and deprecations

From 1.0.0 on, the bundle follows Semantic Versioning for its public API:

  • the @api types above, and the services registered under those interface names, which you can alias or decorate;
  • the configuration keys under stixx_openapi_command:;
  • the service tags named by ResponderInterface::TAG_NAME and RequestValidatorInterface::TAG_NAME;
  • the OpenAPI component names the bundle registers for your annotations when openapi.problem_details is enabled (the default): the schemas ProblemDetails, Violation and ProblemDetailsInvalidRequestBody, and the *ProblemDetailsResponse responses.

The policy:

  • Minor and patch releases don't break the public API.
  • Removing or renaming part of it starts with a deprecation in a minor release. PHP code triggers trigger_deprecation('stixx/openapi-command-bundle', '<version>', ...) and a configuration key is marked with setDeprecated(), naming the replacement; the old name keeps working alongside the new one. The changelog lists the deprecation under Deprecated.
  • Deprecated code is removed in the next major release, listed under Removed with Upgrading notes. Run your test suite with deprecations reported before upgrading a major: Symfony's PHPUnit bridge reports them by default; with plain PHPUnit, set ignoreSuppressionOfDeprecations="true" on the <source> element of phpunit.xml, because trigger_deprecation() raises silenced deprecations.
  • @internal code can change in any release, without a deprecation.

Before 1.0.0, minor releases may break the public API; every such change comes with an Upgrading note in the changelog.

Requirements

  • PHP 8.4+
  • Symfony 7.4+ or 8.0+
  • NelmioApiDocBundle 5.8+, registered and configured with at least one area

License

This bundle is released under the MIT License.