SlideShare une entreprise Scribd logo
Modèle de domaine riche
dans une application métier complexe
un exemple pratique
https://vria.eu
contact@vria.eu
https://twitter.com/RiaVlad
RIABCHENKO Vladyslav
5+ ans full stack web-développeur
Certifié Symfony
Architecte technique à Webnet
3
Modèle anémique et modèle riche
Architecture
Fonctionnalité à développer
Entités, Repositories, Fabriques
Objet-valeurs, Agrégats
Services, Exceptions
Plan
Un modèle anémique et un modèle riche
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
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
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)
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.
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
Architecture
Repositories
Domaine
Services
Application Controllers
Services
Forms
Commands
Infrastructure
…
Architecture
Repositories
Domaine
Services
Application
Infrastructure
Entités
Objets Valeurs
Services
Fabriques
Reposoitoires
Architecture
Repositories
Domaine
Services
Application
Infrastructure
RepositoryInterface
Domain dépend seulement de repositories
Couche Infrastructure dépend de couche domaine
Architecture
14
Layered architecture
Onion architecture
Hexagonal architecture
CQRS + event sourcing
...
MVC, MVVM, MV-Whatever
Architecture
Fonctionnalité à développer
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
Personne
Entités, Repositories, Fabriques
Personne
Ajouter une personne
Lister
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
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é
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.
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é
Repository
23
AppDomain
Personne
- email
- nom
- personneRepository
+ __construct($email, $nom, $personneRepository)
+ getEmail()
+ getNom()
PersonneRepositoryInterface
+ emailAlreadyExist(string $email): bool
+ save(Personne $personne): void
+ get(string $email): Personne
+ getAllInfo(): array
AppInfrastructureDoctrineRepository
PersonneRepository
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.
PersonneRepository
25
Controller PersonneFactory PersonneRepositoryInterface
create(…)
$personne
save($personne)
get($email)
$personne
Cycle de vie d’éntité
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
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
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
Fabrique & Repository
29
Contrôleurs et commandes
Personne
PersonneFactory
PersonneRepositoryInterface
PersonneRepository
PersonneCreerDTO
Personne
DTO
30
namespace AppApplicationDTO;
use AppDomainPersonne;
use SymfonyComponentValidatorConstraints as Assert;
class PersonneCreerDTO
{
/**
* @AssertNotBlank()
* @AssertEmail()
*/
public $email;
/**
* @AssertNotBlank()
*/
public $nom;
}
class PersonneController
{
/**
* @param Request $request
* @param FormFactoryInterface $formFactory
* @param UrlGeneratorInterface $urlGenerator
* @param PersonneRepositoryInterface $personneRepository
*/
public function creer(...)
{
$creerPersonneDTO = new PersonneCreerDTO();
$form = $formFactory->create(PersonneCreerType::class, $creerPersonneDTO);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
$personneFactory = new PersonneFactory($personneRepository);
$personne = $personneFactory->create($creerPersonneDTO);
$personneRepository->save($personne);
return new RedirectResponse($urlGenerator->generate('personne_lister'));
} catch (PersonneEmailAlreadyTakenException $e) {
$form->get('email')->addError(new FormError($e->getMessage()));
}
}
return ['form' => $form->createView()];
}
}
31
DTO
32
namespace AppApplicationCommand;
class PersonneCreerCommand extends Command
{
protected static $defaultName = 'app:personne:creer';
/** @var ValidatorInterface */
private $validator;
/** @var PersonneRepositoryInterface */
private $personneRepository;
/** {@inheritdoc} */
protected function configure()
{
$this->setDescription('Créer une nouvelle personne.')
->addArgument('email', InputArgument::REQUIRED)
->addArgument('nom', InputArgument::REQUIRED)
;
}
// ...
DTO
protected function execute(InputInterface $input, OutputInterface $output)
{
// Construire DTO.
$creerPersonneDTO = new PersonneCreerDTO();
$creerPersonneDTO->email = $input->getArgument('email');
$creerPersonneDTO->nom = $input->getArgument('nom');
// Valider la saisie de l'utilisateur.
$constraintViolationList = $this->validator->validate($creerPersonneDTO);
if ($constraintViolationList->count() > 0) {
foreach ($constraintViolationList as $violation) {
$output->writeln(
sprintf('<error>%s: %s</error>', $violation->getPropertyPath(), $violation->getMessage())
);
}
return;
}
try {
// Créer une personne.
$personneFactory = new PersonneFactory($this->personneRepository);
$personne = $personneFactory->create($creerPersonneDTO);
$this->personneRepository->save($personne);
$output->writeln('<info>Personne a été créée avec succès.</info>');
} catch (PersonneEmailAlreadyTakenException $e) {
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
}
} 33
DTO
34
/**
* @param string $email
* @param Request $request
* @param FormFactoryInterface $formFactory
* @param UrlGeneratorInterface $urlGenerator
* @param PersonneRepositoryInterface $personneRepository
*/
public function modifier(...)
{
try {
$personne = $personneRepository->get($email);
} catch (PersonneNotFoundException $e) {
throw new NotFoundHttpException($e->getMessage(), $e);
}
$form = $formFactory->create(PersonneModifierType::class, $personne);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$personneRepository->save($personne);
return new RedirectResponse($urlGenerator->generate('personne_lister'));
}
return ['form' => $form->createView()];
}
sans DTO
Formulaire
35
namespace AppApplicationForm;
class PersonneModifierType extends AbstractType implements DataMapperInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class, [
'disabled' => true,
])
->add('nom', TextType::class, [
'constraints' => [
new AssertNotBlank(),
],
])
->setDataMapper($this);
}
public function mapDataToForms($personne, $forms)
{
//...
}
public function mapFormsToData($forms, &$personne)
{
//...
}
}
Personne::renommer()
au lieu d’utiliser les setters
Formulaire
36
public function mapDataToForms($personne, $forms)
{
/** @var FormInterface[] $forms */
$forms = iterator_to_array($forms);
/* @var Personne $personne */
$forms['email']->setData($personne->getEmail());
$forms['nom']->setData($personne->getNom());
}
public function mapFormsToData($forms, &$personne)
{
/** @var FormInterface[] $forms */
$forms = iterator_to_array($forms);
/* @var Personne $personne */
if ($nom = $forms['nom']->getData()) {
$personne->update($nom);
}
}
SymfonyLive London 2018 - Christopher Hertel & Christian Flothmann - Using Symfony Forms
Formulaire
Absence
Objet-valeurs, Agrégats
Absence
38
Ajouter/modifier des absences
Afficher le calendrier
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
AppDomain
40
Personne
- email
- nom
- personneRepository
...
Absence
- id: int
- personne: Personne
- type: AbsenceType
- debut: DateTime
- fin: DateTime
AbsenceType
- type: int [MALADIE, CONGES_PAYES, ...]
+ __construct($type)
+ getType(): int
+ getLabel(): string
+ isEqualTo(AbsenceType $absenceType)
...
Objets-valeurs
41
AppDomainAbsence:
type: entity
id:
id:
type: integer
generator:
strategy: AUTO
embedded:
type:
class: AppDomainAbsenceType
fields:
debut:
type: datetime_immutable
fin:
type: datetime_immutable
manyToOne:
personne:
targetEntity: AppDomainPersonne
joinColumn:
name: personne_email
referencedColumnName: email
AppDomainAbsenceType:
type: embeddable
fields:
type:
type: integer
Objets-valeurs
Agrégats
42
Personne: Rick
+ getEmail(): string
+ getNom(): string
+ renommer($nom)
+ deposerAbsence(AbsenceDeposerDTO $dto)
+ modifierAbsence(AbsenceModifierDTO $dto)
+ annulerAbsence($id)
+ getAbsences($startPeriod, $endPeriod)
Congé payé 07/05/2019
Maladie 11/04/2019
...
Controller
…
…
…
…
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
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
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
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
47
Personne Absences
- personne
+ deposerAbsence(...)
+ modifierAbsence(...)
+ annulerAbsence(...)
+ getAbsences(...)
- absences
bidirectionnel
Liaison
AppDomain
48
unidirectionnel
Personne
Absences
- personne
+ deposerAbsence(...)
+ modifierAbsence(...)
+ annulerAbsence(...)
+ getAbsences(...)
- absenceRepository
PersonneRepositoryInterface
+ save(Absence $absence): void
+ getAbsence($personne, $id): Absence
+ getAbsences($personne, $startPeriod, $endPeriod): Absences[]
+ absenceDeposeDansPeriode($personne, $debut, $fin): bool
AppInfrastructureDoctrineRepository
AbsenceRepository
Liaison
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
Agrégat
50
AppDomainDTO
AbsenceDeposerDTO
+ type
+ debut
+ fin
+ __construct()
Form
Command
Controller +
Personne
+ deposerAbsence($dto)
Validation simple
Valeurs initiales
Validation métier
AppDomainDTO
51
Form
Command
Controller +
Personne
+ modifierAbsence($dto)
AbsenceModifierDTO
+ type
+ debut
+ fin
- id
+ fromAbsence($absence): self
+ getId()
Agrégat
Compteurs
Services, Exceptions
Compteurs
53
Enregistrer de jours de congé restants
Jours travaillés rapportent des jours de congé
Contrôle de jours restants
54
Personne
...
- absenceRepository
- compteurs: AbsenceCompteur[]
- compteurService
AbsenceCompteur
- id
- personne: Personne
- type
- joursDisponibles
- joursTravailles
+ __construct($personne, $type)
+ incrementerJoursTravailles()
+ deposerAbsence($jours)
+ annulerAbsence($jours)
...
+ deposerAbsence(...)
+ modifierAbsence(...)
+ annulerAbsence($id)
+ reinitialiserComptuers(...)
+ incrementerJoursTravailles(...)
+ getCompteursInfo()
…
…
…
Compteurs
55
Personne
...
- absenceRepository
- compteurs: AbsenceCompteur[]
- compteurService
AbsenceCompteur
...
+ deposerAbsence(...)
+ modifierAbsence(...)
+ annulerAbsence($id)
+ reinitialiserComptuers(...)
+ incrementerJoursTravailles(...)
+ getCompteursInfo()
AbsenceCompteur
- absenceRepository
+ init(): AbsenceCompteur[]
+ incrementerJoursTravailles(...)
+ deposerAbsence(...)
+ modifierAbsence(...)
+ ...
Services
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
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
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
Exceptions
59
Merci pour votre attention
https://vria.eu
contact@vria.eu
https://twitter.com/RiaVlad
https://webnet.fr
https://github.com/vria/symfony-rich-domain-model

