Search code examples
phpsymfonysymfony4symfony-forms

Get initial value of entity in FormExtension


In my update form, I want to add a data attribute on the inputs that will contains the initial value of the entity. This way, I will be able to highlight the input when the user will modify it.

In the end, only the input modified by the users will be highlighted.

I want to use this only in update, not in creation.

To do so, I created a form extension like this:

class IFormTypeExtension extends AbstractTypeExtension
{
...

public static function getExtendedTypes()
{
    //I want to be able to extend any form type
    return [FormType::class];
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'is_iform' => false,
        'is_iform_modification' => function (Options $options) {
            return $options['is_iform'] ? null : false;
        },
    ]);
    $resolver->setAllowedTypes('is_iform', 'bool');
    $resolver->setAllowedTypes('is_iform_modification', ['bool', 'null']);
}

public function buildView(FormView $view, FormInterface $form, array $options)
{
    if (!$options['is_iform'] && !$this->isParentIForm($form)) {
        return;
    }

    //We need to add the original value in the input as data-attributes
    if (is_string($form->getViewData()) || is_int($form->getViewData())) {
        $originValue = $form->getViewData();
    } elseif (is_array($form->getViewData())) {
        if (is_object($form->getNormData())) {
            $originValue = implode('###', array_keys($form->getViewData()));
        } elseif (is_array($form->getNormData()) && count($form->getNormData()) > 0 && is_object($form->getNormData()[0])) {
            $originValue = implode('###', array_keys($form->getViewData()));
        } else {
            $originValue = implode('###', $form->getViewData());
        }
    } else {
        //There's no value yet
        $originValue = '';
    }

    $view->vars['attr'] = array_merge($view->vars['attr'], ['data-orig-value' => $originValue]);
}

private function isParentIForm(FormInterface $form)
{
    if (null === $form->getParent()) {
        return $form->getConfig()->getOption('is_iform');
    }

    return $this->isParentIForm($form->getParent());
}
}

As you can see in the buildView method, I get the originValue from the ViewData.

In a lot of cases, this works well.

But if I have any validation error in my form OR if I reload my form through AJAX, the ViewData contains the new information and not the values of the entity I want to update.

How can I get the values of the original entity?

  1. I don't want to make a DB request in here.
  2. I think I can use the FormEvents::POST_SET_DATA event, then save the entity values in session and use these in the buildView.
  3. I could also give a new Option in my OptionResolver to ask for the initial entity.
  4. Is it possible to have the original data of the entity directly form the buildView? (If I'm not wrong, this means the form before we call the handleRequest method).

Someone wanted to have an example with a controller. I don't think it's really interresting, because with the FormExtension, the code will be added automatically. But anyway, here is how I create a form in my controller :

$form = $this->createForm(CustomerType::class, $customer)->handleRequest($request);

And in the CustomerType, I will add the 'is_iform' key with configureOptions() :

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        "translation_domain" => "customer",
        "data_class" => Customer::class,
        'is_iform' => true //This line will activate the extension
    ]);
}

Solution

  • In the end, I managed to make it work BUT I'm not fully convinced by what I did.

    It was not possible to get the original data from the form OR add a new property (the form is read only in the form extension).

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->addEventListener(
            FormEvents::POST_SET_DATA,
            function (FormEvent $event) {
                $form = $event->getForm();
                if ('_token' === $form->getName()) {
                    return;
                }
    
                $data = $event->getData();
                $this->session->set('iform_'.$form->getName(), is_object($data) ? clone $data : $data);
            }
        );
    }
    

    What I do here, is simply register the form values by its name in the session. If it's an object, I need to clone it, because the form will modify it later in the process and I want to work with the original state of the form.

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'is_iform' => false,
            'is_iform_modification' => function (Options $options) {
                return $options['is_iform'] ? null : false;
            },
        ]);
        $resolver->setAllowedTypes('is_iform', 'bool');
        $resolver->setAllowedTypes('is_iform_modification', ['bool', 'null']);
    }
    

    The configure options did not change. And then, depending on the value type, I create my "data-orig-value" :

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        if (!$options['is_iform'] && !$this->isParentIForm($form)) {
            return;
        }
    
        $propertyValue = $this->session->get('iform_'.$form->getName());
        $originValue = '';
    
        try {
            if (null !== $propertyValue) {
                //We need to add the original value in the input as data-attributes
                if (is_bool($propertyValue)) {
                    $originValue = $propertyValue ? 1 : 0;
                } elseif (is_string($propertyValue) || is_int($propertyValue)) {
                    $originValue = $propertyValue;
                } elseif (is_array($propertyValue) || $propertyValue instanceof Collection) {
                    if (is_object($propertyValue)) {
                        $originValue = implode('###', array_map(function ($object) {
                            return $object->getId();
                        }, $propertyValue->toArray()));
                    } elseif (is_array($propertyValue) && count($propertyValue) > 0 && is_object(array_values($propertyValue)[0])) {
                        $originValue = implode('###', array_map(function ($object) {
                            return $object->getId();
                        }, $propertyValue));
                    } else {
                        $originValue = implode('###', $propertyValue);
                    }
                } elseif ($propertyValue instanceof DateTimeInterface) {
                    $originValue = \IntlDateFormatter::formatObject($propertyValue, $form->getConfig()->getOption('format', 'dd/mm/yyyy'));
                } elseif (is_object($propertyValue)) {
                    $originValue = $propertyValue->getId();
                } else {
                    $originValue = $propertyValue;
                }
            }
        } catch (NoSuchPropertyException $e) {
            if (null !== $propertyValue = $this->session->get('iform_'.$form->getName())) {
                $originValue = $propertyValue;
                $this->session->remove('iform_'.$form->getName());
            } else {
                $originValue = '';
            }
        } finally {
            //We remove the value from the session, to not overload the memory
            $this->session->remove('iform_'.$form->getName());
        }
    
        $view->vars['attr'] = array_merge($view->vars['attr'], ['data-orig-value' => $originValue]);
    }
    
    private function isParentIForm(FormInterface $form)
    {
        if (null === $form->getParent()) {
            return $form->getConfig()->getOption('is_iform');
        }
    
        return $this->isParentIForm($form->getParent());
    }
    

    Maybe the code sample will help anyone ! If anyone have a better option, don't hesitate to post it !