Last active
April 15, 2016 12:42
-
-
Save webdevilopers/5ac1f30d88102e24df87 to your computer and use it in GitHub Desktop.
Domain Driven Design Command Bus and Handler example in Symfony
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Sps\Bundle\CalculationBundle\Handler; | |
use Doctrine\ORM\EntityManager; | |
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | |
use Symfony\Component\HttpFoundation\Session\Session; | |
use Symfony\Component\Translation\Translator; | |
use Sps\Bundle\CalculationBundle\Entity\DormerCalculationPrice; | |
abstract class AbstractCreateCalculationHandler | |
{ | |
const DECIMALS = 2; | |
private $entityManager; | |
private $tokenStorage; | |
private $session; | |
private $translator; | |
protected $translationDomain; | |
protected $calculation; | |
private $variables; | |
public function __construct(EntityManager $entityManager, TokenStorageInterface $tokenStorage, Session $session, Translator $translator) | |
{ | |
$this->entityManager = $entityManager; | |
$this->tokenStorage = $tokenStorage; | |
$this->session = $session; | |
$this->translator = $translator; | |
} | |
public function handle($command) | |
{ | |
$this->calculation = $command->calculation; | |
$this->calculate(); | |
$this->save(); | |
} | |
public function getEntityManager() { | |
return $this->entityManager; | |
} | |
public function getTokenStorage() { | |
return $this->tokenStorage; | |
} | |
public function getSession() { | |
return $this->session; | |
} | |
public function getTranslator() { | |
return $this->translator; | |
} | |
public function getTranslationDomain() { | |
return $this->translationDomain; | |
} | |
public function getCalculation() { | |
return $this->calculation; | |
} | |
public function getPartner() | |
{ | |
return $this->getEntityManager() | |
->getRepository('SpsBaseBundle:Partner') | |
->find($this->getTokenStorage()->getToken()->getUser()->getId()); | |
} | |
/** | |
* | |
* @param type $message | |
* @param array $parameters | |
* @param type $type | |
*/ | |
public function addMessage($message, array $parameters = array(), $type = 'notice') | |
{ | |
// bootstrap: notice, error / warning, success | |
$this->session->getFlashBag()->add( | |
$type, $this->translator | |
->trans($message, $parameters, $this->getTranslationDomain())); | |
} | |
public function getMessages() | |
{ | |
return $this->session->getFlashBag()->all(); | |
} | |
public function getVariables() { | |
return $this->variables; | |
} | |
public function getVariable($key) | |
{ | |
if (!isset($this->variables[$key])) { | |
throw new \InvalidArgumentException("Variable `$key` does not exist."); | |
} | |
return $this->variables[$key]; | |
} | |
public function hasVariable($key) | |
{ | |
return isset($this->variables[$key]) ? true : false; | |
} | |
public function setVariable($key, $value) | |
{ | |
$this->variables[$key] = $value; | |
} | |
public function getPrice($key) | |
{ | |
return $this->calculation->getPrice($key); | |
} | |
public function hasPrice($key) | |
{ | |
return $this->calculation->hasPrice($key); | |
} | |
public function setPrice($key, $value) | |
{ | |
$this->calculation->setPrice($key, $value); | |
} | |
public function addPrice($key, $subtotal, $quantity = 1, $showInOffer = false) | |
{ | |
$total = $subtotal*$quantity; | |
$dormerCalculationPrice = new DormerCalculationPrice; | |
$dormerCalculationPrice->setName($key); | |
$dormerCalculationPrice->setValue($subtotal); | |
$dormerCalculationPrice->setQuantity($quantity); | |
$dormerCalculationPrice->setTotal($total); | |
$dormerCalculationPrice->setShowInOffer($showInOffer); | |
$this->getCalculation()->addPrice($dormerCalculationPrice); | |
} | |
private function round($price) | |
{ | |
return round($price, self::DECIMALS); | |
} | |
protected function save() | |
{ | |
$this->getEntityManager()->persist($this->getCalculation()); | |
$this->getEntityManager()->flush(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
services: | |
sps.calculation.abstract_create_calculation_handler: | |
abstract: true | |
class: Sps\Bundle\CalculationBundle\Handler\AbstractCreateCalculationHandler | |
arguments: | |
- "@doctrine.orm.entity_manager" | |
- "@security.token_storage" | |
- "@session" | |
- "@translator.default" | |
sps.calculation.create_dormer_calculation_handler: | |
class: Sps\Bundle\CalculationBundle\DormerCalculation\CreateDormerCalculationHandler | |
parent: sps.calculation.abstract_create_calculation_handler | |
tags: | |
- { name: command_handler, handles: Sps\Bundle\CalculationBundle\DormerCalculation\CreateDormerCalculation } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Sps\Bundle\CalculationBundle\DormerCalculation; | |
use Sps\Bundle\CalculationBundle\Entity\DormerCalculation; | |
class CreateDormerCalculation | |
{ | |
public $calculation; | |
public function __construct(DormerCalculation $dormerCalculation) | |
{ | |
if (null === $dormerCalculation) { | |
throw new \InvalidArgumentException('Missing required "dormerCalculation" parameter'); | |
} | |
$this->calculation = $dormerCalculation; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Sps\Bundle\CalculationBundle\DormerCalculation; | |
use Sps\Bundle\CalculationBundle\Handler\AbstractCreateCalculationHandler; | |
use Sps\Bundle\BaseBundle\Entity\DormerCalculation; | |
use Sps\Bundle\CalculationBundle\Entity\DormerCalculationChargeRate; | |
use Sps\Bundle\BaseBundle\Entity\DormerCalculationPrice; | |
class CreateDormerCalculationHandler extends AbstractCreateCalculationHandler | |
{ | |
// A lot of traits with a lot of sub-calculations re-used for other calculation types | |
use CalculateMounting; | |
use CalculateConstructionElement; | |
use CalculateConstructionSite; | |
use CalculateDelivery; | |
use CalculateDormer; | |
use CalculateDormerWindow; | |
use CalculateDownspout; | |
use CalculateGutter; | |
use CalculateOverhang; | |
const WINDOW_DIMENSIONS_ROUNDING_FACTOR = 10; | |
public function calculate() | |
{ | |
// Do a lot of calculation with values from the entity | |
// But since there will be no more getters and setters | |
// and not the entity but the command will be data_class | |
// of the form and validated should these vars go into | |
// the command too (redundant?)? | |
// Or only into the command and then set them as valid values on the entity | |
// via special method setMeasurements($widht, $height) <- value object?! | |
$width = $this->getCalculation()->getWidth(); | |
$height = $this->getCalculation()->getHeight(); | |
$quantity = $this->getCalculation()->getQuantity(); | |
// In the end set a lot of prices on the entity | |
$total = ($width*$height)*$quantity*1000; | |
$this->getCalculation()->addPrice('total', $total); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Sps\Bundle\CalculationBundle\Entity; | |
use Doctrine\ORM\Mapping as ORM; | |
use Doctrine\Common\Collections\ArrayCollection; | |
/** | |
* DormerCalculation | |
* | |
* @ORM\Entity | |
*/ | |
class DormerCalculation | |
{ | |
/** | |
* @var integer $id | |
* | |
* @ORM\Column(name="id", type="integer", precision=0, scale=0, nullable=false, unique=false) | |
* @ORM\Id | |
* @ORM\GeneratedValue(strategy="IDENTITY") | |
*/ | |
private $id; | |
/** | |
* @var string $quantity | |
* | |
* @ORM\Column(name="quantity", type="integer") | |
*/ | |
private $quantity; | |
/** | |
* @var string $height | |
* | |
* @ORM\Column(name="h", type="float") | |
*/ | |
private $height; | |
/** | |
* @var string $width | |
* | |
* @ORM\Column(name="w", type="float") | |
*/ | |
private $width; | |
/** | |
* @ORM\OneToMany(targetEntity="DormerCalculationPrice", | |
* mappedBy="dormerCalculation", cascade="persist", indexBy="name", fetch="EAGER" | |
* ) | |
*/ | |
private $prices; | |
public function __construct($lotsOfValues) | |
{ | |
// Do not use getters and setters on entity, put all vars here via constructor | |
// or method e.g. addPrice() ?! | |
// Use a single $lotsOfValues var if there are a lot?! | |
} | |
public function addPrice(DormerCalculationPrice $price) { | |
$this->prices[$price->getName()] = $price; | |
$price->setDormerCalculation($this); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
use Sps\Bundle\CalculationBundle\Entity\DormerCalculation; | |
use Sps\Bundle\CalculationBundle\Form\DormerCalculation as CalculationForm; | |
use Sps\Bundle\CalculationBundle\DormerCalculation\CreateDormerCalculation; | |
class DefaultController extends Controller | |
{ | |
/** | |
* @Route("/calculation/{type}") | |
* @Template() | |
*/ | |
public function indexAction($type) | |
{ | |
// ... After form process this will be the valid entity | |
// Validation should be moved from entity to command (=> data_class) instead | |
$dormerCalculation = new DormerCalculation(); | |
// @todo Handle form, then populate entity and pass it to command bus and handler | |
$createDormerCalculation = new CreateDormerCalculation($dormerCalculation); | |
$createDormerCalculationHandler = $this->get('sps.calculation.create_dormer_calculation_handler'); | |
$createDormerCalculationHandler->handle($createDormerCalculation); | |
// Get ID from entity and redirect, no return values required | |
} | |
} |
Or is my general approach wrong and I should not use dormerCalculation
as my entity but dormer
itself and use a method calculate
ot add a dormerCalculation
entity on it?
Thanks to @yvoyer for his improved example:
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If I get it right I should not use the
entity
fordata_class
in my form. I should use the command instead.And it is the command that will have
getters
andsetters
for the properties.Furthermore it is the command that will validate - ergo I could add my constraints to the properties for the form to use it.
When the handler is done I need to change properties on the entity. Some properties that were already defined on the command e.g.
$width
,$height
. If I understood it right this is typical for command bus design and some properties simply repeat?See: http://verraes.net/2013/04/decoupling-symfony2-forms-from-entities/#comment-1098270880 @mathiasverraes
But instead of using
setters
andgetters
on the entity I should create real-life-methods e.g.:or even better pass a value object e.g.
DormerMeasurements
that has the propertieswidth
andheight
?