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

Divide and Conquer

Divide and Conquer

Andreas Hucks

January 30, 2017
Tweet

More Decks by Andreas Hucks

Other Decks in Programming

Transcript

  1. Divide And Conquer
    PHPBenelux 2017
    Andreas Hucks

    View Slide

  2. @meandmymonkey
    Andreas Hucks
    CTO @

    View Slide

  3. What this talk is not about:
    • DDD
    • Generic Package Design
    (but these topics will play a role)

    View Slide

  4. A Generic Framework App

    View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. /**

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

    */

    public function indexAction()

    {

    $robots = $this

    ->getDoctrine()

    ->getRepository('AppBundle:Robot')

    ->findAll()

    ;


    return $this->render(

    '@App/robot/index.html.twig',

    [

    'robots' => $robots

    ]

    );

    }

    View Slide

  9. /**

    * @Route(path="/{id}", name="edit")

    */

    public function editAction(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

  10. /**

    * @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

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

    if (!in_array($this->getPropulsion(), self::$propusions)) {
    $context->buildViolation('Unknown propulsion type!')

    ->atPath('propulsion')

    ->addViolation();

    }

    }

    View Slide

  12. namespace AppBundle\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

  13. The Problem

    View Slide

  14. View Slide

  15. Dependencies of your Code
    • to Validator
    • to Doctrine
    • to Security Layer

    View Slide

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

    View Slide

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

    View Slide

  18. Why Separate?

    View Slide

  19. Clean Packages
    • 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

  20. Extracting the Domain

    View Slide

  21. View Slide

  22. View Slide

  23. View Slide

  24. 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

  25. 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

  26. Domain\Entity\Data\Propulsion:

    type: embeddable

    fields:

    propulsion:

    type: string

    length: 255

    nullable: false

    View Slide

  27. View Slide

  28. class Id

    {

    private $key;


    public function __construct($key)

    {

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

    }


    public function getKey()

    {

    return $this->key;

    }

    View Slide

  29. dbal:

    types:

    uuid:

    class: Domain\Doctrine\Type\Uuid

    commented: true

    View Slide


  30. 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 $pl
    {

    return 'VARCHAR(255) COMMENT \'(DC2Type:uuid)\'';

    }


    public function getName()

    {

    return 'uuid';

    }

    }

    View Slide

  31. View Slide

  32. Deptrac
    • github.com/sensiolabs-de/deptrac
    by @timglabisch and contributors

    View Slide

  33. View Slide

  34. View Slide

  35. layers:

    - name: Application

    collectors:

    - type: className

    regex: ^AppBundle\\.*

    - name: Domain

    collectors:

    - type: className

    regex: ^Domain\\.*

    - name: Framework

    collectors:

    - type: className

    regex: ^Symfony\\|^Doctrine\\.*

    ruleset:

    Application:

    - Domain

    - Framework

    Application:

    - Domain

    View Slide

  36. View Slide

  37. View Slide

  38. Building Bridges

    View Slide

  39. View Slide

  40. View Slide

  41. namespace Domain\Repository;


    use Domain\Entity\Data\Id;

    use Domain\Entity\Robot;
    interface RobotRepository

    {

    public function get(Id $id);


    public function add(Robot $robot);
    // […]

    }

    View Slide

  42. namespace Bridge\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 Reposit
    {

    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

  43. View Slide

  44. View Slide

  45. Decoupling Security

    View Slide

  46. namespace AppBundle\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


  47. interface UserProviderInterface

    {

    public function loadUserByUsername($username);


    public function refreshUser(UserInterface $user);


    public function supportsClass($class);

    }


    View Slide

  48. View Slide

  49. 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

  50. 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

  51. View Slide

  52. Validation, Forms,
    ViewModels

    View Slide

  53. class RobotUpdate

    {

    public $name;

    public $coolness;

    public $propulsion;

    }

    View Slide

  54. Domain\RobotUpdate:

    properties:

    name:

    - NotBlank: ~



    View Slide

  55. class RobotService

    {

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

    {

    // ...

    }

    }

    View Slide

  56. public function configureOptions(OptionsResolver $resolver)

    {

    $resolver->setDefaults(array(

    'data_class' => 'Domain\RobotUpdate'

    ));

    }

    View Slide

  57. Be pragmatic

    View Slide

  58. Thank you!
    legacy.joind.in/20202

    View Slide