Skip to content

Instantly share code, notes, and snippets.

@webdevilopers
Last active April 15, 2016 12:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save webdevilopers/5ac1f30d88102e24df87 to your computer and use it in GitHub Desktop.
Save webdevilopers/5ac1f30d88102e24df87 to your computer and use it in GitHub Desktop.
Domain Driven Design Command Bus and Handler example in Symfony
<?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();
}
}
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 }
<?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;
}
}
<?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);
}
}
<?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);
}
}
<?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
}
}
@webdevilopers
Copy link
Author

@webdevilopers
Copy link
Author

If I get it right I should not use the entity for data_class in my form. I should use the command instead.

And it is the command that will have getters and setters 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 and getters on the entity I should create real-life-methods e.g.:

setMeasurements($width, $height)

or even better pass a value object e.g. DormerMeasurements that has the properties width and height?

@webdevilopers
Copy link
Author

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?

@webdevilopers
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment