-
Notifications
You must be signed in to change notification settings - Fork 0
Description
In most applications it makes sense to show full list of validation errors, not just the first one (as with the case of exception).
Technically it seems to be possible to implement this feature, though with somewhat oddish syntax.
The converter should throw as much details regarding what is wrong with given command as possible.
When combined with https://github.com/phphd/exceptional-validation-bundle/ this could give a strong foundation for enterprise applications.
The key idea is to use one-level indirection during command conversion:
private function createBluePrint(
UpdateFamilyProfileCommandDto $commandDto,
): Closure {
$id = $this->getId($commandDto);
$family = $this->getFamily($commandDto);
return static fn () => new UpdateFamilyProfileCommand($id(), $family());
}Here function is returned, and both $id and $family variables are functions.
This way it would be possible to change the execution flow from DFS into BFS, and therefore catch all the exceptions from given closures.
As a simple convention to introduce - when closure uses closure variables with no parameters, - then we could call it separately.
The rest of thoughs are in the code:
<?php
declare(strict_types=1);
namespace App;
require "/home/rela589n/Projects/opensource/phphd/exceptional-validation-bundle/vendor/autoload.php";
use Closure;
use DateTimeImmutable;
use PhPhD\ExceptionalValidation;
use ReflectionFunction;
use RuntimeException;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Constraints\Valid;
#[ExceptionalValidation]
final class UpdateFamilyProfileCommandDto
{
public function __construct(
private string $id,
#[Valid]
private readonly FamilyDto $family,
) {
}
public function getId(): string
{
return $this->id;
}
public function getFamily(): FamilyDto
{
return $this->family;
}
}
final class FamilyDto
{
public function __construct(
/** @var FamilyMemberDto[] */
#[Valid]
private readonly array $members,
) {
}
public function getMembers(): array
{
return $this->members;
}
}
final class FamilyMemberDto
{
public function __construct(
#[ExceptionalValidation\Capture(BirthDateInFutureException::class, when: [$this, 'matchesException'], message: 'errors.birth_date.in_future')]
private string $birthDate,
) {
}
public function getBirthDate(): string
{
return $this->birthDate;
}
public function matchesException(BirthDateInFutureException $exception): bool
{
return $exception->getBirthDate()->format('Y-m-d') === $this->birthDate;
}
}
final class ConversionBlueprintHandler
{
public function __invoke(UpdateFamilyProfileCommandDto $commandDto)
{
return new ConversionBluePrint($this->createBluePrint($commandDto));
}
private function createBluePrint(
UpdateFamilyProfileCommandDto $commandDto,
): Closure {
$id = $this->getId($commandDto);
$family = $this->getFamily($commandDto);
return static fn () => new UpdateFamilyProfileCommand($id(), $family());
}
private function getId(UpdateFamilyProfileCommandDto $commandDto): Closure
{
return static fn () => Uuid::fromString($commandDto->getId());
}
private function getFamily(UpdateFamilyProfileCommandDto $commandDto): Closure
{
$familyMembers = $this->getFamilyMembers($commandDto->getFamily());
return static fn () => new Family($familyMembers());
}
private function getFamilyMembers(FamilyDto $familyDto): Closure
{
$members = [];
foreach ($familyDto->getMembers() as $member) {
$members[] = $this->getMember($member);
}
return static fn (): array => array_reduce($members, static fn (Closure $closure) => $closure());
}
private function getMember(FamilyMemberDto $familyMemberDto): Closure
{
$birthDate = $this->getBirthDate($familyMemberDto->getBirthDate());
return static fn () => new FamilyMember($birthDate());
}
private function getBirthDate(string $birthDate): Closure
{
return static fn () => new BirthDate(new DateTimeImmutable($birthDate));
}
}
$blueprint = new ConversionBlueprintHandler();
$closure = $blueprint->__invoke(new UpdateFamilyProfileCommandDto('c4feb193-f5fb-40b0-9abf-98fc683c5e96', new FamilyDto([
new FamilyMemberDto('2024-01-01')
])));
$reflectionFunction = new ReflectionFunction($closure);
$rootUsedVariables = $reflectionFunction->getClosureUsedVariables();
$familyReflection = new ReflectionFunction($rootUsedVariables['family']);
$familyUsedVariables = $familyReflection->getClosureUsedVariables();
$membersReflection = new ReflectionFunction($familyUsedVariables['members']);
$membersVariables = $membersReflection->getClosureUsedVariables();
$firstMemberReflection = new ReflectionFunction($membersVariables['members'][0]);
$firstMemberVariables = $firstMemberReflection->getClosureUsedVariables();
$firstMemberBirthDateReflection = new ReflectionFunction($firstMemberVariables['birthDate']);
dd($firstMemberBirthDateReflection->getClosure()());
var_dump($closure);
die;
#[NextForwarded]
final class UpdateFamilyProfileCommand
{
public function __construct(
private Uuid $id,
private Family $family,
) {
}
public function getId(): Uuid
{
return $this->id;
}
public function getFamily(): Family
{
return $this->family;
}
}
final class Family
{
public function __construct(
/** @var FamilyMember[] */
private array $familyMembers,
) {
}
public function getFamilyMembers(): array
{
return $this->familyMembers;
}
}
final class FamilyMember
{
public function __construct(
private BirthDate $birthDate,
) {
}
public function getBirthDate(): BirthDate
{
return $this->birthDate;
}
}
final class BirthDate
{
public function __construct(
private DateTimeImmutable $birthDate,
) {
if ($this->birthDate > (new DateTimeImmutable())) {
throw new BirthDateInFutureException($this->birthDate);
}
}
}
final class BirthDateInFutureException extends RuntimeException
{
public function __construct(
private DateTimeImmutable $birthDate,
) {
parent::__construct();
}
public function getBirthDate(): DateTimeImmutable
{
return $this->birthDate;
}
}
/** @var \Symfony\Component\Messenger\MessageBusInterface $commandBus */
$commandBus->dispatch(new UpdateFamilyProfileCommandDto());