5. DTO – Data Transfer Objects
Modèle anémique vs modèle riche
5
Entité
BDD
- données
+ set($données)
+ get($données)
Entité
- données
+ set($données)
+ get($données)
Application
Services
...
Conrollers
...
Forms
...
Services
...
La logique métier
Conteneurs de données
sans aucun comportement
6. DTO – Data Transfer Objects
Repository
+ fetch()
+ save($objet)
Modèle anémique vs modèle riche
6
BDD
Entité
- données
+ method()
Application
Services
...
Conrollers
...
Forms
...
Commandes
...
Concentrer la logique
métier complexe dans le
couche de domaine
Service
+ method() Factory
+ create()Stockage
des données
de domaine
La logique d’application
7. Modèle anémique vs modèle riche
7
Les applications avec des règles métiers :
• Complexes (difficiles à comprendre, mémoriser)
• Evolutifs (pendant le développement et après la livraison)
8. Modèle anémique vs modèle riche
8
Le but n’est pas :
• D’isoler le couche de domaine de framework, de la bdd.
• D’implémenter l’architecture hexagonale/onion/CQRS/etc.
Le but:
• Concentrer la logique métier complexe dans le coche de domaine
• Maîtriser la complexité
• Eviter les entités anémiques. Ils sont les conteneurs de données, DTO entre la
bdd et le code procédural dans les services, contrôleurs, formulaires.
• Transformer les entités en couche domaine de votre application. Domaine
c’est l’ensemble de classes qui concentre des règles métier : l’intégrité, la
validation, le workflow, les interactions, etc.
9. Langage
9
Le même langage que vous parlez avec vos clients doit être codé
dans votre modèle.
Fonctionnaire Officer
Fonctionnaire
Poste Job
Poste
Métier Profession
Metier
affecter setJob
affecter
assign
16. Fonctionnalité
16
- Créer et modifier des personnes
- Email unique
- Déposer/modifier des absences des ces personnes
- Pas de nouvelles absences dans le passé
- Un seul absence pour un jour donné
- Visualiser sur le calendrier
- Gérer les compteurs d’absence pour certains types
d’absence
19. 19
namespace AppDomain;
class Personne
{
private $email;
private $nom;
public function __construct(string $email, string $nom)
{
$this->email = $email;
$this->nom = $nom;
}
public function getEmail(): string
{
return $this->email;
}
public function getNom(): string
{
return $this->nom;
}
public function renommer(string $nom)
{
$this->nom = $nom;
}
}
Une personne doit être tout de
suite et toujours valide
Aucun « setter » direct,
que des méthodes significatives
Email est un ID
Entité
20. 20
namespace AppDomain;
class Personne
{
private $email;
private $nom;
public function __construct(string $email, string $nom)
{
$this->email = $email;
$this->nom = $nom;
}
public function getEmail(): string
{
return $this->email;
}
public function getNom(): string
{
return $this->nom;
}
public function renommer(string $nom)
{
$this->nom = $nom;
}
}
AppDomainPersonne:
type: entity
id:
email:
type: string
length: 128
fields:
nom:
type: string
Mapping
Entité
21. ID
21
• Champ significatif (l’email, le numéro de la facture, la plaque immatriculation) :
• Sémantique
• Divulgation d'information quand ce type d’ID est mis dans URL
• Clé étrangère pas optimal dans les BDDs relationnelles
• Integer autoincréménté :
• Aucun sens
• Divulgation de nombre d’entités
• Clé étrangère optimal dans les BDDs relationnelles
• UUID:
• Aucun sens
• Opaque
• Clé étrangère pas optimal dans les BDDs relationnelles
• https://hashids.org, https://github.com/pascaldevink/shortuuid, etc.
22. namespace AppDomain;
use AppDomainRepositoryPersonneRepositoryInterface;
class Personne
{
private $email;
private $nom;
/** @var PersonneRepositoryInterface */
private $personneRepository;
public function __construct(string $email, string $nom, PersonneRepositoryInterface $personneRepository)
{
// Vérifier que l'email n'est pas encore enregistré.
if ($personneRepository->emailAlreadyExist($email)) {
throw new PersonneEmailAlreadyTakenException($email.' a été déjà enregistré');
}
$this->email = $email;
$this->nom = $nom;
$this->personneRepository = $personneRepository;
}
}
22
Entité
24. Cycle de vie d’éntité
24
Fabrique
Instancie un objet Personne quand elle crée une nouvelle personne.
Utilise Personne::__construct.
Repositoire
Instancie un objet Personne quand il reconstitue une personne déjà enregistrée
dans l’application à partir des données fournies par la BDD.
C’est un EntityRepository de Doctrine avec un petit ajout.
PersonneRepository n’appelle pas le Personne::__construct.
26. namespace AppInfrastructureDoctrineRepository;
use AppDomainExceptionPersonneNotFoundException;
use AppDomainPersonne;
use AppDomainRepositoryPersonneRepositoryInterface;
use DoctrineBundleDoctrineBundleRepositoryServiceEntityRepository;
use DoctrineCommonPersistenceManagerRegistry;
class PersonneRepository extends ServiceEntityRepository implements PersonneRepositoryInterface
{
// Désigner PersonneRepository en tant que le repositoire de @see Personne
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Personne::class);
}
// Implementation de @see PersonneRepositoryInterface::get()
public function get(string $email): Personne
{
$personne = $this->find($email);
if (!$personne instanceof Personne) {
throw new PersonneNotFoundException('Personne '.$email." n'est pas trouvée");
}
return $personne;
}
}
26
Repository
27. namespace AppInfrastructureDoctrineListener;
use AppDomainPersonne;
use AppDomainRepositoryPersonneRepositoryInterface;
use DoctrineCommonEventSubscriber;
use DoctrineORMEventLifecycleEventArgs;
class PersonneLifecycleListener implements EventSubscriber
{
private $personneRepository;
public function getSubscribedEvents()
{
return [DoctrineORMEvents::postLoad];
}
public function postLoad(LifecycleEventArgs $event)
{
$personne = $event->getEntity();
if (!$personne instanceof Personne) {
return;
}
$reflProp = (new ReflectionClass(Personne::class))->getProperty('personneRepository');
$reflProp->setAccessible(true);
$reflProp->setValue($personne, $this->personneRepository);
}
}
27
Repository
28. 28
namespace AppInfrastructureDoctrineRepository;
class PersonneRepository extends ServiceEntityRepository implements PersonneRepositoryInterface
{
public function getAllInfo(): array
{
return $this->createQueryBuilder('p')
->select('p.email, p.nom')
->orderBy('p.email', 'ASC')
->getQuery()
->getArrayResult();
}
}
Repository
39. 39
Les objets-valeurs (en. Value objects)
Des objets qui sont définies par les valeurs qu’ils contient.
Les objets-valeurs sont immutables.
Un objet valeur pour un
distributeur de billets
Une entité pour Hercule Poirot
Objets-valeurs
43. namespace AppDomain;
class Personne
{
/** @var Absence[] */
private $absences = [];
/**
* Déposer une absence.
* Il n'est pas possible de déposer une absence
* qui chevauche une absence déjà existante.
*/
public function deposerAbsence(int $type, $debut, $fin)
{
foreach ($this->absences as $a) {
if ($a->getDebut() <= $fin && $a->getFin() >= $debut) {
throw new AbsenceAlreadyTakenException();
}
}
$absence = new Absence($this, $type, $debut, $fin);
$this->absences[] = $absence;
}
}
Liaison un-à-plusieurs entre Personne et Absence
43
1. Déclaration un liaison bidirectionnel
AppDomainPersonne:
type: entity
oneToMany:
absences:
targetEntity: AppDomainAbsence
mappedBy: personne
cascade: ["persist", "remove"]
44. 44
public function getAbsence($id)
{
foreach ($this->absences as $absence) {
if ($absence->getId() == $id) {
return $absence;
}
}
throw new AbsenceNonTrouveeException();
}
public function getAbsences($startPeriod, $endPeriod)
{
$return = [];
foreach ($this->absences as $absence) {
if ($absence->getDebut() <= $endPeriod && $startPeriod <= $absence->getFin()) {
$return[] = $absence;
}
}
// Trier par date de début
return $return;
}
Il est trop lourd de parcourir des larges tableaux.
Liaison
45. namespace AppDomain;
class Personne
{
/** @var PersistentCollection */
private $absences = [];
/**
* Déposer une absence.
* Il n'est pas possible de déposer
* une absence qui chevauche une absence déjà existante.
*/
public function deposerAbsence(int $type, $debut, $fin)
{
$criteria = Criteria::create();
$criteria
->andWhere(Criteria::expr()->lte('debut', $fin))
->andWhere(Criteria::expr()->gte('fin', $debut));
if (count($this->absences->matching($criteria)) > 0) {
throw new AbsenceAlreadyTakenException();
};
$absence = new Absence($this, $type, $debut, $fin);
$this->absences->add($absence);
}
}
45
AppDomainPersonne:
type: entity
oneToMany:
absences:
targetEntity: AppDomainAbsence
mappedBy: personne
cascade: ["persist", "remove"]
Liaison
46. 46
public function getAbsence($id)
{
$criteria = Criteria::create();
$criteria->andWhere(Criteria::expr()->eq('id', $id));
$absence = $this->absences->matching($criteria)->current();
if (!$absence) {
throw new AbsenceNonTrouveeException();
}
return $absence;
}
public function getAbsences($startPeriod, $endPeriod)
{
$criteria = Criteria::create();
$criteria
->andWhere(Criteria::expr()->lte('debut', $endPeriod))
->andWhere(Criteria::expr()->gte('fin', $startPeriod))
->orderBy(['debut' => 'ASC']);
return $this->absences->matching($criteria);
}
Liaison
49. namespace AppDomain;
class Personne
{
/** @var AbsenceRepositoryInterface */
private $absenceRepository;
/**
* Déposer une absence.
* Il n'est pas possible de déposer une absence qui chevauche une absence déjà existante.
*/
public function deposerAbsence(AbsenceDeposerDTO $dto)
{
if ($this->absenceRepository->absenceDeposeDansPeriode($this, $dto->debut, $dto->fin)) {
throw new AbsenceDejaDeposeeException();
}
$absence = new Absence($this, $dto->type, $dto->debut, $dto->fin);
$this->absenceRepository->save($absence);
}
}
49
Liaison
56. Services
56
namespace AppDomain;
class Personne
{
/**
* Déposer une absence.
*
* @param AbsenceDeposerDTO $dto
*/
public function deposerAbsence(AbsenceDeposerDTO $dto)
{
if ($this->absenceRepository->absenceDeposeDansPeriode($this, $dto->debut, $dto->fin)) {
throw new AbsenceDejaDeposeeException('Une absence pour ces dates a été déjà déposée');
}
$absence = new Absence($this, $dto->type, $dto->debut, $dto->fin);
$this->absenceCompteurService->deposerAbsence(
$this->compteurs, new AbsenceType($dto->type), $dto->debut, $dto->fin
);
$this->absenceRepository->save($absence);
}
}
57. 57
namespace AppDomainService;
class AbsenceCompteurService
{
/**
* @param AbsenceCompteur[] $compteurs
* @param AbsenceType $type
* @param DateTimeImmutable $debut
* @param DateTimeImmutable $fin
*/
public function deposerAbsence($compteurs, $type, $debut, $fin): void
{
if (!self::typeCompteurs($type)) {
return;
}
foreach ($compteurs as $compteur) {
if ($compteur->getType()->isEqualTo($type)) {
$jours = self::calculerJoursAbsence($debut, $fin);
$compteur->deposerAbsence($jours);
}
}
}
}
Services
58. 58
namespace AppDomain;
class AbsenceCompteur
{
/**
* @param int $jours
*
* @throws AbsenceJoursDisponiblesInsuffisantsException
*/
public function deposerAbsence(int $jours)
{
if ($this->joursDisponibles < $jours) {
throw new AbsenceJoursDisponiblesInsuffisantsException(
'Il ne vous reste plus de jours disponibles pour ce type d'absence’
);
}
$this->joursDisponibles -= $jours;
}
}
Services