Skip to content

Instantly share code, notes, and snippets.

@webdevilopers
Last active February 3, 2019 11:03
Show Gist options
  • Save webdevilopers/e2263debd573c90b51ab18702c3db4c9 to your computer and use it in GitHub Desktop.
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
<?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()
));
}
}
<?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);
}
}
<?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;
}
}
@webdevilopers
Copy link
Author

webdevilopers commented Dec 31, 2018

General discussion here:

Applying domain-driven design @dddinphp and CQRS I prefer using value objects in my Commands. A Command 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 or TransformationFailedException but the DomainException I suggest to change the Data Transformer:

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->addModelTransformer(new CallbackTransformer(
                    function (?DormerInnerWidth $width) {
                        return null !== $width ? $width->toCentimeters() : null;
                    },
                    function (?int $width) {
                        if (null === $width) { return null; }

                        try {
                            return DormerInnerWidth::fromCentimeters($width);
                        } catch (\DomainException $exception) {
                            throw $exception;
                        }
                    }
                )
            )
        ;
    }

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.:

final class DormerInnerWidthOutOfRange extends \DomainException {}

Thanks for your feedback!

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