I use Symfony2.1's FormBuilder to create a dynamic form in the controller:
$form = $this->createFormBuilder($defaultData);
$form->add('field','text');
I would like to embed another form in the same way and embed it with the main form.
$subForm = $this->createFormBuilder();
$subForm->add('subfield','text');
// Does not work
$form->add('subform', $subForm);
Unfortunately this setup does not work properly. I could not find any way how to add a dynamically generated subform into a dynamically generated form, like the example above.
What's the function call I am missing?
The exception thrown by the example code above is
UnexpectedTypeException: Expected argument of type "string or
Symfony\Component\Form\FormTypeInterface",
"Symfony\Component\Form\Form" given.
I would create two Form Clases in two separate fields:
<?php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class FirstFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('field', 'text')
->add('subform', new SecondFormType());
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Entity\FirstEntity'
));
}
public function getName()
{
return 'first';
}
}
<?php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SecondFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('subfield', 'text');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Entity\SecondEntity',
));
}
public function getName()
{
return 'second';
}
}
After this creation you can create your form from any Controller with this line for creation forms:
$form = $this->createForm(new FirstFormType());
or for edition forms:
$form = $this->createForm(new FirstFormType(), $first);
In your example code you are trying to assig to a field an entire form instead of a Form Type.
Hope it helps
Recommended symfony-way is to create separate FormType classes, as mentioned above. But if you really want to..
You can add dynamic subform into form builder, by calling
$formBuilder->add($subform /* FormBuilder */);
This subform will have name "form" if created with $this->createFormBuilder(); , that means that you can not add two or more subforms created this way - newer one will overwrite previous with same name.
If you need to add multiple sub-forms, you have to create their builders with
$this->get('form.factory')->createNamedBuilder($uniqName)
Without making classes... inside a Controller Action:
$subFormBuilder = $this->createFormBuilder(
null /* default data */,
['label' => 'Sub Form'] /* options */
)
->add('name');
$form = $this->createFormBuilder()
->add($subFormBuilder)
->add('number')
->getForm();
Related
In Symfony's How to Unit Test your Forms document is quite straight forward.
But I got a problem.
I have an doctrine entity with some default values. And a form type which is bound to the model. I mean 'data_class' is the entity. In this case, when I call $form->submit(); and $form->isSynchronized(), then the default values are gone. And the $this->assertEquals($object, $objectToCompare); fails.
How can I test a form type which is bound to a model class with some default values?
I suspect this is a bug. When the form type instance is created in $this->factory->create(TestedType::class, $objectToCompare);, the default values of $objectToCompare should be applied.
Or, should I create an extension to handle that?
Here're the example codes. To simplify the code, I changed the doctrine entity into the simple model.
The model
For comparing, I added 2 properties: one with the default value and the other without.
// AppBundle/Model/TestObject.php
namespace AppBundle\Model;
class TestObject
{
private $test;
private $test2 = 'default value';
public function getTest()
{
return $this->test;
}
public function setTest($test)
{
$this->test = $test;
}
public function getTest2()
{
return $this->test2;
}
public function setTest2($test2)
{
$this->test2 = $test2;
}
}
The form type
It is straight forward.
adding 2 text type fields and 1 submit type field
set default option 'data_class' to the model
// AppBundle/Form/Type/TestedType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use AppBundle\Model\TestObject;
class TestedType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('test', TextType::class);
$builder->add('test2', TextType::class);
$builder->add('save', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults(array(
'data_class' => TestObject::class,
));
}
}
The testing
It's little modified version of How to Unit Test your Forms.
$object is populated with property access
checking fields existence is omitted.
// tests/AppBundle/Form/Type/TestedTypeTest.php
namespace Tests\AppBundle\Form\Type;
use AppBundle\Form\Type\TestedType;
use AppBundle\Model\TestObject;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\PropertyAccess\PropertyAccess;
class TestedTypeTest extends TypeTestCase
{
public function testSubmitValidData()
{
$formData = [
'test' => 'test',
];
$objectToCompare = new TestObject();
$form = $this->factory->create(TestedType::class, $objectToCompare);
$object = new TestObject();
$propertyAccessor = PropertyAccess::createPropertyAccessor();
foreach ($formData as $key => $value) {
$propertyAccessor->setValue($object, $key, $value);
}
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($object, $objectToCompare);
}
}
I have the following form:
class TestFormType extends AbstractType
{
protected $testArgument;
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (isset($options['testArgument'])) {
$this->testArgument = $options['testArgument'];
}
$builder->add('textField', 'Symfony\Component\Form\Extension\Core\Type\TextType');
}
public function configureOptions(OptionsResolver $optionsResolver)
{
$optionsResolver->setRequired('testArgument');
$optionsResolver->setDefaults(
array(
'data_class' => get_class($this->testArgument)
)
);
}
}
I am passing the value for the testArgument attribute via form options (Symfony 3 modifications), but when is comes to get the class name of the attribute to set the 'data_class' inside configureOptions method, it is always null. Basically I need to depend on the form type class attribute inside the configureOptions method.Can someone please help me out here to the right direction ?
I had to pass the dependency in configureOptions method from the form factory create method itself:
$form = $this->factory->create(
'app\TestBundle\Form\Type\TestFormType',
$this->testArgument,
array(
'data_class' => get_class($this->testArgument)
)
);
as it would not be set by the default settings in the form type and had to refactor the form type class as follows:
class TestFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('textField', 'Symfony\Component\Form\Extension\Core\Type\TextType');
}
}
You should pass the *Type __constructor for use to
use App\Entity\Blog;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class BlogType extends AbstractType {
private $someDependency;
public function __construct($someDependency)
{
$this->someDependency = $someDependency;
}
// ...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'empty_data' => new Blog($this->someDependency),
]);
} }
I've been using Symfony a bit and i'm trying to figure out a way to create a form.
I need to use a MVC based solution.
My form needs to ask several information of different Entities and then i need to process that information extracting it in the database.
The database wont be a problem.
I was just figuring out how do i make a form with different types of entities ?
And how do i make a scrolldown menu with the data contained in the database for an entity ?
If the comment by #chalasr does not apply, i.e., the entities are not related, it is possible to do something like the following in a controller. Simply create a $form variable for each Entity{x}Type form as in:
$formA = $this->createForm(AppBundle\Entity\EntityAType($entityA));
$formB = $this->createForm(AppBundle\Entity\EntityBType($entityB));
...
return array(
'formA' => $formA->createView(),
'formB' => $formB->createView(),
...
);
You can simply combine the Forms while keeping each separate like so
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ReallyBigFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('foo', FooType::class, [
// label, required, ... as well as options of FooType
])
->add('bar', BarType::class, [
// label, required, ... as well as options of BarType
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([]);
}
}
and define FooType and BarType like a regular Form
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType
class FooType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, [
'label' => 'foo.name',
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Foo',
]);
}
}
I could get this to work prior to v2.8 but as symfony now uses fully qualified class names name i'm unsure sure how to proceed.
I can pass an array (to populate a choice field) to a form without issue but if there is an another formType added via a collectionType how can a pass the array?
BTW - the array is gathered from data from a custom annotations - NOT an entity
Heres my code:
PageType.php
<?php
namespace Prototype\PageBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
const ActiveComponentsType = 'Prototype\PageBundle\Form\ActiveComponentsType';
const collectionType = 'Symfony\Component\Form\Extension\Core\Type\CollectionType';
class PageType extends AbstractType
{
private $cmsComponentArray;
public function __construct($cmsComponentArray = null)
{
$this->cmsComponentArray = $cmsComponentArray;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$cmsComponentArray = $options['cmsComponentArray'];
$componentChoices = array();
foreach($cmsComponentArray as $cmsComponent){
$componentChoices[$cmsComponent['name']] = $cmsComponent['route'];
}
//correct values are shown here
//print_r($componentChoices);
$builder
->add('title')
->add('parent')
->add('template')
->add('active')
->add('content')
->add('components', collectionType, array(
'entry_type' => ActiveComponentsType, // i want to pass $cmsComponentArray to ActiveComponentsType
'allow_add' => true,
'allow_delete' => true
))
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Prototype\PageBundle\Entity\Page',
'cmsComponentArray' => null
));
}
}
The ActiveComponentsType embeded form does work - except I'm unsure how to pass the $componentChoices array to it.
Any ideas?
The collection type defines the entry_options option which is used to configure the options that are passed to the embedded form type.
I want to count the choice's items after the form has been created. The field is a simple Symfony's choice field with a query_builder to create the items. How can I achieve this?
<?php
class MyController
{
public function indexAction()
{
$form = $this->createForm(new MyFormWithChoiceFieldType());
// suppose that the field is named by "countries"
$items = count(???);
}
}
Thanks in advance.
Here's how I do this with Categories.
Notice that I have a CategoryRepository. You can use methods from this repository inside the query_builder options in your FormType classes and also in your controller.
My findAllCategories() method returns a query builder object, therefore I can have another method in the repository called countCategories() which returns the scalar count of that same query builder object.
This allows me to access the count method in my controller and makes sure that the couting will be consistent with the query builder I am using to find the categories.
This is a very simple example but it becomes more useful if you have more complex finder methods with joins and where clauses.
In my Controller:
<?php
use Site\FrontendBundle\Form\Type\CategoryType;
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$repo = $em->getRepository('SiteFrontendBundle:Category');
$form = $this->createForm(new CategoryType());
$count = $repo->countAllCategories();
return $this->render('SiteFrontendBundle:Category:count.html.twig', array(
'form' => $form->createView(),
'count' => $count
));
}
In my Form Type:
<?php
namespace Site\FrontendBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Site\FrontendBundle\Repository\CategoryRepository;
class CategoryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('category', 'entity', array(
'class' => 'SiteFrontendBundle:Category',
'property' => 'title',
'query_builder' => function(CategoryRepository $cr) {
return $cr->findAllCategories();
}
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Site\FrontendBundle\Entity\Category'
));
}
public function getName()
{
return 'category_type';
}
}
and in my Category Repository:
<?php
namespace Site\FrontendBundle\Repository;
use Doctrine\ORM\EntityRepository;
class CategoryRepository extends EntityRepository
{
public function findAllCategories()
{
return $this->createQueryBuilder('c')
->orderBy('c.lft', 'ASC')
;
}
public function countAllCategories()
{
return $this
->findAllCategories()
->select('COUNT(c.id)')
->getQuery()
->getSingleScalarResult()
;
}
}
If you have any questions let me know.
If you need to check in twig:
form.countries.vars.choices|length
Replace countries with the right form field name.