Last active
February 3, 2019 11:03
-
-
Save webdevilopers/e2263debd573c90b51ab18702c3db4c9 to your computer and use it in GitHub Desktop.
Catching domain exceptions when using value objects with data transformers in Symfony forms
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 Acme\DormerCalculation\Infrastructure\Symfony\DormerCalculationBundle\Controller; | |
use Symfony\Bundle\FrameworkBundle\Controller\Controller; | |
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; | |
use Acme\DormerCalculation\Infrastructure\Symfony\DormerCalculationBundle\Form\DormerCalculation as CalculationForm; | |
use Acme\DormerCalculation\Domain\Model\DormerCalculation\Command\CalculateDormerCommand; | |
use Symfony\Component\Form\FormError; | |
class DormerCalculationController extends Controller | |
{ | |
/** | |
* @Template() | |
*/ | |
public function createAction() | |
{ | |
$command = new CalculateDormerCommand(); | |
$form = $this->createForm(CalculationForm::class, $command); | |
$form->handleRequest($this->getRequest()); | |
try { | |
if ($form->isValid()) { | |
} | |
} catch (\DomainException $exception) { | |
// This exception is not caught by the controller but the Domain Exception of the data transformer! | |
$form->get('width')->addError(new FormError('Width out of range.')); | |
} | |
return $this->render('AcmeDormerCalculationBundle:Dormer:calculate.html.twig', array( | |
'form' => $form->createView() | |
)); | |
} | |
} |
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 Acme\DormerCalculation\Domain\Model\DormerCalculation; | |
use Acme\Calculation\Domain\Model\Meter; | |
/** | |
* Class DormerInnerWidth | |
* @package Acme\DormerCalculation\Domain\Model\DormerCalculation | |
*/ | |
final class DormerInnerWidth | |
{ | |
const MIN_WIDTH = 60; | |
const MAX_WIDTH = 1600; | |
private $width; | |
/** | |
* DormerInnerWidth constructor. | |
* @param int $width | |
*/ | |
private function __construct(int $width) | |
{ | |
if ($width < self::MIN_WIDTH || $width > self::MAX_WIDTH) { | |
throw new \DomainException(sprintf('Width `%s` is out of range.', $width)); | |
} | |
$this->width = $width; | |
} | |
/** | |
* @param float $meters | |
* @return DormerInnerWidth | |
*/ | |
public static function fromMeters(float $meters) : DormerInnerWidth | |
{ | |
return new self((int)($meters*100)); | |
} | |
/** | |
* @param int $centimeters | |
* @return DormerInnerWidth | |
*/ | |
public static function fromCentimeters(int $centimeters) : DormerInnerWidth | |
{ | |
return new self($centimeters); | |
} | |
/** | |
* @return int | |
*/ | |
public function toCentimeters() : int | |
{ | |
return $this->width; | |
} | |
/** | |
* @return float | |
*/ | |
public function toMeters() : float | |
{ | |
return number_format($this->width/100, Meter::METERS_DECIMALS, Meter::METERS_DECIMAL_SEPARATOR, Meter::METERS_THOUSAND_SEPARATOR); | |
} | |
} |
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 Acme\DormerCalculation\Infrastructure\Symfony\DormerCalculationBundle\Form\Type; | |
use Acme\DormerCalculation\Domain\Model\DormerCalculation\DormerInnerWidth; | |
use Symfony\Component\Form\AbstractType; | |
use Symfony\Component\Form\CallbackTransformer; | |
use Symfony\Component\Form\Extension\Core\Type\NumberType; | |
use Symfony\Component\Form\FormBuilderInterface; | |
/** | |
* Class DormerInnerWidthType | |
*/ | |
final class DormerInnerWidthType extends AbstractType | |
{ | |
/** | |
* @param FormBuilderInterface $builder | |
* @param array $options | |
*/ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
->addModelTransformer(new CallbackTransformer( | |
function (?DormerInnerWidth $width) { | |
return null !== $width ? $width->toCentimeters() : null; | |
}, | |
function (?int $width) { | |
return null !== $width ? DormerInnerWidth::fromCentimeters($width) : null; | |
} | |
) | |
) | |
; | |
} | |
/** | |
* @return null|string|\Symfony\Component\Form\FormTypeInterface | |
*/ | |
public function getParent() | |
{ | |
return NumberType::class; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
General discussion here:
Applying domain-driven design @dddinphp and CQRS I prefer using value objects in my
Command
s. ACommand
becomes the "data_class" in my @symfony forms.In order to use value objects in Symfony forms I followed this example by @webmozart:
Recently I had to add a value object that has an internal validation:
When populating the command with a value out of range with the symfony form the
DomainException
is directly thrown.Catching the exception inside the controller won't help since the Exception is thrown by the Data Transformer in the first place.
@hasumedic added a solution to this problem. Since I don't want to catch a general Exception e.g.
ErrorMappingException
orTransformationFailedException
but theDomainException
I suggest to change the Data Transformer:And then catch the individual exception and the create a form error for the specific form property inside the controller.
Do you agree @webmozart, @hasumedic?
The idea is to pass the concrete domain exception to the controller in order to add the form error.
Of course my DomainException are usually using the ubiquitous language and would be more explicite e.g.:
Thanks for your feedback!