Code moi une RH! (PHP tour 2017)

Post on 28-Jan-2018

1.070 views 0 download

Transcript of Code moi une RH! (PHP tour 2017)

CODE ME A HR!

HI, I AM ARNAUD!

ANEMIC MODEL VERSUS MODEL RICH

PLAYGROUNDBusiness: Human resources management

Technical: Symfony / Doctrine ORM

WHAT DO WE USUALLY DO? CRUD!Create Read Update Delete

WE START WITH CREATING ANEMIC MODEL

namespace Al\Domain;

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 }

THEN A FORM AND A CONTROLLER

namespace Al\Presenter\Form;

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 Al\Presenter\Controller;

class EmployeeController extends Controller { public function indexAction(Request $request) { }

public function showAction(Request $request) { }

public function createAction(Request $request) { }

public function updateAction(Request $request) { }

public function deleteAction(Request $request) { }}

namespace Al\Presenter\Controller;

class EmployeeController extends Controller { public function createAction(Request $request) { $form = $this->createForm(EmployeeType::class, new Employee()); $form->handleRequest($request);

// First, data are mapped to the model and it is validated thereafter. 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'S REFACTOR OUR ANEMIC MODEL

HOW DOES ESTELLE TALK ABOUT HERWORK?

namespace Al\Domain;

final class Employee implements EmployeeInterface { public function hire(Uuid $identifier, string $name, string $forPosition) { }

public function promote(string $toNewPosition) { }

public function fire() { }

public function retire() { }}

namespace Al\Domain;

final class Employee implements EmployeeInterface { /** @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 Al\Domain;

final class Employee implements EmployeeInterface { 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 Al\Domain;

final class Employee implements EmployeeInterface { private function __construct(Uuid $identifier, string $name, string $position) { if ($hiredAt < new \DateTime('2013-01-01')) { throw new \OutOfRangeException( 'The company did not exist before 2013-01-01' ); }

$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(); }}

HOW CAN WE USE IT IN OUR APPLICATION?

FIRST PROBLEM: HOW DO WE USEDOCTRINE?

We use them as query repositories

interface EmployeeRepository { public function findByNameAndPositionWithoutFiredPeople( string $name, string $position );}

WHAT IS A REPOSITORY?«A repository behaves like a collection of unique entities

without taking care about the storage»

WHAT DOES IT LOOK LIKE?

namespace Al\Infrastructure;

final class EmployeeRepository implements EmployeeRepositoryInterface { public function get(Uuid $identifier) { }

public function add(EmployeeInterface $employee) { }

public function remove(EmployeeInterface $employee) { }}

namespace Al\Infrastructure;

final class EmployeeRepository implements EmployeeRepositoryInterface { /** @var EntityManagerInterface */ private $entityManager;

public function __construct(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; }}

namespace Al\Infrastructure;

final class EmployeeRepository implements EmployeeRepositoryInterface { public function get(Uuid $identifier) { $employee = $this->entityManager->find( Employee::class, $identifier->toString() );

if (null === $employee) { throw new NonExistingEmployee($identifier->toString()); }

return $employee; }

public function add(EmployeeInterface $employee) { $this->entityManager->persist($employee); $this->entityManager->flush($employee); }

public function remove(EmployeeInterface $employee) { $this->entityManager->remove($employee); $this->entityManager->flush($employee); }}

IS IT MANDATORY TO USE SETTER? NO!Doctrine uses the re�ection to map data

Doctrine does not instantiate objects (Ocramius/Instantiator)

SECOND PROBLEM: FORM COMPONENTPropertyAccessor is used to map data, it needs public properties or setter.

COMMAND TO THE RESCUE!« A Command is an object that represents all the information

needed to call a method.»

LET’S CREATE A COMMAND

namespace Al\Application;

final class HireEmployeeCommand { /** @var string */ public $name = '';

/** @var string */ public $position = ''; }

LET’S UPDATE OUR CONTROLLER

namespace Al\Presenter\Controller;

class EmployeeController extends Controller { public function hireAction(Request $request) { $form = $this->createForm(EmployeeType::class, new HireEmployeeCommand()); $form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) { $employeeCommand = $form->getData();

$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, ESTELLE WANTS TO IMPORTEMPLOYEES!

COMMAND BUS TO THE RESCUE«A Command Bus accepts a Command object and delegates it

to a Command Handler.»

LET’S UPDATE OUR CONTROLLERHere, we are going to use simple-bus/message-bus

namespace Al\Presenter\Controller;

class EmployeeController extends Controller { public function hireAction(Request $request) { $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!'); }

return $this->redirectToRoute('employee_list'); }

return $this->render('employee/hire.html.twig', [ 'form' => $form->createView() ]); }}

LET’S CREATE A COMMAND HANDLER

namespace Al\Application\Handler;

final class HireEmployeeHandler { /** @var EmployeeRepositoryInterface */ private $employeeRepository;

public function __construct(EmployeeRepositoryInterface $employeeRepository) { $this->employeeRepository = $employeeRepository; }

public function handle(HireEmployee $command) { $employee = Employee::hire( Uuid::uuid4(), $command->getName(), $command->getPosition() );

$this->employeeRepository->add($employee); }}

ESTELLE WON'T USE PHPMYADMIN TO READDATA!

DO OUR MODELS NEED GETTER? NOTNECESSARILY !

DTO (DATA TRANSFER OBJECT) TO THERESCUE!

«A Data Transfer Object is an object that is used toencapsulate data, and to send it from one subsystem of an

application to another.»

DOCTRINE (>=2.4): OPERATOR NEW

namespace Al\Application;

final class EmployeeDTO { /** 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 Al\Infrastructure;

final class EmployeeSearcher { private $entityManager;

public function __construct(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; }

public function findAll() { $queryBuilder = $this->entityManager->createQueryBuilder() ->from(Employee::class, 'e') ->select( sprintf('NEW %s(e.id, e.name, e.position)', EmployeeDTO::class) );

// NEW Al\Application\EmployeeDTO(e.id, e.name, e.position)

return $queryBuilder->getQuery()->getResult(); }}

WE CAN SEARCH AND DISPLAY EMPLOYEEDATA!

MISREADING OF DOCTRINE / DDD / CQRSDomain Driven Design

Command Query Responsibility Segregation

WHAT IS THE BEST SOLUTION?It depends on what you want!

THAT'S THE END!https://joind.in/talk/6c974

THANK YOU! QUESTIONS?

aRn0D _aRn0Dhttps://joind.in/talk/6c974