Automatic OpenAPI 3.0 specification generator from routes and DTOs
Automatically generates OpenAPI specifications by analyzing your application's routes and Data Transfer Objects (DTOs). Perfect for Mezzio, Laminas, and any PSR-15 application.
- 🚀 Automatic Generation: Scans routes and DTOs to generate complete OpenAPI specs
- 📝 DTO Analysis: Extracts request/response schemas from PHP DTOs with property promotion
- ✅ Validation Integration: Reads Symfony Validator attributes for schema constraints
- 🎯 Handler Detection: Automatically finds request and response DTOs in handlers
- 📦 Multiple Formats: Generates both YAML and JSON outputs
- 🔧 Zero Configuration: Works out-of-the-box with sensible defaults
- 🎨 Customizable: Configure via application config
- 🔗 Nested DTOs: Automatically generates schemas for nested DTO references
- 📚 Collections: Supports typed arrays with
@param array<Type>PHPDoc - 🎲 Enums: Full support for backed and unit enums (PHP 8.1+)
- 🔀 Union Types: Generates
oneOfschemas for union types (PHP 8.0+) - ⚡ Performance: Schema caching for efficient generation
composer require methorz/openapi-generatorAdd to your application's command configuration:
// config/autoload/dependencies.global.php use Methorz\OpenApi\Command\GenerateOpenApiCommand; return [ 'dependencies' => [ 'factories' => [ GenerateOpenApiCommand::class => function ($container) { return new GenerateOpenApiCommand($container); }, ], ], ];php bin/console openapi:generateThis will create:
public/openapi.yaml- YAML formatpublic/openapi.json- JSON format
// config/autoload/openapi.global.php return [ 'openapi' => [ 'title' => 'My API', 'version' => '1.0.0', ], ];The generator automatically analyzes your handlers:
namespace App\Handler; use App\Request\CreateItemRequest; use App\Response\ItemResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; final class CreateItemHandler implements RequestHandlerInterface { public function handle( ServerRequestInterface $request, CreateItemRequest $dto // ← Request DTO detected ): ItemResponse { // ← Response DTO detected // Handler logic... } }namespace App\Request; use Symfony\Component\Validator\Constraints as Assert; final readonly class CreateItemRequest { public function __construct( #[Assert\NotBlank] #[Assert\Length(min: 3, max: 100)] public string $name, #[Assert\NotBlank] #[Assert\Length(min: 10, max: 500)] public string $description, #[Assert\Email] public string $email, ) {} }Generated Schema:
components: schemas: CreateItemRequest: type: object required: - name - description - email properties: name: type: string minLength: 3 maxLength: 100 description: type: string minLength: 10 maxLength: 500 email: type: string format: emailThe generator extracts constraints from Symfony Validator attributes:
| Attribute | OpenAPI Property |
|---|---|
@Assert\NotBlank | required: true |
@Assert\Length(min, max) | minLength, maxLength |
@Assert\Range(min, max) | minimum, maximum |
@Assert\Email | format: email |
@Assert\Url | format: uri |
@Assert\Uuid | format: uuid |
Generates enum schemas from PHP 8.1+ backed enums:
enum StatusEnum: string { case DRAFT = 'draft'; case ACTIVE = 'active'; case ARCHIVED = 'archived'; } final readonly class CreateItemRequest { public function __construct( public StatusEnum $status, ) {} }Generated Schema:
CreateItemRequest: type: object properties: status: type: string enum: ['draft', 'active', 'archived']Automatically generates schemas for nested DTO objects:
final readonly class AddressDto { public function __construct( #[Assert\NotBlank] public string $street, #[Assert\NotBlank] public string $city, public ?string $country = null, ) {} } final readonly class CreateUserRequest { public function __construct( public string $name, public AddressDto $address, // ← Nested DTO public ?AddressDto $billingAddress = null, // ← Nullable nested DTO ) {} }Generated Schema:
CreateUserRequest: type: object required: ['name', 'address'] properties: name: type: string address: $ref: '#/components/schemas/AddressDto' billingAddress: $ref: '#/components/schemas/AddressDto' nullable: true AddressDto: type: object required: ['street', 'city'] properties: street: type: string city: type: string country: type: string nullable: trueSupports typed arrays using PHPDoc annotations:
/** * @param array<int, AddressDto> $addresses * @param array<string> $tags */ final readonly class CreateOrderRequest { public function __construct( public string $orderId, public array $addresses, public array $tags, ) {} }Generated Schema:
CreateOrderRequest: type: object properties: orderId: type: string addresses: type: array items: $ref: '#/components/schemas/AddressDto' tags: type: array items: type: stringGenerates oneOf schemas for union types:
final readonly class FlexibleRequest { public function __construct( public string|int $identifier, // ← Union type ) {} }Generated Schema:
FlexibleRequest: type: object properties: identifier: oneOf: - type: string - type: integerScans your application's route configuration:
// config/autoload/routes.global.php return [ 'routes' => [ [ 'path' => '/api/v1/items', 'middleware' => [CreateItemHandler::class], 'allowed_methods' => ['POST'], ], ], ];Creates OpenAPI operations with:
- HTTP method (GET, POST, PUT, DELETE, etc.)
- Path parameters (extracted from
{id}patterns) - Request body (for POST/PUT/PATCH)
- Response schemas
- Summary and operationId
- Tags (from module namespace)
Automatically detects and types path parameters:
'/api/v1/items/{id}' → parameter: id (format: uuid) '/api/v1/users/{userId}' → parameter: userId (type: integer)openapi: 3.0.0 info: title: My API version: 1.0.0 description: Automatically generated from routes and DTOs servers: - url: http://localhost:8080 description: Local development paths: /api/v1/items: post: operationId: createItem summary: create item tags: - Items requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateItemRequest' responses: 201: description: Success content: application/json: schema: $ref: '#/components/schemas/ItemResponse' 400: description: Bad Request 404: description: Not Found components: schemas: CreateItemRequest: # ... schema definition ItemResponse: # ... schema definition// config/autoload/openapi.global.php return [ 'openapi' => [ 'title' => 'My API', 'version' => '1.0.0', 'description' => 'API for managing items', 'servers' => [ [ 'url' => 'https://api.example.com', 'description' => 'Production', ], [ 'url' => 'http://localhost:8080', 'description' => 'Development', ], ], ], ];View your generated OpenAPI specification:
# Install Swagger UI composer require swagger-api/swagger-ui # Access at: http://localhost:8080/swagger-uiOr use online tools:
# Run all tests composer test # Run with coverage composer test:coverage # Code style check composer cs-check # Fix code style composer cs-fix # Static analysis composer analyze # All quality checks composer qualityContributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Write tests for new features
- Ensure all quality checks pass (
composer quality) - Submit a pull request
MIT License. See LICENSE for details.
- methorz/http-dto - Automatic DTO mapping and validation
- mezzio/mezzio - PSR-15 middleware microframework
Built with:
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Made with ❤️ by Thorsten Merz