8000 [Form] Impossible to affect form options during buildForm in FormExtensions · Issue #8513 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Form] Impossible to affect form options during buildForm in FormExtensions #8513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
lwc opened this issue Jul 18, 2013 · 4 comments
Closed
Labels

Comments

@lwc
Copy link
lwc commented Jul 18, 2013

Hi,

For background, I am attempting to write a FormTypeExtension that serialises form field constraints into data attributes on the input elements so that it is possible to write a generalised javascript library capable of validating most typical symfony forms client side.

One place that breaks down is when validation rules depend on submitted data - I've seen and have been using this to successfully validate on the server side, but without the ability to define field interdependencies declaratively, it becomes impossible to bake these rules into data attributes: there is no way to serialise the decision that happens in that closure.

With that in mind, I've been attempting to build another FormTypeExtension that introduces the notion of dependencies, see comments inline:

<?php

namespace App\Form\Extension;

use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use App\Form\EventListener\DependsListener;

class DependsExtension extends AbstractTypeExtension
{
    public function getExtendedType()
    {
        return 'form';
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setOptional(array(
            'depends'
        ));
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        if (array_key_exists('depends', $options)) {

            // should builder be needed here? is this even correct?
            $builder->addEventSubscriber(new DependsListener($builder, $options['depends']));
        }
    }

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        // bake dependencies into data attributes for client side js to act on
        if (array_key_exists('depends', $options)) {
            $view->vars['attr']['data-depends'] = json_encode($options['depends']);
        }
    }
}

And the listener:

<?php

namespace App\Form\EventListener;

use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\PropertyAccess\PropertyAccess;


class DependsListener implements EventSubscriberInterface
{
    /**
     *
     * @var FormBuilderInterface
     */
    private $builder;
    private $depends;

    public function __construct($builder, $depends)
    {
        $this->builder = $builder;
        $this->depends = $depends;
    }

    public static function getSubscribedEvents()
    {
        return array(
            FormEvents::PRE_SET_DATA => 'preSetData',
            FormEvents::PRE_SUBMIT => 'preSubmit'
        );
    }

    public function preSetData(FormEvent $event)
    {
        // TODO probably have to do something here when loading the form
    }

    public function preSubmit(FormEvent $event)
    {
        $root = $event->getForm()->getRoot();
        $accessor = PropertyAccess::createPropertyAccessor();

        $removeMe = true;
        foreach ($this->depends as $fieldPath => $value) {

            // TODO: use sf constraints for this? allows for more options than simple equality
            if ($accessor->getValue($root, $fieldPath)->getData() != $value) {
                $removeMe = $removeMe && true;
            }
            else {
                $removeMe = false;
            }
        }

        if ($removeMe) {

            // this is where I'd like to somehow unset / interact with the constraints defined on the field
            // 
            // note it is possible to just straight up remove the field:
            $event->getForm()->getParent()->remove($event->getForm()->getName());
            // but this is not ideal. ideally we'd disable server side validation and hide this field with js
        }
    }
}

As you can see, it seems impossible to unset / interact with the constraints associated with the field, even though it's possible to simply outright delete the field.

Could anyone shed any light on if what I'm trying to do is possible / desired / insane / stupid and help point me in the direction of a solution?

Thanks,
Luke Cawood

@lwc
Copy link
Author
lwc commented Jul 18, 2013

I forgot to give a usage example, here an "other: please specify" type field is dependant on it's choice field

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder

            ->add('refer', 'choice', array(
                'label' => 'How did you hear about us?',
                'constraints' => array(
                    new NotBlank(),
                ),
                'choices' => array(
                    'internet' => 'Internets',
                    'other' => 'Other'
                ),
            ))
            ->add('referOther', 'text', array(
                'label' => 'Please specify',
                'depends' => array(
                    '[refer]' => 'other'
                ),
                'constraints' => array(
                    new NotBlank(),
                ),
            ));
    }

@webmozart
Copy link
Contributor

I think you are posing two questions in your issue that I'll answer separately:

  1. You want to copy the decision logic for determining the validation groups to JavaScript. As you said yourself, this is clearly impossible without something like an abstract expression language. So this part always needs to be recoded in JavaScript.
  2. You want to declaratively build field dependencies. This is exactly what [Form] Support dependent fields #5807 proposes. Unfortunately the solution you propose is not generic enough to be acceptable for [Form] Support dependent fields #5807, because you only cover static dependency definitions (a depends on b), but not dynamic ones (a depends on b if b is set to x, a depends on b if c is not empty etc.)

Since 1. is not easily solvable and 2. is covered by #5807, I'll close this as a duplicate.

@webmozart
Copy link
Contributor

I forgot to answer a third question. You want to change options of a field after that field was constructed. This is impossible by design. Instead, you can overwrite the field with a new one and different options:

$config = $form->get('field')->getConfig();
$type = $config->getType()->getName();
$options = $config->getOptions();

$form->add('field', $type, array_replace($options, array(
    'constraints' => // changed constraints...
));

@lwc
Copy link
Author
lwc commented Aug 1, 2013

Thanks for responding!

As far as point 1 goes, my plan was to serialise constraints and their associated options to data attributes, somewhat like <input type="email" name="email" data-constraints="{'NotBlank': {}, 'Email': {'checkMX': true}}" />

With the exception of validation groups, it would be possible to write a generic js library that could pick up all those data attributes and re-create the validation schema client side.

As far as point 2 goes, I was considering (re-)using the constraints / validation system to allow for complex, value based dependencies, and at least dependencies on certain values would be needed for our use case - see the TODO in DependsListener::preSubmit

Thanks for the pointer on overwriting fields - maybe I can use that in the interim.

Once again, thanks for the feedback

  • Luke Cawood

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants
0