Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Divide and Conquer (LonghornPHP 2019)

Divide and Conquer (LonghornPHP 2019)

Andreas Hucks

May 04, 2019
Tweet

More Decks by Andreas Hucks

Other Decks in Programming

Transcript

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

    View Slide

  2. @meandmymonkey
    Andreas Hucks
    SensioLabs

    View Slide

  3. Motivation

    View Slide

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

    View Slide

  5. A Generic Framework App

    View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. /**

    * @Route(path="/", name="list")

    */

    public function index()

    {

    $robots = $this

    ->getDoctrine()

    ->getRepository(Robot::class)

    ->findAll()

    ;


    return $this->render(

    'robot/index.html.twig',

    [

    'robots' => $robots

    ]

    );

    }

    View Slide

  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');

    }

    View Slide

  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;

    View Slide

  12. public function validatePropulsion(
    ExecutionContextInterface $context,
    $payload
    ) {

    if (!in_array($this->getPropulsion(), self::$propusions)) {


    }

    }

    View Slide

  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

    {

    // …

    View Slide

  14. The Problem

    View Slide

  15. View Slide

  16. Dependencies of your Code
    • to Validator
    • to Doctrine
    • to Security Layer
    • To the FrameworkBundle

    View Slide

  17. Direct Access to Entities
    • from View
    • in Forms

    View Slide

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

    View Slide

  19. Why Separate?

    View Slide

  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…)

    View Slide

  21. Architecture Patterns
    !

    View Slide

  22. Extracting the Domain

    View Slide

  23. View Slide

  24. Dedicated Top Level
    Namespaces?
    !

    View Slide

  25. View Slide

  26. View Slide

  27. View Slide

  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

    View Slide

  29. public function __construct($value)

    {

    $propulsion = (string) $value;


    if (!in_array($propulsion, array_keys(self::$propusions))) {

    throw new Exception('Invalid key');

    }


    $this->propulsion = $propulsion;

    }

    View Slide

  30. Domain\Entity\Data\Propulsion:

    type: embeddable

    fields:

    propulsion:

    type: string

    length: 255

    nullable: false

    View Slide

  31. View Slide

  32. class Id

    {

    private $key;


    public function __construct($key)

    {

    $this->key = (string) $key;

    }


    public function getKey()

    {

    return $this->key;

    }

    View Slide

  33. dbal:

    types:

    uuid:

    class: Domain\Doctrine\Type\Uuid

    commented: true

    View Slide


  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';

    }

    }

    View Slide

  35. View Slide

  36. class InvalidPropulsionException
    extends \InvalidArgumentException
    implements RobotException
    {
    }

    View Slide

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

    View Slide

  38. View Slide

  39. View Slide

  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

    View Slide

  41. View Slide

  42. View Slide

  43. Sidetrack: How Symfony
    Packages Compare

    View Slide

  44. View Slide

  45. View Slide

  46. Extracting Infrastructure

    View Slide

  47. View Slide

  48. View Slide

  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);
    // […]

    }

    View Slide

  50. Naming…
    RobotRepository
    vs.
    RobotRespositoryInterface
    !

    View Slide

  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);

    }

    }

    View Slide

  52. Extending
    EntityRepository?
    !

    View Slide

  53. View Slide

  54. View Slide

  55. Decoupling Security

    View Slide

  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

    {

    // …

    View Slide


  57. interface UserProviderInterface

    {

    public function loadUserByUsername($username);


    public function refreshUser(UserInterface $user);


    public function supportsClass($class);

    }


    View Slide

  58. View Slide

  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;

    }

    View Slide

  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);

    }

    View Slide

  61. Validation, Forms,
    ViewModels

    View Slide

  62. class RobotUpdate

    {

    public $name;

    public $coolness;

    public $propulsion;

    }

    View Slide

  63. App\Domain\RobotUpdate:

    properties:

    name:

    - NotBlank: ~



    View Slide

  64. class RobotService

    {

    public function updateRobot(RobotUpdate $date, Robot $robot)

    {

    // ...

    }

    }

    View Slide

  65. public function configureOptions(OptionsResolver $resolver)

    {

    $resolver->setDefaults(array(

    'data_class' => ‘App\Domain\RobotUpdate'

    ));

    }

    View Slide

  66. Missing pieces

    View Slide

  67. Be pragmatic
    !

    View Slide

  68. Going further

    View Slide

  69. What about Controllers?
    !

    View Slide

  70. Thank you!

    View Slide

  71. Specification Pattern
    !

    View Slide