Contenu connexe

Tendances

Formulaires Symfony2 - Cas pratiques et explications
Formulaires Symfony2 - Cas pratiques et explicationsFormulaires Symfony2 - Cas pratiques et explications
Formulaires Symfony2 - Cas pratiques et explications
Alexandre Salomé
 
Les Streams de Java 8
Les Streams de Java 8Les Streams de Java 8
Les Streams de Java 8
Antoine Rey
 
Introduction à Symfony
Introduction à SymfonyIntroduction à Symfony
Introduction à Symfony
Abdoulaye Dieng
 
PHP 7 et Symfony 3
PHP 7 et Symfony 3PHP 7 et Symfony 3
PHP 7 et Symfony 3
Eddy RICHARD
 
Notions de base de JavaScript
Notions de base de JavaScriptNotions de base de JavaScript
Notions de base de JavaScript
Kristen Le Liboux
 
Php 2 - Approfondissement MySQL, PDO et MVC
Php 2 - Approfondissement MySQL, PDO et MVCPhp 2 - Approfondissement MySQL, PDO et MVC
Php 2 - Approfondissement MySQL, PDO et MVC
Pierre Faure
 
Javascript : fondamentaux et OOP
Javascript : fondamentaux et OOPJavascript : fondamentaux et OOP
Javascript : fondamentaux et OOP
Jean-Pierre Vincent
 
