Symfony2 - Display a form recursively - symfony

Hello everybody (please excuse my English).
I want to do an application which needs to allow that the users must fill out on a form their personal data, their children, grandchildren and great-grandchildren (a little family tree).
class Person
{
/**
* #var int
*
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var string
*
* #ORM\Column(type="string")
*/
private $firstname;
/**
* #var string
*
* #ORM\Column(type="string")
*/
private $lastname;
/**
* #var \DateTime
*
* #ORM\Column(type="datetime")
*/
private $dateOfBirth;
/**
* #var Person
*
* #ORM\ManyToMany(targetEntity="Person")
*/
private $children;
public function __construct()
{
$this->children = new ArrayCollection();
}
}
}
In the PersonType class, I do the following:
class PersonType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('firstname');
$builder->add('lastname');
$builder->add('dateOfBirth');
$builder->add('children', 'collection', array(
'type' => new PersonType(),
'allow_add' => true,
'by_reference' => false,)
);
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Anything\YourBundle\Entity\Person'
));
}
/**
* #return string
*/
public function getName()
{
return 'person';
}
}
In this way, I use the PersonType in the controller as below:
public function newAction()
{
$entity = new Person();
$form = $this->createForm(new PersonType(), $entity, array(
'action' => $this->generateUrl('person_create'),
'method' => 'POST',
));
return array(
'entity' => $entity,
'form' => $form->createView(),
);
}
But the problem is when I request the url of this action, and the view of this action has to be rendered, there is a problem because doesn't give a response, because is in a infinite loop (I think that is the reason). I would like to know if is this possible to do using the Symfony forms, or if I have to look at other alternatives. If this was possible, how could I do that and how could I limit the form to only render the four levels that I need (me, my children, my grandchildren and my great-grandchildren)??
I hope that the problem has been understood.
Thanks in advance.

You could add a custom parameter to your form that indicates the current level of recursion.
To archive this you first need to implement a new option:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Anything\YourBundle\Entity\Person',
'recursionLevel' => 4
));
}
Now you update this value in your buildForm method:
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
if (--$options['recursionLevel'] > 0) {
$resolver = new OptionsResolver();
$resolver->setDefaults(
$options
);
$childType = new PersonType();
$childType->setDefaultOptions($resolver);
$builder->add('children', 'collection', array(
'type' => $childType,
'allow_add' => true,
'by_reference' => false
));
}
}
This is not tested.

I had the same problem and tried the solutions provided here.
They come with significant drawbacks like a depth limitation and performance overhead - you always create form objects even if there is no data submited.
What I did to overcome this problem was to add a listener for the FormEvents::PRE_SUBMIT event and add the collection type field dynamically if there is data to be parsed.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('content');
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$node = $event->getData();
$form = $event->getForm();
if (!$node) {
return;
}
if(sizeof(#$node['children'])){
$form->add('children', CollectionType::class,
array(
'entry_type' => NodeType::class,
'allow_add' => true,
'allow_delete' => true
));
}
});
}
I hope this helps someone that has this issue in the future

Thanks for the answer Ferdynator!!
I didn't solve the problem in the way you proposed, but that approach helped me. I passed the recursion level in the constructor of the Person form, and thus, I could know when I had to stop:
class PersonType extends AbstractType
{
private $recursionLevel;
public function __construct( $recursionLevel ){
$this->recursionLevel = $recursionLevel;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if($this->recursionLevel > 0)
{
$builder->add('children', 'collection', array(
'type' => new PersonType(--$this->recursionLevel),
'allow_add' => true,
'by_reference' => false,)
);
}
}
}

Ferdynator, thanks for your answers. And I want to propose my decision based on yours:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Anything\YourBundle\Entity\Person',
'recursionLevel' => 4
));
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
if (--$options['recursionLevel'] > 0) {
$builder->add('children', 'collection', array(
'type' => $childType,
'allow_add' => true,
'by_reference' => false,
'options' => [
'recursionLevel' => $options['recursionLevel']
],
));
}
}
It solves our problem.

Related

EntityForm on a ManyToMany bidirectional, for the two sides of the relation

