database? meh, implementation detail

61
Database? Meh, implementation detail https://joind.in/16534

Upload: maciej-malarz

Post on 12-Apr-2017

460 views

Category:

Engineering


3 download

TRANSCRIPT

Page 1: Database? Meh, implementation detail

Database? Meh,

implementation detail

https://joind.in/16534

Page 2: Database? Meh, implementation detail

Entity

/** @ORM\Entity */

class Submission

{

/** @ORM\OneToOne(targetEntity="User") */

private $acceptedBy;

/** @ORM\Column(type="datetime") */

private $acceptedOn;

/** @ORM\Column(type="string") */

private $applicant;

/** @ORM\OneToOne(targetEntity="User") */

private $rejectedBy;

/** @ORM\Column(type="datetime") */

private $rejectedOn;

/** @ORM\Column(type="string") */

private $title;

}

Maciej Malarz (@malarzm)

Page 3: Database? Meh, implementation detail

List action

/**

* @Route("/", name="homepage")

*/

public function listAction()

{

$submissions = $this->getDoctrine()->getManager()

->getRepository(Submission::class)

->findBy(['acceptedOn' => null, 'rejectedOn' => null]);

return [ 'submissions' => $submissions ];

}

Maciej Malarz (@malarzm)

Page 4: Database? Meh, implementation detail

View action

/**

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

* @Template()

*/

public function viewAction(Request $request)

{

$s = $this->getDoctrine()->getManager()->find(Submission::class, $request->get('id'));

if ($s === null) {

throw $this->createNotFoundException();

}

return [ 'submission' => $s ];

}

/**

* @ParamConverter("submission", class="AppBundle\Entity\Submission")

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

* @Template

*/

public function viewAction(Submission $submission)

{

return [ 'submission' => $submission ];

}

Maciej Malarz (@malarzm)

Page 5: Database? Meh, implementation detail

Accept & reject actions

/**

* @ParamConverter("submission", class="AppBundle\Entity\Submission")

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

*/

public function acceptAction(Submission $submission)

{

$submission->setAcceptedBy($this->getUser());

$submission->setAcceptedOn(new \DateTime());

$this->getDoctrine()->getManager()->flush();

return $this->redirectToRoute('homepage');

}

/**

* @ParamConverter("submission", class="AppBundle\Entity\Submission")

* @Route("/accept/{id}", name="reject")

*/

public function rejectAction(Submission $submission)

{

$submission->setRejectedBy($this->getUser());

$submission->setRejectedOn(new \DateTime());

$this->getDoctrine()->getManager()->flush();

return $this->redirectToRoute('homepage');

}

Maciej Malarz (@malarzm)

Page 6: Database? Meh, implementation detail

et voilà!

Maciej Malarz (@malarzm)

Page 7: Database? Meh, implementation detail
Page 8: Database? Meh, implementation detail

Forgot the slug

class SubmissionsSlugger

{

public function prePersist(LifecycleEventArgs $event)

{

/** @var Submission $entity */

$entity = $event->getEntity();

if ( ! ($entity instanceof Submission)) {

return;

}

$entity->setSlug($this->slugify($entity->getTitle()));

}

private function slugify($string)

{

return preg_replace('/[^a-z0-9]/', '-', strtolower(trim(strip_tags($string))));

}

}

Maciej Malarz (@malarzm)

Page 9: Database? Meh, implementation detail
Page 10: Database? Meh, implementation detail

New features

Maciej Malarz (@malarzm)

Page 11: Database? Meh, implementation detail

Oh hey, project was a success!

Maciej Malarz (@malarzm)

Page 12: Database? Meh, implementation detail
Page 13: Database? Meh, implementation detail
Page 14: Database? Meh, implementation detail

1. Value Objects

Maciej Malarz (@malarzm)

Page 15: Database? Meh, implementation detail

VO candidate

/** @ORM\Entity */

class Submission

