database? meh, implementation detail
TRANSCRIPT
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)
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)
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)
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)
et voilà!
Maciej Malarz (@malarzm)
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)
New features
Maciej Malarz (@malarzm)
Oh hey, project was a success!
Maciej Malarz (@malarzm)
1. Value Objects
Maciej Malarz (@malarzm)
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)
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)
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)
Actually you can but with MongoDB ODM ;)
Maciej Malarz (@malarzm)
/** @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)
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)
We can do better
Just don't use DB mapped entities as domain entities
Maciej Malarz (@malarzm)
Recap: Value Objects
A small simple object, like money or a
date range, whose equality isn't based on
identity.
Maciej Malarz (@malarzm)
2. Object Managers
Maciej Malarz (@malarzm)
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)
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)
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)
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)
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)
Recap: Object Managers
Manages lifecycle and instances of entities.
Maciej Malarz (@malarzm)
3. Repositories
Maciej Malarz (@malarzm)
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)
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)
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)
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)
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)
Recap: Repositories
Repositories contains business-specific
methods for locating entities.
Maciej Malarz (@malarzm)
4. Entities
Maciej Malarz (@malarzm)
Stay valid after __construct
$payment = new Payment();
$payment->setCurrency('USD');
$payment->setAmount(-69);
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)
Stay valid after __construct
Maciej Malarz (@malarzm)
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)
Recap: Entities
A thing with unique and independent
existence. Since it does exist it shall be
always valid.
Maciej Malarz (@malarzm)
Recap: All ze stuff
Value Objects
Maciej Malarz (@malarzm)
Object Managers
Repositories Entities
5. Holy Grails
Maciej Malarz (@malarzm)
5.1 Tests
Maciej Malarz (@malarzm)
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)
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)
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)
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)
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)
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)
5.2 One-off storages
Maciej Malarz (@malarzm)
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)
5.3 Changing ORM
Maciej Malarz (@malarzm)
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)
But for this you should really have own persistence
interface and write adapters
Maciej Malarz (@malarzm)
5.4 Integrity
Maciej Malarz (@malarzm)
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)
6. Thank you!
Maciej Malarz (@malarzm)
7. Questions?
Maciej Malarz (@malarzm)