I've 2 entity: User and Strain with a ManyToMany bidirectional relation, the owner of the relation is User.
I want do a form for edit the rights (the User own some Strains), when I do a form for the User where I can select some Strains I want, it works fine (I use an EntityType on Strain). But... Sometimes, I want edit the rights by the other side of the relation: Strain. ie edit the Strain and select the Users I want. But it doesn't work...
I give you my entities User and Strain and the two FormType, and my Uglys Solution...
User.php
/**
* The authorized strains for this user.
*
* #var Strain|ArrayCollection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Strain", inversedBy="authorizedUsers")
*/
private $authorizedStrains;
/**
* User constructor.
*/
public function __construct()
{
$this->authorizedStrains = new ArrayCollection();
}
/**
* Add an authorized strain.
*
* #param Strain $strain
*
* #return $this
*/
public function addAuthorizedStrain(Strain $strain)
{
$this->authorizedStrains[] = $strain;
$strain->addAuthorizedUser($this);
return $this;
}
/**
* Remove an authorized strain.
*
* #param Strain $strain
*/
public function removeAuthorizedStrain(Strain $strain)
{
$this->authorizedStrains->removeElement($strain);
$strain->removeAuthorizedUser($this);
}
/**
* Get authorized strains.
*
* #return Strain|ArrayCollection
*/
public function getAuthorizedStrains()
{
return $this->authorizedStrains;
}
Strain.php
/**
* The authorized user.
* For private strains only.
*
* #var User|ArrayCollection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\User", mappedBy="authorizedStrains")
*/
private $authorizedUsers;
/**
* Strain constructor.
*/
public function __construct()
{
/**
* Add authorized user.
*
* #param User $user
*
* #return $this
*/
public function addAuthorizedUser(User $user)
{
$this->authorizedUsers[] = $user;
return $this;
}
/**
* Remove authorized user.
*
* #param User $user
*/
public function removeAuthorizedUser(User $user)
{
$this->authorizedUsers->removeElement($user);
}
/**
* Get authorized users.
*
* #return User|ArrayCollection
*/
public function getAuthorizedUsers()
{
return $this->authorizedUsers;
}
UserRightsType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('authorizedStrains', EntityType::class, array(
'class' => 'AppBundle\Entity\Strain',
'choice_label' => 'name',
'expanded' => true,
'multiple' => true,
'required' => false,
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\User',
));
}
StrainRightsType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('authorizedUsers', EntityType::class, array(
'class' => 'AppBundle\Entity\User',
'query_builder' => function(UserRepository $ur) {
return $ur->createQueryBuilder('u')
->orderBy('u.username', 'ASC');
},
'choice_label' => function ($user) {
return $user->getUsername().' ('.$user->getFirstName().' '.$user->getLastName().')';
},
'expanded' => true,
'multiple' => true,
'required' => false,
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Strain',
));
}
StrainController.php the ugly solution
public function userRightsAction(Request $request, Strain $strain)
{
$form = $this->createForm(StrainRightsType::class, $strain);
$form->add('save', SubmitType::class, [
'label' => 'Valid the rights',
]);
foreach($strain->getAuthorizedUsers() as $authorizedUser) {
$authorizedUser->removeAuthorizedStrain($strain);
}
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
foreach($strain->getAuthorizedUsers() as $authorizedUser)
{
$authorizedUser->addAuthorizedStrain($strain);
$em->persist($authorizedUser);
}
$em->flush();
$request->getSession()->getFlashBag()->add('success', 'The user\'s rights for the strain '.$strain->getName().' were successfully edited.');
return $this->redirectToRoute('strain_list');
}
return $this->render('strain/userRights.html.twig', [
'strain' => $strain,
'form' => $form->createView(),
]);
}
As you can see, I do 2 foreach: the first to remove all the rights on the Strain, and the second to give rights.
I think Symfony have anticipated this problem, but I don't know how to do, and I've found nothing in the documentation...
Thank you in advance for your help,
Sheppard
Finaly, I've found.
On the inversed side (Strain.php):
public function addAuthorizedUser(User $user)
{
$user->addAuthorizedStrain($this);
$this->authorizedUsers[] = $user;
return $this;
}
public function removeAuthorizedUser(User $user)
{
$user->removeAuthorizedStrain($this);
$this->authorizedUsers->removeElement($user);
}
And, on the owner side (User.php)
public function addAuthorizedStrain(Strain $strain)
{
if (!$this->authorizedStrains->contains($strain)) {
$this->authorizedStrains[] = $strain;
}
return $this;
}
public function removeAuthorizedStrain(Strain $strain)
{
if ($this->authorizedStrains->contains($strain)) {
$this->authorizedStrains->removeElement($strain);
}
}
And in the FormType (for the inverse side) (StrainRightsType)), add 'by_reference' => false
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('authorizedUsers', EntityType::class, array(
'class' => 'AppBundle\Entity\User',
'query_builder' => function(UserRepository $ur) {
return $ur->createQueryBuilder('u')
->orderBy('u.username', 'ASC');
},
'choice_label' => function ($user) {
return $user->getUsername().' ('.$user->getFirstName().' '.$user->getLastName().')';
},
'by_reference' => false,
'expanded' => true,
'multiple' => true,
'required' => false,
))
;
}

Select option of choice dropdown in a form

