Comment migrer une application peu testée, développée avec des outils RAD, pour intégrer de nombreuses évolutions de règles métiers et par la même occasion la rendre plus compréhensible, en ayant une démarche orientée Domain Driven Design ? La réponse n’est pas si évidente.
On peut bien sûr opter pour une réécriture complète du système mais ce n’est pas sans problème. Comment continuer à délivrer des fonctionnalités ? Comment s’assurer que le nouveau système aura bien le même comportement que l’ancien ? Et surtout, combien de temps cela va t’il prendre ?
Une autre piste est de refactorer le code petit à petit, d’introduire les nouveaux concepts lorsqu’un besoin se fait sentir, sans avoir besoin de bloquer tout autre développement. C’est ce que nous avons choisi de faire pour migrer un backoffice construit autour du bundle Symfony EasyAdmin.
Dans ce retour d’expérience nous verrons comment nous avons commencé à mieux connaître les règles métiers en lisant le code et en animant des Event Storming. Nous parlerons de la manière dont nous avons réussi à créer un filet de sécurité avec des tests en nous laissant guider par le code coverage, ou encore de comment nous avons introduit graduellement les nouveaux concepts métiers à l’aide des interfaces avant de modifier la base de données. Tout au long de la conférence nous présenterons les difficultés rencontrées et ce que nous avons mis en œuvre pour les déjouer.
3. Context
Application de santé qui propose des jeux à des centres
de soins ou des entreprises
pour favoriser une activité physique.
@SelrahcD
4. Context
Une entreprise qui grandit et recrute.
4 Automatiser les tâches d'administration
chronophages
4 Faciliter la compréhension de son métier par ses
nouveaux arrivants
4 Pouvoir adapter son système rapidement
@SelrahcD
5. Le backend est développé au dessus d'EasyAdmin 2
@SelrahcD
7. class Player {
/**
* @ORMColumn(type="string")
*/
private $firstname;
public function getFirstname(): string {
return $this->firstname;
}
public function setFirstname(string $firstname): void {
$this->firstname = $firstname;
}
}
@SelrahcD
25. public function test_startIntervention(): void
{
$manager = new Player2GameManager();
$game = new Game(
new DateTimeImmutable(),
new DateTimeImmutable()
);
$player2Game = new Player2Game($game, new Player());
$player2GameResult = $manager->startIntervention($player2Game);
Approvals::verifyString($this->print($player2GameResult));
}
@SelrahcD
28. public function test_startIntervention(): void
{
$manager = new Player2GameManager();
$game = new Game(
new DateTimeImmutable(),
new DateTimeImmutable()
);
$player2Game = new Player2Game($game, new Player());
$player2GameResult = $manager->startIntervention($player2Game);
Approvals::verifyString($this->print($player2GameResult));
}
@SelrahcD
29. public function test_startIntervention(): void
{
$manager = new Player2GameManager();
$game = new Game(
new DateTimeImmutable('2017-12-18 22:14:17'),
new DateTimeImmutable('2019-10-09 06:06:16')
);
$player2Game = new Player2Game($game, new Player());
$player2GameResult = $manager->startIntervention($player2Game);
Approvals::verifyString($this->print($player2GameResult));
}
@SelrahcD
35. public function test_startIntervention(): void
{
$manager = new TestablePlayer2GameManager();
$manager->now = new DateTimeImmutable('2017-12-22 02:34:18');
$game = new Game(
new DateTimeImmutable('2017-12-18 22:14:17'),
new DateTimeImmutable('2019-10-09 06:06:16')
);
$player2Game = new Player2Game($game, new Player());
$player2GameResult = $manager->startIntervention($player2Game);
Approvals::verifyString($this->print($player2GameResult));
}
@SelrahcD
38. public function test_startIntervention_start_date_after_now(): void
{
$manager = new TestablePlayer2GameManager();
$manager->now = new DateTimeImmutable('2017-12-15 02:34:18');
$game = new Game(
new DateTimeImmutable('2017-12-18 22:14:17'),
new DateTimeImmutable('2019-10-09 06:06:16')
);
$player2Game = new Player2Game($game, new Player());
$player2GameResult = $manager->startIntervention($player2Game);
Approvals::verifyString($this->print($player2GameResult));
}
@SelrahcD
41. public function test_startIntervention_start_date_after_now(): void
{
$manager = new TestablePlayer2GameManager();
$manager->now = new DateTimeImmutable('2017-12-15 02:34:18');
$duration = 2;
$game = new Game(
new DateTimeImmutable('2017-12-18 22:14:17'),
new DateTimeImmutable('2019-10-09 06:06:16'),
$duration
);
$player2Game = new Player2Game($game, new Player());
$player2GameResult = $manager->startIntervention($player2Game);
Approvals::verifyString($this->print($player2GameResult));
}
@SelrahcD
49. class Player2GameManager {
public function startIntervention(Player2Game $player2Game): Player2Game
{
return $player2Game->startIntervention($this->getNow());
}
}
@SelrahcD
51. final class InterventionDates
{
public readonly DateTimeImmutable $startDate;
public readonly DateTimeImmutable $endDate;
public function __construct(
DateTimeImmutable $startDate,
DateTimeImmutable $endDate)
{
if($startDate > $endDate) {
throw new Exception('Start date must be before end date');
}
$this->startDate = $startDate;
$this->endDate = $endDate;
}
}
@SelrahcD
52. class Game {
public function computeInterventionDates(DateTimeImmutable $now): InterventionDates
{
$startDate = $this->startDate > $now ?
$this->startDate : $now;
$endDate = $this->duration === null ?
$this->endDate : $startDate->modify(sprintf('+ %d weeks', $this->duration));
return new InterventionDates($startDate, $endDate);
}
}
@SelrahcD
53. class Player2Game {
public function startIntervention(DateTimeImmutable $now): Player2Game
{
$interventionDates = $this->game->computeInterventionDates($now);
$this->startDate = $interventionDates->startDate;
$this->endDate = $interventionDates->endDate;
return $this;
}
}
@SelrahcD
55. class Player2Game {
public function startIntervention(DateTimeImmutable $now): Player2Game {}
public function startInitialAssessment(): Player2Game {}
public function restartOn(DateTimeImmutable $restartDate): void {}
public function validate(): void {}
public function accept(): self {}
}
@SelrahcD
56. Utilisation des interfaces
interface Registration {
public function accept(): self;
public function startIntervention(): Intervention;
public function startInitialAssessment(): Assessment;
}
interface Intervention {
public function restartOn(DateTimeImmutable $restartDate): void;
}
interface Assessment {
public function validate(): void;
}
class Player2Game implements Registration, Assessment, Intervention {}
@SelrahcD
57. class Player2GameManager {
public function startIntervention(Player2Game $player2Game): Player2Game
{
return $player2Game->startIntervention($this->getNow());
}
}
@SelrahcD
58. class Player2GameManager {
public function startIntervention(Registration $registration): Intervention
{
return $registration->startIntervention($this->getClock());
}
}
@SelrahcD
66. Tentative 3 : 3 tables, 3 entités d'écriture, 1 entitée de
lecture et 1 vue
@SelrahcD
67. Création d'une vue MySQL
CREATE VIEW view_player_2_game AS
SELECT
r.id,
r.player_id,
r.game_id,
r.is_validated,
CASE WHEN a.id IS NULL THEN 0
ELSE 1 END AS has_running_assessment,
i.start_date,
i.end_date
FROM registration r
LEFT JOIN assessement a ON a.player_id = r.player_id AND a.game_id = r.game_id AND a.validation_date IS NULL
LEFT JOIN intervention i ON i.player_id = r.player_id AND i.game_id = r.game_id
@SelrahcD
68. Création d'un entité Doctrine
/**
* @ORMEntity(readOnly=true)
* @ORMTable (name="view_player_2_game")
*/
class Player2Game {
/**
* @ORMId
* @ORMColumn(type="guid")
*/
public $id;
/**
* @ORMManyToOne(targetEntity="AppDomainEntityPlayer")
*/
public $player;
/**
* @ORMColumn(type="datetime_immutable")
*/
public $validationDate;
}
@SelrahcD
69. Utilisation d'entités uniquement pour
la vue
/**
* @ORMEntity(readOnly=true)
* @ORMTable (name="view_player_2_game")
*/
class Player2Game {
/**
* @ORMId
* @ORMColumn(type="guid")
*/
public $id;
/**
* @ORMManyToOne(targetEntity="AppUIViewModelPlayer")
*/
public $player;
/**
* @ORMColumn(type="datetime_immutable")
*/
public $validationDate;
}
@SelrahcD
70. Faites le à plusieurs !
Photo by Helena Lopes on Unsplash
@SelrahcD
74. Rollup
4 Why & How to use Doctrine Migrations Rollup? -
Olivier Dolbeau
4 Rolling up database migrations with Doctrine -
Andreas Möller
@SelrahcD
75. Doctrine et vues
4 Aggregate your data by using SQL views and
Doctrine. - Robbert Stevens
4 Separating Concerns using Embeddables - Doctrine
documentation
@SelrahcD