Automatic HTTP ↔ DTO conversion via PSR-15 middleware for Mezzio
This package provides automatic Data Transfer Object (DTO) handling for PSR-15 middleware applications (Mezzio, Laminas). It eliminates boilerplate code by:
- 🎯 Automatically mapping HTTP requests to Request DTOs
- ✅ Automatically validating Request DTOs using Symfony Validator
- 🚀 Automatically injecting validated DTOs as handler parameters
- 📦 Automatically serializing Response DTOs to JSON responses
composer require methorz/http-dtopublic function handle(ServerRequestInterface $request): ResponseInterface
{
// 1. Get request body
$data = $request->getParsedBody();
// 2. Map to DTO
$dto = new CreateItemRequest(
name: $data['name'] ?? '',
description: $data['description'] ?? ''
);
// 3. Validate DTO
$violations = $this->validator->validate($dto);
if (count($violations) > 0) {
return new JsonResponse(['errors' => ...], 422);
}
// 4. Execute service
$result = $this->service->execute($dto);
// 5. Serialize response
return new JsonResponse($result->toArray(), 201);
}public function __invoke(
ServerRequestInterface $request,
CreateItemRequest $dto // ← Automatically mapped, validated, and injected!
): ItemResponse { // ← Automatically serialized to JSON!
return $this->service->execute($dto); // One line! 🎉
}Define Request DTOs with Symfony Validator attributes:
use Symfony\Component\Validator\Constraints as Assert;
final readonly class CreateItemRequest
{
public function __construct(
#[Assert\NotBlank(message: 'Name is required')]
#[Assert\Length(min: 3, max: 100)]
public string $name,
#[Assert\NotBlank(message: 'Description is required')]
public string $description,
) {}
}Define Response DTOs with JsonSerializableDto:
use Methorz\Dto\Response\JsonSerializableDto;
final readonly class ItemResponse implements JsonSerializableDto
{
public function __construct(
public string $id,
public string $name,
public string $description,
) {}
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
];
}
public function getStatusCode(): int
{
return 201; // Created
}
}Implement DtoHandlerInterface and use __invoke():
use Methorz\Dto\Handler\DtoHandlerInterface;
final readonly class CreateItemHandler implements DtoHandlerInterface
{
public function __construct(
private CreateItemService $service,
) {}
public function __invoke(
ServerRequestInterface $request,
CreateItemRequest $dto // ← Injected automatically!
): ItemResponse { // ← Serialized automatically!
return $this->service->execute($dto);
}
// PSR-15 compatibility method (not used directly)
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->__invoke($request, new CreateItemRequest('', ''));
}
}Add to your config/pipeline.php in this order:
use Methorz\Dto\Middleware\AutoDtoInjectionMiddleware;
use Methorz\Dto\Middleware\AutoJsonResponseMiddleware;
return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container): void {
// ... other middleware ...
$app->pipe(RouteMiddleware::class);
// Request DTO injection (BEFORE dispatch)
$app->pipe(AutoDtoInjectionMiddleware::class);
$app->pipe(DispatchMiddleware::class);
// Response DTO serialization (AFTER dispatch)
$app->pipe(AutoJsonResponseMiddleware::class);
// ... other middleware ...
};use Methorz\Dto\Middleware\AutoDtoInjectionMiddleware;
use Methorz\Dto\Middleware\AutoJsonResponseMiddleware;
use Methorz\Dto\RequestDtoMapperInterface;
use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;
public function getDependencies(): array
{
return [
'factories' => [
RequestDtoMapperInterface::class => ReflectionBasedAbstractFactory::class,
AutoDtoInjectionMiddleware::class => ReflectionBasedAbstractFactory::class,
AutoJsonResponseMiddleware::class => ReflectionBasedAbstractFactory::class,
],
];
}HTTP POST /api/items
↓
AutoDtoInjectionMiddleware
├─ Maps request → CreateItemRequest
├─ Validates CreateItemRequest
└─ Injects as parameter
↓
Handler.__invoke(request, CreateItemRequest)
├─ Calls: service.execute($dto)
└─ Returns: ItemResponse
↓
AutoJsonResponseMiddleware
├─ Detects: ItemResponse implements JsonSerializableDto
├─ Calls: $response->jsonSerialize()
├─ Gets: $response->getStatusCode() → 201
└─ Returns: JsonResponse(data, 201)
↓
HTTP Response: 201 Created
{"id": "...", "name": "...", "description": "..."}
The middleware automatically handles validation errors:
// HTTP 422 Unprocessable Entity
{
"status": "error",
"message": "DTO validation failed",
"errors": {
"name": ["Name is required", "Name must be at least 3 characters"],
"description": ["Description is required"]
}
}✅ Return DTOs directly (not ResponseInterface)
✅ No ApiResponse wrapper calls
✅ No manual ->toArray() calls
✅ Perfect type safety
✅ Ultra clean (often one line!)
✅ Control their own HTTP status code
✅ Self-serializing (jsonSerialize())
✅ Single Responsibility Principle
✅ Test handler returns actual DTO
✅ No mocking ApiResponse
✅ Test serialization separately
✅ More maintainable
✅ Perfect symmetry: Request DTOs IN, Response DTOs OUT ✅ Consistent pattern across all handlers ✅ Type-safe end-to-end
- PHP 8.2+
- PSR-7 (HTTP Message Interface)
- PSR-15 (HTTP Server Middleware)
- Symfony Validator
- Mezzio or any PSR-15 compatible framework
MIT License. See LICENSE file for details.
Thorsten Merz Website: methorz.com
Made with ❤️ for clean, type-safe APIs