Permettre un embeddable Doctrine d'être nullable

Doctrine propose une fonctionnalité très utile pour créer des propriétés objets sur vos entité, il s'agit des embeddable.

Les embeddables permette de créer des objets dont les propriétés seront stockés dans la même table que l'entité à laquelle ils appartiennent (une sorte de one-to-one dans la même table). Ce très utile pour créer des objets réutilisable pour des données qui n'ont pas vocation à se trouver dans une table dédié. Je l'utilise beaucoup pour stocker des adresse, des coordonnées, des prix (montant et devise) par exemple.

class Address
{
    private string $streetNumber;
    private string $route;
    private string $postalCode;
    private string $locality;
    private string $country;
}

class People
{
    /**
     * @ORM\Embedded(class="App\Entity\Address")
     */
    private $address;

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

Seulement il existe une limitation aux embeddable, il ne peux pas être nullable. Au mieux vous pouvez rendre toutes les propriétés de l'embbedable nullable mais Doctrine vous hydratera toujours l'objet embeddable même si toutes ses propriétés sont null.

Un workaround à cela est d'utiliser un event listener Doctrine en postLoad afin d'assigner null à la place de embedable si toutes ses propriétés sont null.

<?php

namespace App\Doctrine\EventListener;

use App\Entity\Address;
use App\Entity\People;
use Doctrine\ORM\Event\LifecycleEventArgs;

/**
 * This class set embeddable Address to null on People when all properties are null
 * as Doctrine does not support nullable embeddable natively.
 *
 * @see https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/events.html#entity-listeners-class
 */
class NullableEmbeddableEventListener
{
    public function onApplicationPostLoad(People $people, LifecycleEventArgs $args): void
    {
        if ($people->getAddress() !== null && $this->isEmpty($people->getAddress())) {
            $reflection = new \ReflectionClass($people);

            if (!$reflection->hasProperty('address')) {
                return;
            }

            $property = $reflection->getProperty('address');
            $property->setAccessible(true);
            $property->setValue($people, null);
        }
    }

    public function isAddressEmpty(Address $address): bool
    {
        return $address->getStreetNumber() === null
            && $address->getRoute() === null
            && $address->getLocality() === null
            && $address->getPostalCode() === null
            && $address->getCountry() === null
        ;
    }
}

Les commentaires