1. Zehreen Soogun / zehreen@extension-interactive.com
Jihad Sahebdin / jihad@extension-interactive.com
S y m f o n y 3
31/03/2017
C Q R S e t E v e n t s o u r c i n g
2. 2
Who Am I
SOOGUN Bibi Zehreen
¬ Chef de Projet Technique
¬ Extension Interactive
Je suis #Zehreen
Find me on :-
https://twitter.com/bzehreens
https://mu.linkedin.com/in/bibi-zehreen-soogun-53115190
3. 3
Speakers / Présentation intervenants
Zehreen
¬ CQRS
¬ ES
Jihad
¬ Cas réel
¬ MEP CQRS et ES dans un projet SF 3.
6. 6
CQRS / Command
Contexte boutique : Je valide ma commande
Contexte Symfony : Je lance une commande via console
¬ php bin/console c:c
7. 7
CQRS / Command
Nom d’une tâche
¬ Temps impératif
Données requises pour effectuer la tâche
a.k.a
¬ Mutators
¬ Modifiers
Contrast avec CRUD
¬ Point de données
openCustomerAccountCommand
8. 8
CQRS / Query
Types de requêtes classiques
¬ Insert
¬ Select
¬ Update
¬ Delete
ReadOnly
¬ Récupération des données
Constraste avec CRUD
¬ TOUTES les opérations VS Lecture seule
9. 9
CQRS & CQS
Similarités
¬ Utilisation commandes (modification) et requêtes (récupération)
Différence
¬ Write Model
¬ Read Model
19. 19
Aperçu
Application de type API/UI
Enregistrement dans 2 bases de données
¬ Write model
¬ Read model
La UI envoie des requêtes sur l’API et récupère les
données depuis Firebase
UI
API Firebase
20. 20
Le Domain Model
Système de gestion:
Une business unit emploie des salaries qui vont travailler
sur des contrats liés à des clients
Business Units
Salariés Contrats
Clients
23. 23
Bounded context
Le Domain Model s’organise en morceaux
Chaque morceau correspond à:
¬ un métier
¬ Un composant métier
¬ Une problématique métier qu’il propose de solutionner
Morceau = Bounded Context
Exemple:
¬ Un context RH qui s’occupe de gérer les employés
¬ Un context Contrat qui s’occupe de gérer ses clients et ses
ressources
24. 24
Bounded context / Composants autonomes
Un bounded context peut être composé d’un ou de
plusieurs composants autonomes.
Les composants apportent une solution aux
problématiques identifiés dans un bounded context.
Exemple:
¬ On va s’occuper de la gestion des business units dans le
composant Business Units management
25. 25
Bounded context / Composants autonomes
3 bounded contexts:
¬ Business units
¬ Human Resource
¬ Contracts
Business Unit
Human
Resource
Contract
26. 26
Bounded context / Composants autonomes
Fonctionnement
¬ Un composant reçoit des messages en provenance du
MessageBus:
- Soit par l’intermédiaire de ses CommandHandlers s’il s’agit de
commandes
- Soit par l’intermédiaire de ses ProcessManagers s’il s’agit
d’évènements
28. 28
Commands
Commands:
¬ Les ordres données au Domain Model
¬ Sources diverses:
- Application web
- Application mobile
- Ligne de commandes
¬ Format json
¬ Construction d’un objet Command à partir des données
Domain
Model
Application
Web
Application
mobile
Ligne de
commande
29. 29
Commands
Exemple:
¬ La classe HireEmployeeCommand
class HireEmployeeCommand
{
public $employeeId;
public $employerId;
public $employeeFirstName;
public $employeeLastName;
public $employeeCivility;
public $employeeJobTitle;
public $employeeEmail;
public $requestedBy;
}
30. 30
Commands
Controller:
¬ HRManagementController
- hireEmployeeAction
use pathtoCommandingCommandsHireEmployeeCommand;
class HRManagementController extends Controller
{
/**
* @Route("/hire-employee", name="hire_employee")
*/
public function hireEmployeeAction(Request $request)
{
$commandBus = $this->get('command_bus');
$hireEmployeeCommand = new HireEmployeeCommand();
//Populate command data
$commandBus->handle($hireEmployeeCommand);
return new Response("Employee hired");
}
}
33. 33
Command Handlers
Reconstruction de l’agrégat à partir de son historique
Enregistrement des évènements
public function handle(HireEmployeeCommand $command)
{
$employeeId = $command->employeeId;
$history = $this->eventStore->getHistoryFor($employeeId);
$employeeAggregate = employeeAggregate::reconstituteFrom($employeeId, $history);
34. 34
Command Handlers
On passe les données à l’agrégat
$employeeAggregate->hireEmployee(
$command->employeeId,
$command->employerId,
$command->employeeFirstName,
$command->employeeLastName,
$command->employeeCivility,
$command->employeeJobTitle,
$command->employeeEmail,
$command->requestedBy,
$command->requestedOn
);
35. 35
Command Handlers
Enregistrement des évènements
Dispatch de l’évènement
foreach ($events as $event) {
$this->eventBus->handle($event);
}
$events = $this->eventStore->save($employeeAggregate);
36. 36
Agrégats
Contient la logique du Domain model
Validation sémantique des commandes
Reçoit les commandes du MessageBus
Emet un ou plusieurs évènements relatant l’opération
qu’il vient d’executer
¬ employeeHiredEvent
37. 37
Agrégats
Validation des commandes
¬ Un agrégat peut réfuser d’éxécuter une commande
- Exemple:
› Par exemple, si HRManagementAggregate reçoit la commande
HireEmployeeCommand pour une BU donnée, si la BU n’existe pas, il doit
renvoyer une exception
class HRManagementAggregate
{
public function hireEmployee($employeeId, $employerId, $employeeFirstName, $employeeLastName)
{
$this->isBusinessUnitIdValid($employerId);
$this->recordThat(new EmployeeHiredEvent($employeeId, $employerId, $employeeFirstName,
$employeeLastName));
}
private function isBusinessUnitIdValid($businessUnit)
{
if (!$businessUnit instanceof BusinessUnit) {
throw new BusinessUnitIdIsNotValidException('This business Unit is not valid');
}
return;
}
}
38. 38
Domain Events
Domain Events
¬ Les évènements métiers produits par les aggrégats
¬ Exemple:
- L’évènement EmployeeHiredEvent émis par l’agrégat
HRManagementAggregate
namespace pathtoAggregatesDomainEvents;
class EmployeeHiredEvent
{
/**
* @Type("string")
*/
private $employeeId;
public function __construct($employeeId)
{
$this->employeeId = $employeeId;
}
public function getEmployeeId()
{
return $this->employeeId;
}
}
39. 39
Bounded context / Récapitulatif
Récapitulatif:
¬ On a donc réussi à construire une commande à partir d’une
requête venant de l’extérieur
¬ Le command handler s’est chargé de le communiquer à
l’agrégat
¬ L’agrégat valide la commande, l’éxécute et génère un
évènement
¬ L’évènement est stocké dans l’event store puis dispatché a
l’event bus
40. 40
Bounded context / Suite
L’évènement peut alors être:
¬ Propagé dans la UI par le biais des denormalizers
¬ Capté par les process managers.
44. 44
Firebase
Avantages
¬ L’authentification est gérée ‘in-built’ par Google
¬ Base de données en temps réel
¬ Firebase et Angular JS
¬ Permet de déployer une application (UI) par ligne de commande
- > firebase deploy
Inconvenients
¬ On fait confiance à Google concernant la securité des données
46. 46
Firebase
Utilisation dans symfony
¬ Evènement EmployeeHiredEvent
class UpdateEmployeesListDenormalizer
{
public function notify(EmployeeHiredEvent $event)
{
$path = '/users/list/'.$event->getEmployeeId();
$firebase->set($path.'/buId', $event->getEmployerId());
$firebase->set($path.'/id', $event->getEmployeeId());
$firebase->set($path.'/civility', $event->getEmployeeCivility());
$firebase->set($path.'/email', $event->getEmployeeEmail());
$firebase->set($path.'/firstname', $event->getEmployeeFirstName());
$firebase->set($path.'/lastname', $event->getEmployeeLastName());
$firebase->set($path.'/job_title', $event->getEmployeeJobTitle());
}
}
47. 47
Firebase
Utilisation dans symfony
¬ Evènement EmployeeHiredEvent
¬ Service
- Un denormalizer va souscrire à l’évènement
update_employees_list_denormalizer:
class: pathtoDenormalizersUpdateEmployeesListDenormalizer
tags:
- { name: event_subscriber,
subscribes_to: pathtoAggregatesDomainEventsEmployeeHiredEvent,
method: notify }
48. 48
Process Managers
a.k.a Saga
Ecoute des évènements et dispatch de nouvelles
commandes
Utile pour coordonner les actions entre les bounded
contexts
Exemple
¬ HRManagement et BusinessUnitManagement
¬ Combien de salariés possède une BU?
- Informer l’agrégat BU pour qu’il incrémente/décrémente son
nombre de salariés à chaque fois qu’une personne est embauche
pour cette BU.
- Solution: Création d’un service qui souscrit à l’évènement
EmployeeHiredEvent
49. 49
Process Managers
Exemple (suite)
¬ ChangeNumberOfEmployeesInBusinessUnitCommand
¬ AddEmployeeToBusinessUnitSaga
class changeNumberOfEmployeesInBusinessUnitCommand
{
public $businessUnitId;
}
class AddEmployeeToBusinessUnitSaga
{
public function notify(EmployeeHiredEvent $event)
{
$command = new changeNumberOfEmployeesInBusinessUnitCommand();
$command->businessUnitId = $event->getEmployerId();
$this->commandBus->handle($command);
}
}
50. 50
Conclusion
Complexe à mettre en oeuvre
Difficile à faire en CRUD
¬ Opérations de lecture et d’écriture sont dissosiées
¬ Pas de conflits dans le cas où plusieurs utilisateurs mettent à jour
le même objet
¬ Enregistrement des actions et des données sur une entité
¬ Historique des évènements possible
¬ Restauration du read model à partir du write model
+ Restauration à partir d’un évènement
¬ Séparation de l’implementation plus facile
52. 52
Bonnes pratiques
Le nom des méthodes est important
¬ Le nom doit reflécter au maximum l’objectif ce que
l’utilisateur veut faire
¬ Exemple:
- HireEmployeeCommand
- EmployeeHiredEvent
- EmployeeLastNameChanged
- ContractTransferredToOtherDirectorEvent
- RessourceAddedToContractEvent
- AccountUpdated / AccountModified
Les commandes sont au présent, les évènements sont
au passé
53. 53
Bonnes pratiques
Un évènement ne peut/doit pas être modifié
¬ Pas de setters dans les classes Event
Un évènement est idempotent
¬ Exemple:
- FireEmployeeCommand sur une même personne n fois
- L’état de l’agrégat ne changera pas après la première fois
- L’agrégat de contentera d’ignorer la commande (et ne pas
renvoyer d’exception)
Valider les commandes
54. 54
Bonnes pratiques
Les bounded contexts doivent être identifiés dès le
début
¬ Division du problème initial en sous-problèmes (bounded
contexts)
¬ Division des bounded contexts en composants autonomes
Identifier le langage du domain expert
Personne
Employé
(RH)
Ressource
(Contrat)
Collaborateur
(Business Unit)
56. 56
Sécurité
Restriction sur la méthode
¬ Autoriser uniquement les POST
¬ MethodListener
app.method_listener:
class: AppBundleListenerMethodListener
arguments: ['POST']
tags:
- { name: kernel.event_listener,
event: kernel.controller,
method: onKernelController,
}
57. 57
Sécurité
Restriction sur la méthode (suite)
¬ Autoriser uniquement les POST
¬ onKernelController
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if ($controller[0] instanceof Controller){
$request = $event->getRequest()->getMethod();
if (!in_array($request, $this->method)){
$event->stopPropagation();
throw new MethodNotAllowedHttpException(
array('POST'),
'Method Not Allowed‘
);
}
}
}
58. 58
Sécurité
Authentifier les requêtes grâce aux headers
d’authentificatiom
JWT = Json Web Token
¬ Encodage de la requête et insertion dans le header
¬ Composé de troix parties
- header
- payload
- signature
63. 63
Sécurité / Nonce
Nonce = Number used ONCE
Insertion dans la requête pour l’encodage
Vérification de l’unicité du nonce avec les nonces déjà
utilisés
66. 66
Sécurité / CORS
Modification de la request
¬ Dans un event listener: onKernelRequest
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($request->headers->has("Access-Control-Request-Headers")
&& $request->headers->has("Access-Control-Request-Method"))
{
$response = new Response();
//enable CORS - return the requested methods as allowed
$response->headers->add(
array(
'Access-Control-Allow-Headers' => $request->headers->get("Access-
Control-Request-Headers"),
'Access-Control-Allow-Methods' => $request->headers->get("Access-
Control-Request-Method"),
'Access-Control-Allow-Origin' => '*'));
$event->setResponse($response);
}
67. 67
Un mot sur la UI
Données stockés sur Firebase
¬ Correspondance entre l’arborescence des données et les urls
(pages) de la UI
¬ Utilisation d’un resolve avant le chargement de la page
resolve: {
listEmployees : function(firebaseService) {
return firebaseService.get('/users/list');
},
Introduction / C’est quoi
Avantages / Pourquoi l’utiliser
Désavantages / Pourquoi ne pas l’utiliser
Utilisation / Quand l’utiliser
Case Study par Jihad
CQRS
C’est un concept intéressant qui a la responsabilité de séparer la commande et la requête.
- Pioneer
Greg Young - premier à avoir parlé sur le CQRS
Origine
CQRS applique le principe CQS, dévéloppé par Bertrand Meyer. Une méthode doit être soit une commande ou une requête
C’est quoi une commande ?
Dans un context boutique, on utilise ce mot quand on valide notre commande, par exemple. Dans le context d’un projet Symfony, on lance des commande, par ex. Pour supprimer le cache on lancer un php bin/console cache:clear
Dans le context CQRS, une commande c’est un objet simple avec le nom d’une operation et les données qu’on aura besoin pour effectuer l’opération.
A noter que le nom de la commande doit toujours être dans le temps impératif. Ceci montre que l’application peut valider ou refuser la commande.
Une commande est aussi connu comme des “mutators” / “modifiers” car cela va modifier l’état d’un système.
En comparaison avec l’architecture classqiue, une commande renvoi uniquement les données requises pour traiter la tache tandis que dans le CRUD, on envoie toutes les données de l’objet
Ex, openCust….
Quand on parle de requête, très souvent on fait référence aux mots comme SELECT, INSERT, UPDATE, DELETE.
Dans le context de CQRS, une requête renvoi des résultats et à aucun moment, la requête peut modifier l’état du système.
En comparison avec l’architecture classique, une requête fera uniquement la lecture dans la BdD tandis que dans le CRUD, une requête peut faire toutes les operations (sauvegarde / mise à jour / récupération des informations)
CQRS applique la notion de CQS en utilisant des objets Command et Query séparés pour modifier et récupérer les données respectivement.
CQRS utilise un modèle différent pour mettre à jour les informations et un autre modèle pour lire les informations, ce qu’on appelle le Write Model et le Read Model
Voici quelques points forts de ce design pattern :
- Performance
On gagne plus en performance. En general, on fait plus de lecture dans une BdD que d’écriture. Et souvent des fois, la volumétrie des données renvoyées est conséquente. Du point de vue utilisateur, le temps d’atttente de réponse devrait être minime. Le fait qu’on a dissocié le Read Model permet d’avoir un temps de chargement rapide.
Evolution
On a plus de flexibilité pour faire évoluer le système
Dans le cas ou on applique CQRS sur un domaine qui ne correspond pas, on rajoute plus de compléxité et de risques et conséquemment cela va réduire la productivité.
Quand peut-on utiliser CQRS ?
CQRS sera plus approprié quand on a un système collaboratif ou les utilisateurs peuvent opérer sur les données simultanément
Dans le cas ou on a un gros système qui requiert une haute performance
Un système complèxe qu’on peut decomposer en plusieurs morceaux autonomes
Jihad reviendra la dessus avec plus de détail
Un exemple serait un système bancaire. On a beaucoup de transactions, beaucoup de données qui seront impactées et tous les utilisateurs devraient avoir données synchronisées et non des données obsoletes.
Un point à faire ressortir, c’est qu’on peut utiliser CQRS dans une partie de notre système et pas necessaire de l’appliquer sur l’ensemble de l’application.
Dans les cas suivants, on evite d’utiliser CQRS
Séparer la Read et la Write Model est un processus couteux en terme de prix et de temps.
En terme de cout, l’implementation de CQRS dans un projet doit être égale ou inférieure à celui d’un projet classique
Il fauut prendre du temps pour sortir d’un projet classique en CRUD pour y aller sur du Domain Driven Design.
Reflechir en tant que domaine et non en tant que BdD est une étape très importante pour faire notre choix et pour ensuite l’appliquer
Si on arrive à bien identifier le domain, c’est qu’on peut implementer le CQRS dans notre projet
Event Sourcing par Greg Young
Event Sourcing c’est la notion de rejouer les évènements pour arriver à l’état actuel du système.
C’est quoi Un Domaine Event : On a effectué une action dans le passé et donc générer un événement. Ex. custAccOpe
On peut voir qu’on a segmenté le Select du CRUD dans le Read Model du CQRS
Le Insert et Update du CRUD dans le Write Model du CQRS
Mais qu’est ce qui se passe pour le Delete du CRUD ??? C’est simple, on ne fait pas de suppression dans le CQRS, c’est une étape supplementaire qui se rajoute.
Pour simplifier, on part donc de 3 bounded context (BU, RH, Contrats) composé chacun d’un seul composant autonome.
Commands : les ordres données au Domain Model
CommandHandlers: passe les commandes à l’agrégat
L’agrégat valide la sémantique de la commande, l’éxécute et génère un évènement
Le denormalizer reconstruit les données à partir des données de l’évènement
Le process manager lance de nouvelles commandes à partir d’un évènement
Puisque la requête arrive par un POST sur une url, on définit donc une action dans un controller.
‘command_bus’ est un service qui va faire le lien (en quelques sortes) entre la commande et son handler.
Toute la logique métier du Domain Model doit être traitée au sein des agrégats
C’est aux agrégats que les commandHandlers adressent les commandes qu’ils ont reçu du MessageBus
!!!: Pas de setters puisqu’un évènement ne peut pas être modifié, ça s’est produit, on ne peut pas revenir en arrière.