Wybór bazy danych lub jej warstwy abstrakcji na początku tworzenia projektu jest czymś naturalnym, ale czy na pewno dobrym? Jak to może wpłynąć na modelowanie domeny? Co mógłbyś osiągnąć, gdybyś dla odmiany zapomniał, że używasz Doctrine/Eloquent/Hibernate/TwojegoUlubionegoORMa?
19. Introducing VO
/** @ORMEmbeddable */
abstract class AbstractStatus
{
/** @ORMOneToOne(targetEntity="User") */
private $by;
/** @ORMColumn(type="datetime") */
private $on;
/** @ORMColumn(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)
20. Mapping
class Submission
{
/** @ORMColumn(type="string") */
private $applicant;
/** @ORMColumn(type="string") */
private $slug;
/** @ORMEmbedded(class="???") */
private $status;
/** @ORMColumn(type="string") */
private $title;
}
Maciej Malarz (@malarzm)
21. Actually you can but with
MongoDB ODM ;)
Maciej Malarz (@malarzm)
22. /** @ORMEmbeddable */
class Status
{
const ACCEPTED = 0;
const CANCELLED = 1;
const REJECTED = 2;
/** @ORMOneToOne(targetEntity="User") */
private $by;
/** @ORMColumn(type="datetime") */
private $on;
/** @ORMColumn(type="string") */
private $message;
/** @ORMColumn(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)
23. A bit better...
/** @ORMEntity */
class Submission
{
/**
* @ORMEmbedded(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)
24. We can do better
Just don't use DB mapped entities as domain entities
Maciej Malarz (@malarzm)
25. Recap: Value Objects
A small simple object, like money or a
date range, whose equality isn't based on
identity.
Maciej Malarz (@malarzm)
27. 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="AppBundleEntitySubmission")
* @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)
28. 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)
29. 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="AppBundleEntitySubmission")
* @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)
30. 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)
33. 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)
34. 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)
35. 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)
36. Verbs matter
class SubmissionRepository
{
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)
37. 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)
38. 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)
41. Stay valid after __construct
$payment = new Payment();
$payment->setCurrency('USD');
$payment->setAmount(-69);
42. 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)
44. You may need DTO
class PaymentDTO
{
/**
* @AssertGreaterThan(0)
*/
public $amount;
public $currency;
public function toPayment()
{
return new Payment($this->amount, $this->currency);
}
}
Maciej Malarz (@malarzm)
45. Recap: Entities
A thing with unique and independent
existence. Since it does exist it shall be
always valid.
Maciej Malarz (@malarzm)
46. Recap: All ze stuff
Value Objects
Maciej Malarz (@malarzm)
Object Managers
Repositories Entities
49. 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)
50. 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)
51. 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)
52. 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)
53. 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)
54. 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)
56. 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)
61. 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)