Symfony Form - Allow removal of nested form associated entity - symfony

I have a /checkout JSON API endpoint which allows an optional billingAddress parameter alongside other parameters such as email and deliveryAddress.
These addresses are stored in an Address entity related to an Order entity.
Everything works nicely if a user enters their billingAddress, but if a user removes a previously submitted billing address, I can find no way to remove the billingAddress entity. Ideally to remove the billing address I'd use the following JSON POST request.
{
"email": "nick#example.com",
"deliveryAddress": {
"line1": "1 Box Lane"
},
"billingAddress": null
}
Is this at all possible with Symfony forms?
See below for a simplified explanation of the current setup.
Entities
/**
* #ORM\Entity
*/
class Order
{
// ...
/**
* #var Address
*
* #ORM\OneToOne(targetEntity = "Address", cascade = {"persist", "remove"})
* #ORM\JoinColumn(name = "deliveryAddressId", referencedColumnName = "addressId")
*/
private $deliveryAddress;
/**
* #var Address
*
* #ORM\OneToOne(targetEntity = "Address", cascade = {"persist", "remove"}, orphanRemoval = true)
* #ORM\JoinColumn(name = "billingAddressId", referencedColumnName = "addressId", nullable = true)
*/
private $billingAddress;
public function setDeliveryAddress(Address $deliveryAddress = null)
{
$this->deliveryAddress = $deliveryAddress;
return $this;
}
public function getDeliveryAddress()
{
return $this->deliveryAddress;
}
public function setBillingAddress(Address $billingAddress = null)
{
$this->billingAddress = $billingAddress;
return $this;
}
public function getBillingAddress()
{
return $this->billingAddress;
}
// ...
}
.
/**
* #ORM\Entity
*/
class Address
{
// ...
/**
* #var string
*
* #ORM\Column(type = "string", length = 45, nullable = true)
*/
private $line1;
// ...
}
Forms
class CheckoutType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class)
->add('deliveryAddress', AddressType::class, [
'required' => true
])
->add('billingAddress', AddressType::class, [
'required' => false
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Order::class,
'csrf_protection' => false,
'allow_extra_fields' => true,
'cascade_validation' => true
]);
}
public function getBlockPrefix()
{
return '';
}
}
.
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('line1', TextType::class);
// ...
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Address::class,
'allow_extra_fields' => true
]);
}
public function getBlockPrefix()
{
return '';
}
}

Form events are what you need: https://symfony.com/doc/current/form/events.html
For example if you want to remove the billingAddress field after the form submission you can do that:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class)
->add('deliveryAddress', AddressType::class, [
'required' => true
])
->add('billingAddress', AddressType::class, [
'required' => false
]);
$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
if (empty($data['billingAddress'])) {
$form->remove('billingAddress');
}
});
}
Read carefully the documentation to know which event will be the best for your scenario.

Try setting the "by_reference" option for the "billingAddress" field to false in order to make sure that the setter is called.
http://symfony.com/doc/current/reference/forms/types/form.html#by-reference

Big thanks to Renan and Raphael's answers as they led me to discovering the below solution which works for both a partial PATCH and full POST request.
class CheckoutType extends AbstractType
{
/** #var bool */
private $removeBilling = false;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class)
->add('deliveryAddress', AddressType::class, [
'constraints' => [new Valid]
])
->add('billingAddress', AddressType::class, [
'required' => false,
'constraints' => [new Valid]
])
->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'onPreSubmit'])
->addEventListener(FormEvents::POST_SUBMIT, [$this, 'onPostSubmit']);
}
public function onPreSubmit(FormEvent $event)
{
$data = $event->getData();
$this->removeBilling = array_key_exists('billingAddress', $data) && is_null($data['billingAddress']);
}
public function onPostSubmit(FormEvent $event)
{
if ($this->removeBilling) {
$event->getData()->setBillingAddress(null);
}
}
}

Related

Symfony Form with ManyToMany Mapping

