CODEMEAHR!CODEMEAHR!
HI,IAMHI,IAMARNAUD!ARNAUD!
ANEMICMODELANEMICMODELVERSUSVERSUSMODELRICHMODELRICH
PLAYGROUNDPLAYGROUND
Business: Human resources management
Technical: Symfony / Doctrine ORM
WHATDOWEUSUALLYDO?WHATDOWEUSUALLYDO?CRUD!CRUD!
Create Read Update Delete
WESTARTWITHCREATINGWESTARTWITHCREATINGANEMICMODELANEMICMODEL
namespace AlDomain;
final class Employee
{
/** @var Uuid */
private $id;
/** @var string */
private $name;
/** @var string */
private $position;
/** @var DateTimeInterface */
private $createdAt;
/** @var DateTimeInterface */
private $deletedAt;
// Getter and Setter
}
THENATHENAFORMFORMANDAANDACONTROLLERCONTROLLER
namespace AlInfrastructureUserInterfaceWebForm;
final class EmployeeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
$builder->add('position');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Employee::class,
));
}
}
namespace AlInfrastructureUserInterfaceWebController;
class EmployeeController extends Controller
{
public function indexAction(Request $request): Response
{
}
public function showAction(Request $request): Response
{
}
public function createAction(Request $request): Response
{
}
public function updateAction(Request $request): Response
{
}
public function deleteAction(Request $request): Response
{
}
}
namespace AlInfrastructureUserInterfaceWebController;
class EmployeeController extends Controller
{
public function createAction(Request $request): Response
{
$form = $this->createForm(EmployeeType::class, new Employee());
$form->handleRequest($request);
// First, data are mapped to the model then it is validated.
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->get('doctrine.orm.entity_manager');
$em->persist($form->getData());
$em->flush();
return $this->redirectToRoute('employee_list');
}
return $this->render('employee/hire.html.twig', [
'form' => $form->createView()
]);
}
}
LET'SREFACTOROURLET'SREFACTOROURANEMICMODELANEMICMODEL
HOWDOESESTELLETALKABOUTHERHOWDOESESTELLETALKABOUTHER
WORK?WORK?
namespace AlDomain;
final class Employee
{
public function hire(Uuid $identifier, string $name, string $forPosition)
{
}
public function promote(string $toNewPosition)
{
}
public function fire()
{
}
public function retire()
{
}
}
namespace AlDomain;
final class Employee
{
/** @var Uuid */
private $id;
/** @var string */
private $name;
/** @var string */
private $position;
/** @var DateTimeInterface */
private $hiredAt;
/** @var DateTimeInterface */
private $firedAt = null;
/** @var DateTimeInterface */
private $retiredAt = null;
}
namespace AlDomain;
final class Employee
{
private function __construct(Uuid $identifier, string $name, string $position)
{
$this->id = $identifier;
$this->name = $name;
$this->position = $position;
$this->hireAt = new DateTime();
}
public static function hire(Uuid $identifier, string $name, string $forPosition)
{
return new self($identifier, $name, $forPosition, $hiredAt);
}
public function promote(string $toNewPosition)
{
$this->position = $toNewPosition;
}
public function fire()
{
$this->firedAt = new DateTime();
}
public function retire()
{
$this->retiredAt = new DateTime();
}
}
namespace AlDomain;
final class Employee
{
private function __construct(Uuid $identifier, string $name, string $position)
{
if (empty($name)) {
throw new Exception('The name of the employee must not be empty');
}
if (empty($position)) {
throw new Exception('The position of the employee must not be empty');
}
$this->id = $identifier;
$this->name = $name;
$this->position = $position;
$this->hireAt = new DateTime();
}
public function retire()
{
if (null !== $this->fired) {
throw new Exception(
sprint('%s employee has been fired!', $this->name)
);
}
$this->retiredAt = new DateTime();
}
}
namespace AlDomain;
final class Name
{
private $name;
private function __construct(string $name)
{
if (empty($name)) {
throw new Exception('The name of the employee must not be empty');
}
$this->name = $name
}
public function __toString(): string
{
return $this->name;
}
}
namespace AlDomain;
final class Employee
{
private function __construct(
EmployeeIdentifier $identifier,
Name $name,
Position $position
) {
$this->id = $identifier;
$this->name = $name;
$this->position = $position;
$this->hireAt = new DateTime();
}
}
HOWCANWEUSEITINOURAPPLICATION?HOWCANWEUSEITINOURAPPLICATION?
FIRSTPROBLEM:FIRSTPROBLEM:HOWDOWEUSEHOWDOWEUSE
DOCTRINE?DOCTRINE?
We use them as query repositories
interface EmployeeRepository
{
public function findByNameAndPositionWithoutFiredPeople(
string $name,
string $position
): mixed;
}
WHATISAWHATISAREPOSITORYREPOSITORY??
«A repository behaves like a collection of unique entities
without taking care about the storage»
WHATDOESITLOOKLIKE?WHATDOESITLOOKLIKE?
namespace AlInfrastructure;
final class EmployeeRepository implements EmployeeRepositoryInterface
{
public function get(Uuid $identifier): Employee
{
}
public function add(Employee $employee): void
{
}
public function remove(Employee $employee): void
{
}
}
namespace AlInfrastructure;
final class EmployeeRepository implements EmployeeRepositoryInterface
{
/** @var EntityManagerInterface */
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
}
namespace AlInfrastructure;
final class EmployeeRepository implements EmployeeRepositoryInterface
{
public function get(Uuid $identifier): Employee
{
$employee = $this->entityManager->find(
Employee::class,
$identifier->toString()
);
if (null === $employee) {
throw new NonExistingEmployee($identifier->toString());
}
return $employee;
}
public function add(Employee $employee): void
{
$this->entityManager->persist($employee);
$this->entityManager->flush($employee);
}
public function remove(Employee $employee): void
{
$this->entityManager->remove($employee);
$this->entityManager->flush($employee);
}
}
ISITMANDATORYTOUSESETTER?ISITMANDATORYTOUSESETTER?NO!NO!
Doctrine uses the re ection to map data
Doctrine does not instantiate objects (Ocramius/Instantiator)
SECONDPROBLEM:SECONDPROBLEM:FORMCOMPONENTFORMCOMPONENT
PropertyAccessor is used to map data, it needs public properties or setter.
COMMANDCOMMANDTOTHERESCUE!TOTHERESCUE!
« A Command is an object that represents all the information
needed to call a method.»
LET’SCREATEACOMMANDLET’SCREATEACOMMAND
namespace AlApplication;
final class HireEmployeeCommand
{
/** @var string */
public $name = '';
/** @var string */
public $position = '';
}
LET’SUPDATEOURCONTROLLERLET’SUPDATEOURCONTROLLER
namespace AlInfrastructureUserInterfaceWebController;
class EmployeeController extends Controller
{
public function hireAction(Request $request): Response
{
$form = $this->createForm(EmployeeType::class, new HireEmployeeCommand());
$form->handleRequest($request);
// Superficial validation
if ($form->isSubmitted() && $form->isValid()) {
$employeeCommand = $form->getData();
// Domain validation
$employee = Employee::hire(
Uuid::uuid4(),
$employeeCommand->getName(),
$employeeCommand->getPosition()
);
$this->get('employee.repository')->add($employee);
return $this->redirectToRoute('employee_list');
}
return $this->render('employee/hire.html.twig', [
'form' => $form->createView()
]);
}
}
NOW,ESTELLEWANTSTOIMPORTNOW,ESTELLEWANTSTOIMPORT
EMPLOYEES!EMPLOYEES!
COMMANDBUSCOMMANDBUSTOTHERESCUETOTHERESCUE
«A Command Bus accepts a Command object and delegates it
to a Command Handler.»
LET’SUPDATEOURCONTROLLERLET’SUPDATEOURCONTROLLER
Here, we are going to use simple-bus/message-bus
namespace AlInfrastructureUserInterfaceWebController;
class EmployeeController extends Controller
{
public function hireAction(Request $request): Response
{
$form = $this->createForm(EmployeeType::class, new HireEmployeeCommand());
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$employeeCommand = $form->getData();
try {
$this->get('command_bus')->handle($employeeCommand);
} catch (Exception $e) {
$this->addFlash('error', 'An error occurs!');
}code
return $this->redirectToRoute('employee_list');
}
return $this->render('employee/hire.html.twig', [
'form' => $form->createView()
]);
}
}
LET’SCREATEACOMMANDHANDLERLET’SCREATEACOMMANDHANDLER
namespace AlApplicationHandler;
final class HireEmployeeHandler
{
/** @var EmployeeRepositoryInterface */
private $employeeRepository;
public function __construct(EmployeeRepositoryInterface $employeeRepository)
{
$this->employeeRepository = $employeeRepository;
}
public function handle(HireEmployee $command): void
{
$employee = Employee::hire(
Uuid::uuid4(),
$command->getName(),
$command->getPosition()
);
$this->employeeRepository->add($employee);
}
}
ESTELLEWON'TUSEESTELLEWON'TUSEPHPMYADMINPHPMYADMINTOREADTOREAD
DATA!DATA!
DOOURMODELSNEEDGETTER?DOOURMODELSNEEDGETTER?NOTNOT
NECESSARILY!NECESSARILY!
DTO(DATATRANSFEROBJECT)DTO(DATATRANSFEROBJECT)TOTHETOTHE
RESCUE!RESCUE!
«A Data Transfer Object is an object that is used to
encapsulate data, and to send it from one subsystem of an
application to another.»
DOCTRINE(>=2.4):DOCTRINE(>=2.4):OPERATORNEWOPERATORNEW
namespace AlDomainReadModel;
final class Employee
{
/** string */
private $id;
/** string */
private $name;
/** string */
private $position;
public function __construct(string $id, string $name, string $position)
{
$this->id = $id;
$this->name = $name;
$this->position = $position;
}
// Define an accessor for each property
}
namespace AlInfrastructure;
final class ListEmployeeQuery implement SearchEmployeeQueryInterface
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function search(): array
{
$queryBuilder = $this->entityManager->createQueryBuilder()
->from(Employee::class, 'e')
->select(
sprintf('NEW %s(e.id, e.name, e.position)', Employee::class)
);
// NEW AlDomainReadModelEmployee(e.id, e.name, e.position)
return $queryBuilder->getQuery()->getResult();
}
}
WECANSEARCHANDDISPLAYEMPLOYEEWECANSEARCHANDDISPLAYEMPLOYEE
DATA!DATA!
LEGACYBLINDNESSLEGACYBLINDNESS//DDDDDD//CQRSCQRS
Domain Driven Design
Command Query Responsibility Segregation
WHATISTHEBESTSOLUTION?WHATISTHEBESTSOLUTION?
It depends on what you want!
THAT'STHEEND!THAT'STHEEND!
THANKYOU!QUESTIONS?THANKYOU!QUESTIONS?
aRn0D _aRn0D

Code me a HR