jorge07/symfony-6-es-cqrs-boilerplate

Unique email validation

Closed this issue · 5 comments

First of all, thank you for a great example!

We have an aggregate domain rule "user must have an unique email", but we have just one assertion in factory. This is not a defensive way. In DDD domain object is permanently valid and can be used in stand alone manner. You can use specification pattern for validating aggregate rules.

public function changeEmail(Email $email, UniqueEmailSpecification $uniqueEmailSpecification): void
{
        Assertion::notEq((string)$this->email, (string)$email, 'New email should be different');

	if (!$uniqueEmailSpecification->isSatisfiedBy($email)) {
		throw new EmailAlreadyExistException('Email already registered.');
	}

    $this->apply(new UserEmailChanged($this->uuid, $email));
}

The same in User::create

Hi @ferrius

In SignUp we handle this by UserFactory doing this:

class UserFactory
{
    public function register(UuidInterface $uuid, Credentials $credentials): User
    {
        if ($this->userCollection->existsEmail($credentials->email)) {
            throw new EmailAlreadyExistException('Email already registered.');
        }

        return User::create($uuid, $credentials);
    }

The specification pattern can help also with this removing some code and being more DRY. Good catch 👍

zerai commented

Hi,
I think a domain service (interface + implementation) should be better for this type of 'domain rule', not all domain rules must be aggregates invariant.
Check uniqueness requirement inside aggregate is not a good idea in this case.
Maybe this article can clarify a little bit the difference.
Email uniqueness as an aggregate invariant

Thank you for the article.
But see the next article of this author and especially discussion in the comments.

I think the point here is that it's needed in more than one place so the UserFactory is taking this responsibility and as soon as this is needed in other places, it gets invalidated. It can also be validated in the application layer (command handler) but I prefer be as close as possible to the domain layer

Feedback appreciated here #99 ;)