Forum PHP 2015 : Mets du Value Object dans ton modèle

Damien ALEXANDRE - Novembre 2015

Mets du Value Object dans ton modèle

Forum PHP 2015 - Damien ALEXANDRE - Novembre 2015

Le barbu sur scène : Damien ALEXANDRE

Le DDD Domain Driven Design

Ceci n'est pas une conférence sur le DDD

Allez plutôt voir :

Alexandre Balmes

CHRONIQUE D'UN PROJET DRIVEN DESIGN
par Alexandre Balmes
le 23 à 14h dans l'Auditorium Lucienne et André Blin

Le Value Object

Le VO C'est son petit nom :)

Les VO connus

Avec l'aide de mon assistant

Une cafetière Conférence non sponsorisée.

Un objet cafetière

class CoffeeMaker
{
    private $id;
    private $model;
    private $currentCapsule;
}

La capsule ?

$maker = new CoffeeMaker();
$maker->setModel('Magimix 42');
$maker->setCurrentCapsule(
    // ??? Wat ???
);

Une capsule possède un nom, un arôme, une couleur... et une identité.

On pourrait faire ce genre de capsule

class Capsule
{
    private $id;
    private $name;
    private $color;
    private $intensity;
}

MAIS

Et si on en faisait un VO !

Notre VO CapsuleType !

Notre VO CapsuleType !

class CapsuleType
{
    private $name;
    private $color;
    private $intensity;
}

$capsule = new Capsule(
    new CapsuleType("JoliBrew")
);

$maker->setCurrentCapsule($capsule);

CapsuleType n'a pas d'identité

Un CapsuleType s'identifie par son contenu.

On peut dire que deux Capsules sont du même type si les attributs de CapsuleType sont identiques dans les deux Capsules.

La Capsule a une identité

La Capsule est identifiée, deux Capsules du même type ne sont pas considérées comme étant la même : elles se manipulent, se suppriment, se consomment - elles ont une vie propre dans notre domaine.

Capsule est une entité. Tout comme CoffeeMaker.

L'exemple complet

$roma  = new CapsuleType('Roma', 'purple', 9);

$capsule = new Capsule($roma);

$maker = new CoffeeMaker();
$maker->setModel('Magimix 42');
$maker->setCurrentCapsule($capsule);

Identité

Pour savoir si deux cafétières sont sur le point de préparer le même café, on compare donc désormais des objets !

$ristretto  = new CapsuleType('Ristretto', 'black');
$roma       = new CapsuleType('Roma', 'purple');

$ristretto == $roma; // false

Oui on utilise ==.

Égalité d'objets en PHP Le saviez-vous ?

class VO {
    private $test;
    public function __construct($test) {
        $this->test = $test;
    }
}

$one = new VO('I need coffee');
$two = new VO('I need coffee');

var_dump($one == $two); // true !
var_dump($one === $two); // false

Égalité d'objet en PHP

On compare donc nos VO avec == et on assume.

Ou avec une méthode userland isEqual() si vous voulez aussi comparer les types et faire de l'héritage de VO.

Deux VO identiques != même instance

$roma = new CapsuleType('Roma', 'purple', 'intense et crémeux', 9);

Dans un monde idéal, toutes les capsules Roma seraient représentées par la même instance de CapsuleType.
Dans la vraie vie, ce n'est pas le cas :

Deux VO identiques != même instance

Aucun problème. Créez autant de $roma que vous voulez.

Ce qui est important, c'est que vous puissiez toujours les comparer :

$roma = new CapsuleType('Roma', 'purple', 'intense et crémeux', 9);
$romaFromORM == $roma; // TRUE \o/

Changer de café ?

$maker; // Notre cafetière
$maker->getCurrentCapsule(); // Notre Capsule
$maker->getCurrentCapsule()->getType(); // Notre CapsuleType
$maker->getCurrentCapsule()->getType()->setName('Ristretto'); // NO!!

On va toujours utiliser une autre instance de CapsuleType !

IRL on ne "change" pas le café de la capsule 

Immutabilité

Le propre d'un VO est d'être immutable : vous ne pouvez devez pas en changer le contenu. C'est super important.

