Slide 1

Slide 1 text

Divide And Conquer PHPBenelux 2017 Andreas Hucks

Slide 2

Slide 2 text

@meandmymonkey Andreas Hucks CTO @

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

A Generic Framework App

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

/**
 * @Route(path="/", name="list")
 */
 public function indexAction()
 {
 $robots = $this
 ->getDoctrine()
 ->getRepository('AppBundle:Robot')
 ->findAll()
 ;
 
 return $this->render(
 '@App/robot/index.html.twig',
 [
 'robots' => $robots
 ]
 );
 }

Slide 9

Slide 9 text

/**
 * @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');
 }

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

public function validatePropulsion( ExecutionContextInterface $context, $payload ) {
 if (!in_array($this->getPropulsion(), self::$propusions)) { $context->buildViolation('Unknown propulsion type!')
 ->atPath('propulsion')
 ->addViolation();
 }
 }

Slide 12

Slide 12 text

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
 {
 // …

Slide 13

Slide 13 text

The Problem

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Direct Access to Entities • from View • in Forms

Slide 17

Slide 17 text

Other Concerns • Mixing responsibilities • Controller uses Doctrine directly

Slide 18

Slide 18 text

Why Separate?

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Extracting the Domain

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

public function __construct($value)
 {
 $propulsion = (string) $value;
 
 if (!in_array($propulsion, array_keys(self::$propusions))) {
 throw new Exception('Invalid key');
 }
 
 $this->propulsion = $propulsion;
 }

Slide 26

Slide 26 text

Domain\Entity\Data\Propulsion:
 type: embeddable
 fields:
 propulsion:
 type: string
 length: 255
 nullable: false

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

class Id
 {
 private $key;
 
 public function __construct($key)
 {
 $this->key = (string) $key;
 }
 
 public function getKey()
 {
 return $this->key;
 }

Slide 29

Slide 29 text

dbal:
 types:
 uuid:
 class: Domain\Doctrine\Type\Uuid
 commented: true

Slide 30

Slide 30 text


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

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

Building Bridges

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

namespace Domain\Repository;
 
 use Domain\Entity\Data\Id;
 use Domain\Entity\Robot; interface RobotRepository
 {
 public function get(Id $id);
 
 public function add(Robot $robot); // […]
 }

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

Decoupling Security

Slide 46

Slide 46 text

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
 {
 // …

Slide 47

Slide 47 text


 interface UserProviderInterface
 {
 public function loadUserByUsername($username);
 
 public function refreshUser(UserInterface $user);
 
 public function supportsClass($class);
 }


Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

Validation, Forms, ViewModels

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

Domain\RobotUpdate:
 properties:
 name:
 - NotBlank: ~
 
 


Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Be pragmatic

Slide 58

Slide 58 text

Thank you! legacy.joind.in/20202