PHP Implementation of Domain-Driven Design - Value Object

Keywords: PHP Database JSON xml

Value object

By using the self keyword, we do not use value objects as basic building blocks for domain-driven design, and they are used in code to model common language concepts.Value objects are not just things measured, quantified or described in the field.Value objects can be thought of as small and simple objects - such as money or date ranges - whose equality is based not on their identity but on what they hold.

For example, product prices can be modeled using value objects.In this case, it does not represent a thing, but a value that can be used to measure the value of a product.The memory footprint of these objects is trivial, indeterminate (calculated from their components), and minimal.Therefore, even if you represent the same value, creating a new instance is better than referencing reuse, then checking for equality based on field comparability between the two instances.

Definition

Ward Cunningham defines the following value objects:

Something that can be measured or described.Examples of value objects are values, dates, currencies, and strings.Often, they are small objects that are fairly widely used.Their identity is based on their status, not their object identity.This allows you to have multiple copies of the same concept value object.Each $5 bill has its own identification (thanks to its serial number), but the cash economy relies on every $5 having the same value as the other $5.

Martin Fowler defines the following value objects:

A small object, such as a currency or date range object.Their key properties are value semantics, not reference semantics.You can often say that their conceptual equivalence is not based on their identity, but on whether two value objects are equal in all their fields.Although all fields are equal, you do not need to compare all fields if the subset is unique - for example, the currency code is sufficient for a currency object to demonstrate its equality.The value object of a general criterion should be completely immutable.If you want to change a value object, replacing it with a new object instead of allowing the value object itself to update the Value-Value object can cause confusion.

Examples of value objects are numeric values, text characters, date, time, a person's full name (consisting of last name, middle name, first name, title, etc.), currency, color, phone number, mailbox address.

Value Object VS. Entity

Consider the following example from Wikipedia to better understand the differences between value objects and entities:

  • Value object: When people exchange dollars, they usually don't distinguish between each unique bill; they only care about the literal value of the dollar.In this context, the dollar is the value object.However, the Federal Reserve may need to care about every single bill; in this context, every bill is an entity.
  • Entity: Most Airlines distinguish the uniqueness of each seat on a flight.Each seat in this upper and lower space is an entity.However, Southwest, Easy Jet and Ryanair don't distinguish between each seat. All seats are the same, where a seat is actually a value object.

Examples of money and money

Money and money objects are probably the most useful examples of explanatory value objects, thanks to money patterns.This design pattern provides a way to model problems to avoid floating-point rounding, which in turn allows deterministic operations to be performed.

In the real world, money describes a unit of money in the same way as distance units in meters and codes.Each currency is represented by three uppercase ISO codes:

class Currency
{
    private $isoCode;

    public function __construct($anIsoCode)
    {
        $this->setIsoCode($anIsoCode);
    }

    private function setIsoCode($anIsoCode)
    {
        if (!preg_match('/^[A-Z]{3}$/', $anIsoCode)) {
            throw new InvalidArgumentException();
        }
        $this->isoCode = $anIsoCode;
    }

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

One of the main goals of value objects is also the Holy Grail of object-oriented design: encapsulation.By following this pattern, you will eventually get a dedicated location where all validation, comparison logic, and behavior can be put together.

Extended Validator for Money
In the previous code example, we could build a currency class with an ISO code similar to AAA.It doesn't help to write a specific rule that checks for legitimate ISO code.Here is a complete list of ISO currency codes.If you need help, check out the Money packagist library.

Money is used to measure a specific amount of money.Its model consists of money and money.In the case of the money model, the amount is represented by the least valuable fraction of the currency - for example, in the case of dollars, euros, cents.

In addition, you may notice that we use self-encapsulation to set ISO code, which centralizes changes to the value object itself.

class Money
{
    private $amount;
    private $currency;

    public function __construct($anAmount, Currency $aCurrency)
    {
        $this->setAmount($anAmount);
        $this->setCurrency($aCurrency);
    }

    private function setAmount($anAmount)
    {
        $this->amount = (int)$anAmount;
    }

    private function setCurrency(Currency $aCurrency)
    {
        $this->currency = $aCurrency;
    }

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

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

Now that you know the formal definition of value objects, let's take a closer look at the maximum functionality they offer.

Features

When modeling a common language concept with code, you should always prefer to use value objects on entities.Value object problems are easier to create, test, use, and manage.

  • It tests, quantifies, or describes a thing in the field.
  • It can stay the same
  • It models a conceptual whole by integrating related attributes into a unified unit
  • It can be equal in value to compare with other value objects
  • It is completely replaceable when the measurement or description changes
  • It provides partners with side-effect-free behavior

Examine, quantify, or describe

As discussed earlier, a value object should not be considered a thing in your domain.As a value, it measures, quantifies, and describes concepts in the field.

In our example, money objects describe what kind of money is.Money objects measure or quantify a given currency unit.

Permanent

This is one of the most important aspects to master.Value objects should not change during their life weeks.Because of this permanence, value objects are easy to deduce and test and have no undesirable/unexpected side effects.Therefore, value objects should be created by their constructors.In order to generate a value object, you usually pass the necessary native type or other value object through a constructor.

Value objects are always in a valid state; that's why we create them in an atomic step.An empty constructor with multiple getter and setter methods transfers creation responsibility to the client, resulting in an anemia model, which is considered an antipattern.

It is also important to note that we do not recommend retaining references to entities in value objects.Entities are mutable, and retaining references to them may cause undesirable side effects in value objects.

In languages with method overloads, such as Java, you can create multiple constructors with the same name.Each constructor provides different options to generate the same type of object.In PHP, we can provide similar capabilities through factory methods.These specific factory methods are also known as construction semantics.The main goal of fromMoney is to provide more contextual meaning than ordinary constructors.A more radical approach suggests privatizing the u construct method and building each instance using a semantic constructor.

In our Money object, we can add some useful factory methods as follows:

class Money
{
// ...
    public static function fromMoney(Money $aMoney)
    {
        return new self(
            $aMoney->amount(),
            $aMoney->currency()
        );
    }