Pourquoi immutable

Une cafetière peut changer de Capsule, d'état (allumée, éteinte), d'emplacement (cuisine, WC). Elle sera toujours la même cafetière. C'est une entité.

Mais le type de capsule Roma, si on change sa couleur, ce n'est plus le même type.

Rendre un objet PHP immutable

class CapsuleType
{
    private $name; // Privé, sans setter

    public function __construct($name)
    {
        $this->name = $name;
    }
}

Trop permissif

class MyFlexibleCapsuleType extends CapsuleType
{
    private $name;

    public function setName($name) {
        $this->name = $name;
    }
}

Mot clé final

final class CapsuleType // final = no extends
{
    private $name;

    public function __construct($name)
    {
        $this->name = $name;
    }
}

Toujours trop permissif !

$roma = new CapsuleType('Roma');

// Malicious code…
$roma->__construct("HAHA");

// var_dump($roma);

class CapsuleType#5 (1) {
    private $name => string(4) "HAHA"
}

Faire du "vrai" immutable

final class CapsuleType
{
    private $name;

    private function __construct() {} // Pas de constructeur

    public static function fromName($name) // Une statique
    {
        $type = new CapsuleType();
        $type->name = $name;

        return $type;
    }
}

Enfin !

$capsuleType = CapsuleType::fromName("Roma");

Ou pas...

On est en PHP 😜

$refObject   = new ReflectionObject($roma);
$refProperty = $refObject->getProperty('name');
$refProperty->setAccessible(true);
$refProperty->setValue($one, 'Trolololol');

Mais on s'égare...

Immutable c'est dans la tête surtout en PHP

Communiquez, documentez, adoptez une convention, un namespace… Ne laissez pas vos collègues implémenter un setter et tout ira bien.

La persistence

Avec PHP et Doctrine 2.5

Je suis là pour vous parler de Doctrine, deux stratégies :

Type object

La façon historique de faire du VO avec Doctrine.

Notre exemple

Chaque utilisateur dans notre domaine peut avoir un type de capsule préféré.

Nous avons une entité User et un VO CapsuleType.

Utilisation du type object

/** @Entity */
class User
{
    /**
     * @var CapsuleType
     * @Column(type="object")
     */
    private $favoriteCapsuleType;
}

Rien de particulier sur l'objet ! #win

Attention à bien forcer un type

/**
 * @param CapsuleType $type
 */
public function setFavoriteCapsuleType(CapsuleType $type)
{
    $this->favoriteCapsuleType = $type;
}

Type hint obligatoire, pas de validation par Doctrine.

Le schéma généré

id int(11)
favoriteCapsuleType longtext

Avec un commentaire : (DC2Type:object), attention à votre SGBD.

Utilisation naturelle

$ristretto = CapsuleType::fromValues('Ristretto');

$user   = new User();
$user->setFavoriteCapsuleType($ristretto);

$entityManager->persist($user);
$entityManager->flush();

En base de données

id 1
favoriteCapsuleType O:17:"Model\CapsuleType":1:{s:23:"Model\CapsuleTypename";s:9:"Ristretto"}

C'est juste un peu moche.

Hydratation propre, on récupère notre VO

$entityManager->find(User::class, 1);

// Retour de Doctrine
class Model\User {
    private $id => int(1)
    private $favoriteCapsuleType => class Model\CapsuleType {
        private $name => string(9) "Ristretto"
    }
}

Quelques inconvénients

Introducing Embeddable !

Notre objet Embeddable, le VO CapsuleType

/** @Embeddable */
final class CapsuleType
{
    /** @Column(type="string") */
    private $name;

    private function __construct() {}

    public static function fromValues($name)
    {
        $type = new CapsuleType();
        $type->name = $name; // ...
        return $type;
    }
}

Notre entité User

/** @Entity */
class User {
    /**
     * @Id
     * @Column(type="integer")
     * @GeneratedValue
     */
    private $id;

    /**
     * @Embedded(class="CapsuleType")
     */
    private $favoriteCapsuleType;

