Validation of Doctrine 2 Entities

Posted about 1 year ago by beberlei

While Doctrine 1 had validation nested inside the Doctrine_Record instance this is not the case in Doctrine 2 anymore. We won't ship Doctrine 2 with any validators, the reason being that we think all the frameworks out there already ship with quite decents ones that can be integrated into your Domain easily. Besides us being ORM experts not wanting to maintain yet another validation library, moving the responsibility of validation into the domain layer also allows you to integrate it much easier into frameworks form libraries for example.

What we do offer are hooks to execute any kind of validation inside the Doctrine ORM.

Entities can register lifecycle event methods with Doctrine that are called on different occasions. For validation we would need to hook into the events called before persisting and updating. Even though we don't support validation out of the box, the implementation is even simpler than in Doctrine 1 and you will get the additional benefit of being able to re-use your validation in any other part of your domain.

Say we have an Order with several OrderLine instances. We never want to allow any customer to order for a larger sum than he is allowed to:

<?php

class Order
{
    public function assertCustomerAllowedBuying()
    {
        $orderLimit = $this->customer->getOrderLimit();

        $amount = 0;
        foreach ($this->orderLines AS $line) {
            $amount += $line->getAmount();
        }

        if ($amount > $orderLimit) {
            throw new CustomerOrderLimitExceededException();
        }
    }
}

Now this is some pretty important piece of business logic in your code, enforcing it at any time is important so that customers with a unknown reputation don't owe your business too much money.

We can enforce this constraint in any of the metadata drivers. First Annotations:

<?php

/**
 * @Entity
 * @HasLifecycleCallbacks
 */
class Order
{
    /**
     * @PrePersist @PreUpdate
     */
    public function assertCustomerAllowedBuying() {}
}

In XML Mappings:

<doctrine-mapping>
    <entity name="Order">
        <lifecycle-callbacks>
            <lifecycle-callback type="prePersist" method="assertCustomerAllowedBuying" />
            <lifecycle-callback type="preUpdate" method="assertCustomerAllowedBuying" />
        </lifecycle-callbacks>
    </entity>
</doctirne-mapping>

YAML needs some little change yet, to allow multiple lifecycle events for one method, this will happen before Beta 1 though.

Now validation is performed whenever you call EntityManager#persist($order) or when you call EntityManager#flush() and an order is about to be updated. Any Exception that happens in the lifecycle callbacks will be catched by the EntityManager and the current transaction is rolled back.

Of course you can do any type of primitive checks, not null, email-validation, string size, integer and date ranges in your validation callbacks.

<?php

class Order
{
    /**
     * @PrePersist @PreUpdate
     */
    public function validate()
    {
        if (!($this->plannedShipDate instanceof DateTime)) {
            throw new ValidateException();
        }

        if ($this->plannedShipDate->format('U') < time()) {
            throw new ValidateException();
        }

        if ($this->customer == null) {
            throw new OrderRequiresCustomerException();
        }
    }
}

What is nice about lifecycle events is, you can also re-use the methods at other places in your domain, for example in combination with your form library. Additionally there is no limitation in the number of methods you register on one particular event, i.e. you can register multiple methods for validation in "PrePersist" or "PreUpdate" or mix and share them in any combinations between those two events.

There is no limit to what you can and can't validate in "PrePersist" and "PreUpdate" aslong as you don't create new entity instances. This was already discussed in the previous blog post on the Versionable extension, which requires another type of event called "onFlush".

Also read:


Comments (6) [ add comment ]

Useful Posted by Grekker about about 1 year ago.

Another insightful article about Doctrine 2, and another nice example about how cleanly you can separate D2 from your domain model.

Thanks beberlei.

Looking forward to your blog post on internationalization in D2! :)

Thanks! Posted by annis about about 1 year ago.

These kind of posts are highly appreciated and show the power of D2! :)

Cheers, Daniel

very good Posted by alexey_baranov about about 1 year ago.

beberlei Best

setter? Posted by Jaka Jancar about about 1 year ago.

Wouldn't the better place to check this be in the setOrderLines() method of the entity?

re: setter? Posted by Matt about about 1 year ago.

I think the order lines validation is best placed as it is in the example...consider that when shopping, people may want to add items to their shopping cart and this may exceed the limit even though they're planning to remove some of those items later as they decide what they really want. So I think the validation is best done at the very end, right before persisting the order.

exceptions for everything? Posted by Matt about about 1 year ago.

What I would question about the example is whether it makes sense to throw exceptions for something like an invalid date...this is a fairly routine validation concern, and it seems it would make more sense to call a validate() method from the client code and retrieve any errors using a getErrors() method. But I'm not sure how this could best be combined with the prepersist lifecycle method...

I guess the idea with the example is that a ValidateException would be used for any typical data entry validation concern, rather than having an InvalidDateException, MaximumLengthExceededException, etc.... and that this generic ValidateException would contain the error message as well as an errorType property, whatever it might be.

Any other thoughts on this?

Create Comment