Damien ALEXANDRE - Novembre 2015
Forum PHP 2015 - Damien ALEXANDRE - Novembre 2015
Allez plutôt voir :
CHRONIQUE D'UN PROJET DRIVEN DESIGN
par Alexandre Balmes
le 23 à 14h dans l'Auditorium Lucienne et André Blin
class CoffeeMaker
{
private $id;
private $model;
private $currentCapsule;
}
$maker = new CoffeeMaker();
$maker->setModel('Magimix 42');
$maker->setCurrentCapsule(
// ??? Wat ???
);
Une capsule possède un nom, un arôme, une couleur... et une identité.
class Capsule
{
private $id;
private $name;
private $color;
private $intensity;
}
Et si on en faisait un VO !
isDecaffeinato()
)...class CapsuleType
{
private $name;
private $color;
private $intensity;
}
$capsule = new Capsule(
new CapsuleType("JoliBrew")
);
$maker->setCurrentCapsule($capsule);
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 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.
$roma = new CapsuleType('Roma', 'purple', 9);
$capsule = new Capsule($roma);
$maker = new CoffeeMaker();
$maker->setModel('Magimix 42');
$maker->setCurrentCapsule($capsule);
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 ==
.
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
===
compare uniquement la référence mémoire de l'objet ;==
compare le nom de la classe et les valeurs des attributs ;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.
$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 :
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/
$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
!
Le propre d'un VO est d'être immutable : vous ne pouvez devez pas en changer le contenu. C'est super important.
CapsuleType
ne peut pas se retrouver dans un état invalide (couleur manquante par exemple) ;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.
class CapsuleType
{
private $name; // Privé, sans setter
public function __construct($name)
{
$this->name = $name;
}
}
class MyFlexibleCapsuleType extends CapsuleType
{
private $name;
public function setName($name) {
$this->name = $name;
}
}
final class CapsuleType // final = no extends
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
}
$roma = new CapsuleType('Roma');
// Malicious code…
$roma->__construct("HAHA");
// var_dump($roma);
class CapsuleType#5 (1) {
private $name => string(4) "HAHA"
}
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;
}
}
$capsuleType = CapsuleType::fromName("Roma");
Ou pas...
$refObject = new ReflectionObject($roma);
$refProperty = $refObject->getProperty('name');
$refProperty->setAccessible(true);
$refProperty->setValue($one, 'Trolololol');
Mais on s'égare...
Communiquez, documentez, adoptez une convention, un namespace… Ne laissez pas vos collègues implémenter un setter et tout ira bien.
Je suis là pour vous parler de Doctrine, deux stratégies :
Embeddable
;object
.La façon historique de faire du VO avec Doctrine.
serialize/unserialize
pour stocker vos VO ;Chaque utilisateur dans notre domaine peut avoir un type de capsule préféré.
Nous avons une entité User et un VO CapsuleType.
/** @Entity */
class User
{
/**
* @var CapsuleType
* @Column(type="object")
*/
private $favoriteCapsuleType;
}
Rien de particulier sur l'objet ! #win
/**
* @param CapsuleType $type
*/
public function setFavoriteCapsuleType(CapsuleType $type)
{
$this->favoriteCapsuleType = $type;
}
Type hint obligatoire, pas de validation par Doctrine.
id | int(11) |
---|---|
favoriteCapsuleType | longtext |
Avec un commentaire : (DC2Type:object)
, attention à votre SGBD.
$ristretto = CapsuleType::fromValues('Ristretto');
$user = new User();
$user->setFavoriteCapsuleType($ristretto);
$entityManager->persist($user);
$entityManager->flush();
id | 1 |
---|---|
favoriteCapsuleType | O:17:"Model\CapsuleType":1:{s:23:"Model\CapsuleTypename";s:9:"Ristretto"} |
C'est juste un peu moche.
$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"
}
}
@Embedded
et @Embeddable
;/** @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;
}
}
/** @Entity */
class User {
/**
* @Id
* @Column(type="integer")
* @GeneratedValue
*/
private $id;
/**
* @Embedded(class="CapsuleType")
*/
private $favoriteCapsuleType;
/**
* @param CapsuleType $favoriteCapsuleType
*/
public function setFavoriteCapsuleType(CapsuleType $type) // ...
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.
$ristretto = CapsuleType::fromValues('Ristretto');
$user = new User();
$user->setFavoriteCapsuleType($ristretto);
$entityManager->persist($capsule);
$entityManager->flush();
Rien de nouveau, on manipule des objets.
id | 1 |
---|---|
favoriteCapsuleType_name | Ristretto |
favoriteCapsuleType_intensity | 12 |
favoriteCapsuleType_color | pink |
Beaucoup plus propre !
$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
.
$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.
__construct()
fromValues()
$loick = new User();
$loick->setFavoriteCapsuleType(null); // Aucun type de capsule préféré
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'favoriteCapsuleType' cannot be null
/** @Entity */
class User
{
/**
* @var CapsuleType
* @Column(type="object", nullable=true)
*/
private $favoriteCapsuleType;
}
object
id | name | favoriteCapsuleType |
---|---|---|
1 | Damien | O:17:"Model\CapsuleType"... |
2 | Loick | N; |
serialize(NULL)
=== N;
;WHERE x IS NULL
;nullable=true
ne sert à rien sur un type object
.Embedded
nullable
sur @Embedded
;nullable
sur tous les champs du @Embeddable
😞
// 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.
/** @Entity */
class User
{
/**
* @Column(type="boolean")
*/
private $hasFavoriteCapsuleType = false;
/**
* @Embedded(class="CapsuleType")
*/
private $favoriteCapsuleType;
On ajoute un boolean hasFavoriteCapsuleType
.
/**
* @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;
}
$qb = $entityManager->createQueryBuilder();
$qb->from(User::class, 'u')->select('u')
->where('u.hasFavoriteCapsuleType = :has');
$qb->setParameter('has', true);
nullable
.Que l'ORM gère ça pour nous !
@Embedded(class="CapsuleType", nullable=true)
Votez avec vos pouces : https://github.com/doctrine/doctrine2/pull/1275.
Posez-vous les bonnes questions. Votre objet :
object
:
Embedded
♥ ♥ ♥ :
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é.