Skip to content

PHP Command DTO with Symfony Constraints equivalent in Angular Forms #66

@webdevilopers

Description

@webdevilopers

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions