romaricdrigon / romaricdrigon.github.io

My personal blog
https://romaricdrigon.github.io/
2 stars 0 forks source link

2019/07/05/value-objects-doctrine-and-symfony-forms #6

Open romaricdrigon opened 5 years ago

romaricdrigon commented 5 years ago

Please comment below! Comments will automatically be published on the blog, too.

seb-jean commented 5 years ago

Hi @romaricdrigon , How are we going to do with latitude and longitude? With ValueObject? With Embeddable? Thanks

romaricdrigon commented 5 years ago

Hello @seb-jean, a value object, stored as a Doctrine embeddable, is usually a very good solution. Especially if you also want to store altitude, or a precision, etc.

seb-jean commented 5 years ago

I use ValueObject and Doctrine Embeddable ? That is to say ? Can you give an example please?

romaricdrigon commented 5 years ago

Sure:

/** @ORM\Embeddable */
class Coords
{
    /** @ORM\Column(type="string") */
    private $latitude;

    /** @ORM\Column(type="string") */
    private $longitude;

    // Below and after I will just put the methods signature 
    public function __construct(string $latitude, string $longitude) 

    // As a bonus, you can add more methods to your value object
    // For instance, converting to another format (for instance, CH1903 which is popular here)
    public function getInCh1903(): array
}

/** @ORM\Entity */
class Place
{
    /** @ORM\Embedded(class="Coords") */
    private $coords;

    // ...

    public function setCoords(Coords $coords): void
    public function getCoords(): Coords;
}
seb-jean commented 5 years ago

Thank you. It may be off-topic, but I saw that latitude and longitude could be stored in a POINT datatype (https://dev.mysql.com/doc/refman/8.0/en/gis-class-point.html) Using a Value Object is Required Or Embeddable?

romaricdrigon commented 5 years ago

Having a value object is something different than how data is stored. Value object are exclusively PHP-side, and should not be concerned by how Doctrine will handle them. So value object !== embeddable, embeddable are merely a way to store a value object.

In your case, sure, you can use a POINT MySQL side. Then you will need a custom Doctrine mapping type (https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/cookbook/custom-mapping-types.html), and you can transform it to a PHP object. Which can be a value object.

seb-jean commented 5 years ago

Thanks @romaricdrigon

danakil commented 5 years ago

Hello! Good article, thank you. Do you know of the performance impact of using Embeddable with Doctrine ? I suppose that created and hydrating those value objects can create some overhead, especially when querying and displaying a lot of entities in a list. I'm wondering the same question for json serialized data in Doctrine.

romaricdrigon commented 5 years ago

Hello, thank you. I never had performance issues with Embeddables, so I never quite dig into that subject. I would presume Embeddables hydratation to be pretty lightweight, entity hydratation becomes really expensive and complex because entities are tracked. Regarding memory usage, PHP objects are lighter than arrays (in PHP7), so it would likely be better than JSON serialized data which is unpacked to associative arrays.

timiTao commented 4 years ago

Hello :)

I feel that this covers only part of lifecycle of VO. What about optional Value object? How you implement it? When you have BuildingNumber|null situation? To advance problem, what you do with strict type and return type? In DDD, you should move with that approach. With embeddable, you can't. You are forced to have buildingNumber(): BuildingNumber and BuildingNumber::getValue(): ?string as solution. What do you think about this? How you consider this problem with mentioned points First thoughts, Embeddables and As Doctrine entities

☮️ 👋

o-alquimista commented 4 years ago

"As Doctrine entities" was a bit confusing to me. It's possibly a combination of me not being a native English speaker and not being so familiar with the terms used here. I have a few questions.

Here's my setup

I have an entity named User, and it must be allowed to own multiple addresses. I create an entity named Address with a many-to-one relation to the User.

It is tempting to want to store all those in the same address table, so that would be an Address entity for Doctrine.

When you said "it's tempting to..." it sounded like it would be followed by "BUT... don't do it". But in some cases, like mine, it is unavoidable to make Address an entity, right? Or maybe that's not what you were talking about here?

So as long as you never expose an Address ID in your domain code

What does this mean, exactly? Expose the id of an Address? Domain code?

If a customer edits an address, should his past order address be updated?

I was thinking... maybe I can use an embeddable named Address in the Order entity to store the Address without relations, which would prevent it from changing in case the user makes changes to their addresses, right? The user would still have a collection of Address entities in relation, but the Order would store just an immutable version of the selected Address as an embeddable. I have yet to try this.


I could not find any documentation regarding embeddables in the Symfony Docs. Your examples of integration with Symfony are really helpful for beginners to get the hang of it. The Doctrine documentation on embeddables only goes so far.

romaricdrigon commented 4 years ago

Hello all,

Sorry for the long answer, I have been quite busy past weeks. Now I'm catching on.

About nullable value objects

@timiTao you raise a good point. Doctrine types and entities are good options for this scenario. Personally I think I would pick entities.

With embeddable, you can hack your way around, but it feels suboptimal:

public function getPrice(): ?Money
{
    return $this->price->getValue() ? $this->price : null;
}

Though it can be quite adapted if you want to use null object pattern:

public function getPrice(): MoneyInterface
{
    return $this->price->getValue() ? $this->price : new NullMoney();
}

About Doctrine entities

@o-alquimista thank you for your feedback, I will consider rephrasing that paragraph. My point is that while Address is not an entity as per Domain-Driven Design recommendations, it can be a Doctrine entity. Doctrine use a slightly different terminology.

In your scenario, using a Doctrine entity feels right. You don't want to expose Address::id, as it does not quite make any sense domain-wise. If you want to compare than 2 addresses are "equal", you have to compare every of their fields, so you may want to implement a method for that.

o-alquimista commented 4 years ago

Thanks. So, in my example, Address is a Doctrine entity, but it is also a Value Object. If you want to compare two Address instances, you have to leave the id out, since it's always unique for each of them (it gives an "identity", which violates the Value Object pattern).

romaricdrigon commented 4 years ago

Exactly!

o-alquimista commented 4 years ago

I have another question about Embeddables. I couldn't find the answer anywhere else, so I have to ask here. It may help others with the same question.

Can I put multiple embeddables in something like an ArrayCollection?

For example, I have an Order entity, and I need to store all the products involved in that order without making relations (because products can be deleted or modified).


Now that I come to think more about it, I no longer believe that what I asked is possible. Embeddables map to specific columns, and these columns must exist on the database. It won't add more columns dynamically.

I will probably have to create an OrderItem entity with a ManyToOne relation to Order and use it to keep product information. It will be built with the data from the actual products involved in the purchase.

If you have any better ideas to suggest, I appreciate it.

romaricdrigon commented 4 years ago

Hello,

In short, no, embeddables are single objects. You exactly got the reason why: embeddables properties are flattened to columns in the entity table. So the schema tool can't handle a potentially unlimited number of embeddables.

An OrderItem entity sounds indeed as the way to go indeed :)