{

/** @ORM\OneToOne(targetEntity="User") */

private $acceptedBy;

/** @ORM\Column(type="datetime") */

private $acceptedOn;

/** @ORM\Column(type="string") */

private $applicant;

/** @ORM\OneToOne(targetEntity="User") */

private $rejectedBy;

/** @ORM\Column(type="datetime") */

private $rejectedOn;

/** @ORM\Column(type="string") */

private $title;

}

Maciej Malarz (@malarzm)

Page 16: Database? Meh, implementation detail

Introducing VO

/** @ORM\Embeddable */

abstract class AbstractStatus

{

/** @ORM\OneToOne(targetEntity="User") */

private $by;

/** @ORM\Column(type="datetime") */

private $on;

/** @ORM\Column(type="string") */

private $message;

public function __construct(User $by, $message)

{

$this->by = $by;

$this->on = new \DateTime();

$this->message = $message;

}

}

class StatusAccepted extends AbstractStatus { }

class StatusCancelled extends AbstractStatus { }

class StatusRejected extends AbstractStatus { }

Maciej Malarz (@malarzm)

Page 17: Database? Meh, implementation detail

Mapping

class Submission

{

/** @ORM\Column(type="string") */

private $applicant;

/** @ORM\Column(type="string") */

private $slug;

/** @ORM\Embedded(class="???") */

private $status;

/** @ORM\Column(type="string") */

private $title;

}

Maciej Malarz (@malarzm)

Page 18: Database? Meh, implementation detail

Actually you can but with MongoDB ODM ;)

Maciej Malarz (@malarzm)

Page 19: Database? Meh, implementation detail

/** @ORM\Embeddable */

class Status

{

const ACCEPTED = 0;

const CANCELLED = 1;

const REJECTED = 2;

/** @ORM\OneToOne(targetEntity="User") */

private $by;

/** @ORM\Column(type="datetime") */

private $on;

/** @ORM\Column(type="string") */

private $message;

/** @ORM\Column(type="integer") */

private $type;

public function __construct($type, User $by, $message)

{

$this->by = $by;

$this->on = new \DateTime();

$this->message = $message;

$this->type = $type;

}

}

Not very good on its own...

Maciej Malarz (@malarzm)

Page 20: Database? Meh, implementation detail

A bit better...

/** @ORM\Entity */

class Submission

