Validating Value Objects

Previous blog post about Value Objects generated quite a lot of questions about validation in context of VO:

  • should be the VO responsible for validating its state?
  • how we should handle this validation?
  • how to interact with users?

And in this post I will try to answer these questions and show some code samples.

Are Value Objects responsible for self-validation?

No. But they are responsible for keeping their state consistent so sanity checking data on construction can be considered a good practice. And by sanity check you can understand a simple action that will analyse passed value and decide whether this value makes sense in context of VO or not.

So for example creating an EmailAddress with a random string that is not a valid email doesn't make sense in any context. The same applies to Money which for example in your application cannot be negative - it is impossible that you have -£10 in your pocket - but you can owe someone a tenner. Another example could be DateRange - end date cannot be lower than start date and so on.

Validation is contextual

So where the validation of values belongs to? The answer is: to your application. It is your application responsibility to validate the fact that user's email address will not be duplicated, will be from whitelisted domain, refer to valid MX record and so on. And indeed these rules can differ depends on context - you may want to allow register to site only people that have company's address but anyone can send a complaint using contact form. Regardless EmailAddress VO constraints remain the same - just a valid email string.

Although it doesn't mean Value Objects are universal in all cases and you cannot make their constraints stronger/weaker. They can vary depending on the Domain/Bounded Context of your application and in some cases it may actually make sense to have negative amount of money. Even inside one application you may end up with two different SKU VO formats for two Bounded Contexts - one in the context of one platform you integrate with and second for the other.

Communication with users

As important as having valid state of application and domain model is to give feedback to users. And again - this is not responsibility of the domain model to be coupled to user messages (that can also depend on the context). Especially that throwing an exception is not the best way to tell the user that something is no ok (but it is good to communicate in such way with developers). Good frameworks will allow you to do so and will not force you to couple together application layers. And here is the whole point.

Validation keeps layers consistent

When you think about validation and making data consistent think about it in context of the layer you are working on. Presentation layer goal is to communicate with user and should do it nicely to support user. Application layer should make sure that data make sense in a context of the application, and finally Domain layer is the foundation that your application is build upon and you don't want it to break unexpectedly, don't you?

Importance of Domain consistency

I'd like to give an example how using Value Objects could save you from trouble. Couple of years ago I was working on e-commerce platform. Users were registering on the website following pretty standard process - by providing username, email and password. During registration process user input was validated and then saved to database. Couple months later we got notification of error on the website that system was trying to send a reset password message to malformed email address. It turned out that database mapping was incorrect and all email addresses longer than certain number of characters were truncated and we spotted that only because of mailer failure.

It was clear we have a bug in the system, and it happens. The problem was that by this time we had already registered number of users with truncated email addresses and potentially we have lost revenue (no way to contact that users and fix the situation). Back then we were not using VO but if we would then things would look differently. We could use EmailAddress VO and have caught this problem when data was fetched from database and hydrated to objects. And in that case problem will be raised straight after user login, not late in the process when we actually needed to use that data.

Example of implementation in Symfony

To explain concepts described above we will solve problem of users requesting access to the application. Invitation contains email address and age of person which needs to be greater than 18 (business rule). Also users must agree with terms and conditions and in order to persist request to database.

Examples below shows usage of Symfony2 forms, Value Objects, Entities and makes use of Command pattern.

First we have two Value Objects - EmailAddress and AdultAge that are part of the Domain.

class EmailAddress  
{
    private $address;

    public function __construct($address)
    {
        if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException(sprintf('"%s" is not a valid email', $address));
        }

        $this->address = $address;
    }

    public function __toString()
    {
        return $this->address;
    }
}
class AdultAge  
{
    private $age;

    public function __construct($age)
    {
        if (!is_integer($age) || $age <= 18) {
            throw new InvalidArgumentException(sprintf('Age must be a number greater than 18. "%s" given', $age));
        }

        $this->age = (int)$age;
    }

    public function asInteger()
    {
        return $this->age;
    }
}

Also there is an InvitationRequest and related InvitationRequestRepository:

class InvitationRequest  
{
    private $id;
    private $emailAddress;
    private $age;

    function __construct(EmailAddress $emailAddress, AdultAge $age)
    {
        $this->emailAddress = $emailAddress;
        $this->age = $age;
    }

    public function getEmailAddress()
    {
        return $this->emailAddress;
    }
}
interface InvitationRequestRepository  
{
    public function save(InvitationRequest $interest);
}

RequestInvitationCommand is a Data-Transfer Object which will be validated and used to create request.

class RequestInvitationCommand  
{
    /**
     * @Assert\NotBlank()
     * @Assert\Email(
     *     message = "The email '{{ value }}' is not a valid address.",
     *     checkMX = true
     * )
     */
    public $emailAddress;

    /**
     * @Assert\NotBlank()
     * @Assert\Type(
     *     message="The age {{ value }} is not a valid number.",
     *     type = "integer"
     * )
     * @Assert\GreaterThan(
     *     value = 18
     * )
     */
    public $age;

    /**
     * @Assert\True(message = "You must accept terms and conditions.")
     */
    public $acceptTermsAndConditions = false;

RequestInvitationCommandHandler knows how to handle process of creating new request.

class RequestInvitationCommandHandler  
{
    private $repository;

    function __construct(InvitationRequestRepository $repository)
    {
        $this->repository = $repository;
    }

    public function handle(RequestInvitationCommand $command)
    {
        $interest = new InvitationRequest(
            new EmailAddress($command->emailAddress),
            new AdultAge($command->age)
        );
        $this->repository->save($interest);
    }
}

Finally there is a BetaAccessController that integrates everything together:

class BetaAccessController  
{
    private $formBuilder;
    private $requestInvitationCommandHandler;

    function __construct(
        FormBuilder $formBuilder,
        RequestInvitationCommandHandler $requestInvitationCommandHandler
    ) {
        $this->formBuilder = $formBuilder;
        $this->requestInvitationCommandHandler = $requestInvitationCommandHandler;
    }

    public function requestInvitationAction(Request $request)
    {
        $command = new RequestInvitationCommand();

        $form = $this->formBuilder->create('requestInvitation', $command)
            ->add('emailAddress', 'email')
            ->add('age', 'number')
            ->add('acceptTermsAndConditions', 'checkbox')
            ->add('save', 'submit', array('label' => 'Request invitation'))
            ->getForm();

        $form->handleRequest($request);

        if ($form->isValid()) {
            $this->requestInvitationCommandHandler->handle($command);

            return ['message' => 'Your request was saved.'];
        }

        return ['requestInvitationForm' => $form->createView()];
    }
}

Summary

I hope above explanations and examples gave you and idea where validation belongs in the application and how to integrate it with sanity checks of Value Object. The key point I'd like to stress here is that in Domain-Driven Design we strongly rely on assumption that Domain Model is always in valid state and this rule not only applies to Entities but as well to Value Objects.

@mrrmiller and @everzet - many thanks for review and valuable comments about the article