I want to select an option in the dropdown from the controller. I am trying with the following piece of code:
$form = $this->createForm(new SearchAdvancedType());
$form->get('option')->setData($session->get('option'));
But it is doing nothing in the dropdown. Nothing is selected when the page loads.
To check if the value was well set I print it using:
$form->get('brand')->Data();
and the result was a number (it changes depending of what I choosed in the dropdown before).
I need to know the way to select the value of the dropdown properly.
To preset a select option I would pass the value into the form.
class MyFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('type', 'choice', [
'required' => true,
'choices' => ['yes' => 'Yes', 'no' => 'No'],
'data' => $options['select_option']
])
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => null,
'select_option' => null
));
}
/**
* #return string
*/
public function getName()
{
return 'my_form';
}
}
Then in your controller, pass the value in;
$form = $this->createForm(new MyFormType(), null, ['select_option' => 'no');

Form CollectionType with radio buttons fails after Symfony 2.7 upgrade

Getting below error..
An exception has been thrown during the rendering of a template ("The form's view data is expected to be of type scalar, array or an instance of \ArrayAccess, but is an instance of class Proxies__CG__\BLA\MyBundle\Entity\TransportType. You can avoid this error by setting the "data_class" option to "Proxies__CG__\BLA\MyBundle\Entity\TransportType" or by adding a view transformer that transforms an instance of class Proxies__CG__\BLA\MyBundle\Entity\TransportType to scalar, array or an instance of \ArrayAccess.") in MyBundle:Shipping:form.html.twig at line 8.
$builder->add('variables','collection', array(
'type' => new AbcType(),
'options' => array(
'required' => true,
),
'constraints' => new NotNull()));
AbcType.php
class AbcType extends AbstractType
{
/**
* Build form
*
* #param FormBuilder $builder
* #param array $options
*
* #return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('importance', null, array('empty_value'=>false,'expanded'=>true,
'required'=>true,'multiple'=>false,
'constraints' => new NotNull()))
->add('timeSpent', null, array(
'empty_value'=>false,'expanded'=>true,
'required'=>true,'multiple'=>false,
'constraints' => new NotNull()
)
);
}
/**
* setDefaultOptions set Default values
*
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Demo\MyBundle\Entity\Abc'
));
}
/**
* getName will return Form name
* #return string
*/
public function getName()
{
return 'demo_mybundle_abctype';
}
}
I fixed issue using below link....
https://github.com/symfony/symfony/issues/14877
Make sure you have the 'data_class' default specified in the setDefaultOptions in the AbcType class:
... build form ...
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AbcBundle\Entity\Abc'
));
}

Type of choice field Symfony2

I try to save values of one - three checkboxes in field category in database, but i get the error :
Notice: Array to string conversion in /var/www/OnTheWay/vendor/doctrine/dbal/lib/Doctrine/DBAL/Statement.php line 120
The field:
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $category;
Get & Set:
/**
* #return mixed
*/
public function getCategory()
{
return $this->category;
}
/**
* #param $category
*/
public function setCategory($category)
{
$this->category[] = $category;
}
Profile type:
namespace Vputi\UserBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ProfileType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('fio');
$builder->add('birthDate', null, array('widget' => 'single_text'));
$builder->add('file');
$builder->add('yearOnRoad');
$builder->add('telephone');
$builder->add('contactMail');
$builder->add('role', 'choice', array('choices' => array(1 => 'За рулем') ,'expanded'=>true, 'multiple' => true,));
$builder->add('category', 'choice', array(
'choices' => array('A' => 'Категория А', 'B' => 'Категория B', 'C' => 'Категория C',),
'expanded' => true,
'multiple' => true,
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' =>'Vputi\UserBundle\Entity\Profile',
'cascade_validation' => true,
));
}
}
Here is my form type, I hope you help me, and iam ommit getName() method.
The problem is $category is defined as a string but you're using it like an array.
The solution depends on exactly what you want to accomplish. If you want it to be mapped as an array you have to do this:
/**
* #ORM\Column(type="array", nullable=true)
*/
private $category;
When using Doctrine's array type, make sure you take this into account: How to force Doctrine to update array type fields?

Symfony2 form type with collection field of same type (hierarchy)

How can you have a form type with a collection field of the same form type embedded inside it? I've got the following form type within my Symfony 2 project:
class MenuType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('label')
->add('uri')
// the following does not work
->add('children', 'collection', array(
'type' => new MenuType(),
'allow_add' => true,
'by_reference' => false
))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Mango\CoreDomain\Model\Menu',
'csrf_protection' => false,
'cascade_validation' => true
));
}
/**
* #return string
*/
public function getName()
{
return 'menu';
}
}
When you run this, it will not work. How would something like this be solved in a clean way?
The class that this form type uses is called Menu and has a property children which is an array of Menu objects.
Thanks!

Resources