Requêtes HTTP synchrones et asynchrones
Requêtes HTTPsynchrones et asynchronesRequêtes HTTPsynchrones et asynchrones
Requêtes HTTP synchrones et asynchrones
Abdoulaye Dieng
 
Softshake 2013 Apiness SA l'envers du décor
Softshake 2013 Apiness SA l'envers du décorSoftshake 2013 Apiness SA l'envers du décor
Softshake 2013 Apiness SA l'envers du décor
michaelmiguel2013
 
Introduction à JavaScript
Introduction à JavaScriptIntroduction à JavaScript
Introduction à JavaScript
Abdoulaye Dieng
 
Php mysql cours
Php mysql coursPhp mysql cours
Php mysql cours
zan
 
Introduction à ajax
Introduction à ajaxIntroduction à ajax
Introduction à ajax
Abdoulaye Dieng
 
Introduction à JavaScript
Introduction à JavaScriptIntroduction à JavaScript
Introduction à JavaScript
Abdoulaye Dieng
 
Bases de PHP - Partie 1
Bases de PHP - Partie 1Bases de PHP - Partie 1
Bases de PHP - Partie 1
Régis Lutter
 
Trucs et astuces PHP et MySQL
Trucs et astuces PHP et MySQLTrucs et astuces PHP et MySQL
Trucs et astuces PHP et MySQL
Damien Seguy
 
Les structures de données PHP5
Les structures de données PHP5Les structures de données PHP5
Les structures de données PHP5
Jean-Marie Renouard
 
Introduction à React
Introduction à ReactIntroduction à React
Introduction à React
Abdoulaye Dieng
 
