This bundle provides an easy way to automatically map the incoming request data to a DTO and optionally validate it. It's using the powerful Serializer component under the hood along with the Validator component (optional).
- PHP ^8.0
- Symfony ^5.0 or ^6.0
composer require artyuum/request-dto-mapper-bundle
# config/packages/artyum_request_dto_mapper.yaml
artyum_request_dto_mapper:
# Used if the attribute does not specify any (must be a FQCN implementing "\Artyum\RequestDtoMapperBundle\Extractor\ExtractorInterface").
default_extractor: null # Example: Artyum\RequestDtoMapperBundle\Extractor\JsonExtractor
# The configuration related to the denormalizer (https://symfony.com/doc/current/components/serializer.html).
denormalizer:
# Used when mapping the request data to the DTO if the attribute does not set any.
default_options: []
# Used when mapping the request data to the DTO (merged with the values passed by the attribute or "default_options").
additional_options: []
# The configuration related to the validator (https://symfony.com/doc/current/validation.html).
validation:
# Whether to validate the DTO after mapping it.
enabled: false
# Used when validating the DTO if the attribute does not set any.
default_groups: []
# Used when validating the DTO (merged with the values passed by the attribute or "default_groups").
additional_groups: []
# Whether to throw an exception if the DTO validation failed (constraint violations).
throw_on_violation: true
This is a simple step-by-step example of how to make a DTO that will be used by the bundle.
- Create the DTO that represents the structure of the content the user will send to your controller.
class PostPayload {
/**
* @Assert\Sequentially({
* @Assert\NotBlank,
* @Assert\Type("string")
* })
*
* @var string|null
*/
public $content;
}
- Inject the DTO into your controller & configure it using the Dto attribute.
use Artyum\RequestDtoMapperBundle\Attribute\Dto;
use Artyum\RequestDtoMapperBundle\Extractor\JsonExtractor;
class CreatePostController extends AbstractController
{
#[Dto(extractor: JsonExtractor::class, subject: PostPayload::class, validate: true)]
public function __invoke(PostPayload $postPayload): Response
{
// At this stage, your DTO has automatically been mapped (from the JSON input) and validated.
// Your controller can safely be executed knowing that the submitted content
// matches your requirements (defined in your DTO through the validator constraints).
}
}
Alternatively, you can set the attribute directly on the argument:
public function __invoke(#[Dto(extractor: JsonExtractor::class, validate: true)] PostPayload $postPayload): Response
{
}
If you have set some default options in the configuration file (the default extractor to use, whether to enable the validation), you can even make it shorter:
public function __invoke(#[Dto] PostPayload $postPayload): Response
{
}
- That's it!
The Dto attribute has the following seven properties:
The FQCN (Fully-Qualified Class Name) of a class that implements the ExtractorInterface
. It basically contains the extraction logic and it's called by the mapper in order to extract the data from the request.
The bundle already comes with 3 built-in extractors that should meet most of your use-cases:
- BodyParameterExtractor (extracts the data from
$request->request->all()
) - JsonExtractor (extracts the data from
$request->toArray()
) - QueryStringExtractor (extracts the data from
$request->query->all()
)
If an error occurs when the extract()
method is called from the extractor class, the ExtractionFailedException will be thrown.
If these built-in extractor classes don't meet your needs, you can implement your own extractor like this:
use Artyum\RequestDtoMapperBundle\Extractor\ExtractorInterface;
use Symfony\Component\HttpFoundation\Request;
class CustomExtractor implements ExtractorInterface
{
// you can optionally inject dependencies
public function __construct() {
}
public function extract(Request $request): array
{
// your custom extraction logic here
}
}
Then pass it to the Dto
attribute like this:
#[Dto(extractor: CustomExtractor::class)]
If you don't set any value, the default value (defined in the bundle's configuration file) will be used.
Note: All classes implementing ExtractorInterface
are automatically tagged as "artyum_request_dto_mapper.extractor",
and this is needed by the mapper in order to retrieve the needed extractor class instance from the container.
The FQCN (Fully-Qualified Class Name) of the DTO you want to map (it must be present as your controller argument).
The "subject" property is required only if you're setting the attribute directly on the method. Example:
#[Dto(subject: PostPayload::class)]
public function __invoke(PostPayload $postPayload): Response
{
}
If you're setting the attribute on the method argument instead, the "subject" value can be omitted and won't be read by the mapper. Example:
public function __invoke(#[Dto] PostPayload $postPayload): Response
{
}
It can contain a single or an array of HTTP methods that will "enable" the mapping/validation depending on the current HTTP method. In the following example, the DTO will be mapped & validated only if the request method is "GET".
#[Dto(methods: 'GET')]
or
#[Dto(methods: ['GET'])]
If the array is empty (this is the default value), the mapper will always map the DTO and validate it.
The options that will be passed to the Denormalizer before mapping the DTO.
Example:
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
#[Dto(denormalizerOptions: [ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true])]
If an error occurs when the denormalize()
method is called from the Denormalizer, the DtoMappingFailedException will be thrown.
Whether to validate the DTO (once the mapping is done). Internally, the validator component will be used, and if you do not have it installed a LogicException
will be thrown.
Example:
#[Dto(validate: true)]
If the validation failed (due to the constraint violations), the constraint violations will be available as request attribute:
$request->attributes->get('_constraint_violations')
If you don't set any value, the configured value (defined in the bundle's configuration file) will be used.
The validation groups to pass to the validator.
Example:
#[Dto(validationGroups: ['creation'])]
If you don't set any value, the configured value (defined in the bundle's configuration file) will be used.
When the validation failed, the DtoValidationFailedException will be thrown, and you will be able to get a list of these violations by calling the getViolations()
method.
Setting the value to false
will prevent the exception from being thrown, and your controller will still be executed.
Example:
#[Dto(throwOnViolation: false)]
If you don't set any value, the configured value (defined in the bundle's configuration file) will be used.
- PreDtoMappingEvent - dispatched before the mapping is made.
- PostDtoMappingEvent - dispatched once the mapping is made.
- PreDtoValidationEvent - dispatched before the validation is made (if the validation is enabled).
- PostDtoValidationEvent - dispatched once the validation is made (if the validation is enabled).