{

/**

* @ORM\Embedded(class="Status")

* @var Status

*/

private $status;

public function getStatus()

{

switch ($this->status->getType()) {

case Status::ACCEPTED:

return new StatusAccepted(/* ... */);

/* ... */

}

}

public function setStatus(Status $status)

{

switch (get_class($status)) {

case StatusAccepted::class:

$this->status = new Status(/* ... */);

break;

/* ... */

}

}

Maciej Malarz (@malarzm)

Page 21: Database? Meh, implementation detail

We can do better

Just don't use DB mapped entities as domain entities

Maciej Malarz (@malarzm)

Page 22: Database? Meh, implementation detail

Recap: Value Objects

A small simple object, like money or a

date range, whose equality isn't based on

identity.

Maciej Malarz (@malarzm)

Page 23: Database? Meh, implementation detail

2. Object Managers

Maciej Malarz (@malarzm)

Page 24: Database? Meh, implementation detail

Remember these ones?

/**

* @Route("{id}", name="submission")

*/

public function viewAction(Request $request)

{

$s = $this->getDoctrine()->getManager()->find(Submission::class, $request->get('id'));

if ($s === null) {

throw $this->createNotFoundException();

}

return [ 'submission' => $s ];

}

/**

* @ParamConverter("submission", class="AppBundle\Entity\Submission")

* @Route("/accept/{id}", name="reject")

*/

public function rejectAction(Submission $submission)

{

$submission->setRejectedBy($this->getUser());

$submission->setRejectedOn(new \DateTime());

$this->getDoctrine()->getManager()->flush();

return $this->redirectToRoute('homepage');

}

Maciej Malarz (@malarzm)

Page 25: Database? Meh, implementation detail

Your very own manager!

class SubmissionManager

{

/** @var ObjectManager */

private $objectManager;

public function __construct(ObjectManager $objectManager)

{

$this->objectManager = $objectManager;

}

public function find($id)

{

return $this->objectManager->find(Submission::class, $id);

}

public function save(Submission $submission)

{

$this->objectManager->persist($submission);

$this->objectManager->flush($submission);

}

}

Maciej Malarz (@malarzm)

Page 26: Database? Meh, implementation detail

Look ma! Using only my own stuff

/**

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

* @Template

*/

public function viewAction(Request $request)

{

$s = $this->get('submission_manager')->find($request->get('id'));

if ($s === null) {

throw $this->createNotFoundException();

}

return [ 'submission' => $s ];

}

/**

* @ParamConverter("submission", class="AppBundle\Entity\Submission")

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

*/

public function acceptAction(Submission $submission)

{

$submission->setAcceptedBy($this->getUser());

$submission->setAcceptedOn(new \DateTime());

$this->get('submission_manager')->save($submission);

return $this->redirectToRoute('homepage');

}

Maciej Malarz (@malarzm)

Page 27: Database? Meh, implementation detail

Verbs matter

class SubmissionManager

{

public function find($id)

{

return $this->objectManager->find(Submission::class, $id);

}

public function get($id)

{

$s = $this->find($id);

if ($s === null) {

throw new NoResultException();

}

return $s;

}

}

Maciej Malarz (@malarzm)

Page 28: Database? Meh, implementation detail

Own events

class SubmissionManager

{

/** @var ObjectManager */

private $objectManager;

/** @var EventDispatcherInterface */

private $eventDispatcher;

public function __construct(ObjectManager $objectManager, EventDispatcherInterface $eventDispatcher)

{

$this->objectManager = $objectManager;

$this->eventDispatcher = $eventDispatcher;

}

public function save(Submission $submission)

{

if ($this->objectManager->contains($submission)) {

$this->eventDispatcher->dispatch('submission.update', new SubmissionEvent($submission));

} else {

$this->objectManager->persist($submission);

$this->eventDispatcher->dispatch('submission.create', new SubmissionEvent($submission));

}

$this->objectManager->flush($submission);

}

}

Maciej Malarz (@malarzm)

Page 29: Database? Meh, implementation detail

Recap: Object Managers

Manages lifecycle and instances of entities.

Maciej Malarz (@malarzm)

Page 30: Database? Meh, implementation detail

3. Repositories

Maciej Malarz (@malarzm)

Page 31: Database? Meh, implementation detail

We had this action:

public function listAction()

{

$submissions = $this->getDoctrine()->getManager()

->getRepository(Submission::class)

->findBy(['acceptedOn' => null, 'rejectedOn' => null]);

return [ 'submissions' => $submissions ];

}

Maciej Malarz (@malarzm)

Page 32: Database? Meh, implementation detail

Custom repositories

class SubmissionRepository extends EntityRepository

{

public function findPending()

{

return $this->findBy(['acceptedOn' => null, 'rejectedOn' => null]);

}

}

class SubmissionManager

{

public function getRepository()

{

return $this->objectManager->getRepository(Submission::class);

}

}

public function listAction()

{

$submissions = $this->get('submission_repository')->findPending();

return [ 'submissions' => $submissions ];

}

Maciej Malarz (@malarzm)

Page 33: Database? Meh, implementation detail

Moar customization

class SubmissionRepository extends EntityRepository

{

/** @var SubmissionContext */

private $context;

public function __construct(EntityManager $em, ClassMetadata $class, SubmissionContext $context)

{

parent::__construct($em, $class);

$this->context = $context;

}

public function findAccepted()

{

$qb = $this->createQueryBuilder('s');

$accepted = $qb->where($qb->expr()->isNotNull('s.accepted'))

->getQuery()->getArrayResult();

return $this->finalizeCollection($accepted);

}

public function findPending()

{

return $this->finalizeCollection($this->findBy(['acceptedOn' => null, 'rejectedOn' => null]));

}

private function finalizeCollection($submissions)

{

return array_filter($submissions, array($this->context, 'applyRules'));

}

}

Maciej Malarz (@malarzm)

Page 34: Database? Meh, implementation detail

Separate queries

class DoctrineSubmissionQuery

{

/** @var SubmissionContext */

private $context;

/** @var SubmissionRepository */

private $repository;

public function __construct(SubmissionRepository $repository, SubmissionContext $context)

{

$this->context = $context;

$this->repository = $repository;

}

public function __invoke()

{

// ...

}

}

Maciej Malarz (@malarzm)

Page 35: Database? Meh, implementation detail

Wanna cache? No problem!

class CacheSubmissionQuery

{

/** @var Cache */

private $cache;

/** @var SubmissionContext */

private $context;

public function __construct(Cache $cache, SubmissionContext $context)

{

$this->cache = $cache;

$this->context = $context;

}

public function __invoke()

{

// Get data from check based on some context hash

}

}

Maciej Malarz (@malarzm)

Page 36: Database? Meh, implementation detail

Recap: Repositories

Repositories contains business-specific

methods for locating entities.

Maciej Malarz (@malarzm)

Page 37: Database? Meh, implementation detail

4. Entities

Maciej Malarz (@malarzm)

Page 38: Database? Meh, implementation detail

Stay valid after __construct

$payment = new Payment();

$payment->setCurrency('USD');

$payment->setAmount(-69);

Page 39: Database? Meh, implementation detail

Stay valid after __construct

class Payment

{

private $amount;

private $currency;

public function __construct($amount, $currency)

{

if ((int) $amount <= 0) {

throw new \InvalidArgumentException('Payment amount must be greater than 0');

}

if ( ! in_array($currency, ['USD', 'PLN'])) {

throw new \InvalidArgumentException($currency . ' currency is not allowed');

}

$this->amount = $amount;

$this->currency = $currency;

}

}

Maciej Malarz (@malarzm)

Page 40: Database? Meh, implementation detail

Stay valid after __construct

Maciej Malarz (@malarzm)

Page 41: Database? Meh, implementation detail

You may need DTO

class PaymentDTO

{

/**

* @Assert\GreaterThan(0)

*/

public $amount;

public $currency;

public function toPayment()

{

return new Payment($this->amount, $this->currency);

}

}

Maciej Malarz (@malarzm)

Page 42: Database? Meh, implementation detail

Recap: Entities

A thing with unique and independent

existence. Since it does exist it shall be

always valid.

Maciej Malarz (@malarzm)

Page 43: Database? Meh, implementation detail

Recap: All ze stuff

Value Objects

Maciej Malarz (@malarzm)

Object Managers

Repositories Entities

Page 44: Database? Meh, implementation detail

5. Holy Grails

Maciej Malarz (@malarzm)

Page 45: Database? Meh, implementation detail

5.1 Tests

Maciej Malarz (@malarzm)

Page 46: Database? Meh, implementation detail

Not very testable

/**

* @Route("/", name="homepage")

*/

public function listAction()

{

$submissions = $this->getDoctrine()->getManager()

->getRepository(Submission::class)

->findBy(['acceptedOn' => null, 'rejectedOn' => null]);

return [ 'submissions' => $submissions ];

}

Maciej Malarz (@malarzm)

Page 47: Database? Meh, implementation detail

Better but...

class SubmissionRepository extends EntityRepository

{

// public function __construct(EntityManager $em, ClassMetadata $class)

// {

// parent::__construct($em, $class);

// }

public function findPending()

{

return $this->findBy(['acceptedOn' => null, 'rejectedOn' => null]);

}

}

Maciej Malarz (@malarzm)

Page 48: Database? Meh, implementation detail

Pull out an interface

interface SubmissionRepository

{

public function findPending();

}

class DoctrineSubmissionRepository extends EntityRepository implements SubmissionRepository

{

public function findPending()

{

return $this->findBy(['acceptedOn' => null, 'rejectedOn' => null]);

}

}

class InMemorySubmissionRepository implements SubmissionRepository

{

private $data = array();

public function findPending()

{

return array_filter($this->data, function(Submission $s) {

return $s->getAcceptedOn() === null && $s->getRejectedOn() === null;

});

}

}

Maciej Malarz (@malarzm)

Page 49: Database? Meh, implementation detail

Need stub?

class InMemorySubmissionRepository implements SubmissionRepository

{

private $data = array();

public function __construct(array $data = array())

{

$this->data = $data;

}

public function findPending()

{

return array_filter($this->data, function(Submission $s) {

return $s->getAcceptedOn() === null && $s->getRejectedOn() === null;

});

}

}

Maciej Malarz (@malarzm)

Page 50: Database? Meh, implementation detail

Or full flow?

interface SubmissionManager

{

public function find($id);

public function save(Submission $submission);

}

class InMemorySubmissionManager implements SubmissionManager

{

/** @var InMemorySubmissionRepository */

private $repository;

public function __construct(InMemorySubmissionRepository $repository)

{

$this->repository = $repository;

}

public function find($id)

{

return $this->repository->find($id);

}

public function save(Submission $submission)

{

$this->repository->store($submission);

}

}

Maciej Malarz (@malarzm)

Page 51: Database? Meh, implementation detail

Applies everywhere!

interface TaxCalculator

{

public function calculate(Income $income);

}

class Some3rdPartTaxCalculator implements TaxCalculator

{

public function calculate(Income $income)

{

// Some expensive API call

}

}

class StubbedTaxCalculator implements TaxCalculator

{

public function calculate(Income $income)

{

return $income->getMoney() * 0.19;

}

}

Maciej Malarz (@malarzm)

Page 52: Database? Meh, implementation detail

5.2 One-off storages

Maciej Malarz (@malarzm)

Page 53: Database? Meh, implementation detail

Hey, let's integrate with UniSubmission8.0

class UniSubmissionManager implements SubmissionManager

{

/** @var UniSubmissionApi */

private $api;

public function __construct(UniSubmissionApi $api)

{

$this->api = $api;

}

public function find($id)

{

$this->api->find($id);

}

public function save(Submission $submission)

{

$this->api->push($submission);

}

}

class UniSubmissionRepository implements SubmissionRepository

{

// ...

}

Maciej Malarz (@malarzm)

Page 54: Database? Meh, implementation detail

5.3 Changing ORM

Maciej Malarz (@malarzm)

Page 55: Database? Meh, implementation detail

Doctrine not cool anymore

class SuperDuperORMSubmissionManager implements SubmissionManager

{

/** @var SuperDuperORM */

private $orm;

/** @var EventDispatcherInterface */

private $eventDispatcher;

public function __construct(SuperDuperORM $orm, EventDispatcherInterface $eventDispatcher)

{

$this->orm = $orm;

$this->eventDispatcher = $eventDispatcher;

}

public function find($id)

{

$this->orm->gimme(Submission::class, $id);

}

public function save(Submission $submission)

{

if ($this->orm->haz($submission)) {

$this->eventDispatcher->dispatch('submission.update', new SubmissionEvent($submission));

} else {

$this->orm->register($submission);

$this->eventDispatcher->dispatch('submission.create', new SubmissionEvent($submission));

}

$this->orm->push($submission);

}

}

Maciej Malarz (@malarzm)

Page 56: Database? Meh, implementation detail

But for this you should really have own persistence

interface and write adapters

Maciej Malarz (@malarzm)

Page 57: Database? Meh, implementation detail

5.4 Integrity

Maciej Malarz (@malarzm)

Page 58: Database? Meh, implementation detail

Saw (actually written, I regret) this

class Menu

{

private $tree;

private $flattened;

public function getFlattened()

{

if ($this->flattened !== null) {

return $this->flattened;

}

return $this->flattened = $this->doFlattenTree();

}

public function removeNode(Node $node)

{

$this->tree->remove($node);

// "Meh, after removing node the page is refreshing, no need to refresh flattened structure"

}

}

Maciej Malarz (@malarzm)

Page 59: Database? Meh, implementation detail

6. Thank you!

Maciej Malarz (@malarzm)

Page 60: Database? Meh, implementation detail

7. Questions?

Maciej Malarz (@malarzm)

Page 61: Database? Meh, implementation detail

8. Thank you!

Maciej Malarz (@malarzm)

https://joind.in/16534