Divide and Conquer (LonghornPHP 2019)

Divide and Conquer (LonghornPHP 2019)

E9612cd342dbddff6640b99db21deee7?s=128

Andreas Hucks

May 04, 2019
Tweet

Transcript

  1. Divide And Conquer LonghornPHP 2019 May 04 2019 Austin,TX Andreas

    Hucks
  2. @meandmymonkey Andreas Hucks SensioLabs

  3. Motivation

  4. What this talk is not about: • DDD • Generic

    Package Design
  5. A Generic Framework App

  6. None
  7. None
  8. None
  9. /**
 * @Route(path="/", name="list")
 */
 public function index()
 {
 $robots

    = $this
 ->getDoctrine()
 ->getRepository(Robot::class)
 ->findAll()
 ;
 
 return $this->render(
 'robot/index.html.twig',
 [
 'robots' => $robots
 ]
 );
 }
  10. /**
 * @Route(path="/{id}", name="edit")
 */
 public function edit(Request $request, Robot

    $robot)
 {
 $form = $this->createForm(RobotType::class, $robot);
 $form->handleRequest($request);
 
 if ($form->isSubmitted() && $form->isValid()) {
 $this->getDoctrine()->getManager()->flush();
 
 return $this->redirectToRoute('list');
 }
  11. /**
 * @ORM\Entity(repositoryClass="…")
 * @ORM\Table(name="robots")
 */
 class Robot
 {
 public

    static $propusionTypes = [
 'legs' => 'Can walk',
 'wheels' => 'Drives',
 'tracks' => 'Tracked Vehicle',
 'jetpack' => 'Hovers',
 'weird' => 'Why would you build that'
 ];
 
 /**
 * @ORM\Column(type=“integer")
 * @ORM\Id()
 * @ORM\GeneratedValue()
 */
 private $id;
  12. public function validatePropulsion( ExecutionContextInterface $context, $payload ) {
 if (!in_array($this->getPropulsion(),

    self::$propusions)) {
 
 }
 }
  13. namespace App\Entity;
 
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\Security\Core\User\UserInterface;
 use

    Symfony\Component\Validator\Constraints as Assert;
 
 /**
 * @ORM\Entity()
 * @ORM\Table(name="mechanics")
 */
 class Mechanic implements UserInterface
 {
 // …
  14. The Problem

  15. None
  16. Dependencies of your Code • to Validator • to Doctrine

    • to Security Layer • To the FrameworkBundle
  17. Direct Access to Entities • from View • in Forms

  18. Other Concerns • Mixing responsibilities • Controller uses Doctrine directly

  19. Why Separate?

  20. Clean Layers • Separate your business code from your framework

    • … or in fact, any other 3rd party libs • Separate your own layers/packages (Domain, Application, Infrastructure…)
  21. Architecture Patterns !

  22. Extracting the Domain

  23. None
  24. Dedicated Top Level Namespaces? !

  25. None
  26. None
  27. None
  28. Domain\Entity\Robot:
 type: entity
 table: robots
 repositoryClass: Domain\Repository\RobotRepository
 id:
 id:
 type:

    uuid
 length: 255
 fields:
 name:
 type: string
 length: 255
 nullable: false
 […]
 embedded:
 propulsion:
 class: Domain\Entity\Data\Propulsion
 columnPrefix: false
  29. public function __construct($value)
 {
 $propulsion = (string) $value;
 
 if

    (!in_array($propulsion, array_keys(self::$propusions))) {
 throw new Exception('Invalid key');
 }
 
 $this->propulsion = $propulsion;
 }
  30. Domain\Entity\Data\Propulsion:
 type: embeddable
 fields:
 propulsion:
 type: string
 length: 255
 nullable:

    false
  31. None
  32. class Id
 {
 private $key;
 
 public function __construct($key)
 {


    $this->key = (string) $key;
 }
 
 public function getKey()
 {
 return $this->key;
 }
  33. dbal:
 types:
 uuid:
 class: Domain\Doctrine\Type\Uuid
 commented: true

  34. 
 class Uuid extends Type
 {
 public function convertToDatabaseValue($value, AbstractPlatform

    $platform)
 {
 if (empty($value)) {
 return null;
 }
 
 if ($value instanceof Id) {
 return $value->getKey();
 }
 
 return null;
 }
 
 
 
 public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
 {
 return 'VARCHAR(255) COMMENT \'(DC2Type:uuid)\'';
 }
 
 public function getName()
 {
 return 'uuid';
 }
 }
  35. None
  36. class InvalidPropulsionException extends \InvalidArgumentException implements RobotException { }

  37. Deptrac • github.com/sensiolabs-de/deptrac

  38. None
  39. None
  40. layers: - name: Application collectors: - type: className regex: ^App\\Controller\\.*|App\\Form\\.*|\\App\\Kernel

    - name: Domain collectors: - type: className regex: ^App\\Domain\\.* - name: Framework collectors: - type: className regex: ^Symfony\\|Doctrine\\.* ruleset: Application: - Domain - Framework Application: - Domain
  41. None
  42. None
  43. Sidetrack: How Symfony Packages Compare

  44. None
  45. None
  46. Extracting Infrastructure

  47. None
  48. None
  49. namespace App\Domain\Repository;
 
 use App\Domain\Entity\Data\Id;
 use App\Domain\Entity\Robot; interface RobotRepository
 {


    public function get(Id $id);
 
 public function add(Robot $robot); // […]
 }
  50. Naming… RobotRepository vs. RobotRespositoryInterface !

  51. namespace App\Infrastructure\Doctrine\Repository;
 
 use Doctrine\ORM\EntityRepository;
 use Domain\Entity\Data\Id;
 use Domain\Entity\Robot;
 use

    Domain\Repository\RobotRepository as RepositoryInterface; class RobotRepository extends EntityRepository implements RepositoryInterface
 {
 public function get(Id $id)
 {
 return $this->findOneBy(['id' => $id]);
 }
 
 public function add(Robot $robot)
 {
 $this->_em->persist($robot);
 $this->_em->flush($robot);
 }
 }
  52. Extending EntityRepository? !

  53. None
  54. None
  55. Decoupling Security

  56. namespace App\Domain\Entity;
 
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\Security\Core\User\UserInterface;
 use

    Symfony\Component\Validator\Constraints as Assert;
 
 /**
 * @ORM\Entity()
 * @ORM\Table(name="mechanics")
 */
 class Mechanic implements UserInterface
 {
 // …
  57. 
 interface UserProviderInterface
 {
 public function loadUserByUsername($username);
 
 public function

    refreshUser(UserInterface $user);
 
 public function supportsClass($class);
 }

  58. None
  59. class MechanicAccount implements UserInterface
 {
 private $mechanicId;
 private $username;
 private

    $password;
 
 public static function fromMechanic(Mechanic $mechanic)
 {
 $account = new static();
 
 $account->mechanicId = $mechanic->getId();
 $account->username = $mechanic->getEmail();
 $account->password = $mechanic->getPassword();
 
 return $account;
 }
  60. public function loadUserByUsername($username)
 {
 $mechanic = $this->repository
 ->findOneBy(['email' => new

    Email($username)]);
 
 if (null === $mechanic) {
 throw new UsernameNotFoundException( 'User does not exist.' );
 }
 
 return MechanicAccount::fromMechanic($mechanic);
 }
  61. Validation, Forms, ViewModels

  62. class RobotUpdate
 {
 public $name;
 public $coolness;
 public $propulsion;
 }

  63. App\Domain\RobotUpdate:
 properties:
 name:
 - NotBlank: ~
 
 


  64. class RobotService
 {
 public function updateRobot(RobotUpdate $date, Robot $robot)
 {


    // ...
 }
 }
  65. public function configureOptions(OptionsResolver $resolver)
 {
 $resolver->setDefaults(array(
 'data_class' => ‘App\Domain\RobotUpdate'
 ));


    }
  66. Missing pieces

  67. Be pragmatic !

  68. Going further

  69. What about Controllers? !

  70. Thank you!

  71. Specification Pattern !