Python avancé : Classe et objet
Python avancé : Classe et objetPython avancé : Classe et objet
Python avancé : Classe et objet
ECAM Brussels Engineering School
 
Marzouk une introduction à jdbc
Marzouk une introduction à jdbcMarzouk une introduction à jdbc
Marzouk une introduction à jdbc
abderrahim marzouk
 

Tendances (19)

Formulaires Symfony2 - Cas pratiques et explications
Formulaires Symfony2 - Cas pratiques et explicationsFormulaires Symfony2 - Cas pratiques et explications
Formulaires Symfony2 - Cas pratiques et explications
 
Les Streams de Java 8
Les Streams de Java 8Les Streams de Java 8
Les Streams de Java 8
 
Introduction à Symfony
Introduction à SymfonyIntroduction à Symfony
Introduction à Symfony
 
PHP 7 et Symfony 3
PHP 7 et Symfony 3PHP 7 et Symfony 3
PHP 7 et Symfony 3
 
Notions de base de JavaScript
Notions de base de JavaScriptNotions de base de JavaScript
Notions de base de JavaScript
 
Php 2 - Approfondissement MySQL, PDO et MVC
Php 2 - Approfondissement MySQL, PDO et MVCPhp 2 - Approfondissement MySQL, PDO et MVC
Php 2 - Approfondissement MySQL, PDO et MVC
 
Javascript : fondamentaux et OOP
Javascript : fondamentaux et OOPJavascript : fondamentaux et OOP
Javascript : fondamentaux et OOP
 
Requêtes HTTP synchrones et asynchrones
Requêtes HTTPsynchrones et asynchronesRequêtes HTTPsynchrones et asynchrones
Requêtes HTTP synchrones et asynchrones
 
Softshake 2013 Apiness SA l'envers du décor
Softshake 2013 Apiness SA l'envers du décorSoftshake 2013 Apiness SA l'envers du décor
Softshake 2013 Apiness SA l'envers du décor
 
Introduction à JavaScript
Introduction à JavaScriptIntroduction à JavaScript
Introduction à JavaScript
 
Php mysql cours
Php mysql coursPhp mysql cours
Php mysql cours
 
Introduction à ajax
Introduction à ajaxIntroduction à ajax
Introduction à ajax
 
Introduction à JavaScript
Introduction à JavaScriptIntroduction à JavaScript
Introduction à JavaScript
 
Bases de PHP - Partie 1
Bases de PHP - Partie 1Bases de PHP - Partie 1
Bases de PHP - Partie 1
 
Trucs et astuces PHP et MySQL
Trucs et astuces PHP et MySQLTrucs et astuces PHP et MySQL
Trucs et astuces PHP et MySQL
 
Les structures de données PHP5
Les structures de données PHP5Les structures de données PHP5
Les structures de données PHP5
 
Introduction à React
Introduction à ReactIntroduction à React
Introduction à React
 
Python avancé : Classe et objet
Python avancé : Classe et objetPython avancé : Classe et objet
Python avancé : Classe et objet
 
Marzouk une introduction à jdbc
Marzouk une introduction à jdbcMarzouk une introduction à jdbc
Marzouk une introduction à jdbc
 

Similaire à Modèle de domaine riche dans une application métier complexe un exemple pratique

Android ORMLite
Android   ORMLiteAndroid   ORMLite
Android ORMLite
Franck SIMON
 
Atelier18
Atelier18 Atelier18
Atelier18
ChaimaaKibal
 
SSL 2011 : Présentation de 2 bases noSQL
SSL 2011 : Présentation de 2 bases noSQLSSL 2011 : Présentation de 2 bases noSQL
SSL 2011 : Présentation de 2 bases noSQL
Hervé Leclerc
 
Linq et Entity framework
Linq et Entity frameworkLinq et Entity framework
Linq et Entity framework
DNG Consulting
 
22410B_04.pptx bdsbsdhbsbdhjbhjdsbhbhbdsh
22410B_04.pptx bdsbsdhbsbdhjbhjdsbhbhbdsh22410B_04.pptx bdsbsdhbsbdhjbhjdsbhbhbdsh
22410B_04.pptx bdsbsdhbsbdhjbhjdsbhbhbdsh
khalidkabbad2
 
S2-02-PHP-objet.pptx
S2-02-PHP-objet.pptxS2-02-PHP-objet.pptx
S2-02-PHP-objet.pptx
kohay75604
 
programmation orienté objet c++
programmation orienté objet c++programmation orienté objet c++
programmation orienté objet c++
coursuniv
 
