Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Patterns Behind Doctrine

Patterns Behind Doctrine

How does Doctrine talk to your database? What are Data Mapper, Unit Of Work, and Identity Map? These are the questions I want to answer in this talk. We will look at how Doctrine ORM implements them and what they are there for. Finally we will look at how they compare to Active Record and what the benefits and drawbacks are to help you choose which one fits your needs best.

Denis Brumann

December 06, 2018
Tweet

More Decks by Denis Brumann

Other Decks in Programming

Transcript

  1. <?php declare(strict_types = 1); namespace App\Entity; use Doctrine\ORM\Mapping as ORM;

    /** * @ORM\Entity(repositoryClass="App\Repository\OrderItemRepository") * @ORM\Table(name="app_order_item") */ class OrderItem { /** * @ORM\Id() * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\ManyToOne(targetEntity="App\Entity\Order", inversedBy="items") * @ORM\JoinColumn(nullable=false)
  2. <?php declare(strict_types = 1); namespace App\Entity; use Doctrine\ORM\Mapping as ORM;

    /** * @ORM\Entity(repositoryClass="App\Repository\OrderItemRepository") * @ORM\Table(name="app_order_item") */ class OrderItem { /** * @ORM\Id() * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\ManyToOne(targetEntity="App\Entity\Order", inversedBy="items") * @ORM\JoinColumn(nullable=false)
  3. /** * @ORM\ManyToOne(targetEntity="App\Entity\Order", inversedBy="items") * @ORM\JoinColumn(nullable=false) */ private $order; /**

    * @ORM\Column() */ private $name; /** * @ORM\Column(length=16) */ private $price; public function getId(): int { return $this->id ?? 0; } public function getOrder(): Order
  4. <?php declare(strict_types = 1); namespace App\Entity; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection;

    use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass="App\Repository\OrderRepository") * @ORM\Table(name="app_order") */ class Order { /** * @ORM\Id() * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\OneToMany(targetEntity="App\Entity\OrderItem", mappedBy="order", cascade={"persist"}) */ private $items; /**
  5. <?php declare(strict_types = 1); namespace App\Entity; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection;

    use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass="App\Repository\OrderRepository") * @ORM\Table(name="app_order") */ class Order { /** * @ORM\Id() * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\OneToMany(targetEntity="App\Entity\OrderItem", mappedBy="order", cascade={"persist"}) */ private $items; /**
  6. <?php declare(strict_types = 1); namespace App\Entity; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection;

    use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass="App\Repository\OrderRepository") * @ORM\Table(name="app_order") */ class Order { /** * @ORM\Id() * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\OneToMany(targetEntity="App\Entity\OrderItem", mappedBy="order", cascade={"persist"}) */ private $items; /**
  7. /** * @ORM\Column(type="datetime_immutable") */ private $createdOn; public function __construct() {

    $this->items = new ArrayCollection(); $this->createdOn = new DateTimeImmutable(); } public function getId(): int { return $this->id ?? 0; } public function getCreatedOn(): DateTimeImmutable { return $this->createdOn; } public function getItems(): array { return $this->items->toArray(); } public function addItem(OrderItem $item): void {
  8. /** * @ORM\Column(type="datetime_immutable") */ private $createdOn; public function __construct() {

    $this->items = new ArrayCollection(); $this->createdOn = new DateTimeImmutable(); } public function getId(): int { return $this->id ?? 0; } public function getCreatedOn(): DateTimeImmutable { return $this->createdOn; } public function getItems(): array { return $this->items->toArray(); } public function addItem(OrderItem $item): void {
  9. } public function addItem(OrderItem $item): void { $this->items->add($item); $item->setOrder($this); }

    public function removeItem(OrderItem $item): void { $this->items->remove($item); } public function getItemCount(): int { return $this->items->count(); } public function getTotal(): string { $total = '0'; foreach ($this->items as $item) { $total = bcadd($total, $item->getPrice()); } return $total; } }
  10. } public function addItem(OrderItem $item): void { $this->items->add($item); $item->setOrder($this); }

    public function removeItem(OrderItem $item): void { $this->items->remove($item); } public function getItemCount(): int { return $this->items->count(); } public function getTotal(): string { $total = '0'; foreach ($this->items as $item) { $total = bcadd($total, $item->getPrice()); } return $total; } }
  11. <?php declare(strict_types = 1); namespace App\Order; use App\Entity\Order; use App\Entity\OrderItem;

    use Doctrine\ORM\EntityManagerInterface; class Checkout { private $manager; public function __construct(EntityManagerInterface $manager) { $this->manager = $manager; } public function checkout(array $cartItems): void { $order = new Order(); foreach ($cartItems as $cartItem) { $orderItem = new OrderItem();
  12. public function checkout(array $cartItems): void { $order = new Order();

    foreach ($cartItems as $cartItem) { $orderItem = new OrderItem(); $orderItem->setName($cartItem->getName()); $orderItem->setPrice($cartItem->getPrice()); $order->addItem($orderItem); } $this->manager->persist($order); $this->manager->flush(); }
  13. HOW DOES DOCTRINE KNOW WHAT TO SAVE? We only called

    persist on our order,
 not on the items.
  14. Maintains a list of objects affected by a business transaction

    and coordinates the writing out of changes and the resolution of concurrency problems. UNIT OF WORK
  15. <?php declare(strict_types = 1); namespace App\Entity; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection;

    use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass="App\Repository\OrderRepository") * @ORM\Table(name="app_order") */ class Order { /** * @ORM\Id() * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\OneToMany(targetEntity="App\Entity\OrderItem", mappedBy="order", cascade={"persist"}) */ private $items; /**
  16. public function checkout(array $cartItems): void { $order = new Order();

    foreach ($cartItems as $cartItem) { $orderItem = new OrderItem(); $orderItem->setName($cartItem->getName()); $orderItem->setPrice($cartItem->getPrice()); $order->addItem($orderItem); $this->manager->persist($orderItem); } $this->manager->persist($order); $this->manager->flush(); }
  17. Doctrine ORM recognizes unintentionally non-persisted entities. You have to add

    cascade to the association or persist each entity manually. Only entities managed by the UnitOfWork will be inserted.
  18. $item = new OrderItem(); $item->setName('Socks'); $item->setPrice('999'); $manager->persist($item); $order = new

    Order(); $order->addItem($item); $manager->persist($order); $manager->flush();
  19. $item = new OrderItem(); $item->setName('Socks'); $item->setPrice('999'); $manager->persist($item); $order = new

    Order(); $order->addItem($item); $manager->persist($order); $manager->flush();
  20. $item = new OrderItem(); $item->setName('Socks'); $item->setPrice('999'); $manager->persist($item); $order = new

    Order(); $order->addItem($item); $manager->persist($order); $manager->flush();
  21. UnitOfWork will take care of correctly ordering each commit. You

    can persist your entities in any order you like.
  22. {% extends "base.html.twig" %} {% block body %} <h1>Order list</h1>

    <div class="btn-group mb-2" role="group"> <a href="{{ path('create') }}" class="btn btn-primary">Create sample orders</a> <a href="{{ path('truncate') }}" class="btn btn-primary">Truncate orders</a> </div> <div class="list-group mb-4"> {% for order in orders %} <a href="{{ path('show', {'id': order.id}) }}" class="list-group-item list-group-item-action flex-column align-items-start"> <div class="d-flex w-100 justify-content-between"> <h5 class="mb-1">Order number #{{ order.id }}</h5> <small>{{ order.createdOn|date('d.m.Y') }} </small> </div> <p class="mb-1">contains {{ order.items|length }} items for a total of <strong>{{ order.total / 100 }}€</strong>.</p> </a> {% endfor %} </div> {% endblock %}
  23. {% extends "base.html.twig" %} {% block body %} <h1>Order list</h1>

    <div class="btn-group mb-2" role="group"> <a href="{{ path('create') }}" class="btn btn-primary">Create sample orders</a> <a href="{{ path('truncate') }}" class="btn btn-primary">Truncate orders</a> </div> <div class="list-group mb-4"> {% for order in orders %} <a href="{{ path('show', {'id': order.id}) }}" class="list-group-item list-group-item-action flex-column align-items-start"> <div class="d-flex w-100 justify-content-between"> <h5 class="mb-1">Order number #{{ order.id }}</h5> <small>{{ order.createdOn|date('d.m.Y') }} </small> </div> <p class="mb-1">contains {{ order.items|length }} items for a total of <strong>{{ order.total / 100 }}€</strong>.</p> </a> {% endfor %} </div> {% endblock %}
  24. <?php namespace Proxies\__CG__\App\Entity; /** * DO NOT EDIT THIS FILE

    - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR */ class Order extends \App\Entity\Order implements \Doctrine\ORM\Proxy\Proxy { /** * @var \Closure the callback responsible for loading properties in the proxy object. This callback is called with * three parameters, being respectively the proxy object to be initialized, the method that triggered the * initialization process and an array of ordered parameters that were passed to that method. * * @see \Doctrine\Common\Persistence\Proxy::__setInitializer */ public $__initializer__; /**
  25. 'getCreatedOn', []); return parent::getCreatedOn(); } /** * {@inheritDoc} */ public

    function getItems(): array { $this->__initializer__ && $this->__initializer__->__invoke($this, 'getItems', []); return parent::getItems(); } /** * {@inheritDoc} */ public function addItem(\App\Entity\OrderItem $item): void {
  26. /** * @ORM\Entity(repositoryClass="App\Repository\OrderRepository") * @ORM\Table(name="app_order") */ class Order { /**

    * @ORM\Id() * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\OneToMany( * targetEntity="App\Entity\OrderItem", * mappedBy="order", * cascade={"persist"}, * fetch="EAGER" * ) */ private $items;
  27. public function findOrderWithItems(int $id): Order { return $this->createQueryBuilder('o') ->select(['o', 'i'])

    ->where('o.id = :order_id') ->setParameter('order_id', $id) ->join('o.items', 'i') ->getQuery() ->getSingleResult(); }
  28. public function findOrderWithItems(int $id): Order { return $this->createQueryBuilder('o') ->select(['o', 'i'])

    ->where('o.id = :order_id') ->setParameter('order_id', $id) ->join('o.items', 'i') ->getQuery() ->getSingleResult(); }
  29. public function findOrderWithItems(int $id): Order { return $this->createQueryBuilder('o') ->select(['o', 'i'])

    ->where('o.id = :order_id') ->setParameter('order_id', $id) ->join('o.items', 'i') ->getQuery() ->getSingleResult(); }
  30. <?php declare(strict_types = 1); namespace App\DTO; use DateTimeImmutable; final class

    OrderSummary { private $orderId; private $orderDate; private $itemCount; private $total; public function __construct(int $orderId, DateTimeImmutable $orderDate, int $itemCount, int $total) { $this->orderId = $orderId; $this->orderDate = $orderDate; $this->itemCount = $itemCount; $this->total = $total; } public function getOrderId(): int { return $this->orderId; } public function getOrderDate(): DateTimeImmutable { return $this->orderDate; }
  31. { $this->orderId = $orderId; $this->orderDate = $orderDate; $this->itemCount = $itemCount;

    $this->total = $total; } public function getOrderId(): int { return $this->orderId; } public function getOrderDate(): DateTimeImmutable { return $this->orderDate; } public function getItemCount(): int { return $this->itemCount; } public function getTotal(): int { return $this->total; } }
  32. public function findSummarizedOrders(): array { $dql = 'SELECT NEW App\\DTO\\OrderSummary(

    ' . 'o.id, o.createdOn, COUNT(i.order), SUM(i.price)' . ') FROM App\\Entity\\Order o ' . 'JOIN o.items i GROUP BY i.order'; return $this->getEntityManager() ->createQuery($dql) ->getResult(); }
  33. public function findMostExpensiveOrder(): ?Order { $sql = <<<SQL SELECT o.*,

    SUM(i.price) AS order_sum FROM app_order AS o JOIN app_order_item i ON o.id = i.order_id GROUP BY i.order_id ORDER BY order_sum DESC LIMIT 1; SQL; $resultSetMapping = new ResultSetMappingBuilder($this->getEntityManager()); $resultSetMapping->addRootEntityFromClassMetadata(Order::class, 'o'); return $this->getEntityManager()->createNativeQuery($sql, $resultSetMapping)->getOneOrNullResult(); }
  34. Doctrine loads associated entities lazily by default. Writing custom queries

    with JOIN, hydrate results into custom DTO or as scalar results can improve read-performance.
  35. Doctrine stores previously loaded entities in an IdentityMap. Loading an

    entity multiple times does only perform 1 database query (in some special cases). Each new find() returns the current entity (with its unsaved changes).
  36. $item = new OrderItem(); $item->setName('Coffee'); $item->setPrice('149'); $order = new Order();

    $order->setId(1); $order->addItem($item); $this->manager->flush();
  37. $item = new OrderItem(); $item->setName('Coffee'); $item->setPrice('149'); $order = new Order();

    $order->setId(1); $order->addItem($item); $this->manager->flush(); WILL THIS WORK?
  38. Entity States A NEW entity instance has no persistent identity,

    and is not yet associated with an EntityManager and a UnitOfWork (i.e. those just created with the "new" operator).
  39. Entity States A MANAGED entity instance is an instance with

    a persistent identity that is associated with an EntityManager and whose persistence is thus managed.
  40. Entity States A DETACHED entity instance is an instance with

    a persistent identity that is not (or no longer) associated with an EntityManager and a UnitOfWork.
  41. Entity States A REMOVED entity instance is an instance with

    a persistent identity, associated with an EntityManager, that will be removed from the database upon transaction commit.
  42. /** * An entity is in MANAGED state when its

    persistence is managed by an EntityManager. */ const STATE_MANAGED = 1; /** * An entity is new if it has just been instantiated (i.e. using the "new" operator) * and is not (yet) managed by an EntityManager. */ const STATE_NEW = 2; /** * A detached entity is an instance with persistent state and identity that is not * (or no longer) associated with an EntityManager (and a UnitOfWork). */ const STATE_DETACHED = 3; /** * A removed entity instance is an instance with a persistent identity, * associated with an EntityManager, whose persistent state will be deleted * on commit. */ const STATE_REMOVED = 4;
  43. $item = new OrderItem(); $item->setName('Coffee'); $item->setPrice('149'); $order = new Order();

    $order->setId(1); $order->addItem($item); $this->manager->persist($order); $this->manager->flush();
  44. $item = new OrderItem(); $item->setName('Coffee'); $item->setPrice('149'); $order = $this->entityManager
 ->find(Order::class,

    $id); $order->addItem($item); $this->manager->persist($order); $this->manager->flush();
  45. $item = new OrderItem(); $item->setName('Coffee'); $item->setPrice('149'); $order = $this->entityManager
 ->find(Order::class,

    $id); $order->addItem($item); $this->manager->persist($order); $this->manager->flush();
  46. $item = new OrderItem(); $item->setName('Coffee'); $item->setPrice('149'); $order = $this->entityManager
 ->find(Order::class,

    $id); $order->addItem($item); $this->manager->persist($order); $this->manager->flush();
  47. $repository = $this->entityManager->getRepository(Order::class); $order = $repository->findOrderWithItems($id); $items = $order->getItems(); $randomItem

    = $items[random_int(0, count($items) - 1)]; $randomItem->setPrice('1'); $this->entityManager->flush();
  48. $oid = spl_object_hash($entity) This function returns a unique identifier for

    the object. This id can be used as a hash key for storing objects, or for identifying an object, as long as the object is not destroyed. Once the object is destroyed, its hash may be reused for other objects.
  49. Doctrine internally references objects through their object hash, instead of

    their id. Simply adding an id to an entity will perform an insert, not an update.
  50. An object that wraps a row in a database table

    or view, encapsulates the database access, and adds domain logic on that data. https://www.martinfowler.com/eaaCatalog/activeRecord.html
  51. Models allow you to query for data in your tables,

    as well as insert new records into the table.
  52. /** * Doctrine_Record * All record classes should inherit this

    super class * * @package Doctrine * @subpackage Record * @author Konsta Vesterinen <[email protected]> * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link www.doctrine-project.org * @since 1.0 * @version $Revision: 7673 $ */ abstract class Doctrine_Record extends Doctrine_Record_Abstract implements Countable, IteratorAggregate, Serializable { /** * STATE CONSTANTS */ /** * DIRTY STATE * a Doctrine_Record is in dirty state when its properties are changed */ const STATE_DIRTY = 1; /** * TDIRTY STATE * a Doctrine_Record is in transient dirty state when it is created * and some of its fields are modified but it is NOT yet persisted into database
  53. * TDIRTY STATE * a Doctrine_Record is in transient dirty

    state when it is created * and some of its fields are modified but it is NOT yet persisted into database */ const STATE_TDIRTY = 2; /** * CLEAN STATE * a Doctrine_Record is in clean state when all of its properties are loaded from the database * and none of its properties are changed */ const STATE_CLEAN = 3; /** * PROXY STATE * a Doctrine_Record is in proxy state when its properties are not fully loaded */ const STATE_PROXY = 4; /** * NEW TCLEAN * a Doctrine_Record is in transient clean state when it is created and none of its fields are modified */ const STATE_TCLEAN = 5; /** * LOCKED STATE * a Doctrine_Record is temporarily locked during deletes and saves *
  54. * TDIRTY STATE * a Doctrine_Record is in transient dirty

    state when it is created * and some of its fields are modified but it is NOT yet persisted into database */ const STATE_TDIRTY = 2; /** * CLEAN STATE * a Doctrine_Record is in clean state when all of its properties are loaded from the database * and none of its properties are changed */ const STATE_CLEAN = 3; /** * PROXY STATE * a Doctrine_Record is in proxy state when its properties are not fully loaded */ const STATE_PROXY = 4; /** * NEW TCLEAN * a Doctrine_Record is in transient clean state when it is created and none of its fields are modified */ const STATE_TCLEAN = 5; /** * LOCKED STATE * a Doctrine_Record is temporarily locked during deletes and saves *
  55. * TDIRTY STATE * a Doctrine_Record is in transient dirty

    state when it is created * and some of its fields are modified but it is NOT yet persisted into database */ const STATE_TDIRTY = 2; /** * CLEAN STATE * a Doctrine_Record is in clean state when all of its properties are loaded from the database * and none of its properties are changed */ const STATE_CLEAN = 3; /** * PROXY STATE * a Doctrine_Record is in proxy state when its properties are not fully loaded */ const STATE_PROXY = 4; /** * NEW TCLEAN * a Doctrine_Record is in transient clean state when it is created and none of its fields are modified */ const STATE_TCLEAN = 5; /** * LOCKED STATE * a Doctrine_Record is temporarily locked during deletes and saves *
  56. $item = new OrderItem(); $item->setName('Coffee'); $item->setPrice('149');
 $order = new Order();

    $order->addItem($item); $this->manager->persist($order); $this->manager->flush();
  57. $item = new OrderItem(); $item->setName('Coffee'); $item->setPrice('149');
 $order = new Order();

    $order->addItem($item); $this->manager->persist($order); $this->manager->flush();
  58. Data Mapper Active Record logic & data is split up

    into multiple classes:
 Entity Manager, UnitOfWork, Repository carries both data and persistence logic:
 Base Model explicit configuration implicit configuration
 (convention over configuration) more code to write less code to write Model can be independent from ORM places restrictions on model
 no private properties, inherit __construct