Skip to content

Instantly share code, notes, and snippets.

@mattsnowboard
Last active December 26, 2015 00:39
Show Gist options
  • Save mattsnowboard/7065865 to your computer and use it in GitHub Desktop.
Save mattsnowboard/7065865 to your computer and use it in GitHub Desktop.
This demonstrates an error in a Symfony2 form collection with dynamically added fields that are not filled out.
{
"require": {
"php": ">=5.4.11",
"silex/silex": "~1.0",
"silex/web-profiler": "~1.0",
"symfony/browser-kit": "~2.3",
"symfony/class-loader": "~2.3",
"symfony/console": "~2.3",
"symfony/config": "~2.3",
"symfony/css-selector": "~2.3",
"symfony/dom-crawler": "~2.3",
"symfony/filesystem": "~2.3",
"symfony/finder": "~2.3",
"symfony/form": "~2.3",
"symfony/locale": "~2.3",
"symfony/process": "~2.3",
"symfony/serializer": "~2.3",
"symfony/translation": "~2.3",
"symfony/validator": "~2.3",
"symfony/twig-bridge": "~2.3",
"twig/twig": ">=1.8,<2.0-dev",
},
"autoload": {
"psr-0": { "": "src/" }
}
}
<?php
use Symfony\Component\ClassLoader\DebugClassLoader;
use Symfony\Component\HttpKernel\Debug\ErrorHandler;
use Symfony\Component\HttpKernel\Debug\ExceptionHandler;
require_once __DIR__.'/../vendor/autoload.php';
error_reporting(-1);
DebugClassLoader::enable();
ErrorHandler::register();
if ('cli' !== php_sapi_name()) {
ExceptionHandler::register();
}
$env = 'dev';
$app = require __DIR__.'/../src/testapp.php';
$app->run();
<html>
<head>
<title>Test</title>
</head>
<body>
{{ 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) }}
<script>window.jQuery || document.write('<script src="/js/vendor/jquery-1.10.1.min.js"><\/script>')</script>
<script>
// Get the ul that holds the collection of tags
var collectionHolder = $('ul.tasks');
// setup an "add a tag" link
var $addTaskLink = $('<a href="#" class="add_task_link">Add a task</a>');
var $newLinkLi = $('<li></li>').append($addTaskLink);
function addTaskForm(collectionHolder, $newLinkLi) {
// Get the data-prototype explained earlier
var prototype = collectionHolder.data('prototype');
// get the new index
var index = collectionHolder.data('index');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
var newForm = prototype.replace(/__name__/g, index);
// increase the index with one for the next item
collectionHolder.data('index', index + 1);
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
}
jQuery(document).ready(function() {
// add the "add a tag" anchor and li to the tags ul
collectionHolder.append($newLinkLi);
// count the current form inputs we have (e.g. 2), use that as the new
// index when inserting a new item (e.g. 2)
collectionHolder.data('index', collectionHolder.find(':input').length);
$addTaskLink.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// add a new tag form (see next code block)
addTaskForm(collectionHolder, $newLinkLi);
});
});
</script>
</body>
</html>
<?php
use Silex\Application;
use Silex\Provider\FormServiceProvider;
use Silex\Provider\ServiceControllerServiceProvider;
use Silex\Provider\SessionServiceProvider;
use Silex\Provider\SwiftmailerServiceProvider;
use Silex\Provider\TranslationServiceProvider;
use Silex\Provider\TwigServiceProvider;
use Silex\Provider\UrlGeneratorServiceProvider;
use Silex\Provider\ValidatorServiceProvider;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
$app = new Application();
$app['debug'] = true;
$app->register(new FormServiceProvider());
$app->register(new ServiceControllerServiceProvider());
$app->register(new SessionServiceProvider());
$app->register(new SwiftmailerServiceProvider());
$app->register(new TranslationServiceProvider(), array(
'locale_fallback' => 'en',
));
$app->register(new UrlGeneratorServiceProvider());
$app->register(new ValidatorServiceProvider());
// TWIG
$app->register(new TwigServiceProvider(), array(
'twig.path' => array(__DIR__.'/views'),
));
//
// MODEL
//
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;
}
}
//
// FORM
//
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, // This causes the issue
'required' => false
))
->add('submit', 'submit')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => '\Project',
));
}
public function getName()
{
return 'project';
}
}
//
// Controller
//
$app->match('/', function (Request $request) use ($app) {
$project = new Project();
// dummy code - this is here just so that the Task has some tags
// otherwise, this isn't an interesting example
$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')
;
return $app;
@mattsnowboard
Copy link
Author

If you try to add a task with javascript, but don't fill in its name and submit the form, you 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

@mattsnowboard
Copy link
Author

Nevermind, the problem was 'required' => false on the collection.

->add('tasks', 'collection', array(
'type' => new TaskType(),
'allow_add' => true,
'by_reference' => false,
'allow_delete' => true,
'required' => false // this causes the issue!
))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment