Forked from webdevilopers/AbstractCreateCalculationHandler.php
Last active
November 22, 2019 01:45
-
-
Save yvoyer/a778ac744ddf10012864 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 | |
/** | |
* This class is used as date_class in form component | |
*/ | |
class CalculationCommand | |
{ | |
const BASIC_TYPE = 'basic_calculaction_type'; | |
public $subTotal; | |
public $quantity; | |
} |
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 | |
class CalculationController | |
{ | |
/** | |
* @var CalculationHandler | |
*/ | |
private $handler; | |
/** | |
* @param CalculationHandler $handler | |
*/ | |
public function __construct(CalculationHandler $handler) { | |
$this->handler = $handler; | |
} | |
/** | |
* @Route("/calculation/{type}") | |
* @Template() | |
*/ | |
public function indexAction($type) | |
{ | |
$command = $this->getFormData($type); | |
$this->handler->handle($command); // todo would probably be the command bus | |
// Get ID from entity and redirect, no return values required | |
} | |
/** | |
* @param string $type | |
* | |
* @return CalculationCommand | |
*/ | |
private function getFormData($type) | |
{ | |
// todo this could be a calculation mapper service | |
$mapping = [ | |
CalculationCommand::BASIC_TYPE => CalculationCommand::class, | |
]; | |
// Form handling generates the configured command object | |
/** | |
* @var CalculationCommand $command | |
*/ | |
$command = new $mapping[$type]; | |
$command->quantity = 12; | |
$command->subTotal = 1000; | |
// Form would configure the command with values | |
return $command; | |
} | |
} |
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 | |
class CalculationHandler | |
{ | |
/** | |
* @var EntityManager | |
*/ | |
private $entityManager; | |
/** | |
* @var TokenStorageInterface | |
*/ | |
private $tokenStorage; | |
/** | |
* @var Session | |
*/ | |
private $session; | |
/** | |
* @var Translator | |
*/ | |
private $translator; | |
/** | |
* @param EntityManager $entityManager | |
* @param TokenStorageInterface $tokenStorage | |
* @param Session $session | |
* @param Translator $translator | |
* | |
* FIXME null values here are just to make prototype work | |
*/ | |
public function __construct( | |
EntityManager $entityManager, | |
TokenStorageInterface $tokenStorage = null, | |
Session $session = null, | |
Translator $translator = null | |
) { | |
$this->entityManager = $entityManager; | |
$this->tokenStorage = $tokenStorage; | |
$this->session = $session; | |
$this->translator = $translator; | |
} | |
/** | |
* @param CalculationCommand $command | |
*/ | |
public function handle($command) | |
{ | |
// transfer required args from command to construct. that way object is in valid state | |
$entity = new DormerCalculation();// Command could contain necessary args for this object construct | |
$entity->addPrice('total', $command->subTotal, $command->quantity); | |
$this->entityManager->persist($entity); | |
$this->entityManager->flush(); | |
// do other stuff with entity (translator etc.) | |
} | |
} |
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
{ | |
"name": "vendor_name/package_name", | |
"description": "description_text", | |
"minimum-stability": "stable", | |
"license": "proprietary", | |
"authors": [ | |
{ | |
"name": "author's name", | |
"email": "email@example.com" | |
} | |
], | |
"require": { | |
"phpunit/phpunit": "^5.2" | |
}, | |
"autoload": { | |
"psr-0": { | |
"": "" | |
} | |
} | |
} |
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 | |
/** | |
* DormerCalculation | |
* | |
* @ORM\Entity | |
*/ | |
class DormerCalculation | |
{ | |
// THIS IS ONLY TO MAKE THE PROTOTYPE WORK | |
public static $hash; | |
/** | |
* @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 DormerCalculationPrice[] | |
* | |
* @ORM\OneToMany(targetEntity="DormerCalculationPrice", | |
* mappedBy="dormerCalculation", cascade="persist", indexBy="name", fetch="EAGER" | |
* ) | |
*/ | |
private $prices = []; // would need to be ArrayCollection in construct | |
public function __construct($unrelevantDependancys = []) // define the required args here | |
{ | |
$this->id = spl_object_hash($this); // FIXME For Prototype only, don't do that | |
self::$hash = $this->id; | |
// todo $this->prices = new ArrayCollection(); | |
} | |
// FIXME try not the make it public via getter | |
public function getId() | |
{ | |
return $this->id; | |
} | |
public function addPrice($key, $subTotal, $quantity) { | |
$price = new DormerCalculationPrice($this, $key, $subTotal, $quantity); | |
$this->prices[$key] = $price; | |
} | |
/** | |
* @return DormerCalculationPrice[] | |
*/ | |
public function getPrices() // would be better if private or absent, unless absolutly needed | |
{ | |
return $this->prices; // todo should be $this->prices->toArray(); | |
} | |
/** | |
* @return int | |
*/ | |
public function getTotal() | |
{ | |
$total = 0; | |
foreach ($this->prices as $price) { | |
$total += $price->getTotal(); | |
} | |
return $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 | |
class DormerCalculationPrice | |
{ | |
private $name; | |
private $quantity; | |
private $total; | |
private $subtotal; | |
private $calculation; // probably not transferable??? so no set once created | |
/** | |
* @param DormerCalculation $calculation | |
* @param $name | |
* @param $subTotal | |
* @param $quantity | |
* | |
* I suggest to pass not nullable relations in construct, and not provide setters | |
* for attribute that don't chance or cannot be changed in the domain | |
*/ | |
public function __construct(DormerCalculation $calculation, $name, $subTotal, $quantity) | |
{ | |
$this->calculation = $calculation; | |
$this->name = $name; | |
$this->subtotal = $subTotal; | |
$this->quantity = $quantity; | |
$this->total = $this->calculateTotal(); | |
} | |
/** | |
* @return int | |
*/ | |
private function calculateTotal() | |
{ | |
return $this->subtotal * $this->quantity; //The result of your calculation | |
} | |
/** | |
* @return int | |
*/ | |
public function getTotal() | |
{ | |
return $this->total; | |
} | |
// setter should be necessary here only for attribute that can change over the life cycle of the entity | |
} |
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 | |
final class EntityManager | |
{ | |
private $objects = []; | |
/** | |
* @param DormerCalculation $object | |
*/ | |
public function persist($object) | |
{ | |
$this->objects[$object->getId()] = $object; | |
} | |
public function flush() | |
{ | |
} | |
/** | |
* @param $id | |
* | |
* @return object | |
*/ | |
public function find($id) | |
{ | |
return $this->objects[$id]; | |
} | |
} |
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 | |
final class WorkflowTest extends \PHPUnit_Framework_TestCase | |
{ | |
public function test_it_should_do_something() | |
{ | |
$em = new EntityManager(); | |
$controller = new CalculationController(new CalculationHandler($em)); | |
$controller->indexAction(CalculationCommand::BASIC_TYPE); | |
$this->assertNotNull(DormerCalculation::$hash); | |
/** | |
* @var DormerCalculation $createdEntity | |
*/ | |
$createdEntity = $em->find(DormerCalculation::$hash); | |
$this->assertInstanceOf(DormerCalculation::class, $createdEntity); | |
$this->assertCount(1, $createdEntity->getPrices()); | |
$this->assertContainsOnlyInstancesOf(DormerCalculationPrice::class, $createdEntity->getPrices()); | |
$this->assertSame(12000, $createdEntity->getTotal()); | |
} | |
} |
Thinking of Symfony Form integration again, what if the CREATE form and the EDIT form would have relevant differences,
would you create to commands e.g. CreateCalculationCommand
and EditCalculationCommand
?
For instance my app offers a recalculation of the original calculation that has a completely different set of properties and constraints.
Therefore I would create a RecalculateCalculationCommand
.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
So you are not using a
Vendor
prefix. Regarding the link by @willdurand would you regardMyDomain
asCoreDomain
with theCalculation
folder inside or consider creating aCalculationDomain
with your structure?My app holds 10 years of legacy code and a lot of what could have been described as bundles (User, Calculation, Superadmin, Intranet, Pricequote, Web) in symfony.