0

I'm quite new to Symfony and I started digging around Symfony forms. As described here https://webmozart.io/blog/2015/09/09/value-objects-in-symfony-forms/ I'm using value objects in my subform. A constructor of value object can throw an exception if invalid values are provided. Therefore when I put invalid value to my field I'm getting ugly exception from VO, hence I want to connect a Validator Constraint on this but the validate() function gets already a Value object... Any thoughts on this issue?

    class AddressType extends AbstractType
    {     
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            /....
           $builder->add('latitude', LatitudeType::class, [
                'label'       => false,
                'constraints' => [new Valid()],
            ]);
       }

Latitude type

 class LatitudeType extends AbstractType implements DataMapperInterface
{
    const INPUT_NAME = 'latitude';

    /**
     * @param FormBuilderInterface $builder
     * @param array                $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add(self::INPUT_NAME, TextType::class, [
            'label'       => 'FORM.LATITUDE',
            'attr'        => [
                'placeholder' => 'PLACEHOLDER.LATITUDE',
            ],
            'required'    => false,
            'constraints' => [new LatitudeValidator()],
        ]);

        $builder->setDataMapper($this);
    }

    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Latitude::class,
            'empty_data' => null,
            'error_bubbling' => true
        ]);
    }

    /**
     * @param Latitude                     $data
     * @param FormInterface[]|\Traversable $forms
     */
    public function mapDataToForms($data, $forms)
    {
        $forms = iterator_to_array($forms);
        $forms[self::INPUT_NAME]->setData($data);
    }

    /**
     * @param FormInterface[]|\Traversable $forms
     * @param mixed                        $data
     */
    public function mapFormsToData($forms, &$data)
    {
        $forms = iterator_to_array($forms);
        if ($forms[self::INPUT_NAME]->getData()) {
            $data = new Latitude((float)$forms[self::INPUT_NAME]->getData());
        }
    }

This validation method is receiving already a created VO

class LatitudeValidator extends ConstraintValidator
{
    /**
     * {@inheritdoc}
     */
    public function validate($value, Constraint $constraint)
    {
        if (null === $value || '' === $value) {
            return;
        }

But I want to be able to do something like

 try {
        new \ValueObject\Latitude((float)$value);
    } catch (\InvalidArgumentException $e) {
        $this->context->buildViolation($e->getMessage())
            ->addViolation();
    }
Fabien Salles
  • 1,101
  • 15
  • 24
micah
  • 1
  • 2

2 Answers2

1

You have differents methods to use form with Value Objects but after a lot of troubles by my side I decided to stop this. Symfony have to construct your Value Object even if your VO is invalid. You gave an example on an invalid state but you have also others example when you form doesn't fit well your Domain like when you have not enought fields to complete your required properties on your VOs.

Symfony Forms can be complexe and the use of VOs inside them can bring more complexity whereas the forms should be linked to the interface and not always to the domain objects.

The best solution for me is to use the command pattern. You have a simple example with other reasons to use it here. You can also avoid to put this logic into your controllers and avoid code duplication with a command bus librairy like tactician or now the messenger component of Symfony.

With a command you can simply represent an action by the form. The form can have validators related to the VO or directly to the form.

With the command bus you can create your Value Object in a valid state and throw exceptions in a second layer when you forget a use case.

This approach is more robust and avoid a lot of troubles for my point of view.

Fabien Salles
  • 1,101
  • 15
  • 24
0

The best thing you achieve this, is to accept any kind of value into the ValueObject and then perform validation on it. This way you're not forced to handle exception due to invalid types passed through constructor. Moreover remember that creation or "value setting" of the underlying object is performed by the framework before validation (otherwise you'll never have to use VO) so you should leverage on this and let the Form component do his job (as you done correclty with transformers). Then, you can perform any kind of validation on underlying object.

DonCallisto
  • 29,419
  • 9
  • 72
  • 100
  • Thanks! I wasn't sure when the validation is processed, eventually I was trying to implement the validator in mapFormsToData. But that was a no-go. – micah Sep 12 '18 at 07:39