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

Implementing DDD in your Symfony project

Implementing DDD in your Symfony project

This presentation shows a simple approach of a DDD implementation in a Symfony project.
Was exposed in deSymfonyDay 2014 (Barcelona)

Sergio Moya

May 31, 2014
Tweet

More Decks by Sergio Moya

Other Decks in Technology

Transcript

  1. $ *$# %& $ * Backend Engineer at Social Point

    ! DDD Fan ! Hip Hop Culture lover ! Badaloní (from Badalona) $#  *! (  ((($#  *$
  2. (%(($ An example of a Business requested feature based on

    a TRUE STORY Project file distribution The Domain Model Use of Domain Events Adapter for our component ! Integrating in our Symfony Project
  3.      %)% &"&% &$ & 

    )!#%  &  %)%  * &## 
  4. &"&% &$ &  &
  %)%   A

    sphere of knowledge, influence, or activity. The subject area to which the user applies a program is the domain of the software. A description of a boundary (typically a subsystem, or the work of a particular team) within which a particular model is defined and applicable. A language structured around the domain model and used by all team members within a bounded context to connect all the activities of the team with the software. %$$
  5. The Lead of the Backend engineers team has decided to

    split the team into smaller two-member teams. ! Each team will develop a different component. ! Our team will develop the Payment Component
  6. %#"&#%$ THERE IS NO BASKET OR CART ! ONE PRODUCT,

    ONE ORDER ! A PRODUCT WILL HAVE CONDITIONS TO BE PURCHASED ! THE SYSTEM SHOULD TRACK THE ORDERS
  7. %%%* ## /**! * CustomerOrder! *! * @ORM\Table()! * @ORM\Entity(repositoryClass="Tato\Bundle\PaymentBundle\Entity

    \CustomerOrderRepository")! */! class CustomerOrder! {! /**! * Set product! *! * @param \stdClass $product! * @return CustomerOrder! */! public function setProduct($product)! {! $this->product = $product;! ! return $this;! }! . . .! }! !
  8. %%%*&$% # /**! * Customer! *! * @ORM\Table()! * @ORM\Entity(repositoryClass="Tato\Bundle\PaymentBundle\Entity

    \CustomerRepository")! */! class Customer! {! /**! * @var integer! *! * @ORM\Column(name="id", type="integer")! * @ORM\Id! * @ORM\GeneratedValue(strategy="AUTO")! */! private $id;! ! . . .! }! !
  9. % ### public function buyProduct(Product $product, Customer $customer, Money $moneyPayed)!

    {! $productPrice = $product->getPrice();! if ($moneyPayed->getCurrency() !== $productPrice->getCurrency()) {! throw new OrderException('The Currency of that payment is invalid');! }! ! if ($moneyPayed->getPrice() !== $productPrice->getPrice()) {! throw new OrderException('You must pay me!');! }! ! $conditions = $product->getConditions();! ! foreach ($conditions as $condition) {! if (!$condition->isValid($customer)) {! throw new OrderException('You must meet the product requirements!');! }! }! ! $goods = $product->getGoods();! foreach ($goods as $good) {! $good->give($customer);! }! }!
  10. &# &$% # public function order(Product $product)! {! $order =

    new Order(rand(), $this, $product);! $this->recordThat(! new OrderCreated($order->getId(), $this->id, $product)! );! ! return $order;! }! We are recording Events!
  11. &# &$% # public function pay(Order $order, Money $money)! {!

    $status = $order->pay($money);! $this->recordThat(new OrderPaid($this->id, $order->getId(), $money));! if ($status === OrderStatus::ORDER_FAILED) {! $this->recordThat(new OrderFailed($order->getId(), $this->id));! }! if ($status === OrderStatus::ORDER_PAID) {! $this->recordThat(! new OrderPaid($order->getId(), $this->id, $money)! );! }! return $status;! }
  12. &#  ## AN ORDER MUST ENSURE THAT THEIR PRODUCT

    MEETS THEIR CONDITIONS ! AN ORDER CAN BE PAID
  13. &#  ## public function __construct($id, Customer $customer, $product)! {!

    $this->id = $id;! $this->customer = $customer;! $this->status = new OrderStatus(OrderStatus::ORDER_STARTED);! $this->guardProductConditions($product);! $this->product = $product;! } Ensure that the product satisfies business conditions
  14. public function pay(Money $money)! {! try {! if (!$this->product->getPrice()->isEqualTo($money)) {!

    throw new OrderException('The Order amount is not satisfied');! }! $this->product->deliver($this->customer);! $this->status = OrderStatus::create(OrderStatus::ORDER_PAID);! } catch (\Exception $e) {! $this->status = OrderStatus::create(OrderStatus::ORDER_FAILED);! }! return $this->status;! } &#  ## Return what we need
  15. interface OrderRepository! {! public function orderExists($orderId);! ! public function find($id,

    $customerId);! ! public function findByCustomerId($customerId);! ! public function save($id, $productId, $customerId, $status = 1);! ! public function remove($id);! ! public function removeByCustomerId($customerId);! ! public function clearAll();! }! &#  ## We provide an Interface
  16. public function buyProductAction(Request $request)! {! $customer = new Customer($request->request->get('customerId'));! !

    $productRepository = $this->get('tato.payment.product_respository');! $product = $productRepository->find($request->request->get('productId'));! ! $order = $customer->order($product);! ! $paid = new Money(! $request->request->get('paid'), ! $request->request->get(‘currency')! );! $status = $order->pay($paid);! ! $this->get(‘tato.payment.order_repository')->save(! ! ! $order->getId(), ! $product, ! $customer->getId(), ! $status->getStatus()! );! ! return new JsonResponse(array('status' => $status->getStatus()));! }! } $* * %# #
  17. public function handle(BuyProductCommand $command)! {! $customer = new Customer($command->customerId);! $product

    = $this->productRepository->find($command->productId);! ! $order = $customer->order($product);! $status = $order->pay(new Money($command->paid, $command->currency));! ! $events = $customer->getRecordedEvents();! ! // @todo do something with this events! ! $this->orderRepository->setOrder(! $command->gatewayName,! $order->getId(),! $command->customerId,! $status->getStatus()! );! ! return $order;! }  # We have Events!
  18. class BuyProductCommand! {! public $customerId;! ! public $productId;! ! public

    $paid;! ! public $currency;! ! public function __construct($customerId, $productId, $paid, $currency)! {! $this->customerId = $customerId;! $this->productId = $productId;! $this->paid = $paid;! $this->currency = $currency;! } ! } %  A simple DTO
  19. ('$ An example of a Business requested feature based on

    a TRUE STORY Project file distribution The Domain Model Use of Domain Events Adapter for our component ! Integrating in our Symfony Project
  20. &# ###! $% #* ! class CustomOrderRepository extends EntityRepository implements

    OrderRepository! {! . . .! ! /**! * Checks if an order already exists.! *! * @param int $orderId! * @return bool! * @throws \Tato\Component\Payment\Exception\Order \OrderRepositoryException! */! public function orderExists($orderId)! {! ! ! ! $order = $this->entityManager->find('Order:Order', $orderId);! ! return $order instanceof Order;! }! ! . . . ! } DOCTRINE!
  21. $#'$ services:! tato.payment.order_repository:! class: Tato\Component\Game\CustomOrderRepository! arguments:! - @doctrine.orm.entity_manager! ! tato.payment.product_repository:!

    class: Tato\Component\Game\Product\CustomProductRepository! arguments:! - @doctrine.orm.entity_manager! ! tato.payment.command.handler.buy_product:! class: Tato\Component\Payment\Command\BuyProductCommandHandler! arguments:! order_repository: @tato.payment.order_repository! product_repository: @tato.payment.product_repository!
  22.  %#!! doctrine:! orm:! auto_generate_proxy_classes: "%kernel.debug%"! auto_mapping: false! mappings:! Order:!

    type: yml! is_bundle: false! dir: %kernel.root_dir%/../src/Tato/Bundle/PaymentBundle/ Resources/config/doctrine! prefix: Tato\Component\Payment\Order! alias: PaymentOrder %%!+$& (% $% # %#%%$ &%$ $* *&
  23. !*% %# # public function buyProductAction(Request $request)! {! $customerId =

    $request->request->get('customerId');! $productId = $request->request->get('productId');! $paid = $request->request->get('paid');! $currency = $request->request->get('currency');! $command = new BuyProductCommand(! $customerId, ! $productId, ! $paid, ! $currency! );! $commandHandler = $this->get('tato.payment.command.handler.buy_product');! $order = $commandHandler->handle($command);! $events = $order->getRecordedEvents();! // @todo do something wit this events! return new JsonResponse(array('status' => $order->getStatus()));! }
  24. public function buyProductAction(Request $request)! {! $customer = new Customer($request->request->get('customerId'));! !

    $productRepository = $this->get('tato.payment.product_respository');! $product = $productRepository->find($request->request->get('productId'));! ! $order = $customer->order($product);! ! $paid = new Money(! $request->request->get('paid'), ! $request->request->get(‘currency')! );! $status = $order->pay($paid);! ! $this->get(‘tato.payment.order_repository')->save(! ! ! $order->getId(), ! $product, ! $customer->getId(), ! $status->getStatus()! );! ! return new JsonResponse(array('status' => $status->getStatus()));! }! } %#$%!!#  Business Specification in a Controller Coupled to Symfony DIC