    public static function ofCurrency(Currency $aCurrency)
    {
        return new self(0, $aCurrency);
    }
}

By using the self keyword, we don't have to use class names to couple code.Therefore, changes to class names or namespaces do not affect factory methods.This small detail implementation will help refactor the code later.

static vs. self

When one value object inherits another value object, using static on the self can cause unexpected problems.

Because of this invariance, we must consider how volatile operations are handled in common locations in stateful contexts.If we need a state change, we need to use it to return a completely new value object representation.If we want to increase the amount, for example, a money value object, we return a new Money instance with the changes we need.

Fortunately, following this rule is relatively simple, as shown in the following example:

class Money
{
// ...
    public function increaseAmountBy($anAmount)
    {
        return new self(
            $this->amount() + $anAmount,
            $this->currency()
        );
    }
}

The Money object returned by increaseAmountBy is different from the Money client object that receives method calls.This can be observed in the following example comparability checks:

$aMoney = new Money(100, new Currency('USD'));
$otherMoney = $aMoney->increaseAmountBy(100);
var_dump($aMoney === otherMoney); // bool(false)
$aMoney = $aMoney->increaseAmountBy(100);
var_dump($aMoney === $otherMoney); // bool(false)

Conceptual Integration

So why not follow the example below and avoid instantiating a new object at the same time?

class Product
{
    private $id;
    private $name;
    /**
     * @var int
     */
    private $amount;
    /**
     * @var string
     */
    private $currency;
// ...
}

There are some obvious drawbacks to this approach, such as what you want to verify ISO. However, for Product, the responsibility to verify ISO is meaningless (thus violating the single responsibility principle).This is especially true if you want to reuse this part of the code in other areas (following the DRY principle).

Considering these factors, this use case is a perfect candidate for being abstracted as a value object, which not only gives you the opportunity to combine related attributes, but also allows you to create higher-order concepts and more specific generic languages.

Practice
Discuss with your partner whether an email can be considered a value object?Does the context it uses affect this?

Value Equality

As discussed at the beginning of this chapter, two value objects are equal if their measurements, quantifications, or descriptions are the same.

For example, imagine two Money objects representing a dollar.Can we say they are equal?In the real world, do two $1 coins have the same value?Certainly.Looking back at the code, the value object in the question refers to a different Money instance.However, they all represent the same value, making them equal.

In PHP, it is common to use==to compare two value objects.Looking at the definition of this operator in PHP Documentation highlights a more interesting behavior:

When using the comparison operator==, comparing the values of objects is a simple way: if two object instances have the same attributes and values, as well as instances of the same class, they are equal.

This behavior is consistent with our formal definition of the value object.However, as an exact class matching predicate, you should be cautious when dealing with value objects of subtypes.

Keeping this in mind, the stricter == operator doesn't help us either, unfortunately:

When using operation identifier===, two value objects are equal, only if they refer to the same instance of the same class.

The following examples can help validate these subtle differences:

$a = new Currency('USD');
$b = new Currency('USD');
var_dump($a == $b); // bool(true)
var_dump($a === $b); // bool(false)
$c = new Currency('EUR');
var_dump($a == $c); // bool(false)
var_dump($a === $c); // bool(false)

One solution is to implement a common method of determining equality in each value object.This method is responsible for checking the type and equality of their composite properties.It is very easy to compare abstract data types with PHP built-in type hints.You can also use the get_class() function to help you check the comparison if necessary.

However, language does not convey the true meaning of equality in your domain concepts, it also means that you must provide an answer.In order to compare Currency objects, we only need to confirm that their associated ISO codes are the same.The === operator is best represented in the following cases:

class Currency
{
    // ...
    public function equals(Currency $currency)
    {
        return $currency->isoCode() === $this->isoCode();
    }
}

Because the Money object uses a Currency object, the equals method needs to be executed along with the amount comparison.

class Money
{
// ...
    public function equals(Money $money)
    {
        return
            $money->currency()->equals($this->currency()) &&
            $money->amount() === $this->amount();
    }
}

Replaceability

Consider a Product entity that contains a Money value object to measure their value.In addition, consider two identical Product entities - for example, $100.This scenario can be modeled using two separate Money objects or two references to a single value object.

Sharing the same value object can be risky.If one changes, both will change.This behavior can be viewed as an abnormal side effect.For example, if Carlos was hired on February 20 and we know Christian was hired on the same day, we might set Christian's hiring date to be the same as Carlos.So as long as Carlos later changes his appointment to May, Charistian's appointment date will also change.Whether right or wrong, this is not what people expect.

Because of the problem highlighted in this example, when holding a reference to a value object, it is recommended that it be replaced as a whole instead of changing its value:

$this−>price = new Money(100, new Currency('USD'));
//...
$this->price = $this->price->increaseAmountBy(200);

This behavior is similar to how basic types in PHP, such as strings, work.Consider the function strtolower. It returns a new string instead of modifying the original value.Instead of using a reference, return a new value.

No side-effect behavior

If you want to introduce some additional behavior into the Money class, such as the add method, it is natural to check that the input meets any prerequisites and remains unchanged.In our example, we just want to increase money with the same currency:

class Money
{
// ...
    public function add(Money $money)
    {
        if ($money->currency() !== $this->currency()) {
            throw new InvalidArgumentException();
        }
        $this->amount += $money->amount();
    }
}

If two currencies do not match, an exception is thrown.Conversely, the amount will increase.However, this code still has some undesirable flaws.Now imagine a mysterious way in our code, otherMethod:

class Banking
{
    public function doSomething()
    {
        $aMoney = new Money(100, new Currency('USD'));
        $this->otherMethod($aMoney);//mysterious call
// ...
    }
}

Everything looks good until for some reason, when we go back or finish otherMethod, we start seeing unexpected results.Suddenly, $aMoney no longer contains $100.What happened?What happens if the other method uses the add method we defined earlier internally?Perhaps you don't understand that the mutated Urrency instance state was added.This is what we call side effects.You must avoid side effects.You can't mutate your argument.If you do, developers may encounter some strange behavior when using your objects.They will complain, and they will be right.So how should we solve this problem?Simply put, by ensuring that the value object remains the same, we can avoid such exceptional problems.A simple way to do this is to return a new instance for each variable operation, as in the add method below:

class Money
{
// ...
    public function add(Money $money)
    {
        if (!$money->currency()->equals($this->currency())) {
            throw new \InvalidArgumentException();
        }
        return new self(
            $money->amount() + $this->amount(),
            $this->currency()
        );
    }
}

With this simple change, immutability is guaranteed.Each time two Money instances are added, a new result instance is returned.Other classes can perform any number of changes without affecting the original copy.Code without side effects is easy to understand, easy to test, and difficult to make mistakes.

Basic types

Consider the following code snippet:

$a = 10;
$b = 10;
var_dump($a == $b);
// bool(true)
var_dump($a === $b);
// bool(true)
$a = 20;
var_dump($a);
// integer(20)
$a = $a + 30;
var_dump($a);
// integer(50);

Although $a and $b are different variables stored in different memory locations, they are the same when compared.They have the same value, so we think they are equal.You can change the value of $a from 10 to 20 at any time, and you can replace as many integers as you can, regardless of the previous value, because you have not modified it at all; you are just replacing it.If you apply any action to these variables (for example, $a +$b), you can get another value that can be assigned to another or previously defined variable.When you pass $a to another function, unless you explicitly pass it by reference, you pass in the value.It doesn't matter if $a changes in this function, because you still have the original copy in your current code.The behavior of value objects is the basic type.

Test Value Object

Value objects are tested in the same way as normal objects.However, invariance and side-effect-free behavior must also be tested.The solution is to create a copy of the value object to be tested before making any changes.Assertions are all equal, using the implementation's equality check.Perform the action to be tested and assert the results.Finally, assert that the original object and the copy are still equal.

Let's put this into practice and test the side-effect-free add method we implemented in the Money class:

class MoneyTest extends FrameworkTestCase
{
    /**
     * @test
     */
    public function copiedMoneyShouldRepresentSameValue()
    {
        $aMoney = new Money(100, new Currency('USD'));
        $copiedMoney = Money::fromMoney($aMoney);
        $this->assertTrue($aMoney->equals($copiedMoney));
    }

