Decoupling Ulabox.com
monolith
From CRUD to DDD
Aleix Vergés
Backend developer
Scrum Master
@avergess
aleix.verges@ulabox.com
1. What’s Ulabox?
2. Decoupling. Why?
/**
* @param Cart $cart
* @return Order $order
*/
public function createOrder(Cart $cart)
{
// Year 2011
$order = $this->moveCartToOrder($cart);
$this->sendConfirmationEmail($order);
$this->reindexSolr($order);
$this->sendToWarehouse($order);
// Year 2012
$this->sendToFinantialErp($order);
$this->sendDonationEmail($order);
// Year 2014
$this->sendToDeliveryRoutingSoftware($order);
// Year 2015
$this->sendToJustInTimeSuppliers($order);
// Year 2016
$this->sendToWarehouseSpoke($order); // WTF!
$this->sendToShipLoadSoftware($order); // WTF!!!!
}
Decoupling Ulabox.com monolith. From CRUD to DDD
Our problem
WTF!
Past
● CRUD doesn’t make sense anymore
● It had sense at the beginning
● Product, Logistic, Delivery, Cart, Customers, ...
● It’s not sustainable.
Decoupling Ulabox.com monolith. From CRUD to DDD
Our solution
/**
* @param Cart $cart
*/
public function createOrder(Cart $cart)
{
$createOrder = new CreateOrderCommand(Cart $cart);
$this->commandBus->dispatch($createOrder)
}
Event bus
CreateOrder command
OrderWasCreated event
subscribe
subscribe
subscribe
subscribe
subscribe
subscribe
subscribe
subscribe
subscribe
Decoupling Ulabox.com monolith. From CRUD to DDD
3. The tools
● Domain
● Aggregate / Aggregate Root
● Repository
● Domain Events
● Service
● Command Bus
● Event Bus
* Domain Drive Design: https://en.wikipedia.org/wiki/Domain-driven_design
Decoupling Ulabox.com monolith. From CRUD to DDD
4. A responsability question
Refactoring and manage technical debt is
not a choice, but a responsability
Decoupling Ulabox.com monolith. From CRUD to DDD
5. Controllers
REFUND!
5.1. OrderController
5. Controllers
class OrderController extends BaseController
{
public function refundAction(Request $request, $id)
{
$em = $this->container->get('doctrine.orm.entity_manager');
$orderPayment = $em->getRepository('UlaboxCoreBundle:OrderPayment')->find($id);
$amount = $request->request->get('refund');
$data = $this->container->get('sermepa')->processRefund($orderPayment, $amount);
$orderRefund = new OrderPayment();
$orderRefund->setAmount($amount);
...
$em->persist($orderRefund);
$em->flush();
return $this->redirectToRoute('order_show', ['id' => $orderPayment->getOrder()->getId()]);
}
public function someOtherAction(Request $request, $id)
...
}
Decoupling Ulabox.com monolith. From CRUD to DDD
Problems
● Hidden dependencies
● Inheritance.
● Biz logic in the controller.
● Non aggregate root.
● Difficult to test.
* Dependency Injection: https://es.wikipedia.org/wiki/Inyecci%C3%B3n_de_dependencias
Decoupling Ulabox.com monolith. From CRUD to DDD
Solutions
● Dependency Injection
● Break inheritance from base controller.
● Application services.
● Testing
5.2. Controller as a service
5. Controllers
# services.yml
imports:
- { resource: controllers.yml }
# controllers.yml
ulabox_ulaoffice.controllers.order:
class: UlaboxUlaofficeBundleControllerOrderController
arguments:
- '@refund'
- '@router'
...
5. Controllers
5.3. Dependency Injection
/**
* @Route("/orders", service="ulabox_ulaoffice.controllers.order")
*/
class OrderController
{
/**
* @param Refund $refund
* @param RouterInterface $router
*/
public function __construct(Refund $refund, RouterInterface $router, ……..)
{
$this->refund = $refund;
$this->router = $router;
...
}
}
5. Controllers
5.4. Delegate logic to services
/**
* @Route("/orders", service="ulabox_ulaoffice.controllers.order")
*/
class OrderController
{
public function refundAction(Request $request, $id)
{
$amount = $request->request->get('refund');
$method = $request->request->get('method');
$orderId = $request->request->get('order_id');
try {
$this->refund->execute($orderId, $id, (float)$amount, $method);
$this->session->getFlashBag()->add('success', 'Refund has been processed correctly');
} catch (Exception $e) {
$this->session->getFlashBag()->add('danger', $e->getMessage());
}
return new RedirectResponse($this->router->generate('order_show', ['id' => $orderId]));
}
}
5. Controllers
5.5. Unit test
class OrderControllerTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->refund = $this->prophesize(Refund::class);
$this->router = $this->prophesize(RouterInterface::class);
$this->orderController = new OrderController(
$this->refund->reveal(),
$this->router->reveal()
);
}
...
}
class OrderControllerTest extends PHPUnit_Framework_TestCase
{
...
public function testShouldDelegateOrderRefund()
{
$orderPaymentId = 34575;
$amount = 10.95;
$orderId = 12345;
$orderRoute = 'some/route';
$request = $this->mockRequest($orderId, $orderPaymentId, $amount, $orderRoute);
$this->refund
->execute($orderId, $orderPaymentId, $amount, PaymentPlatform::REDSYS)
->shouldBeCalled();
$this->router->generate('order_show', ['id' => $orderId])->willReturn($orderRoute);
$actual = $this->orderController->refundAction($request->reveal(), $orderPaymentId);
$this->assertEquals(new RedirectResponse($orderRoute), $actual);
}
}
6. Symfony Forms
RESCHEDULE
6. Symfony Forms
6.1. Anemic Model
class OrderController extends BaseController
{
public function rescheduleAction(Request $request, $id)
{
$order = $this->container->get('order')->reposition($id);
$form = $this->createForm(new OrderType(), $order);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($order);
$em->flush();
$request->getSession()->getFlashBag()->add('success', 'Your changes were saved!');
return $this->redirect($this->generateUrl('reschedule_success'));
}
return ['entity' => $entity, 'form' => $form->createView()];
}
}
Decoupling Ulabox.com monolith. From CRUD to DDD
Problems
● Coupling between entities and Symfony Forms.
● Anemic Model.
● Intention?
Decoupling Ulabox.com monolith. From CRUD to DDD
Solutions
● Use of DTO/Command
● Reflect the Intention!
● Rich Domain.
● Testing.
6. Symfony Forms
6.2. Command !== CLI Command
class Reschedule
{
public $orderId;
public $addressId;
public $slotVars;
public $comments;
public function __construct($orderId, $addressId, $slotVars, $comments)
{
$this->orderId = $orderId;
$this->addressId = $addressId;
$this->slotVars = $slotVars;
$this->comments = $comments;
}
}
6. Symfony Forms
6.3. Building the Form
class OrderController extends BaseController
{
public function rescheduleDisplayingAction(Request $request, $id)
{
$order = $this->orderRepository->get($id);
$address = $order->deliveryAddress()->asAddress();
$rescheduleOrder = Reschedule::fromPayload([
'order_id' => $order->getId(),
'address_id' => $address->getId(),
'slot_vars' => $order->deliverySlotVars(),
'comments' => $order->deliveryComments(),
]);
$rescheduleForm = $this->formFactory->create(OrderRescheduleType::class, $rescheduleOrder);
return ['order' => $order, 'form' => $rescheduleForm->createView()];
}
}
6. Symfony Forms
6.4. Submitting the Form
class OrderController extends BaseController
{
public function rescheduleUpdateAction(Request $request, $id)
{
$requestData = $request->get('order_reschedule');
$rescheduleOrder = Reschedule::fromPayload([
'order_id' => $id,
'address_id' => $requestData['addressId'],
'slot_vars' => $requestData['slotVars'],
'comments' => $requestData['comments'],
]);
$rescheduleForm = $this->formFactory->create(OrderRescheduleType::class, $rescheduleOrder);
if ($rescheduleForm->isValid()) {
$this->commandBus->dispatch($rescheduleOrder);
}
return new RedirectResponse($this->router->generate($this->entity Properties['route']));
}
}
6. Symfony Forms
6.5. Unit test
class OrderControllerTest extends PHPUnit_Framework_TestCase
{
public function testShouldDelegateOrderRescheduleToCommandBus()
{
$orderId = 12345;
$addressId = 6789;
$slotVars = '2016-03-25|523|2|15';
$comments = 'some comments';
$expectedRoute = 'http://some.return.url';
$request = $this->mockRequest($orderId, $addressId, $slotVars, $comments);
$form = $this->mockForm();
$form->isValid()->willReturn(true);
$this->router->generate('order')->willReturn($expectedRoute);
$this->commandBus->dispatch(Argument::type(Reschedule::class))->shouldBeCalled();
$actual = $this->orderController->rescheduleUpdateAction($request->reveal(), $orderId);
$this->assertEquals(new RedirectResponse($expectedRoute), $actual);
}
}
7. From CRUD to DDD
7. From CRUD to DDD
7.1. Summing...
class OrderController extends BaseController
{
public function rescheduleUpdateAction(Request $request, $id)
{
$requestData = $request->get('order_reschedule');
$rescheduleOrder = Reschedule::fromPayload([
'order_id' => $id,
'address_id' => $requestData['addressId'],
'slot_vars' => $requestData['slotVars'],
'comments' => $requestData['comments'],
]);
$rescheduleForm = $this->formFactory->create(OrderRescheduleType::class, $rescheduleOrder);
if ($rescheduleForm>isValid()) {
$this->commandBus->dispatch($rescheduleOrder);
}
return new RedirectResponse($this->router->generate($this->entity Properties['route']));
}
}
7. From CRUD to DDD
7.2. Handling
class RescheduleHandler extends CommandHandler
{
public function __construct( ... ) { ... }
public function handleReschedule(Reschedule $rescheduleOrder)
{
$timeLineSlot = $this->slotManager->createTimelineSlotFromVars($rescheduleOrder->slotVars);
$order = $this->orderRepository->get($rescheduleOrder->aggregateId);
$delivery = $order->getOrderDelivery();
$delivery->setSlot($timeLineSlot->getSlot());
$delivery->setLoadTime($timeLineSlot->getLoadTime());
$delivery->setShift($timeLineSlot->getShift()->getShift());
...
$order->rescheduleDelivery($delivery);
$this->orderRepository->save($order);
$this->eventBus->publish($order->getUncommittedEvents());
}
}
Decoupling Ulabox.com monolith. From CRUD to DDD
Problems
● Biz logic out of domain.
● Aggregate access.
● Aggregate Root?
● Unprotected Domain.
Decoupling Ulabox.com monolith. From CRUD to DDD
Solutions
● Aggregate Root. Order or Delivery?
● Unique acces point to the domain.
● Clear intention!!
● Testing.
7. From CRUD to DDD
7.3. Order or Delivery?
7. From CRUD to DDD
7.4. Aggregate access point
class RescheduleHandler extends CommandHandler
{
public function __construct( ... ) { ... }
public function handleReschedule(Reschedule $rescheduleDelivery)
{
$timeLineSlot = $this->slotManager->createTimelineSlotFromVars($rescheduleDelivery->slotVars);
$delivery = $this->deliveryRepository->get($rescheduleDelivery->deliveryId);
$delivery->reschedule($timeLineSlot);
$this->deliveryRepository->save($delivery);
$this->eventBus->publish($delivery->getUncommittedEvents());
}
}
7. From CRUD to DDD
7.5. Business logic
class Delivery implements AggregateRoot
{
public function reschedule(TimelineSlot $timelineSlot)
{
$this->setDate($timelineSlot->getDate());
$this->setLoadTime($timelineSlot->getLoadTime());
$this->setSlot($timelineSlot->getSlot());
$this->setShift($timelineSlot->getShift());
$this->setLoad($timelineSlot->getLoad());
$this->setPreparation($timelineSlot->getPreparationDate());
$this->apply(
new DeliveryWasRescheduled(
$this->getAggregateRootId(),
$this->getProgrammedDate(),
$this->getTimeStart(),
$this->getTimeEnd(),
$this->getLoad()->spokeId()
)
);
}
}
7. From CRUD to DDD
7.6. Unit test
class RescheduleHandlerTest extends PHPUnit_Framework_TestCase
{
public function testShouldRescheduleDelivery()
{
$deliveryId = 12345;
$slotVars = '2016-03-25|523|2|15';
$timeLineSlot = TimelineSlotStub::random();
$delivery = $this->prophesize(Delivery::class);
$this->deliveryRepository->get($deliveryId)->willReturn($delivery);
$this->slotManager->createTimelineSlotFromVars($slotVars)->willReturn($timeLineSlot);
$delivery->reschedule($timeLineSlot)->shouldBeCalled();
$this->deliveryRepository->save($delivery)->shouldBeCalled();
$this->eventBus->publish($this->expectedEvents())->shouldBeCalled();
$this->rescheduleOrderHandler->handleReschedule(new Reschedule($deliveryId, $slotVars));
}
}
class DeliveryTest extends PHPUnit_Framework_TestCase
{
public function testShouldRescheduleDelivery()
{
$delivery = OrderDeliveryStub::random();
$timeLineSlot = TimelineSlotStub::random();
$delivery->reschedule($timeLineSlot);
static::assertEquals($timeLineSlot->getDate(), $delivery->getProgrammedDate());
static::assertEquals($timeLineSlot->getLoadTime(), $delivery->getLoadTime());
static::assertEquals($timeLineSlot->getSlot(), $delivery->getSlot());
static::assertEquals($timeLineSlot->getShift(), $delivery->getShift());
static::assertEquals($timeLineSlot->getPreparationDate(), $delivery->getPreparation());
$messageIterator = $delivery->getUncommittedEvents()->getIterator();
$this->assertInstanceOf(
DeliveryWasRescheduled::class, $messageIterator->current()->getPayload()
);
}
}
7. From CRUD to DDD
7.7. Domain event
DeliveryWasRescheduled
Delivery Order
Load
Slot
TimeStart
Date
OrderLine
Product
Tax
Deliveries Orders
8. Aggregates and Repositories
CREDIT CARDS
8. Aggregates and Repositories
8.1. Entity / Repository
class CustomerCreditcardModel
{
public function add($number, $type, $token = null, $expiryDate = null)
{
$customer = $this->tokenStorage->getToken()->getUser();
$creditCard = new CustomerCreditcard();
$creditCard->setNumber($number);
$creditCard->setCustomer($customer);
$creditCard->setType($type);
$creditCard->setToken($token);
$creditCard->setExpiryDate($expiryDate);
$this->creditCardRepository->add($creditCard);
return $creditCard;
}
}
Decoupling Ulabox.com monolith. From CRUD to DDD
Problems
● Aggregate?
● CreditCardRepository???
● Unprotected Domain.
Decoupling Ulabox.com monolith. From CRUD to DDD
Solutions
● Which is the Aggregate?
● What’s the Intention?
● Testing
Customer
CreditCard
class Customer implements AggregateRoot
{
public function addCreditCard($number, $type, $token = '', $expiryDate = '')
{
$creditCard = CustomerCreditcard::create($number, $type, $token, $expiryDate);
$this->creditCards->add($creditCard);
$this->apply(new CreditCardWasRegistered($this->getAggregateRootId(), $number));
}
}
Decoupling Ulabox.com monolith. From CRUD to DDD
8. Aggregates and Repositories
8.2. RegisterCreditCard
class RegisterCreditCard
{
public $customerId;
public $cardNumber;
public $type;
public $token;
public $expiry;
public function __construct($customerId, $cardNumber, $type, $token, $expiry)
{
$this->customerId = $customerId;
$this->cardNumber = $cardNumber;
$this->type = $type;
$this->token = $token;
$this->expiry = $expiry;
}
}
8. Aggregates and Repositories
8.3. RegisterCreditCardHandler
class RegisterCreditCardHandler extends CommandHandler
{
private $customerRepository;
private $eventBus;
public function __construct( ... ) { ... }
public function handleRegisterCreditCard(RegisterCreditCard $registerCreditCard)
{
$customer = $this->customerRepository->get($registerCreditCard->customerId())
$customer->addCreditCard(
$registerCreditCard->cardNumber(),
$registerCreditCard->type(),
$registerCreditCard->token(),
$registerCreditCard->expiry()
);
$this->customerRepository->save($customer);
$this->eventBus->publish($customer->getUncommittedEvents());
}
}
8. Aggregates and Repositories
8.4. Business rules
class Customer implements AggregateRoot
{
public function addCreditCard($number, $type, $token, $expiryDate)
{
if ($this->creditCardExists($number, $type)) {
$this->renewCreditCard($number, $type, $token, $expiryDate);
return;
}
$creditCard = CustomerCreditcard::create($number, $type, $token, $expiryDate);
$this->creditCards->add($creditCard);
$this->apply(new CreditCardWasRegistered($this->getAggregateRootId(), $number));
}
private function renewCreditCard($number, $type, $token, $expiryDate)
{
...
}
}
9. Learned lessons
This is not a Big-Bang
Decoupling Ulabox.com monolith. From CRUD to DDD
Aggregate Election
Decoupling Ulabox.com monolith. From CRUD to DDD
Communication
Decoupling Ulabox.com monolith. From CRUD to DDD
Team
Decoupling Ulabox.com monolith. From CRUD to DDD
¡¡¡Be a Professional!!!
Decoupling Ulabox.com monolith. From CRUD to DDD
@avergess
aleix.verges@ulabox.com
www.linkedin.com/in/avergess
Thank’s
Questions?

