Collection of Value Objects with Doctrine

Over the last year, I have been working more and more with value objects in my applications. Value objects are a great way to have a cleaner Domain Model and codebase. It makes those oversized models a lot cleaner and easier to understand.

Half a year ago I also wrote an article about value objects and how we can persist them with the Doctrine ORM. You can read this article here if you want a refresh.

Since then I have learned some new tricks and changed some ideas I had when working with value objects and entities in Symfony. These days I prefer to use an assertion library to validate the inputs of my value objects. We always want to make sure you can’t create an invalid value object. The same is true for entities, we don’t want to have an Entity in an invalid state. For this named constructors might be a good solution. In the end, it’s all about keeping your domain clean and consistent.

I’m also becoming a big fan of using XML for ORM mapping. I have no problem using annotations in some projects. But I find it easier and cleaner to work with XML files. This also keeps our database definitions separate from our domain. ORM mapping has no place in a rich domain in my opinion.

But I came across one irritating issue when working with value objects. How do we persist a collection of value objects in Doctrine ORM? There is currently no built-in solution that exists.

Creating an Entity and Value Object

Before we can look deeper at the problem and try out some solutions, we need to start by creating an entity and a value object. For our solutions, we will be using a Customer entity and an Address value object.

The Customer entity:

class Customer
{
    /**
     * @var int
     */
    private $id;

    /**
     * @var string
     */
    private $name;

    /**
     * @var Collection
     */
    private $addresses;

    public function __construct(int $id, string $name)
    {
        $this->id = $id;
        $this->name = $name;
    }

    public function getAddresses(): Collection
    {
        return $this->addresses;
    }
}

The ORM mapping for this entity:

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping">
    <entity name="App\Entity\Customer">
        <id name="id" column="id" type="integer">
            <generator strategy="AUTO"/>
        </id>
        <field name="name" type="string"/>
    </entity>
</doctrine-mapping>

Then next we create the Address value object. This object has no identity and consists of some basic address fields.

class Address
{
    /**
     * @var string
     */
    private $street;

    /**
     * @var string
     */
    private $postalCode;

    /**
     * @var string
     */
    private $city;

    /**
     * @var string
     */
    private $country;


    public function __construct(string $street, string $postalCode, string $city, string $country)
    {
        $this->street = $street;
        $this->postalCode = $postalCode;
        $this->city = $city;
        $this->country = $country;
    }

    public function getStreet(): string
    {
        return $this->street;
    }

    public function getPostalCode(): string
    {
        return $this->postalCode;
    }

    public function getCity(): string
    {
        return $this->city;
    }

    public function getCountry(): string
    {
        return $this->country;
    }
}

The ORM mapping for this value object:

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping">
    <embeddable name="App\Entity\Address">
        <field name="street" type="string"/>
        <field name="postalCode" type="string"/>
        <field name="city" type="string"/>
        <field name="country" type="string"/>
    </embeddable>
</doctrine-mapping>

Our business rules tell us that a customer can have multiple addresses. But you will notice that we haven’t mapped the address collection yet in Customer and that the value object is an embeddable without an identifier.

The problem

The Customer entity needs to have a collection of addresses. We could use embeddables, but you can’t use them in collections. We don’t have multi-dimensional columns in relational databases, thus we can’t use an embeddable for collections.

When using collections in a relational database, we need to have a separate Address table with a join table between Customer and Address.

This means our Address value object should have an id at a database level. But remember, Value Objects should not have an identity.

Now the question is, how should we handle this problem?

Child Entity with Embeddable

A first solution could be to create a fake entity. This will then hold our Address value object. You could give this entity whatever name you want. But remember,  you will never directly interact with it in your domain. It only exists to translate the value object to Doctrine.

We create this fake entity by mapping the embeddable:

class AddressEntity
{
    /**
     * @var int
     */
    private $id;

    /**
     * @var Address
     */
    private $address;

    /**
     * AddressEntity constructor.
     * @param Address $address
     */
    public function __construct(Address $address)
    {
        $this->address = $address;
    }

    /**
     * @return Address
     */
    public function getAddress(): Address
    {
        return $this->address;
    }
}

The ORM mapping of this fake entity:

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping">
    <entity name="App\Entity\AddressEntity">
        <id name="id" column="id">
            <generator strategy="AUTO"/>
        </id>
        <embedded name="address" class="Address"/>
    </entity>
</doctrine-mapping>

