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
  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 !