Decoupling the Ulabox.com monolith. From CRUD to DDD

  • 1.
  • 2.
  • 3.
  • 8.
  • 9.
    /** * @param Cart$cart * @return Order $order */ public function createOrder(Cart $cart) { // Year 2011 $order = $this->moveCartToOrder($cart); $this->sendConfirmationEmail($order); $this->reindexSolr($order); $this->sendToWarehouse($order); // Year 2012 $this->sendToFinantialErp($order); $this->sendDonationEmail($order); // Year 2014 $this->sendToDeliveryRoutingSoftware($order); // Year 2015 $this->sendToJustInTimeSuppliers($order); // Year 2016 $this->sendToWarehouseSpoke($order); // WTF! $this->sendToShipLoadSoftware($order); // WTF!!!! } Decoupling Ulabox.com monolith. From CRUD to DDD Our problem
  • 10.
  • 11.
    Past ● CRUD doesn’tmake sense anymore ● It had sense at the beginning ● Product, Logistic, Delivery, Cart, Customers, ... ● It’s not sustainable. Decoupling Ulabox.com monolith. From CRUD to DDD
  • 12.
    Our solution /** * @paramCart $cart */ public function createOrder(Cart $cart) { $createOrder = new CreateOrderCommand(Cart $cart); $this->commandBus->dispatch($createOrder) } Event bus CreateOrder command OrderWasCreated event subscribe subscribe subscribe subscribe subscribe subscribe subscribe subscribe subscribe Decoupling Ulabox.com monolith. From CRUD to DDD
  • 13.
  • 14.
    ● Domain ● Aggregate/ Aggregate Root ● Repository ● Domain Events ● Service ● Command Bus ● Event Bus * Domain Drive Design: https://en.wikipedia.org/wiki/Domain-driven_design Decoupling Ulabox.com monolith. From CRUD to DDD
  • 15.
  • 16.
    Refactoring and managetechnical debt is not a choice, but a responsability Decoupling Ulabox.com monolith. From CRUD to DDD
  • 17.
  • 18.
  • 19.
  • 20.
    class OrderController extendsBaseController { public function refundAction(Request $request, $id) { $em = $this->container->get('doctrine.orm.entity_manager'); $orderPayment = $em->getRepository('UlaboxCoreBundle:OrderPayment')->find($id); $amount = $request->request->get('refund'); $data = $this->container->get('sermepa')->processRefund($orderPayment, $amount); $orderRefund = new OrderPayment(); $orderRefund->setAmount($amount); ... $em->persist($orderRefund); $em->flush(); return $this->redirectToRoute('order_show', ['id' => $orderPayment->getOrder()->getId()]); } public function someOtherAction(Request $request, $id) ... }
  • 21.
    Decoupling Ulabox.com monolith.From CRUD to DDD Problems ● Hidden dependencies ● Inheritance. ● Biz logic in the controller. ● Non aggregate root. ● Difficult to test.
  • 22.
    * Dependency Injection:https://es.wikipedia.org/wiki/Inyecci%C3%B3n_de_dependencias Decoupling Ulabox.com monolith. From CRUD to DDD Solutions ● Dependency Injection ● Break inheritance from base controller. ● Application services. ● Testing
  • 23.
    5.2. Controller asa service 5. Controllers
  • 24.
    # services.yml imports: - {resource: controllers.yml } # controllers.yml ulabox_ulaoffice.controllers.order: class: UlaboxUlaofficeBundleControllerOrderController arguments: - '@refund' - '@router' ...
  • 25.
  • 26.
    /** * @Route("/orders", service="ulabox_ulaoffice.controllers.order") */ classOrderController { /** * @param Refund $refund * @param RouterInterface $router */ public function __construct(Refund $refund, RouterInterface $router, ……..) { $this->refund = $refund; $this->router = $router; ... } }
  • 27.
  • 28.
    /** * @Route("/orders", service="ulabox_ulaoffice.controllers.order") */ classOrderController { public function refundAction(Request $request, $id) { $amount = $request->request->get('refund'); $method = $request->request->get('method'); $orderId = $request->request->get('order_id'); try { $this->refund->execute($orderId, $id, (float)$amount, $method); $this->session->getFlashBag()->add('success', 'Refund has been processed correctly'); } catch (Exception $e) { $this->session->getFlashBag()->add('danger', $e->getMessage()); } return new RedirectResponse($this->router->generate('order_show', ['id' => $orderId])); } }
  • 29.
  • 30.
    class OrderControllerTest extendsPHPUnit_Framework_TestCase { public function setUp() { $this->refund = $this->prophesize(Refund::class); $this->router = $this->prophesize(RouterInterface::class); $this->orderController = new OrderController( $this->refund->reveal(), $this->router->reveal() ); } ... }
  • 31.
    class OrderControllerTest extendsPHPUnit_Framework_TestCase { ... public function testShouldDelegateOrderRefund() { $orderPaymentId = 34575; $amount = 10.95; $orderId = 12345; $orderRoute = 'some/route'; $request = $this->mockRequest($orderId, $orderPaymentId, $amount, $orderRoute); $this->refund ->execute($orderId, $orderPaymentId, $amount, PaymentPlatform::REDSYS) ->shouldBeCalled(); $this->router->generate('order_show', ['id' => $orderId])->willReturn($orderRoute); $actual = $this->orderController->refundAction($request->reveal(), $orderPaymentId); $this->assertEquals(new RedirectResponse($orderRoute), $actual); } }
  • 32.
  • 33.
  • 34.
  • 35.
    class OrderController extendsBaseController { public function rescheduleAction(Request $request, $id) { $order = $this->container->get('order')->reposition($id); $form = $this->createForm(new OrderType(), $order); $form->handleRequest($request); if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($order); $em->flush(); $request->getSession()->getFlashBag()->add('success', 'Your changes were saved!'); return $this->redirect($this->generateUrl('reschedule_success')); } return ['entity' => $entity, 'form' => $form->createView()]; } }
  • 36.
    Decoupling Ulabox.com monolith.From CRUD to DDD Problems ● Coupling between entities and Symfony Forms. ● Anemic Model. ● Intention?
  • 37.
    Decoupling Ulabox.com monolith.From CRUD to DDD Solutions ● Use of DTO/Command ● Reflect the Intention! ● Rich Domain. ● Testing.
  • 38.
    6. Symfony Forms 6.2.Command !== CLI Command
  • 39.
    class Reschedule { public $orderId; public$addressId; public $slotVars; public $comments; public function __construct($orderId, $addressId, $slotVars, $comments) { $this->orderId = $orderId; $this->addressId = $addressId; $this->slotVars = $slotVars; $this->comments = $comments; } }
  • 40.
    6. Symfony Forms 6.3.Building the Form
  • 41.
    class OrderController extendsBaseController { public function rescheduleDisplayingAction(Request $request, $id) { $order = $this->orderRepository->get($id); $address = $order->deliveryAddress()->asAddress(); $rescheduleOrder = Reschedule::fromPayload([ 'order_id' => $order->getId(), 'address_id' => $address->getId(), 'slot_vars' => $order->deliverySlotVars(), 'comments' => $order->deliveryComments(), ]); $rescheduleForm = $this->formFactory->create(OrderRescheduleType::class, $rescheduleOrder); return ['order' => $order, 'form' => $rescheduleForm->createView()]; } }
  • 42.
    6. Symfony Forms 6.4.Submitting the Form
  • 43.
    class OrderController extendsBaseController { public function rescheduleUpdateAction(Request $request, $id) { $requestData = $request->get('order_reschedule'); $rescheduleOrder = Reschedule::fromPayload([ 'order_id' => $id, 'address_id' => $requestData['addressId'], 'slot_vars' => $requestData['slotVars'], 'comments' => $requestData['comments'], ]); $rescheduleForm = $this->formFactory->create(OrderRescheduleType::class, $rescheduleOrder); if ($rescheduleForm->isValid()) { $this->commandBus->dispatch($rescheduleOrder); } return new RedirectResponse($this->router->generate($this->entity Properties['route'])); } }
  • 44.
  • 45.
    class OrderControllerTest extendsPHPUnit_Framework_TestCase { public function testShouldDelegateOrderRescheduleToCommandBus() { $orderId = 12345; $addressId = 6789; $slotVars = '2016-03-25|523|2|15'; $comments = 'some comments'; $expectedRoute = 'http://some.return.url'; $request = $this->mockRequest($orderId, $addressId, $slotVars, $comments); $form = $this->mockForm(); $form->isValid()->willReturn(true); $this->router->generate('order')->willReturn($expectedRoute); $this->commandBus->dispatch(Argument::type(Reschedule::class))->shouldBeCalled(); $actual = $this->orderController->rescheduleUpdateAction($request->reveal(), $orderId); $this->assertEquals(new RedirectResponse($expectedRoute), $actual); } }
  • 46.
  • 47.
    7. From CRUDto DDD 7.1. Summing...
  • 48.
    class OrderController extendsBaseController { public function rescheduleUpdateAction(Request $request, $id) { $requestData = $request->get('order_reschedule'); $rescheduleOrder = Reschedule::fromPayload([ 'order_id' => $id, 'address_id' => $requestData['addressId'], 'slot_vars' => $requestData['slotVars'], 'comments' => $requestData['comments'], ]); $rescheduleForm = $this->formFactory->create(OrderRescheduleType::class, $rescheduleOrder); if ($rescheduleForm>isValid()) { $this->commandBus->dispatch($rescheduleOrder); } return new RedirectResponse($this->router->generate($this->entity Properties['route'])); } }
  • 49.
    7. From CRUDto DDD 7.2. Handling
  • 50.
    class RescheduleHandler extendsCommandHandler { public function __construct( ... ) { ... } public function handleReschedule(Reschedule $rescheduleOrder) { $timeLineSlot = $this->slotManager->createTimelineSlotFromVars($rescheduleOrder->slotVars); $order = $this->orderRepository->get($rescheduleOrder->aggregateId); $delivery = $order->getOrderDelivery(); $delivery->setSlot($timeLineSlot->getSlot()); $delivery->setLoadTime($timeLineSlot->getLoadTime()); $delivery->setShift($timeLineSlot->getShift()->getShift()); ... $order->rescheduleDelivery($delivery); $this->orderRepository->save($order); $this->eventBus->publish($order->getUncommittedEvents()); } }
  • 51.
    Decoupling Ulabox.com monolith.From CRUD to DDD Problems ● Biz logic out of domain. ● Aggregate access. ● Aggregate Root? ● Unprotected Domain.
  • 52.
    Decoupling Ulabox.com monolith.From CRUD to DDD Solutions ● Aggregate Root. Order or Delivery? ● Unique acces point to the domain. ● Clear intention!! ● Testing.
  • 53.
    7. From CRUDto DDD 7.3. Order or Delivery?
  • 55.
    7. From CRUDto DDD 7.4. Aggregate access point
  • 56.
    class RescheduleHandler extendsCommandHandler { public function __construct( ... ) { ... } public function handleReschedule(Reschedule $rescheduleDelivery) { $timeLineSlot = $this->slotManager->createTimelineSlotFromVars($rescheduleDelivery->slotVars); $delivery = $this->deliveryRepository->get($rescheduleDelivery->deliveryId); $delivery->reschedule($timeLineSlot); $this->deliveryRepository->save($delivery); $this->eventBus->publish($delivery->getUncommittedEvents()); } }
  • 57.
    7. From CRUDto DDD 7.5. Business logic
  • 58.
    class Delivery implementsAggregateRoot { public function reschedule(TimelineSlot $timelineSlot) { $this->setDate($timelineSlot->getDate()); $this->setLoadTime($timelineSlot->getLoadTime()); $this->setSlot($timelineSlot->getSlot()); $this->setShift($timelineSlot->getShift()); $this->setLoad($timelineSlot->getLoad()); $this->setPreparation($timelineSlot->getPreparationDate()); $this->apply( new DeliveryWasRescheduled( $this->getAggregateRootId(), $this->getProgrammedDate(), $this->getTimeStart(), $this->getTimeEnd(), $this->getLoad()->spokeId() ) ); } }
  • 59.
    7. From CRUDto DDD 7.6. Unit test
  • 60.
    class RescheduleHandlerTest extendsPHPUnit_Framework_TestCase { public function testShouldRescheduleDelivery() { $deliveryId = 12345; $slotVars = '2016-03-25|523|2|15'; $timeLineSlot = TimelineSlotStub::random(); $delivery = $this->prophesize(Delivery::class); $this->deliveryRepository->get($deliveryId)->willReturn($delivery); $this->slotManager->createTimelineSlotFromVars($slotVars)->willReturn($timeLineSlot); $delivery->reschedule($timeLineSlot)->shouldBeCalled(); $this->deliveryRepository->save($delivery)->shouldBeCalled(); $this->eventBus->publish($this->expectedEvents())->shouldBeCalled(); $this->rescheduleOrderHandler->handleReschedule(new Reschedule($deliveryId, $slotVars)); } }
  • 61.
    class DeliveryTest extendsPHPUnit_Framework_TestCase { public function testShouldRescheduleDelivery() { $delivery = OrderDeliveryStub::random(); $timeLineSlot = TimelineSlotStub::random(); $delivery->reschedule($timeLineSlot); static::assertEquals($timeLineSlot->getDate(), $delivery->getProgrammedDate()); static::assertEquals($timeLineSlot->getLoadTime(), $delivery->getLoadTime()); static::assertEquals($timeLineSlot->getSlot(), $delivery->getSlot()); static::assertEquals($timeLineSlot->getShift(), $delivery->getShift()); static::assertEquals($timeLineSlot->getPreparationDate(), $delivery->getPreparation()); $messageIterator = $delivery->getUncommittedEvents()->getIterator(); $this->assertInstanceOf( DeliveryWasRescheduled::class, $messageIterator->current()->getPayload() ); } }
  • 62.
    7. From CRUDto DDD 7.7. Domain event
  • 63.
  • 64.
    8. Aggregates andRepositories
  • 65.
  • 66.
    8. Aggregates andRepositories 8.1. Entity / Repository
  • 67.
    class CustomerCreditcardModel { public functionadd($number, $type, $token = null, $expiryDate = null) { $customer = $this->tokenStorage->getToken()->getUser(); $creditCard = new CustomerCreditcard(); $creditCard->setNumber($number); $creditCard->setCustomer($customer); $creditCard->setType($type); $creditCard->setToken($token); $creditCard->setExpiryDate($expiryDate); $this->creditCardRepository->add($creditCard); return $creditCard; } }
  • 68.
    Decoupling Ulabox.com monolith.From CRUD to DDD Problems ● Aggregate? ● CreditCardRepository??? ● Unprotected Domain.
  • 69.
    Decoupling Ulabox.com monolith.From CRUD to DDD Solutions ● Which is the Aggregate? ● What’s the Intention? ● Testing
  • 70.
    Customer CreditCard class Customer implementsAggregateRoot { public function addCreditCard($number, $type, $token = '', $expiryDate = '') { $creditCard = CustomerCreditcard::create($number, $type, $token, $expiryDate); $this->creditCards->add($creditCard); $this->apply(new CreditCardWasRegistered($this->getAggregateRootId(), $number)); } } Decoupling Ulabox.com monolith. From CRUD to DDD
  • 71.
    8. Aggregates andRepositories 8.2. RegisterCreditCard
  • 72.
    class RegisterCreditCard { public $customerId; public$cardNumber; public $type; public $token; public $expiry; public function __construct($customerId, $cardNumber, $type, $token, $expiry) { $this->customerId = $customerId; $this->cardNumber = $cardNumber; $this->type = $type; $this->token = $token; $this->expiry = $expiry; } }
  • 73.
    8. Aggregates andRepositories 8.3. RegisterCreditCardHandler
  • 74.
    class RegisterCreditCardHandler extendsCommandHandler { private $customerRepository; private $eventBus; public function __construct( ... ) { ... } public function handleRegisterCreditCard(RegisterCreditCard $registerCreditCard) { $customer = $this->customerRepository->get($registerCreditCard->customerId()) $customer->addCreditCard( $registerCreditCard->cardNumber(), $registerCreditCard->type(), $registerCreditCard->token(), $registerCreditCard->expiry() ); $this->customerRepository->save($customer); $this->eventBus->publish($customer->getUncommittedEvents()); } }
  • 75.
    8. Aggregates andRepositories 8.4. Business rules
  • 76.
    class Customer implementsAggregateRoot { public function addCreditCard($number, $type, $token, $expiryDate) { if ($this->creditCardExists($number, $type)) { $this->renewCreditCard($number, $type, $token, $expiryDate); return; } $creditCard = CustomerCreditcard::create($number, $type, $token, $expiryDate); $this->creditCards->add($creditCard); $this->apply(new CreditCardWasRegistered($this->getAggregateRootId(), $number)); } private function renewCreditCard($number, $type, $token, $expiryDate) { ... } }
  • 77.
  • 78.
    This is nota Big-Bang Decoupling Ulabox.com monolith. From CRUD to DDD
  • 79.
    Aggregate Election Decoupling Ulabox.commonolith. From CRUD to DDD
  • 80.
  • 81.
  • 82.
    ¡¡¡Be a Professional!!! DecouplingUlabox.com monolith. From CRUD to DDD
  • 83.