    /**
     * @param CapsuleType $favoriteCapsuleType
     */
    public function setFavoriteCapsuleType(CapsuleType $type) // ...

Le schéma généré

id int(11)
favoriteCapsuleType_name varchar(255)
favoriteCapsuleType_intensity int(2)
favoriteCapsuleType_color varchar(12)
... ...

Pas de commentaire \o/ et un nouveau champ par propriété du VO.

Utilisation aisée

$ristretto = CapsuleType::fromValues('Ristretto');

$user      = new User();
$user->setFavoriteCapsuleType($ristretto);

$entityManager->persist($capsule);
$entityManager->flush();

Rien de nouveau, on manipule des objets.

En base de données

id 1
favoriteCapsuleType_name Ristretto
favoriteCapsuleType_intensity 12
favoriteCapsuleType_color pink

Beaucoup plus propre !

Hydratation en VO aussi !

$entityManager->find(User::class, 1);

class Model\User {
    private $id => int(1)
    private $favoriteCapsuleType => class Model\CapsuleType {
        private $name => string(9) "Ristretto"
        // ...
    }
}

Identique au type object.

DQL Intelligent

$queryBuilder->from(User::class, 'u')->select('u');
$queryBuilder->where('u.favoriteCapsuleType.name = :name');
$queryBuilder->setParameter('name', 'Ristretto');

On n'utilise pas favoriteCapsuleType_name,
mais bien User->favoriteCapsuleType->name !

Et on peut faire des recherches sur les champs du VO.

Dans les deux cas, attention à l'hydratation

Le VO Nullable

Loïck n'aime pas le café

$loick = new User();
$loick->setFavoriteCapsuleType(null); // Aucun type de capsule préféré

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'favoriteCapsuleType' cannot be null

Nullable à la rescousse

/** @Entity */
class User
{
    /**
     * @var CapsuleType
     * @Column(type="object", nullable=true)
     */
    private $favoriteCapsuleType;
}

Moyen supporté avec le type object

id name favoriteCapsuleType
1 Damien O:17:"Model\CapsuleType"...
2 Loick N;

Non supporté avec Embedded

Rendre tous les champs du VO nullable

// Résultat d'une Query

class Model\User {
    private $id => int(1)
    private $name => string(5) "Loick"
    private $favoriteCapsuleType => class Model\CapsuleType {
        private $name => NULL
    }
}

On récupère une instance invalide de CapsuleType depuis l'ORM.

Code userland !

/** @Entity */
class User
{
    /**
     * @Column(type="boolean")
     */
    private $hasFavoriteCapsuleType = false;

    /**
     * @Embedded(class="CapsuleType")
     */
    private $favoriteCapsuleType;

On ajoute un boolean hasFavoriteCapsuleType.

Ajout de logique dans les getter / setter

/**
 * @return CapsuleType|null
 */
public function getFavoriteCapsuleType()
{
    return $this->hasFavoriteCapsuleType() ? $this->favoriteCapsuleType : null;
}

/**
 * @param CapsuleType|null $favoriteCapsuleType
 */
public function setFavoriteCapsuleType(CapsuleType $favoriteCapsuleType = null)
{
    $this->favoriteCapsuleType      = $favoriteCapsuleType;
    $this->hasFavoriteCapsuleType   = $favoriteCapsuleType ? true : false;
}

Beaucoup de code mais plein d'avantages !

$qb = $entityManager->createQueryBuilder();
$qb->from(User::class, 'u')->select('u')
   ->where('u.hasFavoriteCapsuleType = :has');
$qb->setParameter('has', true);

Quelques inconvénients

Mon rêve

Que l'ORM gère ça pour nous !

@Embedded(class="CapsuleType", nullable=true)

Votez avec vos pouces : https://github.com/doctrine/doctrine2/pull/1275.

Conclusion

Quand utiliser un VO ?

Posez-vous les bonnes questions. Votre objet :

Comment persister un VO ?

Détendez-vous

The End À vos questions ! (Validation ? Form ? VO dans un VO ?)

@damienalexandre

Crédits & bisous

The trademarks, trade names or other distinctive brand features of Nespresso ("Trademarks") displayed on the slideshow are the exclusive property of Nespresso and/or Nestlé.