Symfony 3.4 Doctrine 1.9
I have found a lot of articles about this. But nothing could resolve my
problem.
I have two Entities User and Company which are mapped as Many2Many.
Both Entities should be filled with a form submit. Therefor I have the following FormTypes:
CustomerRegistrationType
This includes the CollectionType with entry_type of CompanyFormType, which returns the Company:class
class CustomerRegistrationType extends AbstractType
{
private $translator;
private $session;
public function __construct(TranslatorInterface $translator,Session $session)
{
$this->session = $session;
$this->translator = $translator;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('mandant',HiddenType::class,array('data'=>
->add('newsletter',CheckboxType::class,array('label' => false,'required' => false))
->add('company', CollectionType::class, array('entry_type' => CompanyFormType::class, 'entry_options' => array('label' => false)));
}
CompanyFormType
class CompanyFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add('telephone', TextType::class,array('required' => false))
->add('company_name', TextType::class,array('label' => false,'required' => false))
->add('company_supplement', TextType::class,array('label' =>
->add('send_info', CheckboxType::class,array('label' => false,'required' => false))
->add('send_invoice', CheckboxType::class,array('label' => false,'required' => false))
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Company'));
}
}
Many2Many mapping
class User extends BaseUser
{
public function __construct()
{
parent::__construct();
$this->company = new ArrayCollection();
}
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Company",inversedBy="users")
* #ORM\JoinTable(name="user_comp_comp_user",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="company_id", referencedColumnName="id")}
* )
*/
protected $company;
public function getCompany()
{
return $this->company;
}
public function setCompany($company)
{
$this->company = $company;
return $this;
}
public function addCompany(Company $company)
{
if ($this->company->contains($company)) {
return;
}
$this->company[] = $company;
return $this;
}
public function removeCompany(Company $company)
{
if (!$this->company->contains($company)) {
return;
}
$this->company->removeElement($company);
return $this;
}
class Company
{
public function __construct()
{
$this->users = new ArrayCollection();
}
/**
* #var \Doctrine\Common\Collections\Collection|Company[]
* #ORM\ManyToMany(targetEntity="User",mappedBy="company")
*/
protected $users;
}
In the Controller I want to save all Data to the different Entities.
Therefor I do:
public function registerAction(Request $request)
{
$user = $this->userManager->createUser();
$form = $this->createForm(CustomerRegistrationType::class,$user);
$form->handleRequest($request);
$company = $user->getCompany();
$company->setSendInvoice(true);
$company->setSendInfo(true);
if ($form->isSubmitted()){
$user->addCompany($company);
$this->userManager->updateUser($user);
....
}
return $this->render('Registration/register.html.twig',[
'form' => $form->createView()
]);
}
I does not matter, which way I try, eachtime I get an error. In this example, the company data is empty. Other ways are in conflict with the ArrayCollection() of $this->company in User()
User {#907 ▼
#id: null
#company: ArrayCollection {#926 ▼
-elements: []
}
#groups: null
#mandant: null
#salutation: null
#first_name: null
#last_name: null
#fullname: null
#created: DateTime #1544539763 {#901 ▶}
#telephone: null
Update:
In Twig form I have no access to
{{ form_widget(form.company.company_name, {'attr': {'class': 'form-control'}})}}
Neither the property "company_name" nor one of the methods "company_name()", "getcompany_name()"/"iscompany_name()"/"hascompany_name()" or "__call()" exist and have public access in class "Symfony\Component\Form\FormView".
This comes up before form submit.
I found the solution here:
https://groups.google.com/forum/#!topic/symfony2/DjwwzOfUIuQ
So I used an array based form in the controller
$form = $this->createFormBuilder()
->add('user', CustomerRegistrationType::class, array(
'data_class' => 'AppBundle\Entity\User'
))
->add('company', CompanyFormType::class, array(
'data_class' => 'AppBundle\Entity\Company'
))
->getForm();
The different Objects can catched like this:
$form->handleRequest($request);
$user = $form->get('user')->getData();
$company = $form->get('company')->getData();
and then
if ($form->isSubmitted()){
...
$user->addCompany($company);
$this->userManager->updateUser($user);
}
That's it

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

Extending EntityType to allow extra choices set with AJAX calls

I try to create a Symfony Custom type extending the core "entity" type.
But I want to use it with Select2 version 4.0.0 (ajax now works with "select" html element and not with hidden "input" like before).
This type should create an empty select instead of the full list of entities by the extended "entity" type.
This works by setting the option (see configureOption):
'choices'=>array()
By editing the object attached to the form it should populate the select with the current data of the object. I solved this problem but just for the view with the following buildView method ...
Select2 recognize the content of the html "select", and does its work with ajax.
But when the form is posted back, Symfony doesn't recognize the selected choices, (because there were not allowed ?)
Symfony\Component\Form\Exception\TransformationFailedException
Unable to reverse value for property path "user": The choice "28" does not exist or is not unique
I tried several methods using EventListeners or Subscribers but I can't find a working configuration.
With Select2 3.5.* I solved the problem with form events and overriding the hidden formtype, but here extending the entitytype is much more difficult.
How can I build my type to let it manage the reverse transformation of my entites ?
Custom type :
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
class AjaxEntityType extends AbstractType
{
protected $router;
public function __construct($router)
{
$this->router = $router;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->setAttribute('attr',array_merge($options['attr'],array('class'=>'select2','data-ajax--url'=>$this->router->generate($options['route']))));
}
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['attr'] = $form->getConfig()->getAttribute('attr');
$choices = array();
$data=$form->getData();
if($data instanceOf \Doctrine\ORM\PersistentCollection){$data = $data->toArray();}
$values='';
if($data != null){
if(is_array($data)){
foreach($data as $entity){
$choices[] = new ChoiceView($entity->getAjaxName(),$entity->getId(),$entity,array('selected'=>true));
}
}
else{
$choices[] = new ChoiceView($data->getAjaxName(),$data->getId(),$data,array('selected'=>true));
}
}
$view->vars['choices']=$choices;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired(array('route'));
$resolver->setDefaults(array('choices'=>array(),'choices_as_value'=>true));
}
public function getParent() {
return 'entity';
}
public function getName() {
return 'ajax_entity';
}
}
Parent form
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AlarmsType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name','text',array('required'=>false))
->add('user','ajax_entity',array("class"=>"AppBundle:Users","route"=>"ajax_users"))
->add('submit','submit');
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array('data_class' => 'AppBundle\Entity\Alarms','validation_groups'=>array('Default','form_user')));
}
/**
* #return string
*/
public function getName()
{
return 'alarms';
}
}
Problem solved.
The solution is to recreate the form field with 'choices'=>$selectedChoices in both PRE_SET_DATA and PRE_SUBMIT FormEvents.
Selected choices can be retrived from the event with $event->getData()
Have a look on the bundle I created, it implements this method :
Alsatian/FormBundle - ExtensibleSubscriber
Here is my working code which adds to users (EntityType) related to tag (TagType) ability to fill with options from AJAX calls (jQuery Select2).
class TagType extends AbstractType
{
//...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$modifyForm = function ($form, $users) {
$form->add('users', EntityType::class, [
'class' => User::class,
'multiple' => true,
'expanded' => false,
'choices' => $users,
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($modifyForm) {
$modifyForm($event->getForm(), $event->getData()->getUsers());
}
);
$userRepo = $this->userRepo; // constructor injection
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($modifyForm, $userRepo) {
$userIds = $event->getData()['users'] ?? null;
$users = $userIds ? $userRepo->createQueryBuilder('user')
->where('user.id IN (:userIds)')->setParameter('userIds', $userIds)
->getQuery()->getResult() : [];
$modifyForm($event->getForm(), $users);
}
);
}
//...
}
here's my approach based on Your bundle just for entity type in one formtype.
Usage is
MyType extends ExtensibleEntityType
(dont forget parent calls on build form and configure options)
and the class itself
abstract class ExtensibleEntityType extends AbstractType
{
/**
* #var EntityManagerInterface
*/
private EntityManagerInterface $entityManager;
/**
* ExtensibleEntityType constructor.
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function getParent()
{
return EntityType::class;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->addEventListener(FormEvents::PRE_SET_DATA, [$this, 'preSetData']);
$builder->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'preSubmit'], 50);
}
/**
* #param FormEvent $event
*/
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
$parent = $event->getForm()->getParent();
$options = $form->getConfig()->getOptions();
if (!$options['pre_set_called']) {
$options['pre_set_called'] = true;
$options['choices'] = $this->getChoices($options, $event->getData());
$parent->add($form->getName(), get_class($this), $options);
}
}
/**
* #param FormEvent $event
*/
public function preSubmit(FormEvent $event)
{
$form = $event->getForm();
$parent = $event->getForm()->getParent();
$options = $form->getConfig()->getOptions();
if (!$options['pre_submit_called']) {
$options['pre_submit_called'] = true;
$options['choices'] = $this->getChoices($options, $event->getData());
$parent->add($form->getName(), get_class($this), $options);
$newForm = $parent->get($form->getName());
$newForm->submit($event->getData());
}
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'multiple' => true,
'expanded' => true,
'choices' => [],
'required' => false,
'pre_set_called' => false,
'pre_submit_called' => false,
'validation_groups' => false,
]);
}
/**
* #param array $options
* #param $data
* #return mixed
*/
public function getChoices(array $options, $data)
{
if ($data instanceof PersistentCollection) {
return $data->toArray();
}
return $this->entityManager->getRepository($options['class'])->findBy(['id' => $data]);
}
}

