Skip to content

Instantly share code, notes, and snippets.

@DavidBadura
Last active May 24, 2023 09:49
Show Gist options
  • Save DavidBadura/361c183bb76007b6ca822e10ffe5efbd to your computer and use it in GitHub Desktop.
Save DavidBadura/361c183bb76007b6ca822e10ffe5efbd to your computer and use it in GitHub Desktop.
RequestBodyResolver
<?php
declare(strict_types=1);
namespace App\Share\Application\ArgumentResolver;
use Attribute;
#[Attribute(Attribute::TARGET_PARAMETER)]
class FromRequestBody
{
}
<?php
declare(strict_types=1);
namespace App\Car\Application\Request;
use App\Car\Domain\LicensePlate;
use OpenApi\Attributes as OA;
final class RegisterCar
{
public function __construct(
#[OA\Property(type: 'string', example: 'BN-AA-1234')]
public readonly LicensePlate $licensePlate
) {
}
}
<?php
declare(strict_types=1);
namespace App\Car\Application\Controller;
use App\Car\Application\Command\RegisterCar;
use App\Car\Application\Request\RegisterCar as RegisterCarDto;
use App\Car\Domain\CarId;
use App\Share\Application\ArgumentResolver\FromRequestBody;
use App\Share\Application\Response\Created;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Attributes as OA;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
#[AsController]
#[OA\Tag(name: 'car')]
final class RegisterCarAction
{
public function __construct(
private readonly MessageBusInterface $commandBus,
) {
}
#[OA\Response(
response: 200,
description: 'Returns car id',
content: new OA\JsonContent(
ref: new Model(type: Created::class)
)
)]
#[OA\RequestBody(
required: true,
content: new OA\JsonContent(
ref: new Model(type: Created::class)
)
)]
#[Route('api/car/register', methods: ['POST'])]
public function __invoke(
#[FromRequestBody]
RegisterCarDto $requestDto
): Created {
$carId = CarId::generate();
$this->commandBus->dispatch(
new RegisterCar(
$carId,
$requestDto->licensePlate
)
);
return new Created($carId->toString());
}
}
<?php
declare(strict_types=1);
namespace App\Share\Application\ArgumentResolver;
use App\Share\Application\RequestDtoResolver;
use RuntimeException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use function class_exists;
class RequestBodyResolver implements ArgumentValueResolverInterface
{
public function __construct(
private readonly RequestDtoResolver $dtoResolver
) {
}
public function supports(Request $request, ArgumentMetadata $argument): bool
{
return $argument->getAttributes(FromRequestBody::class) !== [];
}
/**
* @return iterable<object>
*/
public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
$type = $argument->getType();
if (!$type) {
throw new RuntimeException('missing type');
}
if (!class_exists($type)) {
throw new RuntimeException('class not exists');
}
yield $this->dtoResolver->resolve(
$request,
$type
);
}
}
<?php
declare(strict_types=1);
namespace App\Share\Application;
use App\Share\Domain\Id;
use CuyZ\Valinor\Mapper\Object\DynamicConstructor;
use CuyZ\Valinor\Mapper\Source\JsonSource;
use CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder;
use CuyZ\Valinor\Mapper\TreeMapper;
use CuyZ\Valinor\MapperBuilder;
use Symfony\Component\HttpFoundation\Request;
use Throwable;
use function assert;
use function dd;
use function is_a;
use function is_string;
final class RequestDtoResolver
{
private TreeMapper $mapper;
public function __construct()
{
$this->mapper = (new MapperBuilder())
->registerConstructor(
#[DynamicConstructor]
static function (string $className, string $value): Id {
assert(is_a($className, Id::class, true));
return $className::fromString($value);
}
)
->filterExceptions(static fn (Throwable $throwable) => MessageBuilder::from($throwable))
->mapper();
}
/**
* @param class-string<InstanceType> $className
*
* @return InstanceType
*
* @template InstanceType as object
*/
public function resolve(Request $request, string $className): object
{
$content = $request->getContent();
assert(is_string($content));
$dto = $this->mapper->map(
$className,
new JsonSource($content)
);
if (!is_a($dto, $className)) {
dd($dto, $className);
}
assert(is_a($dto, $className));
return $dto;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment