PMG Digital Made for Humans

Keeping Symfony Form Values Around on Empty Submissions

4 MINUTE READ | March 4, 2016

Keeping Symfony Form Values Around on Empty Submissions

Author's headshot

Christopher Davis

Christopher Davis has written this article. More details coming soon.

Last week we ran into a problem with a Symfony form: we had a password field in a form meant to store an FTP password (FTP & SFTP are still the defacto way to move data around in the marketing world). When that form was first submitted with the password in place it was saved just fine, but subsequent submissions where the password was empty on the frontend of our application (for obvious reasons) would erase the password. Not good: expectations were violated and errors happened.

As complex as the Symfony form component is, it’s also incredibly flexible. Like the HTTP Kernel, the form component comes with a set of events that can used to keep original values around.

The essential idea here is that we’ll hook into the PRE_SUBMIT event and inspect the incoming data. If the field we want to keep is empty, we can use the property access component to fetch the original value from the object/array bound to the form on creation. The property accessor is important: data bound to forms may be objects or arrays or just about anything.

KeepValueListener.php

<?phpuse Symfony\Component\PropertyAccess\PropertyAccess;use Symfony\Component\PropertyAccess\PropertyAccessorInterface;use Symfony\Component\Form\FormEvents;use Symfony\Component\Form\FormEvent;use Symfony\Component\EventDispatcher\EventSubscriberInterface;final class KeepValueListener implements EventSubscriberInterface{    private $keepField;    private $accessor;    public function __construct($keepField, PropertyAccessorInterface $accessor=null)    {        $this->keepField = $keepField;        $this->accessor = $accessor ?: PropertyAccess::createPropertyAccessor();    }    public static function getSubscribedEvents()    {        return [            FormEvents::PRE_SUBMIT => 'onPreSubmit',        ];    }    public function onPreSubmit(FormEvent $event)    {        $field = $event->getForm()->get($this->keepField);        $submitData = $event->getData();        if (empty($submitData[$this->keepField])) {            $submitData[$this->keepField] = $this->accessor->getValue(                $event->getForm()->getData(),                $field->getPropertyPath()            );            $event->setData($submitData);        }    }}
Of note here is that $event->getData() returns the data submitted to the form, while $event->getForm()->getData() returns the original values bound to the form on creation — what you passed in as the second argument to your controllers createForm method, for instance.

It’s pretty simple to prove this works with an integration test. I wouldn’t recommend a unit test for this listener or somethign that calls KeepValueListener::onPreSubmit directly because it’s less valuable than knowing that the listener works within the system as a whole.

Our test case will set up a form factory and provide a way to create form with the our listener added.

test_skeleton.php

<?phpuse Symfony\Component\Form\Forms;use Symfony\Component\Form\Extension\Core\Type as CoreTypes;class KeepValueListenerTest extends \PHPUnit_Framework_TestCase{    private $formFactory;        protected function setUp()    {        $this->formFactory = Forms::createFormFactory();    }    private function createForm($data)    {        return $this->formFactory->createBuilder(CoreTypes\FormType::class, $data)            ->add('password', CoreTypes\PasswordType::class, [                'empty_data' => null,            ])            ->add('clear_password', CoreTypes\CheckboxType::class)            ->addEventSubscriber(new KeepValueListener('password'))            ->getForm();    }}

From here there’s two paths we need to test: submission with an empty value which should keep the original password value bound on form creation and submission with a new value which should keep the new value around.

KeepValueListenerTest.php

<?phpuse Symfony\Component\Form\Forms;use Symfony\Component\Form\Extension\Core\Type as CoreTypes;class KeepValueListenerTest extends \PHPUnit_Framework_TestCase{    public function testEmptyFieldValueInSubmissionKeepsTheOriginalValueAround()    {        $form = $this->createForm([            'password' => 'original',        ]);        $form->submit([            'password' => '',        ]);        $data = $form->getData();        $this->assertEquals('original', $data['password']);    }    public function testNonEmptyValueInSubmissionReplacesTheExistingValue()    {        $form = $this->createForm([            'password' => 'original',        ]);        $form->submit([            'password' => 'changed',        ]);        $data = $form->getData();        $this->assertEquals('changed', $data['password']);    }        // ...}

This listener as it stands makes this impossible, so let’s fix it! We’ll add another field the listener is aware of that tells the thing to erase the field on a truthy value.

KeepValueListener changes a bit to accept the $clearField to the constructor. If it sees a truthy value in that field it will erase the other field by setting its data to the forms configured empty value.

KeepValueListener2.php

<?phpfinal class KeepValueListener implements EventSubscriberInterface{    private $keepField;    private $clearField;    private $accessor;    public function __construct($keepField, $clearField=null, PropertyAccessorInterface $accessor=null)    {        $this->keepField = $keepField;        $this->clearField = $clearField;        $this->accessor = $accessor ?: PropertyAccess::createPropertyAccessor();    }    public function onPreSubmit(FormEvent $event)    {        $field = $event->getForm()->get($this->keepField);        $submitData = $event->getData();        $erase = false;        if ($this->clearField && isset($submitData[$this->clearField])) {            $erase = self::asBool($submitData[$this->clearField]);        }        if ($erase) {            $submitData[$this->keepField] = $field->getConfig()->getEmptyData();            $event->setData($submitData);            return;        }        if (empty($submitData[$this->keepField])) {            $submitData[$this->keepField] = $this->accessor->getValue(                $event->getForm()->getData(),                $field->getPropertyPath()            );            $event->setData($submitData);        }    }    private static function asBool($value)    {        return filter_var($value, FILTER_VALIDATE_BOOLEAN);    }}

And of course we need a test case to verify this.

KeepValueListenerTest2.php

<?phpclass KeepValueListenerTest extends \PHPUnit_Framework_TestCase{    public function testFormSubmissionWithClearFieldSetRemovesTheValue()    {        $form = $this->createForm([]);        $form->submit([            'password' => 'test',            'clear_password' => '1',        ]);        $data = $form->getData();        $this->assertNull($data['password']);    }}

Even if this example doesn’t fit your use case(s), it’s a great demonstration of how flexible the form component can be. Events make data and form modification extremely powerful, but there’s also things like data transformers and modification of existing form types with type extensions.

Stay in touch

Bringing news to you

Subscribe to our newsletter

By clicking and subscribing, you agree to our Terms of Service and Privacy Policy

All of the code for this blog post is available on gist.github.com.


Related Content

thumbnail image

AlliPMG CultureCampaigns & Client WorkCompany NewsDigital MarketingData & Technology

PMG Innovation Challenge Inspires New Alli Technology Solutions

4 MINUTES READ | November 2, 2021

thumbnail image

Applying Function Options to Domain Entities in Go

11 MINUTES READ | October 21, 2019

thumbnail image

My Experience Teaching Through Jupyter Notebooks

4 MINUTES READ | September 21, 2019

thumbnail image

Working with an Automation Mindset

5 MINUTES READ | August 22, 2019

thumbnail image

3 Tips for Showing Value in the Tech You Build

5 MINUTES READ | April 24, 2019

thumbnail image

Testing React

13 MINUTES READ | March 12, 2019

thumbnail image

A Beginner’s Experience with Terraform

4 MINUTES READ | December 20, 2018

ALL POSTS