Code moi une RH! (PHP tour 2017)
-
Upload
arnaud-langlade -
Category
Software
-
view
1.069 -
download
0
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