Ecriture de classes javascript
Ecriture de classes javascriptEcriture de classes javascript
Ecriture de classes javascript
Thierry Gayet
 
Workshop spring session 2 - La persistance au sein des applications Java
Workshop spring   session 2 - La persistance au sein des applications JavaWorkshop spring   session 2 - La persistance au sein des applications Java
Workshop spring session 2 - La persistance au sein des applications JavaAntoine Rey
 
Optimisation et administration avancée d’Active Directory - Par Thierry Deman
Optimisation et administration avancée d’Active Directory - Par Thierry DemanOptimisation et administration avancée d’Active Directory - Par Thierry Deman
Optimisation et administration avancée d’Active Directory - Par Thierry Deman
Identity Days
 
Les bonnes pratiques de l'architecture en général
Les bonnes pratiques de l'architecture en généralLes bonnes pratiques de l'architecture en général
Les bonnes pratiques de l'architecture en généralGeoffrey Bachelet
 
Hibernate
HibernateHibernate
Hibernate
Maher Megadmini
 
Cours de C++, en français, 2002 - Cours 3.5
Cours de C++, en français, 2002 - Cours 3.5Cours de C++, en français, 2002 - Cours 3.5
Cours de C++, en français, 2002 - Cours 3.5
Laurent BUNIET
 
Collab365 - Office 365 API & PowerShell : Le meilleur des deux mondes!
Collab365 - Office 365 API & PowerShell : Le meilleur des deux mondes!Collab365 - Office 365 API & PowerShell : Le meilleur des deux mondes!
Collab365 - Office 365 API & PowerShell : Le meilleur des deux mondes!
Sébastien Levert
 
Open close principle, on a dit étendre, pas extends !
Open close principle, on a dit étendre, pas extends !Open close principle, on a dit étendre, pas extends !
Open close principle, on a dit étendre, pas extends !
Engineor
 
Doctrine en dehors des sentiers battus
Doctrine en dehors des sentiers battusDoctrine en dehors des sentiers battus
Doctrine en dehors des sentiers battus
Romaric Drigon
 
Design patterns
Design patternsDesign patterns
Design patterns
Dorra BARTAGUIZ
 
Scala : programmation fonctionnelle
Scala : programmation fonctionnelleScala : programmation fonctionnelle
Scala : programmation fonctionnelle
MICHRAFY MUSTAFA
 

Similaire à Modèle de domaine riche dans une application métier complexe un exemple pratique (20)

Android ORMLite
Android   ORMLiteAndroid   ORMLite
Android ORMLite
 
22410 b 04
22410 b 0422410 b 04
22410 b 04
 
Atelier18
Atelier18 Atelier18
Atelier18
 
SSL 2011 : Présentation de 2 bases noSQL
SSL 2011 : Présentation de 2 bases noSQLSSL 2011 : Présentation de 2 bases noSQL
SSL 2011 : Présentation de 2 bases noSQL
 
Linq et Entity framework
Linq et Entity frameworkLinq et Entity framework
Linq et Entity framework
 
Presentation JPA
Presentation JPAPresentation JPA
Presentation JPA
 
22410B_04.pptx bdsbsdhbsbdhjbhjdsbhbhbdsh
22410B_04.pptx bdsbsdhbsbdhjbhjdsbhbhbdsh22410B_04.pptx bdsbsdhbsbdhjbhjdsbhbhbdsh
22410B_04.pptx bdsbsdhbsbdhjbhjdsbhbhbdsh
 
S2-02-PHP-objet.pptx
S2-02-PHP-objet.pptxS2-02-PHP-objet.pptx
S2-02-PHP-objet.pptx
 
programmation orienté objet c++
programmation orienté objet c++programmation orienté objet c++
programmation orienté objet c++
 
Ecriture de classes javascript
Ecriture de classes javascriptEcriture de classes javascript
Ecriture de classes javascript
 
Workshop spring session 2 - La persistance au sein des applications Java
Workshop spring   session 2 - La persistance au sein des applications JavaWorkshop spring   session 2 - La persistance au sein des applications Java
Workshop spring session 2 - La persistance au sein des applications Java
 
Optimisation et administration avancée d’Active Directory - Par Thierry Deman
Optimisation et administration avancée d’Active Directory - Par Thierry DemanOptimisation et administration avancée d’Active Directory - Par Thierry Deman
Optimisation et administration avancée d’Active Directory - Par Thierry Deman
 
Les bonnes pratiques de l'architecture en général
Les bonnes pratiques de l'architecture en généralLes bonnes pratiques de l'architecture en général
Les bonnes pratiques de l'architecture en général
 
