-
Notifications
You must be signed in to change notification settings - Fork 11
Description
When using CQRS we define a readonly Command DTO with the required public properties and add validation rules (Symfony Constraints) as attributes:
use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;
final readonly class AddPerson
{
public function __construct(
#[OA\Property(type: "string", format: "uuid", example: "756cda02-c20c-4622-9921-ef31759fd555")]
#[Assert\NotBlank]
#[Assert\Uuid]
public string $tenantId,
#[OA\Property(type: "string", format: "uuid", example: "a19f15f1-07b8-46af-8bca-84154153341d")]
#[Assert\NotBlank]
#[Assert\Uuid]
public string $personId,
#[OA\Property(type: "string", example: "John")]
#[Assert\NotBlank]
#[Assert\Type('string')]
public string $firstName,
#[OA\Property(type: "string", example: "Doe")]
#[Assert\NotBlank]
#[Assert\Type('string')]
public string $lastName,
)
{
}
}When using Symfony Forms we can tell the Form to use this DTO as its data_class:
use Symfony\Component\OptionsResolver\OptionsResolver;
class AddPersonType extends AbstractType
{
// ...
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AddPerson::class,
]);
}
}https://symfony.com/doc/current/forms.html#creating-form-classes
This way the constraints are also validated:
https://symfony.com/doc/current/forms.html#validating-forms
Is there an equivalent in Angular Forms?
In the frontend we create - analogous to the backend - (Command) DTOs encapsulating the properties.
type TAddPerson = {
personId: string;
firstName: string;
lastName: string;
}
export class AddPerson {
private _personId: PersonId;
private _firstName: PersonalName;
private _lastName: PersonalName;
constructor(
personId: PersonId,
firstName: PersonalName,
lastName: PersonalName,
) {
this._personId = personId;
this._firstName = firstName;
this._lastName = lastName;
}
static fromRawValues(data: TAddPerson): AddPerson {
return new AddPerson(
PersonId.fromString(data.personId),
PersonalName.fromString(data.firstName),
PersonalName.fromString(data.lastName),
);
}
public toObject(): any {
return {
personId: this._personId.toString(),
firstName: this._firstName.toString(),
lastName: this._lastName.toString(),
}
}
}Please ignore the value objects and the named constructor fromString for now. These could be primitives too.
These can be populated by using a named constructor fromRawValues which receives the data of a form.
export class AddPersonComponent implements OnInit, AfterViewInit {
addPersonFormGroup!: FormGroup;
initFormGroups(): void {
this.addPersonFormGroup = new FormGroup(
{
firstName: new FormControl(null, [
Validators.required,
Validators.minLength(1),
]),
lastName: new FormControl(null, [
Validators.required,
Validators.minLength(1),
]),
}
);
}
addPerson(): void {
this.isSubmitActive = false;
const addPersonData = this.addPersonFormGroup.getRawValue();
addPersonData.personId = PersonId.generate();
const command = AddPerson.fromRawValues(addPersonData);
this.addPersonHandler.addPerson(command).subscribe({
next: () => {
this.modal.close();
this.update.emit();
},
error: (response) => {
this.errorList = response.error;
},
complete: () => {
this.isSubmitActive = true;
}
});
}This makes it easy to serialize the properties and send them to the backend API endpoint.
export class AddPersonHandlerService {
static API_NAME: string = 'PERSONNEL_MANAGEMENT_API';
static LINK_NAME__ADD_PERSON: string = 'personAdd';
constructor(
private restApiService: RestApiService,
private authHttpService: AuthHttpService,
) {
this.restApiService.addApi({
name: AddPersonHandlerService.API_NAME,
uri: of('/'),
});
}
addPerson(command: AddPerson): Observable<IError> {
return this.restApiService.getRoute(
AddPersonHandlerService.API_NAME,
AddPersonHandlerService.LINK_NAME__ADD_PERSON,
).pipe(
mergeMap(uri => this.authHttpService.post<IError>(uri, command.toObject())),
);
}
}Unfortunately the validation constraints are added on the Form Group, NOT the DTO.
Is there a way?