Install the package via Composer:
composer require cuyz/valinor-bundle
Enable the bundle in config/bundles.php:
return [
// ...
CuyZ\ValinorBundle\ValinorBundle::class => ['all' => true],
];
First Use Case: Replace manual request parsing in a controller with attribute-based mapping. For example, convert this:
public function listArticles(Request $request, string $authorId): Response
{
$status = $request->query->get('status');
$page = (int) $request->query->get('page', 1);
// ...
}
To this:
#[Route('/api/authors/{authorId}/articles', methods: 'GET')]
#[MapRequest]
public function __invoke(
#[FromRoute] string $authorId,
#[FromQuery] string $status,
#[FromQuery] positive-int $page = 1,
): Response { /* ... */ }
Key first steps:
#[MapRequest] to your controller method.#[FromRoute], #[FromQuery], or #[FromBody] on method arguments.positive-int, int<10,100>) for validation.Declarative Mapping:
#[FromRoute], #[FromQuery], #[FromBody]).Grouped Mapping:
mapAll: true to map entire query/body payloads to a single DTO (e.g., #[FromQuery(mapAll: true)] ArticleFilters $filters).Controller-Level Configuration:
MapRequestAttribute.snake_case keys across all API controllers.Symfony Forms Integration:
Combine with Symfony’s #[MapEntity] for form submissions:
#[Route('/api/articles', methods: 'POST')]
#[MapRequest]
public function createArticle(
#[FromBody] #[MapEntity] Article $article,
): Response { /* ... */ }
Validation Layer: Use the bundle’s error messages in your API responses (e.g., JSON:API format):
try {
$response = $controller->__invoke($request);
} catch (HttpRequestMappingError $e) {
return new JsonResponse(['errors' => $e->getErrors()], 422);
}
Testing:
Mock request objects with ValinorMapper directly:
$mapper = new ValinorMapper($builder);
$result = $mapper->map($request, new CreateAuthorDto());
$this->assertSame('John', $result->name);
Legacy Code: Mix with manual parsing where needed (e.g., for non-standard request formats):
public function __invoke(Request $request, #[FromRoute] string $id): Response
{
$manualData = $request->get('legacy_field');
// ...
}
Attribute Order Matters:
#[MapRequest] after #[Route] in PHPDoc to avoid IDE warnings.#[Route('/api/articles')]
#[MapRequest] // <-- Correct order
Type Coercion Quirks:
int vs. positive-int: The latter rejects 0 and negative values, while int accepts them.int<1,100>) over loose types (int) for validation.Circular References:
User with self-referencing friends property).#[IgnoreExtraFields] or #[MapExtraFields] to control behavior.Case Sensitivity:
RestrictKeysToCamelCase) are strict. A request with snake_case keys will fail unless converted first.Performance:
bootstrap/cache for production:
php bin/console cache:clear --env=prod
Enable Detailed Errors:
Set VALINOR_DEBUG=1 in your environment to get verbose error messages during development.
Inspect Mapped Data:
Use #[MapRequest(debug: true)] to log the mapped values (useful for troubleshooting).
Common Errors:
| Error Message | Solution |
|---|---|
No mapper found for type X |
Ensure the type is supported or implement a custom mapper. |
Key "invalid_key" not allowed |
Add #[AllowExtraFields] or adjust key case configurators. |
Failed to parse date "2023-01-01" |
Extend supported date formats in configureMapperBuilder. |
Custom Mappers:
CuyZ\Valinor\Mapper\MapperInterface for unsupported types (e.g., custom DTOs):
class CustomDtoMapper implements MapperInterface
{
public function map(mixed $data, string $type): mixed
{
return new CustomDto($data['field1'], $data['field2']);
}
}
MapperBuilder:
$builder->registerMapper(CustomDto::class, new CustomDtoMapper());
Global Configuration:
config/packages/valinor.yaml:
valinor:
default_mapper_configurators:
- CuyZ\Valinor\Mapper\Configurator\ConvertKeysToCamelCase
date_formats: ['Y-m-d', 'Y-m-d H:i:s']
Event Listeners:
valinor.mapper.created events to log or modify mappers dynamically:
$eventDispatcher->addListener(ValinorEvents::MAPPER_CREATED, function (MapperCreatedEvent $event) {
if ($event->getType() === MyDto::class) {
$event->setMapper(new CustomMyDtoMapper());
}
});
Fallback for Missing Fields:
#[DefaultValue] to provide defaults for missing query/body fields:
#[FromQuery] #[DefaultValue('active')] string $status
API Versioning:
Use custom MapRequestAttribute to enforce different validation rules per API version:
#[MapRequest(new RestrictKeysToSnakeCase(), new AllowExtraFields())]
OpenAPI/Swagger:
Generate OpenAPI specs dynamically by combining #[MapRequest] with nelmio/api-doc-bundle:
# config/packages/nelmio_api_doc.yaml
nelmio_api_doc:
schemas:
path: ./var/cache/dev/api
context:
valinor: true
Batch Processing: Map arrays of objects efficiently:
#[FromBody] #[MapEntity] array<int, Article> $articles
Ensure your MapperBuilder supports array mapping:
$builder->supportArrayMapping();
How can I help you explore Laravel packages today?