-->

Symfony2 Form Collection allow_add and allow_delet

2019-05-27 10:09发布

问题:

I've run into a problem when following the Symfony cookbook for form collections with add/remove. See: http://symfony.com/doc/current/cookbook/form/form_collections.html

Now, for some reason, if I dynamically add a form row but don't fill in any of its fields, I get the following error:

ContextErrorException: Catchable Fatal Error: Argument 1 passed to Project::addTask() must be an instance of Task, null given in D:\web_workspace\wedding\src\testapp.php line 82

I would like people to be able to have blank rows in the form which will just get ignored. For example, if you click "Add Task" a few times, but don't fill in the last row, the form should still be submitted, and the last row should be ignored.

I've created a very simple Silex demo that fits in just a couple of files. I'll highlight it here, but the full example is here, and can be run by just adding Silex via composer: https://gist.github.com/mattsnowboard/7065865

I have the following Models (just Project which has a description and a collection of Tasks)

class Task
{
    protected $name;

    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;
        return $this;
    }
}

class Project
{
    protected $description;

    protected $tasks;

    public function __construct()
    {
        $this->tasks = array();
    }

    public function getDescription()
    {
        return $this->description;
    }

    public function setDescription($description)
    {
        $this->description = $description;
        return $this;
    }

    public function getTasks()
    {
        return $this->tasks;
    }
    public function addTask(Task $task)
    {
        if (!is_null($task) && !in_array($task, $this->tasks)) {
            $this->tasks[] = $task;
        }
        return $this;
    }
    public function removeTask(Task $task)
    {
        if (!is_null($task)) {
            $this->tasks = array_diff($this->tasks, array($task));
        }
        return $this;
    }
}

My forms are as follows

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name', 'text', array(
            'required' => false
        ));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => '\Task',
        ));
    }

    public function getName()
    {
        return 'task';
    }
}

class ProjectType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('description', 'text', array())
            ->add('tasks', 'collection', array(
                'type' => new TaskType(),
                'allow_add'    => true,
                'by_reference' => false,
                'allow_delete' => true,
                'required' => false
            ))
            ->add('submit', 'submit')
        ;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => '\Project',
        ));
    }

    public function getName()
    {
        return 'project';
    }
}

The controller adds some test data to the form and just prints the data when it is submitted:

$app->match('/', function (Request $request) use ($app) {
    $project = new Project();

    // dummy code
    $task1 = new Task();
    $task1->setName('A Task');
    $project->addTask($task1);
    // end dummy code

    $form = $app['form.factory']->create(new ProjectType(), $project);

    $form->handleRequest($request);

    if ($form->isValid()) {
        $data = $form->getData();
        $debug = print_r($data, true);
        echo $debug;
    }

    return $app['twig']->render('test.html.twig', array(
        'form' => $form->createView(),
    ));
})
->method('GET|POST');

The view is mostly this, and the javascript sample from the cookbook

{{ form_start(form) }}

    {{ form_row(form.description) }}

    <h3>Tags</h3>
    <ul class="tasks" data-prototype="{{ form_widget(form.tasks.vars.prototype)|e }}">
        {# iterate over each existing tag and render its only field: name #}
        {% for task in form.tasks %}
            <li>{{ form_row(task.name) }}</li>
        {% endfor %}
    </ul>

    {{ form_widget(form.submit) }}
{{ form_end(form) }}

Am I just missing something? Is this a bug? I feel like this example which is very close to the cookbook should work pretty easily...

Updated: Removing "allow_deleted" doesn't actually fix this.

回答1:

Sorry, I figured it out, the collection type shouldn't have 'required' => false

    $builder->add('description', 'text', array())
        ->add('tasks', 'collection', array(
            'type' => new TaskType(),
            'allow_add'    => true,
            'by_reference' => false,
            'allow_delete' => true,
            'required' => false
        ))

Should be

    $builder->add('description', 'text', array())
        ->add('tasks', 'collection', array(
            'type' => new TaskType(),
            'allow_add'    => true,
            'by_reference' => false,
            'allow_delete' => true
        ))