Symfony2 - Display a form recursively

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.

Symfony2 __toString() error

I have a problem with saving entities back to me this error:
Catchable Fatal Error: Method My\BusinessBundle\Entity\Type::__toString()
must return a string value in
/var/www/MyBusiness0_1/vendor/doctrine/orm/lib/Doctrine/ORM/ORMInvalidArgumentException.php line 113
The strange thing is that the method __ toString () is the entity Type!
class Type
{
//..
/**
* #var string
*
* #ORM\Column(name="type", type="string", length=100)
*/
private $type;
/**
* #ORM\OneToMany(targetEntity="MailTelCont", mappedBy="type")
*/
protected $mailTelContacts;
public function __construct()
{
$this->mailTelContacts = new \Doctrine\Common\Collections\ArrayCollection();
}
public function __toString()
{
return $this->getType();
}
//...
Another strange thing is that if I put cascade ={"persist"} class MailTelCont on ManyToOne relationship "type" does not show me this error, but save a new field in Type ..
class MailTelCont
class MailTelCont
{
//..
/**
* #var string
*
* #ORM\Column(name="contact", type="string", length=100)
*/
private $contact;
/**
* #ORM\ManyToOne(targetEntity="Type", inversedBy="mailTelContacts")
* #ORM\JoinColumn(name="type_id", referencedColumnName="id")
*/
private $type;
/**
* #ORM\ManyToOne(targetEntity="Anagrafica", inversedBy="mailTelContacts", cascade={"persist"})
* #ORM\JoinColumn(name="anagrafica_id", referencedColumnName="id")
* #Assert\Type(type="My\BusinessBundle\Entity\Anagrafica")
*/
private $anagrafica;
public function __toString()
{
return $this->getContact();
}
Call the form of nested "AnagraficType" in this way:
class TypeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('type', 'entity', array(
'class' => 'My\BusinessBundle\Entity\Type',
'attr' => array('class' => 'conct'),
'property' => 'type',
'label' => 'Tipologia',
))
;
}
*****
class MailTelContType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('type', new TypeType())
->add('contact', 'text', array('label' => 'Contatto'))
;
}
*****
class AnagraficaType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('mailTelContacts', 'collection', array('type' => new MailTelContType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false
))
Where am I doing wrong?
I think those values are null then. Did you try:
public function __toString()
{
return (string) $this->getType();
}
and
public function __toString()
{
return (string) $this->getContact();
}
That way when value is null will be casted to string and you should not get this exception.

Resources