    /**
     * @test
     */
    public function originalMoneyShouldNotBeModifiedOnAddition()
    {
        $aMoney = new Money(100, new Currency('USD'));
        $aMoney->add(new Money(20, new Currency('USD')));
        $this->assertEquals(100, $aMoney->amount());
    }

    /**
     * @test
     */
    public function moniesShouldBeAdded()
    {
        $aMoney = new Money(100, new Currency('USD'));
        $newMoney = $aMoney->add(new Money(20, new Currency('USD')));
        $this->assertEquals(120, $newMoney->amount());
    }
// ...
}

Persist Value Object

Value objects themselves are not persisted; they are usually persisted by aggregation.Value objects should not be saved as a full record, although in some cases this can be done.Value objects are best stored in either embedded values or serialized LOB mode.Both modes can be used when you have an open source ORM such as Doctrine or a custom ORM to store your objects.Because the value object is small, embedding a value is usually the best choice because it provides a simple way to query an entity, that is, through any attribute in the value object.However, if querying with these fields is not important to you, it will be very easy to persist with serialization strategies.

Consider the following Product entity, which has string id, name, and price (Money value object) attributes.We intentionally simplify the implementation of these examples, so we use a character id instead of a value object:

class Product
{
    private $productId;
    private $name;
    private $price;

