Skip to content

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

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?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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