liquidrazor / dto-api-bundle
A DTO-first Symfony bundle for building boring JSON APIs with streaming responses and autogenerated OpenAPI documentation.
Installs: 11
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 1
Open Issues: 0
Type:symfony-bundle
Requires
- php: >=8.3
- ext-json: *
- monolog/monolog: ^3
- symfony/config: ^7.0
- symfony/dependency-injection: ^7.0
- symfony/http-kernel: ^7
- symfony/options-resolver: ^7.0
- symfony/property-access: ^7.0
- symfony/serializer: ^7
- symfony/validator: ^7.0
Requires (Dev)
README
DTO-first API toolkit for Symfony 7.x
Attributes for request/response contracts, automatic validation, safe (de)serialization, streaming (NDJSON/SSE), OpenAPI generation, and Symfony Profiler integration.
β¨ Features
- Attributes for requests, responses, operations, and properties
- Validation bridge: property metadata β Symfony constraints (no YAML/XML)
- Safe request hydration via events (no fatals, no leaks)
- Response mapping with global defaults (422/500)
- Error handling: exceptions β mapped error DTOs
- Streaming helpers: NDJSON (
application/x-ndjson
) and SSE (text/event-stream
) - OpenAPI 3.1/3.0.3 generator with Swagger UI + Redoc
- Profiler panel: metadata, DTOs, violations, and the actual response used
π§° Requirements
- PHP 8.3+
- Symfony 7.0+
- packages: (already present in composer.json)
ext-json
symfony/dependency-injection
symfony/config
symfony/http-kernel
symfony/serializer
symfony/property-access
symfony/options-resolver
symfony/validator
monolog/monolog
π¦ Install
composer require liquidrazor/dto-api-bundle
Enable the bundle (if Flex doesnβt auto-register):
config/bundles.php
return [ ... LiquidRazor\DtoApiBundle\LiquidRazorDtoApiBundle::class => ['all' => true], ];
βοΈ Configuration
Responses
# config/packages/liquidrazor_dto_api.yaml liquidrazor_dto_api: normalizer_priority: 10 strict_types: true openapi_version: '3.1.0' # or '3.0.3' for Redoc OSS compatibility default_responses: 422: class: LiquidRazor\DtoApiBundle\Response\ValidationErrorResponse description: 'Validation error' 500: class: LiquidRazor\DtoApiBundle\Response\ErrorResponse description: 'Server error'
Edit the file and set up any custom response classes (and|or descriptions)
This is the default configuration and should be there if flex auto-registered the bundle. Otherwise it's probably missing and should be added.
Routes
add the following to your routes.yaml (if not already added by symfony flex)
# config/routes/liquidrazor_dto_api.yaml liquidrazor_dto_api: resource: '@LiquidRazorDtoApiBundle/Resources/config/routes.php'
You now get:
/_schema/openapi.json
β OpenAPI spec/_docs/swagger
β Swagger UI/_docs/redoc
β Redoc UI
π Usage Guides
1. Request DTO
use LiquidRazor\DtoApiBundle\Lib\Attributes\{DtoApiRequest, DtoApiProperty}; #[DtoApiRequest(name: 'UserInput')] final readonly class UserInputDto { public function __construct( #[DtoApiProperty(type: 'string', required: true, minLength: 3)] public ?string $name = null, #[DtoApiProperty(type: 'integer', required: true, minimum: 18, maximum: 100)] public ?int $age = null, ) {} }
Validation is automatic β missing/invalid fields trigger a 422 ValidationErrorResponse (unless you override the default response).
2. Response DTO
use LiquidRazor\DtoApiBundle\Lib\Attributes\{DtoApiResponse, DtoApiProperty}; #[DtoApiResponse(status: 200, description: 'User created')] final readonly class UserResponse { public function __construct( #[DtoApiProperty(type: 'string')] public string $id, #[DtoApiProperty(type: 'string')] public string $name, ) {} }
3. Controller
use LiquidRazor\DtoApiBundle\Lib\Attributes\{DtoApi, DtoApiOperation}; use Symfony\Component\Routing\Attribute\Route; #[DtoApi] final class UserController { #[DtoApiOperation( summary: 'Create user', description: 'Accepts a UserInputDto and returns a UserResponse', request: UserInputDto::class, response: [UserResponse::class] // 422/500 added by defaults )] #[Route('/users', methods: ['POST'])] public function create(UserInputDto $request): UserResponse { return new UserResponse(id: uniqid(), name: $request->name); } }
4. Error Handling
All exceptions are logged
Mapped to a declared #[DtoApiResponse(status: β¦)]
if present
Fallback: ErrorResponse (500 JSON)
5. Streaming
5.1 NDJSON
#[DtoApiOperation(summary: 'NDJSON counter')] #[DtoApiResponse(status: 200, stream: true, contentType: 'application/x-ndjson')] #[Route('/stream/ndjson', methods: ['GET'])] public function streamNdjson(): iterable { for ($i=1; $i<=5; $i++) { yield ['i' => $i, 'ts' => (new \DateTimeImmutable())->format(DATE_ATOM)]; usleep(200_000); } }
5.2 SSE
use LiquidRazor\DtoApiBundle\Lib\Streaming\SseEvent; #[DtoApiOperation(summary: 'SSE clock')] #[DtoApiResponse(status: 200, stream: true, contentType: 'text/event-stream')] #[Route('/stream/sse', methods: ['GET'])] public function sseClock(): iterable { for ($i=0; $i<5; $i++) { yield new SseEvent(['now' => date(DATE_ATOM)], 'tick', (string)$i, 3000); sleep(1); } }
6. Profiler
Symfony Profiler panel shows:
Operation metadata (summary, request/response DTOs)
Request violations (422 errors)
Which response mapping was actually used.
π§© Extensibility
- Custom constraints: tag dtoapi.constraint_contributor to translate custom hints into Symfony constraints
- Global defaults: override liquidrazor_dto_api.default_responses
- OpenAPI hooks: extend components, security, servers, parameters
π Known limitations
- Very strict CSP may require self-hosting Swagger/Redoc assets instead of using CDNs
π License
MIT
π Credits
Built by LiquidRazor with help from Symfonyβs excellent components.