13

Let's say I've got two Doctrine entities, Person and Company. Both have an address field which accepts an Address value object. As per business rules, Company::Address is required while Person::Address can be null.

Doctrine 2.5 proposes the Embeddable type, which was apparently built with value objects in mind and, indeed, I see it as a perfect solution for my case.

However, there's one thing I can't do: declare that Person::Address is nullable while Company::Address is not. A boolean nullable attribute exists for the Embeddable's fields themselves, but of course this applies to every entity the Address is embedded in.

Does anybody know if I'm missing something, or if this is due to a technical limitation, if there's a workaround, etc. ? Right now the only solution I see is to declare all Embeddable fields as nullable: true and handle the constraint in my code.

marcv
  • 1,874
  • 4
  • 24
  • 45

2 Answers2

12

Does anybody know if I'm missing something

Nullable embeddables are not supported in Doctrine 2. They are expected to make it to version 3.

if there's a workaround

The solution "is to NOT use embeddables there, and [...] replace fields with embeddables [manually]" (@Ocramius)

Example:

class Product
{
    private $sale_price_amount;
    private $sale_price_currency;

    public function getSalePrice(): ?SalePrice
    {
        if (is_null($this->sale_price_currency)
            || is_null($this->sale_price_amount)
        ) {
            return null;
        }

        return new SalePrice(
            $this->sale_price_currency,
            $this->sale_price_amount
        );
    }
}

(Snippet by Harrison Brown)

marcv
  • 1,874
  • 4
  • 24
  • 45
  • 1
    Just a friendly reminder that this method can return `null` but is declared with a `SalePrice` return type. – Najki Mar 25 '20 at 13:00
4

The problem having the logic inside the getter is that you can't access directly to property (if you do so you miss this specific behaviour)...

I was trying to solve this using a custom Hydrator but the problem was that doctrine does not allow to use custom hydrators when call to find(), findOneBy()...and the methods that do not use the queryBuilder.

Here is my solution:

  1. Imagine that we have an entity that looks like this:
<?php
interface CanBeInitialized
{
    public function initialize(): void;
}

class Address
{
    private $name;

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

class User implements CanBeInitialized
{
    private $address;

    public function address(): ?Address
    {
        return $this->address;
    }

    public function initialize(): void
    {
        $this->initializeAddress();
    }

    private function initializeAddress(): void
    {
        $addressNameProperty = (new \ReflectionClass($this->address))->getProperty('value');

        $addressNameProperty->setAccessible(true);

        $addressName = $addressNameProperty->getValue($this->address);

        if ($addressName === null) {
            $this->address = null;
        }
    }
}

Then you need to create an EventListener in order to initialize this entity in the postLoad event:

<?php
use Doctrine\ORM\Event\LifecycleEventArgs;
class InitialiseDoctrineEntity
{
    public function postLoad(LifecycleEventArgs $eventArgs): void
    {
        $entity = $eventArgs->getEntity();

        if ($entity instanceof CanBeInitialized) {
            $entity->initialize();
        }
    }
}

The great with this approach is that we can adapt the entities to our needs (not only to have nullable embeddables). For example: In Domain Driven Design, when we use the Hexagonal Architecture as a tactical approach to work with, we can initialize the Doctrine entities with all the changes needed to have our Domain entities as we want.

rescuer255
  • 41
  • 2
  • IMHO the `initialize` logic belong to the `EntityListener`. Having said that, I use this method to handle nullable & collection of `Embedded`/`Value Objects`. – Cethy Oct 17 '18 at 11:25
  • I wrote an article about my solution here: https://medium.com/@wolfgang.klinger/nullable-embeddable-with-symfony-and-doctrine-orm-51f6e2edf623 – Wolfgang Nov 14 '20 at 12:17
  • @Wolfgang that's interesting, but it works only with annotations mapping. You should use the real doctrine mapping instead – Massimiliano Arione Nov 30 '20 at 15:06
  • @MassimilianoArione Thanks, can you elaborate on that (or share some links)? I'm certainly no expert (yet) neither with Symfony nor Doctrine. ;) – Wolfgang Dec 12 '20 at 09:57
  • @Wolfgang as you can see in [documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html), mapping is not limited to annotations. Instead, Doctrine exposes classes (in Doctrine\ORM\Mapping namespace) to handle 3 forms of mapping (annotations, xml, and yaml) – Massimiliano Arione Dec 12 '20 at 16:28
  • This is horrendous if you want to be decoupled from the framework. I know it is not precised in the question but if you have a domain, it is going to be highly contaminated with this implementation. – Alfonso Fernandez-Ocampo Oct 20 '21 at 09:52