5

I've got a simple model (simplified of source):

class Collection
{
    public $page;
    public $limit;
}

And a form type:

class CollectionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('page', 'integer');
        $builder->add('limit', 'integer');
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'FSC\Common\Rest\Form\Model\Collection',
        ));
    }
}

My controller:

public function getUsersAction(Request $request)
{
    $collection = new Collection();
    $collection->page = 1;
    $collection->limit = 10;

    $form = $this->createForm(new CollectionType(), $collection)
    $form->bind($request);

    print_r($collection);exit;
}

When i POST /users/?form[page]=2&form[limit]=20, the response is what i expect:

Collection Object
(
    [page:public] => 2
    [limit:public] => 20
)

Now, when i POST /users/?form[page]=3, the response is:

Collection Object
(
    [page:public] => 3
    [limit:public] =>
)

limit becomes null, because it was not submitted.

I wanted to get

Collection Object
(
    [page:public] => 3
    [limit:public] => 10 // The default value, set before the bind
)

Question: How can i change the form behaviour, so that it ignores non submitted values ?

AdrienBrault
  • 7,747
  • 4
  • 31
  • 42

2 Answers2

10

If is only a problem of parameters (GET parameters) you can define the default value into routing file

route_name:
pattern: /users/?form[page]={page}&form[limit]={limit}
defaults: { _controller: CompanyNameBundleName:ControllerName:ActionName, 
                         limit:10 }

An alternative way could be to use a hook (i.e. PRE_BIND) and update manually that value into this event. In that way you haven't the "logic" spreaded into multi pieces of code.

Final code - suggested by Adrien - will be

<?php

use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;

class IgnoreNonSubmittedFieldSubscriber implements EventSubscriberInterface
{
    private $factory;

    public function __construct(FormFactoryInterface $factory)
    {
        $this->factory = $factory;
    }

    public static function getSubscribedEvents()
    {
        return array(FormEvents::PRE_BIND => 'preBind');
    }

    public function preBind(FormEvent $event)
    {
        $submittedData = $event->getData();
        $form = $event->getForm();

        // We remove every child that has no data to bind, to avoid "overriding" the form default data
        foreach ($form->all() as $name => $child) {
            if (!isset($submittedData[$name])) {
                $form->remove($name);
            }
        }
    }
}
DonCallisto
  • 29,419
  • 9
  • 72
  • 100
  • My form will be used in many controllers, so this will lead to repetition. – AdrienBrault Jul 27 '12 at 12:36
  • @AdrienBrault Yes but you have to define a route for each one equally... A better solution could be to use only a controller that, inside it calls a dispatcher that will lead you to the right controller .... – DonCallisto Jul 27 '12 at 12:39
  • I think that a listener would be better than a dispatcher. Anyway, here, i'm asking for a solution at the form level. – AdrienBrault Jul 27 '12 at 12:46
  • Thanks :), i used an event subscriber on the PRE_BIND, and removed every child form that would have had no data to bind. You can update your answer with my code (solution part of my answer). – AdrienBrault Jul 27 '12 at 14:21
  • Any thoughts on how to tackle this if you want to render the form? I.e. you can't just remove the fields. – Steve Aug 14 '12 at 13:29
  • What I meant was if you have to render the form in a view post-submission, removing the fields obviously means they can't be rendered. What I ended up doing was binding my defaults along with my request data like so `$form->bind($request->query->all() + array('distance' => 100));`, hacky, but the only way I could see around the problem. – Steve Aug 16 '12 at 09:06
  • It's also good to note that removing form fields dynamically can cause 'confusing' behavior when used with validator. E.g. in my case I had a field `title` with validation constraint `NotBlank`. When I posted a form without title field, the validation errors bubbled to root form because title form field had been removed by this subscriber. It took me ages to figure out why errors are not bound to title even if error_bubbling=false. – TomiS May 12 '13 at 19:38
2

Here's a modification of the original answer. The most important benefit of this solution is that validators can now behave as if the form post would always be complete, which means there's no problems with error bubbling and such.

Note that object field names must be identical to form field names for this code to work.

<?php
namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;

class FillNonSubmittedFieldsWithDefaultsSubscriber implements EventSubscriberInterface
{
    private $factory;

    public function __construct(FormFactoryInterface $factory)
    {
        $this->factory = $factory;
    }

    public static function getSubscribedEvents()
    {
        return array(FormEvents::PRE_BIND => 'preBind');
    }

    public function preBind(FormEvent $event)
    {
        $submittedData = $event->getData();
        $form = $event->getForm();

        // We complete partial submitted data by inserting default values from object
        foreach ($form->all() as $name => $child) {
            if (!isset($submittedData[$name])) {
                $obj = $form->getData();

                $getter = "get".ucfirst($name);
                $submittedData[$name] = $obj->$getter();
            }
        }
        $event->setData($submittedData);

    }
}
TomiS
  • 123
  • 7
  • 1
    Yep, that's the solution I ended up using. See https://github.com/adrienbrault/symfony-hateoas-sandbox/blob/master/src/AdrienBrault/ApiBundle/Form/EventListener/ReplaceNotSubmittedValuesByDefaultsListener.php . Make sure not to interact with the data directly like you're doing in your answer – AdrienBrault May 13 '13 at 16:36