vendor/artyuum/request-dto-mapper-bundle/src/Mapper/DtoMapper.php line 153

Open in your IDE?
  1. <?php
  2. namespace Artyum\RequestDtoMapperBundle\Mapper;
  3. use Artyum\RequestDtoMapperBundle\Annotation\Dto;
  4. use Artyum\RequestDtoMapperBundle\Event\PostDtoMappingEvent;
  5. use Artyum\RequestDtoMapperBundle\Event\PreDtoMappingEvent;
  6. use Artyum\RequestDtoMapperBundle\Event\PreDtoValidationEvent;
  7. use Artyum\RequestDtoMapperBundle\Exception\DtoDefinitionException;
  8. use Artyum\RequestDtoMapperBundle\Exception\DtoMappingException;
  9. use Artyum\RequestDtoMapperBundle\Exception\DtoValidationException;
  10. use Doctrine\Common\Annotations\AnnotationReader;
  11. use ReflectionClass;
  12. use ReflectionException;
  13. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  14. use Symfony\Component\HttpFoundation\Request;
  15. use Symfony\Component\Serializer\Exception\ExceptionInterface;
  16. use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
  17. use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
  18. use Symfony\Component\Serializer\SerializerInterface;
  19. use Symfony\Component\Validator\Validator\ValidatorInterface;
  20. use Throwable;
  21. class DtoMapper
  22. {
  23. private EventDispatcherInterface $eventDispatcher;
  24. private SerializerInterface $serializer;
  25. private DenormalizerInterface $denormalizer;
  26. private ValidatorInterface $validator;
  27. private array $defaultMapperOptions = [
  28. ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true,
  29. ];
  30. public function __construct(
  31. EventDispatcherInterface $eventDispatcher, SerializerInterface $serializer, DenormalizerInterface $denormalizer,
  32. ValidatorInterface $validator
  33. ) {
  34. $this->serializer = $serializer;
  35. $this->denormalizer = $denormalizer;
  36. $this->validator = $validator;
  37. $this->eventDispatcher = $eventDispatcher;
  38. }
  39. /**
  40. * Maps the passed array to the DTO.
  41. *
  42. * @throws ExceptionInterface
  43. */
  44. private function mapFromArray(array $array, string $dto, array $options = []): DtoInterface
  45. {
  46. $options = array_merge($this->defaultMapperOptions, $options);
  47. /** @var DtoInterface $dto */
  48. $dto = $this->denormalizer->denormalize($array, $dto, null, $options);
  49. return $dto;
  50. }
  51. /**
  52. * Maps the passed raw body (assuming its JSON) to the DTO.
  53. *
  54. * @throws DtoMappingException
  55. */
  56. private function mapFromJson(string $rawBody, string $dto, array $options = []): DtoInterface
  57. {
  58. $options = array_merge($this->defaultMapperOptions, $options);
  59. try {
  60. /** @var DtoInterface $dto */
  61. $dto = $this->serializer->deserialize($rawBody, $dto, 'json', $options);
  62. } catch (Throwable $throwable) {
  63. throw new DtoMappingException($throwable->getMessage(), 0, $throwable);
  64. }
  65. return $dto;
  66. }
  67. /**
  68. * Gets the right Dto annotation class instance from the passed FQCN for the current HTTP method.
  69. *
  70. * @throws ReflectionException
  71. */
  72. private function getDtoAnnotation(string $dto, string $method): ?Dto
  73. {
  74. $reflectionClass = new ReflectionClass($dto);
  75. $annotationReader = new AnnotationReader();
  76. $annotations = $annotationReader->getClassAnnotations($reflectionClass);
  77. // loops through all annotations of the class and return the Dto annotation class instance if found
  78. foreach ($annotations as $annotation) {
  79. if (!$annotation instanceof Dto) {
  80. continue;
  81. }
  82. if (in_array($method, $annotation->methods, true)) {
  83. return $annotation;
  84. }
  85. }
  86. // gets the Dto annotation class instance from the parent class if it wasn't found above
  87. if ($reflectionClass->getParentClass() && $reflectionClass->getParentClass()->implementsInterface(DtoInterface::class)) {
  88. return $this->getDtoAnnotation($reflectionClass->getParentClass()->getName(), $method);
  89. }
  90. return null;
  91. }
  92. /**
  93. * @return array|bool[]
  94. */
  95. public function getDefaultMapperOptions()
  96. {
  97. return $this->defaultMapperOptions;
  98. }
  99. /**
  100. * Maps the request data to the DTO.
  101. *
  102. * @throws DtoDefinitionException
  103. * @throws DtoMappingException
  104. * @throws DtoValidationException
  105. * @throws ExceptionInterface
  106. * @throws ReflectionException
  107. */
  108. public function map(Request $request, string $dto): DtoInterface
  109. {
  110. $dtoAnnotation = $this->getDtoAnnotation($dto, $request->getMethod());
  111. if (!$dtoAnnotation) {
  112. throw new DtoDefinitionException('There is no context set for the current HTTP method: ' . $request->getMethod());
  113. }
  114. /** @var PreDtoMappingEvent $preDtoMappingEvent */
  115. $preDtoMappingEvent = $this->eventDispatcher->dispatch(new PreDtoMappingEvent($request, $dto, $dtoAnnotation));
  116. // calls the proper "mapper" method based on the passed source
  117. if ($dtoAnnotation->source === 'query_strings') {
  118. $dto = $this->mapFromArray($preDtoMappingEvent->getRequest()->query->all(), $dto, $preDtoMappingEvent->getOptions());
  119. } elseif ($dtoAnnotation->source === 'body_parameters') {
  120. $dto = $this->mapFromArray($preDtoMappingEvent->getRequest()->request->all(), $dto, $preDtoMappingEvent->getOptions());
  121. } elseif ($dtoAnnotation->source === 'json') {
  122. $dto = $this->mapFromJson($preDtoMappingEvent->getRequest()->getContent(), $dto, $preDtoMappingEvent->getOptions());
  123. }
  124. if ($dtoAnnotation->validation) {
  125. /** @var PreDtoValidationEvent $preDtoValidationEvent */
  126. $preDtoValidationEvent = $this->eventDispatcher->dispatch(new PreDtoValidationEvent($request, $dto, $dtoAnnotation));
  127. if (count($errors = $this->validator->validate($preDtoValidationEvent->getDto(), null, $dtoAnnotation->validationGroups))) {
  128. throw new DtoValidationException($errors);
  129. }
  130. }
  131. /** @var PostDtoMappingEvent $postDtoMappingEvent */
  132. $postDtoMappingEvent = $this->eventDispatcher->dispatch(new PostDtoMappingEvent($dto));
  133. return $postDtoMappingEvent->getDto();
  134. }
  135. }