Hibernate
HibernateHibernate
Hibernate
 
Cours de C++, en français, 2002 - Cours 3.5
Cours de C++, en français, 2002 - Cours 3.5Cours de C++, en français, 2002 - Cours 3.5
Cours de C++, en français, 2002 - Cours 3.5
 
Collab365 - Office 365 API & PowerShell : Le meilleur des deux mondes!
Collab365 - Office 365 API & PowerShell : Le meilleur des deux mondes!Collab365 - Office 365 API & PowerShell : Le meilleur des deux mondes!
Collab365 - Office 365 API & PowerShell : Le meilleur des deux mondes!
 
Open close principle, on a dit étendre, pas extends !
Open close principle, on a dit étendre, pas extends !Open close principle, on a dit étendre, pas extends !
Open close principle, on a dit étendre, pas extends !
 
Doctrine en dehors des sentiers battus
Doctrine en dehors des sentiers battusDoctrine en dehors des sentiers battus
Doctrine en dehors des sentiers battus
 
Design patterns
Design patternsDesign patterns
Design patterns
 
Scala : programmation fonctionnelle
Scala : programmation fonctionnelleScala : programmation fonctionnelle
Scala : programmation fonctionnelle
 

Plus de Vladyslav Riabchenko

SOLID: the core principles of success of the Symfony web framework and of you...
SOLID: the core principles of success of the Symfony web framework and of you...SOLID: the core principles of success of the Symfony web framework and of you...
SOLID: the core principles of success of the Symfony web framework and of you...
Vladyslav Riabchenko
 
Sécurisation de vos applications web à l’aide du composant Security de Symfony
Sécurisation de vos applications web  à l’aide du composant Security de SymfonySécurisation de vos applications web  à l’aide du composant Security de Symfony
Sécurisation de vos applications web à l’aide du composant Security de Symfony
Vladyslav Riabchenko
 
Sécurisation de vos applications web à l’aide du composant Security de Symfony
Sécurisation de vos applications web à l’aide du composant Security de SymfonySécurisation de vos applications web à l’aide du composant Security de Symfony
Sécurisation de vos applications web à l’aide du composant Security de Symfony
Vladyslav Riabchenko
 
Git
GitGit
Versionning sémantique et Composer
Versionning sémantique et ComposerVersionning sémantique et Composer
Versionning sémantique et Composer
Vladyslav Riabchenko
 
Injection de dépendances dans Symfony >= 3.3
Injection de dépendances dans Symfony >= 3.3Injection de dépendances dans Symfony >= 3.3
Injection de dépendances dans Symfony >= 3.3
Vladyslav Riabchenko
 
Les patrons de conception du composant Form
Les patrons de conception du composant FormLes patrons de conception du composant Form
Les patrons de conception du composant Form
Vladyslav Riabchenko
 

Plus de Vladyslav Riabchenko (7)

SOLID: the core principles of success of the Symfony web framework and of you...
SOLID: the core principles of success of the Symfony web framework and of you...SOLID: the core principles of success of the Symfony web framework and of you...
SOLID: the core principles of success of the Symfony web framework and of you...
 
Sécurisation de vos applications web à l’aide du composant Security de Symfony
Sécurisation de vos applications web  à l’aide du composant Security de SymfonySécurisation de vos applications web  à l’aide du composant Security de Symfony
Sécurisation de vos applications web à l’aide du composant Security de Symfony
 
Sécurisation de vos applications web à l’aide du composant Security de Symfony
Sécurisation de vos applications web à l’aide du composant Security de SymfonySécurisation de vos applications web à l’aide du composant Security de Symfony
Sécurisation de vos applications web à l’aide du composant Security de Symfony
 
Git
GitGit
Git
 
Versionning sémantique et Composer
Versionning sémantique et ComposerVersionning sémantique et Composer
Versionning sémantique et Composer
 
Injection de dépendances dans Symfony >= 3.3
Injection de dépendances dans Symfony >= 3.3Injection de dépendances dans Symfony >= 3.3
Injection de dépendances dans Symfony >= 3.3
 
Les patrons de conception du composant Form
Les patrons de conception du composant FormLes patrons de conception du composant Form
Les patrons de conception du composant Form
 

