All about value object in Symfony
What is value object and where does it come from?
In this entry, I will focus more on the use of VO in Symfony than on the detailed presentation of all of its features, pros and cons. If the subject is completely new to you and I do not manage to clear out all of your doubts, it is worth looking around the internet. There are a lot of great materials on the subject, I will try to provide a few links at the end. The VO template originates from the Domain Driven Development methodology defined by Eric Evans. It is very universal and can be applied without the entire DDD load to reap great benefits.
Value object is used to represent objects that do not have any special idEntity (in contrast to an Entity), therefore they do not have a distinguished identifier. An example of such an object is an address consisting of a street, house number, zip code and city. To check whether two addresses are the same, we do not need any id type field, it is enough to compare all of the constituents. We use VOs to represent values that are a part of an Entity, but have their own rules or which aggregate several fields, like the aforementioned address. Another example would be an object representing money, which consists of the value and the currency.
VO characteristics
Personally, I learn best from examples, so let’s have a look at one now:
class Money
{
private int $value;
private string $currency;
public function __construct(int $value, string $currency)
{
if (strlen($currency) !== 3) {
throw new \InvalidArgumentException('Invalid currency.');
}
$this->value = $value;
$this->currency = $currency;
}
public function value(): int
{
return $this->value;
}
public function currency(): string
{
return $this->currency;
}
public function toString(): string
{
return $this->value.$this->currency;
}
public function isEqual(Money $otherMoney): bool
{
return $this->currency === $otherMoney->currency()
&& $this->value === $otherMoney->value();
}
public function formatMoney(): string
{
return sprintf('%01.2f %s', ($this->value / 100), $this->currency);
}
}
What can be said about this object type? One of the first things that draws the attention is the lack of setters or any other methods that modify the state of the object after its creation. The state can only be determined by the constructor. This is a result of the fact that VOs should be immutable. Because of that we can be sure that no one can modify the value of the object in any other place in the code, which would result in receiving some weird values. What if we need to modify a value stored within the object? We simply create a new version of the object.
Value objects should also be self-validating. Validation should proceed at the moment the object is created. If the object is created correctly without a notification about an exception, we can safely pass it on further and expect to always receive behavior in accordance with the rules of the defined type. For example, we can be sure that we will never have a date like December the 32nd.
The third element that is worth discussing is VO comparison. As I said earlier, value objects do not have an identifier through which one could check their equality. VO representing classes generally have defined methods that make it possible to compare them with other VOs of the same type through object attribute values (see the isEqual method).
VOs may and even should include methods to determine other behavior matching a specific type. If you have to perform a task like formatting an address to a correct form, then instead of dropping the method to a separate service, it is worth to consider adding it inside value object class.
Use of VOs with Doctrine
It’s time to cut to the chase – how to use VOs in an application written in Symfony. Fortunately, this is not very difficult, but it does require a bit of additional work. Firstly, one has to consider how to store such objects in a database. As I mentioned earlier, VOs most often fulfill the role of attributes in entities. Fortunately, Doctrine is ready for this situation, and offers us the “Embeddable” type. https://www.doctrine-project.org/projects/doctrine-orm/en/2.9/tutorials/embeddables.html . We can add appropriate annotations to our VO class:
/**
* Class Money
* @ORM\Embeddable
*/
class Money
{
/**
* @ORM\Column(type="integer")
*/
private int $value;
/**
* @ORM\Column(type="string")
*/
private string $currency;
...
}
As we can see, we make an annotation for the class, defining it as Embeddable instead of Entity. We map the columns the same way as in the case of a regular Entity. Next, we can use this type in the Entity:
/**
* Class Product
* @ORM\Entity
*/
class Product
{
...
/**
* @ORM\Embedded(class="App\ValueObject\Money", columnPrefix=false)
*/
private $price;
...
}
We have to remember that now the Entity includes an object in itself, not just fields of simple types. This has an influence on the way we use the field, e.g. in configuration, or when building queries:
$productRepository->findOneBy(['price.currency' => 'PLN']);
security:
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email.email
In the second example, we have VO Email with an email field. This VO is a part of the User entity. Next, we configure Symfony to use this field for looking for users via the security module in the security.yaml file.
Use of VOs with Symfony Forms
The integration of value objects with Symfony forms probably needs the most work. It requires defining the FormType representing our VO as well as implementing a data mapper mechanism for it. https://symfony.com/doc/current/form/data_mappers.html. This results mainly from the lack of setters and getters which are normally used by forms. An example for a VO representing an email:
use App\ValueObject\Email;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class EmailType extends AbstractType implements DataMapperInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', \Symfony\Component\Form\Extension\Core\Type\EmailType::class)
->setDataMapper($this);
}
public function mapDataToForms($viewData, iterable $forms)
{
if (null === $viewData) {
return;
}
$forms = iterator_to_array($forms);
$forms['email']->setData($viewData->toString());
}
public function mapFormsToData(iterable $forms, &$viewData)
{
$forms = iterator_to_array($forms);
$viewData = new Email($forms['email']->getData());
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'empty_data' => null,
]
);
}
}
Let’s have a look at it in detail. First, we implement DataMapperInterface, which includes two methods – mapDataToForms and mapFormsToData. They will be responsible for the translation of our object into fields in the form, and the other way round – form fields will be translated into our object.
In the mapDataToForms, we set the appropriate fields in the form to include appropriate values from our VO. The viewData parameter will store our value object:
$forms['email']->setData($viewData->toString());
If the object does not exist yet, we simply exit the method and leave empty fields:
if (null === $viewData) {
return;
}
In the second method – mapFormsToData – we create our symfony forms on the basis of form fields. It is worth highlighting that this time the $viewData parameter is transmitted through reference. https://www.php.net/manual/en/language.references.pass.php. Therefore, our modification is visible beyond the method and we do not return any result from it:
$viewData = new Email($forms['email']->getData());
We can now use our VO and the defined FormType in the form:
use App\Form\EmailType;
...
$user = new User();
$form = $this->createFormBuilder($user)
->add('email', EmailType::class)
->add('submit', SubmitType::class)
->getForm();
The last thing we should pay our attention to is that our new type of form field is of the compound type. The compound field expects to receive data in the form of a table. Therefore, we have to render our form properly so that the data for our VO forms a table:
{{ form_start(form) }}
...
{{ form_row(form.email.email) }}
...
{{ form_end(form) }}
That’s almost it. Lastly, below you will find some useful links to further explore knowledge on the subject.
Links
About the author:
Adam Zając is a Backend Developer with many years of experience, but he is also skilled in Frontend. Among his many interests, there are architecture, clean code and broadly understood quality in programming.
References:
- Polish version: http://adamzajac.info/2021/07/08/value-object-obiekt-wartosci-w-symfony/