Форма: избегайте установки нулевого значения для неотправленного поля

У меня есть простая модель (упрощенная из источника):

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

И тип формы:

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',
        ));
    }
}

Мой контроллер:

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;
}

Когда я POST /users/?form[page]=2&form[limit]=20, ответ будет таким, как я ожидаю:

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

Теперь, когда я POST /users/?form[page]=3, ответ:

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

limit становится нулевым, поскольку он не был отправлен.

я хотел получить

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

Вопрос: как изменить поведение формы, чтобы она игнорировала неотправленные значения?


person AdrienBrault    schedule 27.07.2012    source источник


Ответы (2)


Если проблема заключается только в параметрах (параметры GET), вы можете определить значение по умолчанию в файле маршрутизации.

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

Альтернативным способом может быть использование хука (например, PRE_BIND). и вручную обновить это значение в этом событии. Таким образом, вы не распространяете «логику» на несколько фрагментов кода.

Окончательный код, предложенный Адрианом, будет

<?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);
            }
        }
    }
}
person DonCallisto    schedule 27.07.2012
comment
Моя форма будет использоваться во многих контроллерах, так что это приведет к повторению. - person AdrienBrault; 27.07.2012
comment
@AdrienBrault Да, но вы должны одинаково определить маршрут для каждого ... Лучшим решением может быть использование только контроллера, который внутри него вызывает диспетчер, который приведет вас к нужному контроллеру .... - person DonCallisto; 27.07.2012
comment
Я думаю, что слушатель был бы лучше, чем диспетчер. Во всяком случае, здесь я прошу решения на уровне формы. - person AdrienBrault; 27.07.2012
comment
Спасибо :), я использовал подписчика событий в PRE_BIND и удалил все дочерние формы, у которых не было данных для привязки. Вы можете обновить свой ответ с помощью моего кода (часть решения моего ответа). - person AdrienBrault; 27.07.2012
comment
Любые мысли о том, как решить эту проблему, если вы хотите отобразить форму? т.е. вы не можете просто удалить поля. - person Steve; 14.08.2012
comment
Я имел в виду, что если вам нужно отображать форму в представлении после отправки, удаление полей, очевидно, означает, что они не могут быть отображены. В итоге я привязал свои значения по умолчанию вместе с данными запроса, например, $form->bind($request->query->all() + array('distance' => 100));, хакерский, но единственный способ, которым я мог обойти проблему. - person Steve; 16.08.2012
comment
Также полезно отметить, что динамическое удаление полей формы может привести к «запутанному» поведению при использовании с валидатором. Например. в моем случае у меня было поле title с ограничением проверки NotBlank. Когда я разместил форму без поля заголовка, ошибки проверки всплыли на корневую форму, потому что поле формы заголовка было удалено этим подписчиком. Мне потребовалась целая вечность, чтобы понять, почему ошибки не привязаны к заголовку, даже если error_bubbling=false. - person TomiS; 12.05.2013

Вот модификация исходного ответа. Наиболее важным преимуществом этого решения является то, что валидаторы теперь могут вести себя так, как будто сообщение формы всегда будет полным, что означает отсутствие проблем с всплывающими ошибками и тому подобным.

Обратите внимание, что имена полей объекта должны совпадать с именами полей формы, чтобы этот код работал.

<?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);

    }
}
person TomiS    schedule 13.05.2013
comment
Да, это решение, которое я в итоге использовал. См. github.com /adrienbrault/symfony-hateoas-sandbox/blob/master/src/ . Убедитесь, что вы не взаимодействуете с данными напрямую, как в своем ответе. - person AdrienBrault; 13.05.2013