Then in our Customer entity, we make the translation from the Address value object to the AddressEntity.

class Customer
{
    /**
     * @var int
     */
    private $id;

    /**
     * @var string
     */
    private $name;

    /**
     * @var Collection
     */
    private $addressEntities;

    /**
     * Customer constructor.
     * @param int $id
     * @param string $name
     */
    public function __construct(int $id, string $name)
    {
        $this->id = $id;
        $this->name = $name;
    }

    public function setAddresses(Collection $addresses): void
    {
        // Convert the Address Value Objects collection to an AddressEntity collection
        $this->addressEntities = $addresses->map(function($address) {
            return new AddressEntity($address);
        });
    }
    
    public function getAddresses(): Collection
    {
        // Convert back to a collection of address value objects
        return $this->addressEntities->map(function($addressEntity) {
            /** @var AddressEntity $addressEntity */
            return $addressEntity->getAddress();
        });
    }
}

We then have to create a one-to-many association with a join table.

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping">
    <entity name="App\Entity\Customer">
        <id name="id" column="id" type="integer">
            <generator strategy="AUTO"/>
        </id>
        <field name="name" type="string"/>
        <many-to-many field="addressEntities" target-entity="App\Entity\AddressEntity">
            <join-table name="customers_addresses">
                <join-columns>
                    <join-column name="customer_id" referenced-column-name="id" />
                </join-columns>
                <inverse-join-columns>
                    <join-column name="address_id" referenced-column-name="id" unique="true" />
                </inverse-join-columns>
            </join-table>
        </many-to-many>
    </entity>
</doctrine-mapping>

Now we can create an address value object and add it to the address collections.

$addresses = new ArrayCollection();
$addresses->add(new Address('Street 1', '1111', 'City', 'Belgium'));
$addresses->add(new Address('Street 2', '1111', 'City', 'Belgium'));
$customer->setAddresses($addresses);

Only in our Customer entity, we know about the AddressEntity. But outside that, we can still use this value object as a real value object.

But this is code smell in the domain object. Which is not an ideal solution. So let us look at another one.

Value Object as JSON

Another solution would be to save the value objects inside a JSON string in the database.

We will need to implement a JsonSerializable interface on our value object.

class Address implements \JsonSerializable 
{
    ...

    public function jsonSerialize()
    {
        return get_object_vars($this);
    }
}

Then in the Customer entity, we encode and decode the JSON.

public function setAddresses(array $addresses): void
{
    $this->addresses = array_map(function (Address $address) {
        return json_encode($address);
    }, $addresses);
}

public function getAddresses(): array
{
    return array_map(function ($address) {
        $address = json_decode($address);

        return new Address($address->street, $address->postalCode, $address->city, $address->country);
    }, $this->addresses);
}

Again this is code smell and not really that easy to maintain. There might be better and easier ways to handle encoding and decoding but still. It does not change the fact we are creating a code smell.

Value Object as Child Entity

The last solution is to make your value object a child entity. You basically promote your value object to become a child entity. Giving its own id.

We add an id to the Address object and then use the same doctrine mapping we used on the first solution.

class Address
{
    /**
     * @var int
     */
    private $id;

With the following ORM mapping:

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping">
    <entity name="App\Entity\Address">
        <id name="id" column="id" type="integer">
            <generator strategy="AUTO"/>
        </id>
        <field name="street" type="string"/>
        <field name="postalCode" type="string"/>
        <field name="city" type="string"/>
        <field name="country" type="string"/>
    </entity>
</doctrine-mapping>

Then in the Customer ORM mapping:

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping">
    <entity name="App\Entity\Customer">
        <id name="id" column="id" type="integer">
            <generator strategy="AUTO"/>
        </id>
        <field name="name" type="string"/>
        <many-to-many field="addresses" target-entity="App\Entity\Address">
            <join-table name="customers_addresses">
                <join-columns>
                    <join-column name="customer_id" referenced-column-name="id" />
                </join-columns>
                <inverse-join-columns>
                    <join-column name="address_id" referenced-column-name="id" unique="true" />
                </inverse-join-columns>
            </join-table>
        </many-to-many>
    </entity>
</doctrine-mapping>

This is in my opinion, the best solution. There is no real code smell now. We only made a sacrifice to promote our value object to a child entity to work with Doctrine. But this might be an acceptable sacrifice. As long as we keep this object immutable and we don’t directly interact with its id field. In essence, we still use it like any other value object.