Modèle de domaine riche dans une application métier complexe un exemple pratique

  • 1. Modèle de domaine riche dans une application métier complexe un exemple pratique
  • 2. https://vria.eu contact@vria.eu https://twitter.com/RiaVlad RIABCHENKO Vladyslav 5+ ans full stack web-développeur Certifié Symfony Architecte technique à Webnet
  • 3. 3 Modèle anémique et modèle riche Architecture Fonctionnalité à développer Entités, Repositories, Fabriques Objet-valeurs, Agrégats Services, Exceptions Plan
  • 4. Un modèle anémique et un modèle riche
  • 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
  • 13. Repositories Domaine Services Application Infrastructure RepositoryInterface Domain dépend seulement de repositories Couche Infrastructure dépend de couche domaine Architecture
  • 14. 14 Layered architecture Onion architecture Hexagonal architecture CQRS + event sourcing ... MVC, MVVM, MV-Whatever Architecture
  • 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é
  • 23. Repository 23 AppDomain Personne - email - nom - personneRepository + __construct($email, $nom, $personneRepository) + getEmail() + getNom() PersonneRepositoryInterface + emailAlreadyExist(string $email): bool + save(Personne $personne): void + get(string $email): Personne + getAllInfo(): array AppInfrastructureDoctrineRepository PersonneRepository
  • 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
  • 29. Fabrique & Repository 29 Contrôleurs et commandes Personne PersonneFactory PersonneRepositoryInterface PersonneRepository PersonneCreerDTO Personne
  • 30. DTO 30 namespace AppApplicationDTO; use AppDomainPersonne; use SymfonyComponentValidatorConstraints as Assert; class PersonneCreerDTO { /** * @AssertNotBlank() * @AssertEmail() */ public $email; /** * @AssertNotBlank() */ public $nom; }
  • 31. class PersonneController { /** * @param Request $request * @param FormFactoryInterface $formFactory * @param UrlGeneratorInterface $urlGenerator * @param PersonneRepositoryInterface $personneRepository */ public function creer(...) { $creerPersonneDTO = new PersonneCreerDTO(); $form = $formFactory->create(PersonneCreerType::class, $creerPersonneDTO); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { try { $personneFactory = new PersonneFactory($personneRepository); $personne = $personneFactory->create($creerPersonneDTO); $personneRepository->save($personne); return new RedirectResponse($urlGenerator->generate('personne_lister')); } catch (PersonneEmailAlreadyTakenException $e) { $form->get('email')->addError(new FormError($e->getMessage())); } } return ['form' => $form->createView()]; } } 31 DTO
  • 32. 32 namespace AppApplicationCommand; class PersonneCreerCommand extends Command { protected static $defaultName = 'app:personne:creer'; /** @var ValidatorInterface */ private $validator; /** @var PersonneRepositoryInterface */ private $personneRepository; /** {@inheritdoc} */ protected function configure() { $this->setDescription('Créer une nouvelle personne.') ->addArgument('email', InputArgument::REQUIRED) ->addArgument('nom', InputArgument::REQUIRED) ; } // ... DTO
  • 33. protected function execute(InputInterface $input, OutputInterface $output) { // Construire DTO. $creerPersonneDTO = new PersonneCreerDTO(); $creerPersonneDTO->email = $input->getArgument('email'); $creerPersonneDTO->nom = $input->getArgument('nom'); // Valider la saisie de l'utilisateur. $constraintViolationList = $this->validator->validate($creerPersonneDTO); if ($constraintViolationList->count() > 0) { foreach ($constraintViolationList as $violation) { $output->writeln( sprintf('<error>%s: %s</error>', $violation->getPropertyPath(), $violation->getMessage()) ); } return; } try { // Créer une personne. $personneFactory = new PersonneFactory($this->personneRepository); $personne = $personneFactory->create($creerPersonneDTO); $this->personneRepository->save($personne); $output->writeln('<info>Personne a été créée avec succès.</info>'); } catch (PersonneEmailAlreadyTakenException $e) { $output->writeln(sprintf('<error>%s</error>', $e->getMessage())); } } 33 DTO
  • 34. 34 /** * @param string $email * @param Request $request * @param FormFactoryInterface $formFactory * @param UrlGeneratorInterface $urlGenerator * @param PersonneRepositoryInterface $personneRepository */ public function modifier(...) { try { $personne = $personneRepository->get($email); } catch (PersonneNotFoundException $e) { throw new NotFoundHttpException($e->getMessage(), $e); } $form = $formFactory->create(PersonneModifierType::class, $personne); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $personneRepository->save($personne); return new RedirectResponse($urlGenerator->generate('personne_lister')); } return ['form' => $form->createView()]; } sans DTO Formulaire
  • 35. 35 namespace AppApplicationForm; class PersonneModifierType extends AbstractType implements DataMapperInterface { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('email', EmailType::class, [ 'disabled' => true, ]) ->add('nom', TextType::class, [ 'constraints' => [ new AssertNotBlank(), ], ]) ->setDataMapper($this); } public function mapDataToForms($personne, $forms) { //... } public function mapFormsToData($forms, &$personne) { //... } } Personne::renommer() au lieu d’utiliser les setters Formulaire
  • 36. 36 public function mapDataToForms($personne, $forms) { /** @var FormInterface[] $forms */ $forms = iterator_to_array($forms); /* @var Personne $personne */ $forms['email']->setData($personne->getEmail()); $forms['nom']->setData($personne->getNom()); } public function mapFormsToData($forms, &$personne) { /** @var FormInterface[] $forms */ $forms = iterator_to_array($forms); /* @var Personne $personne */ if ($nom = $forms['nom']->getData()) { $personne->update($nom); } } SymfonyLive London 2018 - Christopher Hertel & Christian Flothmann - Using Symfony Forms Formulaire
  • 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
  • 40. AppDomain 40 Personne - email - nom - personneRepository ... Absence - id: int - personne: Personne - type: AbsenceType - debut: DateTime - fin: DateTime AbsenceType - type: int [MALADIE, CONGES_PAYES, ...] + __construct($type) + getType(): int + getLabel(): string + isEqualTo(AbsenceType $absenceType) ... Objets-valeurs
  • 41. 41 AppDomainAbsence: type: entity id: id: type: integer generator: strategy: AUTO embedded: type: class: AppDomainAbsenceType fields: debut: type: datetime_immutable fin: type: datetime_immutable manyToOne: personne: targetEntity: AppDomainPersonne joinColumn: name: personne_email referencedColumnName: email AppDomainAbsenceType: type: embeddable fields: type: type: integer Objets-valeurs
  • 42. Agrégats 42 Personne: Rick + getEmail(): string + getNom(): string + renommer($nom) + deposerAbsence(AbsenceDeposerDTO $dto) + modifierAbsence(AbsenceModifierDTO $dto) + annulerAbsence($id) + getAbsences($startPeriod, $endPeriod) Congé payé 07/05/2019 Maladie 11/04/2019 ... Controller … … … …
  • 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
  • 47. 47 Personne Absences - personne + deposerAbsence(...) + modifierAbsence(...) + annulerAbsence(...) + getAbsences(...) - absences bidirectionnel Liaison
  • 48. AppDomain 48 unidirectionnel Personne Absences - personne + deposerAbsence(...) + modifierAbsence(...) + annulerAbsence(...) + getAbsences(...) - absenceRepository PersonneRepositoryInterface + save(Absence $absence): void + getAbsence($personne, $id): Absence + getAbsences($personne, $startPeriod, $endPeriod): Absences[] + absenceDeposeDansPeriode($personne, $debut, $fin): bool AppInfrastructureDoctrineRepository AbsenceRepository 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
  • 50. Agrégat 50 AppDomainDTO AbsenceDeposerDTO + type + debut + fin + __construct() Form Command Controller + Personne + deposerAbsence($dto) Validation simple Valeurs initiales Validation métier
  • 51. AppDomainDTO 51 Form Command Controller + Personne + modifierAbsence($dto) AbsenceModifierDTO + type + debut + fin - id + fromAbsence($absence): self + getId() Agrégat
  • 53. Compteurs 53 Enregistrer de jours de congé restants Jours travaillés rapportent des jours de congé Contrôle de jours restants
  • 54. 54 Personne ... - absenceRepository - compteurs: AbsenceCompteur[] - compteurService AbsenceCompteur - id - personne: Personne - type - joursDisponibles - joursTravailles + __construct($personne, $type) + incrementerJoursTravailles() + deposerAbsence($jours) + annulerAbsence($jours) ... + deposerAbsence(...) + modifierAbsence(...) + annulerAbsence($id) + reinitialiserComptuers(...) + incrementerJoursTravailles(...) + getCompteursInfo() … … … Compteurs
  • 55. 55 Personne ... - absenceRepository - compteurs: AbsenceCompteur[] - compteurService AbsenceCompteur ... + deposerAbsence(...) + modifierAbsence(...) + annulerAbsence($id) + reinitialiserComptuers(...) + incrementerJoursTravailles(...) + getCompteursInfo() AbsenceCompteur - absenceRepository + init(): AbsenceCompteur[] + incrementerJoursTravailles(...) + deposerAbsence(...) + modifierAbsence(...) + ... Services
  • 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
  • 60. Merci pour votre attention https://vria.eu contact@vria.eu https://twitter.com/RiaVlad https://webnet.fr https://github.com/vria/symfony-rich-domain-model

Notes de l'éditeur

  1. z