    public function __construct(
        $aProductId,
        $aName,
        Money $aPrice
    )
    {
        $this->setProductId($aProductId);
        $this->setName($aName);
        $this->setPrice($aPrice);
    }
// ...
}

Assuming that you have learned Chapter 10 to persist a ProductEntity in a warehouse, you can create and persist a new Product in the following ways:

$product = new Product(
    $productRepository->nextIdentity(),
    'Domain-Driven Design in PHP',
    new Money(999, new Currency('USD'))
);
$productRepository−>persist(product);

Now we see that both custom ORM and Doctrine implementations can be used to persist Product Entities containing value objects.We will highlight the application of embedded values and serialized LOB patterns, as well as the differences between persisting individual value objects and collections.

Why use Doctrine?

Doctrine is a powerful ORM.It solves over 80% of the problems PHP applications face.It also has a strong community.As long as it is properly configured, it can produce the same or even better results as a custom ORM without losing maintainability.We recommend using Doctrine for entity and business logic in most scenarios.It can help you save a lot of time and brain cells.

Persist a single value object

There are many ways to persist a single value object, from using serialized LOB s or embedded values as mapping strategies to using custom ORM or open source methods, such as Doctrine.We consider that in order to persist entities to the database, your company may have developed custom ORMs of its own.In our scenario, custom ORMs should be implemented using DBAL libraries.According to official documentation, DBAL, the Doctrine Database Abstraction & Access Layer, provides a lightweight, thin runtime level similar to the PDO API and many additional, horizontal capabilities, such as handling database architecture and some operations through an object-oriented API.

Embedding value objects with a specific ORM

If we want to use a custom ORM that implements the embedded value pattern, we need to create a field in the entity table for the attributes in each value object.In this case, a prolonged Product Entity requires two extended fields: the amount of the value object and its own currency ISO code.

CREATE TABLE `products` (
id INT NOT NULL,
name VARCHAR( 255) NOT NULL,
price_amount INT NOT NULL,
price_currency VARCHAR( 3) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

For persistent objects to databases, in Chapter 10, the warehouse must map each field in the entity and each property in the Money value object.

If you use an ORM repository tailored to DBAL (let's call it DbalProductRepository), you must carefully create INSERT statements, build parameters, and execute statements:

class DbalProductRepository
    extends DbalRepository
    implements ProductRepository
{
    public function add(Product $aProduct)
    {
        $sql = 'INSERT INTO products VALUES (?, ?, ?, ?)';
        $stmt = $this->connection()->prepare($sql);
        $stmt->bindValue(1, $aProduct->id());
        $stmt->bindValue(2, $aProduct->name());
        $stmt->bindValue(3, $aProduct->price()->amount());
        $stmt->bindValue(4, $aProduct
            ->price()->currency()->isoCode());
        $stmt->execute();
// ...
    }
}

After executing this snippet, a Product Entity is created and saved to the database, with each column in the table showing the expected results:

mysql> select * from products \G
*************************** 1. row ***************************
id: 1
name: Domain-Driven Design in PHP
price_amount: 999
price_currency: USD
1 row in set (0.00 sec)

As you can see, you can persist value objects by customizing methods to map value objects and query parameters.However, everything is not as simple as it seems.Let's try to get the Product using its associated Money value object.The usual method is to execute a SELECT statement and return a new entity:

class DbalProductRepository
    extends DbalRepository
    implements ProductRepository
{
    public function productOfId($anId)
    {
        $sql = 'SELECT * FROM products WHERE id = ?';
        $stmt = $this->connection()->prepare($sql);
        $stmt->bindValue(1, $anId);
        $res = $stmt->execute();
// ...
        return new Product(
            $row['id'],
            $row['name'],
            new Money(
                $row['price_amount'],
                new Currency($row['price_currency'])
            )
        );
    }
}

This method has several advantages.First, you can easily step through the process of persistence and its subsequent authoring.Second, you can query based on any attribute in the value object.Finally, the space needed to persist an entity is as much as we need, no more or no less.

However, there are drawbacks to using a custom ORM approach, as explained in Chapter 6, Domain Events, if your domain model is interested in the process of creating aggregates, entities (in the form of aggregates) should trigger an event in the constructor.If you use the new operator, the event is triggered multiple times when an aggregation is found in the database.

This is why Doctrine uses internal proxy and serialization, as well as deserialization methods, to rebuild an object with a specific state of its properties without using a constructor.An entity should be created using the new operator only during its life cycle.

Constructor

The constructor does not need to declare parameters for each property of the object.Imagine a blog post where the constructor might need an id and title; however, it can internally set its status property to Draft.When you publish a post, you need to call a publishing method in order to modify it to a publishing state.

If you still intend to launch your own ORM, be prepared to address fundamental issues such as events, different constructors, value objects, lazy load relationships, and so on.That's why we recommend giving Doctrine an opportunity in domain-driven design applications.

In addition, in this example, you need to create a BalProduct entity that inherits from the Products in order to rebuild the entity from the database using a static factory method without using the new operator.

Embed value object with Doctrine >= 2.5. *

The latest release of Doctrine is 2.5, and it supports value object mapping.This eliminates the need for you to do this manually in version 2.4.Doctrine also has support for nested values since December 2015.Although not 100%, it's worth trying.In case it doesn't apply to your scene, look at the next section.For official documents, look at the Doctrine Embeddables reference section.If this is achieved correctly, it is definitely our best recommendation.This will be the simplest and most elegant solution, and it provides search capabilities through the DQL query language.

Because the Product, Money, and Currency classes have already been shown, the only thing left is to show the Doctrine map file:

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="
http://doctrine-project.org/schemas/orm/doctrine-mapping
https://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd">
    <entity name="Product" table="product">
        <id name="id" column="id" type="string" length="255">
            <generator strategy="NONE">
            </generator>
        </id>
        <field name="name" type="string" length="255" />
        <embedded name="price" class="Ddd\Domain\Model\Money" />
    </entity>
</doctrine-mapping>

In the Product Map, we define Price as an instance holding a Money object.At the same time, Money is designed to have instances of amounts and Urrency:

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="
http://doctrine-project.org/schemas/orm/doctrine-mapping
https://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd">
    <embeddable name="Ddd\Domain\Model\Money">
        <field name="amount" type="integer" />
        <embedded name="currency" class="Ddd\Domain\Model\Currency" />
    </embeddable>
</doctrine-mapping>

Finally, it's time to show that our current value object's Doctrine map:

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="
http://doctrine-project.org/schemas/orm/doctrine-mapping
https://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd">
    <embeddable name="Ddd\Domain\Model\Currency">
        <field name="iso" type="string" length="3" />
    </embeddable>
</doctrine-mapping>

As you can see, the above code has a standard nested definition through a string type field that holds ISO code.This is the easiest way to use nesting, or even more efficient.By default, Doctrine names your fields by prefixing the value object name.You can change these behaviors by altering the field prefix properties in XML comments to meet your needs.

Embedding value objects with Doctrine <= 2.4. *

If you're still stuck in Doctrine 2.4, you may want to know what acceptable alternatives to using nested values are when the version is less than 2.5.Now we need to proxy all the value object properties in the ProductEntity, which means that a new property with value object information will be created.With this, we can map all these new properties using Doctrine.Let's see how this affects the ProductEntity:

<?php
class Product
{
    private $productId;
    private $name;
    private $price;
    private $surrogateCurrencyIsoCode;
    private $surrogateAmount;
    public function __construct($aProductId, $aName, Money $aPrice)
    {
        $this->setProductId($aProductId);
        $this->setName($aName);
        $this->setPrice($aPrice);
    }
    private function setPrice(Money $aMoney)
    {
        $this->price = $aMoney;
        $this->surrogateAmount = $aMoney->amount();
        $this->surrogateCurrencyIsoCode =
        $aMoney->currency()->isoCode();
    }
    private function price()
    {
        if (null === $this->price) {
            $this->price = new Money(
                $this->surrogateAmount,
                new Currency($this->surrogateCurrency)
            );
        }
        return $this->price;
    }
    // ...
}

As you can see, there are two new attributes: amount and current's IOS code.We have updated the setPrice method to keep the properties consistent when setting it.Above, we updated price's getter method to return the Money value object generated from the new field.Let's see how the corresponding Doctrince XML mapping file should be changed:

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="
http://doctrine-project.org/schemas/orm/doctrine-mapping
https://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd">
    <entity name="Product" table="product">
        <id name="id" column="id" type="string" length="255">
            <generator strategy="NONE">
            </generator>
        </id>
        <field name="name" type="string" length="255" />
        <field name="surrogateAmount" type="integer" column="price_amount" />
        <field name="surrogateCurrencyIsoCode" type="string" column="price_currency" />
    </entity>
</doctrine-mapping>

Agent Properties

Strictly speaking, these two new fields do not belong to this domain model because they do not refer to infrastructure-specific information.Conversely, nested values are essential because they are not supported in Doctrine.There are alternatives to putting these two attributes outside the domain; however, this approach is the simplest, easiest, and most acceptable as a trade-off.There is also an example of using proxy attributes in this book; you can find them in Chapter 4, Entities, Subparts of Identity Operations Proxy Identities.

This can be achieved by using an abstract factory if we want to put these two attributes outside the domain.First, we need to create a new entity, DoctrineProduct, in our Infrastructure folder.It inherits from the Product entity.All proxy fields are placed in this new class, and methods such as price or setPrice need to be re-implemented.We will use DoctrineProduct to map to Doctrine instead of Product entities.

Now we can retrieve entities from the database, but how do we create a new Product?At some point, we need to call a new Product, but since we need to use the DoctrineProduct to handle it without exposing specific infrastructure to the application services, we will need to use the factory to create the Product entity.Therefore, where each entity constructs a new instance, you need to call createProuct in ProductFactory instead.

This results in many additional classes to avoid proxy attributes polluting the source entity.Therefore, we recommend proxying all value objects with the same entity, although this will undoubtedly lead to a less pure solution.

Serialized LOB and custom ORM

If the ability to search for value-added objects is not important, consider another pattern: serializing LOB s.This pattern makes it easy to store and retrieve by serializing the entire value object into a string format.The biggest difference between this and nested schemes is that in the latter option, the persistent footprint requirement is reduced to a single column:

CREATE TABLE ` products` (
id INT NOT NULL,
name VARCHAR( 255) NOT NULL,
price TEXT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

In order to persist the Productentity in this way, a change needs to be made to the DbalProductRepository.The Money`value object needs to be serialized to a string before the final entity can be persisted:

class DbalProductRepository extends DbalRepository implements
    ProductRepository
{
    public function add(Product $aProduct)
    {
        $sql = 'INSERT INTO products VALUES (?, ?, ?)';
        $stmt = $this->connection()->prepare(sql);
        $stmt->bindValue(1, aProduct−> id());
        $stmt->bindValue(2, aProduct−> name());
        $stmt->bindValue(3, $this−> serialize($aProduct->price()));
        // ...
    }

    private function serialize($object)
    {
        return serialize($object);
    }
}

Now let's see how Product s are presented in the database.The price column of a table is a TEXT type column that contains a sequence of Money objects representing 9.99 USD:

mysql > select * from products \G
*************************** 1.row***************************
id : 1
name : Domain-Driven Design in PHP
price : O:22:"Ddd\Domain\Model\Money":2:{s:30:"Ddd\Domain\Model\\
Money amount";i :
999;s:32:"Ddd\Domain\Model\Money currency";O : 25:"Ddd\Domain\Model\\
Currency":1:{\
s:34:" Ddd\Domain\Model\Currency isoCode";s:3:"USD";}}1 row in set(\ 0.00
sec)

This is what this method does.However, we do not recommend this because of some problems that occur when refactoring these classes.Can you imagine these problems?If we decide to rename the Money class?When you move a Money class from one namespace to another, how does it appear at the database level?Another trade-off, as explained earlier, is to weaken query capabilities.It has nothing to do with whether you use Doctrine or not; when using serialization strategies, it is almost impossible to write a query statement to make products more affordable, such as 200 USD.

This query problem can only be solved by nested values.However, the serialization refactoring problem can be solved with a specific library that handles the serialization process.

Improving serialization with JMS Serializer

One problem with PHP's native sequence/deserialization strategy is the refactoring of namespaces and classes.An alternative is to use your own serialization mechanism--for example, to concatenate amounts and currency ISO codes using a character split such as'|'.However, there is another more popular method: using an open source serialization library, such as JSM Serializer. Let's take a look at an example of how it can be used to serialize Money objects:

$myMoney = new Money(999, new Currency('USD'));
$serializer = JMS\Serializer\SerializerBuilder::create()->build();
$jsonData = $serializer−>serialize(myMoney, 'json');

Deserializing objects is also a simple process:

$serializer = JMS\Serializer\SerializerBuilder::create()->build();
// ...
$myMoney = $serializer−>deserialize(jsonData, 'Ddd', 'json');

In this example, you can refactor your Money class without updating the database.JMS Serializer can be used in many different scenarios -- for example, when using the REST API.One important feature is that you can specify attributes in the serialization process that should be omitted from the object -- such as passwords.

Check Mapping Reference and Cookbook for more information.JMS Serializer is required in any domain-driven design project.

Serialize LOB with Doctrine

In Doctrine, there are many different ways to serialize objects for ultimate persistence.

Doctrine Object Mapping Type

Doctrine supports serialized LOB mode.There are a number of predefined mapping types to match entity attributes to database columns and even tables.One of the mapping types is the object type, which maps SQL CLOB to PHP's serializer() and unserialize().

As described in Doctrince DBAL 2, Documentation:

  • Value object types are based on PHP serialization to map and transform object data.If you need to store an accurate representation of the object's data, consider using this type because it uses a sequence to present an accurate copy of the object, while the object is stored in the database as a string.Values retrieved from the database are always converted to PHP object types through deserialization, or null values, if there is no data.
  • Value object types are always mapped to the type of text provided by the database because there is no other way to store PHP object representations in the database natively.In addition, this type requires SQL field comment hints to reverse from the database.Doctrine cannot correctly use database products that do not support column annotations to return the value object type correctly. Instead, it returns the text type directly instead.

Because the built-in text type in PostgreSQL does not support null bytes, the object type will result in a deserialization error.One solution to this problem is to use serialize()/unserialize() and base64_encode()/bease64_decode() to process PHP objects and store them manually in the database.

Let's look at one possible XML mapping of Product Entities that use object types:

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="
http://doctrine-project.org/schemas/orm/doctrine-mapping
https://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd">
    <entity name="Product" table="products">
        <id name="id" column="id" type="string" length="255">
            <generator strategy="NONE">
            </generator>
        </id>
        <field name="name" type="string" length="255" />
        <field name="price" type="object" />
    </entity>
</doctrine-mapping>

The key to adding is type="object", which tells Doctrine that we will use an object type mapping.Next, let's see how we use Doctrine to create and persist a Product entity:

// ...
$em−>persist($product);
$em−>flush($product);

Now let's check if Product Entity returns in the expected state if we query it from the database:

// ...
$repository = $em->getRepository('Ddd\\Domain\\Model\\Product');
$item = $repository->find(1);
var_dump($item);

Last but not least, Doctrine DBAL 2 Documentation declares:
Object types are compared by reference, not by value.If the reference changes, Doctrine updates the value, so it behaves as if the objects are immutable value objects.

This approach has the same refactoring problems as a custom ORM.Object mapping types internally use serialize/unserialize.So why not use our own serialization?

Doctrine Custom Type

Another way is to use the Doctrine custom type to handle persistence of value objects.Custom type adds a new mapping type to Doctrine -- describes a custom conversion between entity fields and database representations to persist the former.
As the Doctrine DBAL 2 document explains:

Simply redefining the mapping between data field types and doctrine existing types is not enough.You can inherit `DotrineDBALTypes'
Type` to define your own type.You need four different ways to do this.

Because of the use of object types, the serialization steps contain information such as classes, which makes it very difficult to safely refactor our code.

Let's try to optimize our solution.Consider a custom serialization process to solve this problem.

One way to do this, for example, is to persist the Money value object as a string to the database, encoding it in amount|isoCode format:

use Ddd\Domain\Model\Currency;
use Ddd\Domain\Model\Money;
use Doctrine\DBAL\Types\TextType;
use Doctrine\DBAL\Platforms\AbstractPlatform;

class MoneyType extends TextType
{
    const MONEY = 'money';

    public function convertToPHPValue(
        $value,
        AbstractPlatform $platform
    )
    {
        $value = parent::convertToPHPValue($value, $platform);
        $value = explode('|', $value);
        return new Money(
            $value[0],
            new Currency($value[1])
        );
    }

    public function convertToDatabaseValue(
        $value,
        AbstractPlatform $platform
    )
    {
        return implode(
            '|',
            [
                $value->amount(),
                $value->currency()->isoCode()
            ]
        );
    }

    public function getName()
    {
        return self::MONEY;
    }
}

You must register all custom types to use Doctrine.It typically creates an EntityManger centrally with an EntityMangerFactory.

Or, if you do this step by applying startup:

use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Setup;

class EntityManagerFactory
{
    public function build()
    {
        Type::addType(
            'money',
            'Ddd\Infrastructure\Persistence\Doctrine\Type\MoneyType'
        );
        return EntityManager::create(
            [
                'driver' => 'pdo_mysql',
                'user' => 'root',
                'password' => '',
                'dbname' => 'ddd',
            ],
            Setup::createXMLMetadataConfiguration(
                [__DIR__ . '/config'],
                true
            )
        );
    }
}

Next we need to specify our custom type in the map:

<?xml version = "1.0" encoding = "utf-8"?>
<doctrine-mapping>
    <entity
name = "Product"
table = "product">
        <!-- ... -->
        <field
name = "price"
type = "money" />
    </entity>
</doctrine-mapping>

Why use XML mapping?

Thanks to XSH syntax validation in the header of the XML mapping file, many integrated development environment (IDE) settings provide auto-completion to display all elements and attributes in the mapping definition.However, in other parts of the book, we use YAML to show different grammars.

Let's examine the database to see how prices are stored using this method:

mysql> select * from products \G
*************************** 1. row***************************
id: 1
name: Domain-Driven Design in PHP
price: 999|USD
1 row in set (0.00 sec)

This approach is an improvement in future refactoring.However, the ability to search is still limited by column formats.With Doctrine custom types, you can improve things a little, but it's not yet the best option for building DQL queries.You can refer to Doctrine Cumstom Mapping Types for more information.

Discussion Time

Think and discuss with your partner how you use JMS to create custom types to serialize and deserialize value objects.

Collection of persisted value objects

Imagine that we now want to add a price set to the Productentity.These can represent Product life cycles or prices in different currencies.These can be named HistoricalPrice, as follows:

class HistoricalProduct extends Product
{
    /**
     * @var Money[]
     */
    protected $prices;

    public function __construct(
        $aProductId,
        $aName,
        Money $aPrice,
        array $somePrices
    )
    {
        parent::__construct($aProductId, $aName, $aPrice);
        $this->setPrices($somePrices);
    }

    private function setPrices(array $somePrices)
    {
        $this->prices = $somePrices;
    }

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

HistoricalProduct inherits from Product, so it inherits the same behavior as well as the functions of the price collection.

As mentioned in the previous section, serialization is a viable approach if you don't care about search capabilities.However, embedding values is possible when we know exactly how much price to persist.But what if we wanted to persist an indeterminate set of historical prices?

Persist collection to a single field

Collecting persisted value objects into a column seems to be the simplest solution.All the persistence of single value objects explained in the previous section can be applied to this case.With Doctrine, you can use an object or custom type -- with some caution in mind: Value objects should be small, but if you want to persist a large collection, you must ensure the maximum length and capacity that the database engine can support for each row.

Practice

Identify implementation strategies for Doctrine object types and Doctrine custom types to persist a Product with a price set.

Return a collection by joining tables

If you want to persist and query through a value object associated with an entity, you can choose to store the value object as an entity.In terms of domains, these objects are still value objects, but we need to assign them a primary key and associate them with the owner, a real entity, in a one-to-many/one way.In short, your ORM treats collections of value objects as entities, while your domain still treats them as value objects.

The main idea behind the join table strategy is to create a table to connect entities to their value objects.Let's look at rendering in the database:

CREATE TABLE ` historical_products` (
`id` char( 36) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar( 255) COLLATE utf8mb4_unicode_ci NOT NULL,
`price_amount` int( 11 ) NOT NULL,
`price_currency` char( 3) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

The history_products table looks like products.Keep in mind that HistoricalProduct inherits the Productentity for the first reason that it is easier to show how to persist a collection.A new prices table is now needed to persist different Money value objects in all product entities:

CREATE TABLE `prices`(
`id` int(11) NOT NULL AUTO_INCREMENT,
`amount` int(11) NOT NULL,
`currency` char(3) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Finally, you need a table associated with products and prices:

CREATE TABLE `products_prices` (
`product_id` char( 36) COLLATE utf8mb4_unicode_ci NOT NULL,
`price_id` int( 11 ) NOT NULL,
PRIMARY KEY (`product_id`, `price_id`),
UNIQUE KEY `UNIQ_62F8E673D614C7E7` (`price_id`),
KEY `IDX_62F8E6734584665A` (`product_id`),
CONSTRAINT `FK_62F8E6734584665A` FOREIGN KEY (`product_id`)
REFERENCES `historical_products` (`id`),
CONSTRAINT `FK_62F8E673D614C7E7` FOREIGN KEY (`price_id`)
REFERENCES `prices`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Return Set with Doctrine Joint Table

Doctrine requires that all database entities have a unique identifier.Because we want to persist Money value objects, we need to add an artificial identifier so that Doctrine can handle it.There are two options: to include this proxy identity in the Money value object, or to place it in an extension class.

The first is that the new identity is simply needed for the database persistence layer.Identity is not part of the domain.

The second way is to avoid so-called boundary leakage, which requires a lot of change.Creating a new instance of a Money value object from any realm object with a class extension is not recommended because it breaks the dependency inversion principle.The solution is to create another Money factory and pass it to the application services and any other domain objects.

In this case, we recommend the first option.Let's review the changes that need to be made to implement Money value objects:

class Money
{
    private $amount;
    private $currency;
    private $surrogateId;
    private $surrogateCurrencyIsoCode;

    public function __construct($amount, Currency $currency)
    {
        $this->setAmount($amount);
        $this->setCurrency($currency);
    }

    private function setAmount($amount)
    {
        $this->amount = $amount;
    }

    private function setCurrency(Currency $currency)
    {
        $this->currency = $currency;
        $this->surrogateCurrencyIsoCode =
            $currency->isoCode();
    }

    public function currency()
    {
        if (null === $this->currency) {
            $this->currency = new Currency(
                $this->surrogateCurrencyIsoCode
            );
        }
        return $this->currency;
    }

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

    public function equals(Money $aMoney)
    {
        return
            $this->amount() === $aMoney->amount() &&
            $this->currency()->equals($this->currency());
    }
}

As you can see above, two attributes have been added, the first one is surrogateId, which is not used in our field, but the infrastructure requires that it persist the value object as an entity into our database.The second is surrogateCurrencyIsoCode, which holds the ISO code for the currency.With these new properties, it's really easy to map our value objects into doctrine.

Money's mapping is also straightforward:

<?xml version = "1.0" encoding = "utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="
http://doctrine-project.org/schemas/orm/doctrine-mapping
https://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd">
    <entity name="Ddd\Domain\Model\Money" table="prices">
        <id name="surrogateId" type="integer" column="id">
            <generator strategy="AUTO">
            </generator>
        </id>
        <field name="amount" type="integer" column="amount" />
        <field name="surrogateCurrencyIsoCode" type="string" column="currency" />
    </entity>
</doctrine-mapping>

With Doctrine, the HistoricalProduct entity will have the following mappings:

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="
http://doctrine-project.org/schemas/orm/doctrine-mapping
https://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd">
    <entity name="Ddd\Domain\Model\HistoricalProduct" table="historical_products" repository-class="
Ddd\Infrastructure\Domain\Model\DoctrineHistoricalProductRepository
">
        <many-to-many field="prices" target-entity="Ddd\Domain\Model\Money">
            <cascade>
                <cascade-all/>
            </cascade>
            <join-table name="products_prices">
                <join-columns>
                    <join-column name="product_id" referenced-column-name="id" />
                </join-columns>
                <inverse-join-columns>
                    <join-column name="price_id" referenced-column-name="id" unique="true" />
                </inverse-join-columns>
            </join-table>
        </many-to-many>
    </entity>
</doctrine-mapping>
Return collection with custom ORM join table

You can also use a custom ORM for the same processing, which requires cascading INSERTS and JION queries.Importantly, how to carefully handle removing Money value objects in order not to isolate them.

Practice

Provide a solution for how DbalHisotircalRepository can handle persistence methods.

Return collection with database entity

Database entities are the same schema as joined tables, adding only value objects managed by the owner entity.In the current scenario, join tables would be too complex considering that Money value objects are only used by HistoricalProduct entities.Because the same results can be achieved through a one-to-many database relationship.

Practice

If you use a database entity method, consider the mappings required between HistoricalProduct and Money.

Non-relational database

What happens if you use a NoSQL mechanism like Redis, Mongodb, or CouchDB?Unfortunately, you can't avoid these issues. To persist an aggregate using Redis, you need to serialize it with a string before setting its value.If you use the serialize/unserialize method of PHP, you will need to face namespace or class name refactoring issues again.If you choose a custom implementation path (JSON, custom string, and so on), you will need to rebuild the value object again during the Redis retrieval.

PostgreSQL JSONB and MySQL JSON Type

If our database engine allows us not only to use the serialized LOB strategy, but also to search based on their values, we will have the best method at the same time.Good news: Now you can do it.Support for JSONB has been added because of PostGreSQL version 9.4.Value objects can be stored in JSON serialization and subqueried in JSON sequences.

MySQL does the same.In MySQL version 5.7.8, MySQL supports a native JSON data type that provides efficient access to data in JSON (JavaScript Object Representation) documents.According to the MySQL 5.7 Reference Manual, JSON data types offer more advantages than storing JSON formats as strings:

  • In the JSON column, there is an automatic validator for JSON documents.An incorrect JSON document will produce an error.
  • Optimize the storage format.JSON documents stored in JSON columns are converted to an internal format that allows quick read access to them.When the server has to read these JSON values stored in binary format in the future, there is no need to parse them as text.The binary format is structured so that the server can find child objects or nested values directly through a key or array index without reading all the values in the document before and after.

If a relational database adds high-performance support for document and nested document search and has all the benefits of an atomicity, consistency, isolation, and durability (ACID) philosophy, it can reduce a lot of complexity in many projects.

Security

Another interesting detail is the security benefits of using value objects to model domain concepts.Consider a context for selling a flight ticket application.If you work with International Air Transport Association Airport codes, also known as IATA codes, you can decide to use strings or use value objects to model concepts.If you choose string style, consider all the places where you need to verify that a string is IATA code.What if you might have forgotten something important?On the other hand, consider trying to instantiate an IATA("BCN'; DROP TABLE users;--").Avoiding SQL injection or similar attacks is easier if you focus your guard in the constructor and pass in an IATA value object.

If you want to know more details about domain-driven design security, you can follow Dan Bergh Johnsson or read his blog.

Summary

It is strongly recommended that domain concepts be modeled using value objects, which are easy to create, maintain, and test as described above.In order to address persistence issues in a domain-driven design, the use of ORM is required.However, in order to persist a value object using Doctrine, the better option is to use embeddables.If you are stuck in version 2.4, you have two options: add value object fields directly to your entities and map them (less elegant, but easier), or extend your entities (more optimized, but more complex).

Posted by iShaun on Mon, 09 Dec 2019 19:39:24 -0800