I'm doing an app with Symfony 5 and there is a problem i don't manage to find a solution, I have no idea.
I want to make a form of an entity "Person".
A Person can add in his family other Person.
So in my entity I made a Many-To-Many self referencing to Person.
class Person
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=50)
*/
private $name;
/**
* #ORM\Column(type="string", length=50)
*/
private $firstname;
/**
* #ORM\Column(type="string", length=255)
*/
private $birthdaydate;
/**
* #ORM\Column(type="string", length=255)
*/
private $gender;
/**
* #ManyToMany(targetEntity="Person")
* #JoinTable(name="family",
* joinColumns={#JoinColumn(name="person__id", referencedColumnName="person__id")},
* inverseJoinColumns={#JoinColumn(name="family_id", referencedColumnName="person__id")}
* )
*/
private $myFamily;
And now, I want to make a form in which I can add new Person, in a person.
I did a CollectionType, like symfony said, but when i want to print it to the page, I get a timeout because of an infinite loop.
It's the "allow_add" which causes the problem.
And i need the prototype variable returned by "allow_add" to add new field in the front.
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['attr' => ['class' => 'form_textfield']])
->add('firstname')
->add('birthdayDate', TextType::class, ['attr' => ['class' => 'form_datetime']])
->add('gender', GenderType::class)
->add('submit', SubmitType::class)
->add('myFamily', CollectionType::class, array('entry_type' => PersonType::class, 'mapped' => false, 'allow_add' => true, 'by_reference' => false, 'allow_delete' => true));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Person::class,
]);
}
}
Here is my form, but there is nothing interesting, I will add the js necessary when i will be resolve this problem.
{% extends 'base.html.twig' %}
{% block title %}Hello PersonController!
{% endblock %}
{% block body %}
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.firstname) }}
{{ form_row(form.birthdayDate) }}
{{ form_row(form.gender) }}
{{ form_row(form.myFamily) }}
<button type="button" class="add_item_link" data-collection-holder-class="tags">Add a tag</but
{{ form_end(form) }}
{% endblock %}
Thanks everyone in advance.
The answer from Dylan Kas was good, just by adding a new form, it's good.
The Person Form
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['attr' => ['class' => 'form_textfield']])
->add('firstname')
->add('birthdayDate', TextType::class, ['attr' => ['class' => 'form_datetime']])
->add('gender', GenderType::class)
->add('submit', SubmitType::class)
->add('myFamily', CollectionType::class, array('entry_type' => ChildType::class, 'by_reference' => false, 'allow_add' => true, 'allow_delete' => true));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Person::class,
]);
}
}
The child, referenced by myFamily :
class ChildType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['attr' => ['class' => 'form_textfield']])
->add('firstname')
->add('birthdayDate', TextType::class, ['attr' => ['class' => 'form_datetime']])
->add('gender', GenderType::class)
->add('submit', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Person::class,
]);
}
}
And the view :
{% block body %}
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.firstname) }}
{{ form_row(form.birthdayDate) }}
{{ form_row(form.gender) }}
<button type="button" class="add_item_link" data-collection-holder-class="myFamily">Add a tag</button>
<ul class="myFamily" data-index="{{ form.myFamily|length > 0 ? form.myFamily|last.vars.name + 1 : 0 }}" data-prototype="{{ form_widget(form.myFamily.vars.prototype)|e('html_attr') }}"></ul>
{{ form_end(form) }}
{% endblock %}
With the js associated
const addFormToCollection = (e) => {
const collectionHolder = document.querySelector(
"." + e.currentTarget.dataset.collectionHolderClass
);
const item = document.createElement("li");
item.innerHTML = collectionHolder.dataset.prototype.replace(
/__name__/g,
collectionHolder.dataset.index
);
collectionHolder.appendChild(item);
collectionHolder.dataset.index++;
};
document
.querySelectorAll(".add_item_link")
.forEach((btn) => btn.addEventListener("click", addFormToCollection));
It still need some work, maybe I can make the child form extending the person form. The front need some work too. But the next people facing this problem will have the solution here.
I am still asking myself how could I do if I've needed to have a form including itself the same form, including itself the same form etc...
The form would be recursivable.
There is an infinite loop because the myFamily property references a Person entity which itself references a myFamily property ...
To keep things simple, one way to manage the family of a person would be to create a separate Family entity.
From the Person point of view, it seems more coherent to have a ManyToOne relationship with a family.
After that, you can add the family of a Person by using the EntityType:class inside the PersonFormType.
Here is the documentation for EntityType : https://symfony.com/doc/current/reference/forms/types/entity.html
Related
I'm new to Symfony 4 and I'm trying to render a form with a ChoiceType Field with numeric choices in order to generate the exact Number of tags Chosen by the user.
This is my controller:
class ContactController extends AbstractController
{
/**
* #Route("/matrix", name="matrix")
*/
public function index(Request $request)
{
$contact = new Contact();
// i've already added some tags
$tag3 = new Tag();
$tag3->setName('tag3');
$contact->getTags()->add($tag3);
$tag4=new Tag();
$tag4->setName('ciao');
$contact->getTags()->add($tag4);
$form = $this->createForm(ContactType::class, $contact);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$contactFormData = $form->getData();
dump($contactFormData);
}
return $this->render('contact/index.html.twig', array(
//'our_form' => $form,
'form' => $form->createView(),
));
}
At this Point of my code, the form seems to be filled, I've checked with some dumps.
This is my twig
{% block body %}
<div>
{{ form_start(form) }}
{{ form_widget(form) }}
<ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}">
{% for tag in form.tags %}
<li> {{ form_row(tag.name) }}
</li>
{% endfor %}
</ul>
<input type="submit" value="Send" class="btn btn-success" />
{{ form_end(form) }}
</div>
{% endblock %}
It seems there's no visibility between this two Files, in fact, he is not able to enter into the for a loop. I've dumped some stuff and I've seen that tags have no children at this Point but it should.
class ContactType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('motto')
->add('expectations', ChoiceType::class, array(
'choices' => array(
'1' => '1',
'2' => '2',
'3' => '3',
'4' => '4',
'5' => '5',
),
));
$builder->add('tags', CollectionType::class, array(
'entry_type' => TagType::class,
'entry_options' => array('label' => false),
'allow_add' => true,
'by_reference' => false,
'mapped' => false,
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// Configure your form options here
]);
}
}
I'm perplex about this code => $contact->getTags()->add($tag3);. It seems Tags to be an entity and Contact another one so your Contact entity should have adders/removers and/or setters/removers (event if cumulate both is not necessary).
So your entity should like:
class Contact
{
// ...
/** #var Collection */
protected $tag;
// ...
public function __construct()
{
$this->tags = new ArrayCollection();
}
// ...
public function addTag(Tag $tag)
{
$this->tags->add($tag);
}
public function removeTag(Tag $tag)
{
// ...
}
}
A good example to implement your case: How to Embed a Collection of Forms
Then i don't know what's your TagType form looks like but even if it's well develop your twig is not ok.
First form_widget(form) render the entire form
From Symfony Doc
Renders the HTML widget of a given field. If you apply this to an entire form or collection of fields, each underlying form row will be rendered.
So re-render the collection will have no effect. And even if your twig code is not te good one to render a collection.
I try to build a huge form in symfony 3 with the use of the CollectionType. I have to define multiple sub-forms, some multiple, some single.
This is my FormType for that:
public function buildRegistrationForm(FormBuilderInterface $builder, array $options) {
$builder->add('userRegistration', CollectionType::class, [
'entry_type' => UserRegistrationType::class,
'entry_options' => ['label' => true],
]);
$builder->add('meters', CollectionType::class, [
'entry_type' => MeterType::class,
'entry_options' => ['label' => true],
'allow_add' => true,
]);
...
}
Now I try to access the CollectionType fields in the view. The code for this is:
{{ form_label(registrationForm.email, null, {'label_attr': {'class': 'form-label'}}) }}
{{ form_widget(registrationForm.email, {'attr': {'class': 'form-control'}}) }}
but I get the error:
Neither the property "email" nor one of the methods "email()", "getemail()"/"isemail()"/"hasemail()" or "__call()" exist and have public access in class "Symfony\Component\Form\FormView".
I know that Symfony tries to get the email field directly out of the main form (registrationForm), but I don't know how to access the subform. In the documentation (http://symfony.com/doc/current/form/form_collections.html) it is described that I can simply access the sub form by using registrationForm.userRegistration.email. But this gives me the error:
Neither the property "userRegistration" nor one of the methods ...
How can I access the subfields in the view?
First step is to understand why we want to use collectionType?
In case if you have One-To-Many or Many-To-Many relationship you should use CollectionType.
Example:
/**
* Class Team
*
* #ORM\Entity
* #ORM\Table(name="example_project_team")
*/
class Team
{
// ...
/**
* Unidirectional Many-To-Many
*
* Many Teams has many users accounts.
*
* #var ArrayCollection $users
*
* #ORM\ManyToMany(
* targetEntity="AppBundle\Entity\User",
* cascade={"persist", "remove"},
* orphanRemoval=true
* )
*
* #ORM\JoinTable(name="teams_to_users",
* joinColumns={#ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")}
* )
*/
protected $users;
// ...
}
In this example we have some entity called team. Each team has many users (this is just example related to you). I guess, you have already created User entity.
Imagine that for you have UserRegistrationType for your user entity.
/**
* Class UserRegistrationType
*/
class UserRegistrationType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username', 'Symfony\Component\Form\Extension\Core\Type\TextType', [
'label' => false,
'translation_domain' => 'messages'
])
->add('email', 'Symfony\Component\Form\Extension\Core\Type\TextType', [
'label' => false,
'translation_domain' => 'messages'
])
// ... the other fields
;
}
/**
* #return string
*/
public function getName()
{
return 'app_user_registration_type';
}
/**
* #return null|string
*/
public function getBlockPrefix()
{
return 'app_user_registration';
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => User::class,
'csrf_protection' => true,
'validation' => true,
));
}
}
Please pay attention !!!
We used here 'data_class' => User::class,
As you can see we use User object in userRegistrationType. It means, that we can use this form for every object, which has field of type User or field with type CollectionType (your case!) but collection of Users!
Our Team entity has field users.
Now, as we have already created the userRegistrationType we can add it to TeamFormType.
public function teamRegistrationFormType(FormBuilderInterface $builder, array $options) {
$builder->add('users', CollectionType::class, [
'entry_type' => UserRegistrationType::class,
'entry_options' => [
'label' => false,
],
'label' => false,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
]);
// ...
}
Finally. Your form in twig:
{# you can add this form in your twig file #}
<div class="box">
{% block team_form %}
{{ form_start(team_form, { 'attr': {'class': 'form-horizontal', 'role' : 'form'}}) }}
<div class="box-body">
<div class="form-group">
{% block users_collection %}
<label for="" class="col-sm-2 control-label">
{{ 'admin.label.phones'|trans }}
</label>
<div class="col-sm-10 users" data-prototype="{{ form_widget(team_form.users.vars.prototype)|e('html_attr') }}">
{% for user in users %}
<div>
{{ form_widget(user) }}
</div>
{% endfor %}
</div>
<span class="help-block">
{{ form_errors(users) }}
</span>
{% endblock users_collection %}
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-danger">
{{ 'admin.button.submit'|trans }}
</button>
</div>
</div>
{{ form_end(team_form) }}
</div>
{% endblock team_form %}
</div>
Here we have strange widget {{ form_widget(user) }}. I guess, you want to edit the style of this widget. You can do it with Symfony built-in form styles. Create file (if you haven't created it yet) path_to_your_project/src/AppBundle/Resources/views/Form/fields.html.twig and add to it the style of your userRegistrationType. Notice, that name of this widget is created from name of you form block prefix and string "_widget" (public function getBlockPrefix())
{% file fields.html.twig %}
{% trans_default_domain 'messages' %}
{% block app_user_registration_widget %}
{% spaceless %}
<div class="form-group" {{ block('widget_container_attributes') }}>
<div class="col-sm-2">
{{ form_widget(form.username, {'attr' : {'class' : 'form-control' }}) }}
</div>
<div class="col-sm-4">
{{ form_widget(form.email, {'attr' : {'class' : 'form-control' }}) }}
</div>
</div>
{% endspaceless %}
{% endblock %}
And of course you will have to set the add and delete buttons. For this purposes I suggest you use official documentation: How to Embed a Collection of Forms or you can use bundle (but not official): ninsuo/symfony-collection
I have following form in Symfony 2.8:
$form = $this->createFormBuilder()
->add('name', 'text')
->add('email', 'text')
->add('phone', 'text')
->add($this->createFormBuilder()
->create('address', 'form', array('virtual' => true))
->add('street', 'text')
->add('city', 'text')
->add('zip', 'text')
)
->getForm();
And I would like to dynamically add addresses in JS. I can add by CollectionType single input, as per following documentation: https://symfony.com/doc/current/reference/forms/types/collection.html
But I would like to add whole subform address. So I would like to achieve following HTML result:
<input name="form[address][0][street]" />
<input name="form[address][0][city]" />
<input name="form[address][0][zip]" />
not
<input name="form[address][street][0]" />
<input name="form[address][city][0]" />
<input name="form[address][zip][0]" />
Can anybody help? Thanks!
thanks to the comments I solved in in following way:
class Address
{
private $street;
private $city;
public function getStreet()
{
return $this->street;
}
public function setStreet($street)
{
$this->street = $street;
}
public function getCity()
{
return $this->city;
}
public function setCity($city)
{
$this->street = $city;
}
}
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('street', 'text')
->add('city', 'text');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Address::class,
));
}
}
then in controller:
$data = array(
"addresses" => array(
0 => new Address())
);
// $data is to show first empty subform
$form = $this->createFormBuilder($data)
->add('name', 'text')
->add('email', 'text')
->add('phone', 'text')
->add('addresses',
CollectionType::class,
array(
'entry_type' => AddressType::class,
'empty_data' => true
))
->getForm();
and the twig template looks like this:
{{ form_start(form) }}
{{ form_label(form.name) }}
{{ form_widget(form.name) }}
{{ form_label(form.email) }}
{{ form_widget(form.email) }}
{% for address in form.addresses %}
{{ form_widget(address) }}
{% endfor %}
{{ form_end(form) }}
I have this form in my symfony application:
namespace MyNamespace\EntityBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class OrganizationType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// profession checkboxes imbrication
->add('professions', 'collection', array(
'type' => new ProfessionType(),
'allow_add' => true,// if unrecognized items are submitted to the collection, they will be added as new items
'allow_delete' => false,
'by_reference' => false, //in order that the adders are called.
'mapped' => true,
))
->add('name')
->add('siret')
->add('corporation')
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyNamespace\EntityBundle\Entity\Organization',
'csrf_protection' => true,
'csrf_field_name' => '_token_',
// a unique key to help generate the secret token
'intention' => 'organization_stuff',
));
}
/**
* #return string
*/
public function getName()
{
return 'organization';
}
}
And this how I render the form in my twig view:
<div>
{{ form_start(form, {'action': path('path_action'), 'method': 'POST'}) }}
{{ form_errors(form) }}
{{ form_row(form.professions.vars.prototype) }}
{{ form_row(form.name) }}
{{ form_row(form.siret) }}
{{ form_row(form.corporation) }}
{{ form_end(form) }}
</div>
It renders me this in my html view on my browser:
As you can see I have a required label named __name__label__ (at the top of the form) and the embedded form label Professions above the submit button.
How can I fix that, or customize this behavior ?
Note: in my twig if I only use {{ form_row(form.professions) }}, my professionType does not display the fields.
This is the code of ProfessionType.php :
$builder
->add('production', 'checkbox', array('required' => false ))
->add('transport', 'checkbox', array('required' => false ))
->add('pumping', 'checkbox', array('required' => false ))
;
I think you are having those labels because you have used the default view format predefined by symfony you need to customize it , the other reason is that you have displayed the embedded form prototype, you need to set this prototype as data type attribute :
<ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e }}">
...
See http://symfony.com/doc/current/cookbook/form/form_collections.html
I am trying to save entities of 2 classes in 1 form I have read this article about it. My code is :
class MeetingType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('meetingDay', 'text', array('label' => 'Dzień zbiórki'))
->add('meetingTime', 'text', array('label' => 'Godzina zbiórki'))
->add('congregation', 'entity', array(
'class' => 'ViszmanCongregationBundle:Congregation',
'property' => 'name', 'label' => 'Zbór'
));
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Viszman\CongregationBundle\Entity\Meeting'
));
}
/**
* #return string
*/
public function getName()
{
return 'viszman_congregationbundle_meeting';
}
}
And another TYPE:
class CongregationType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$plDays = array('day1', 'day1', 'day1', 'day1', 'day1', 'day1', 'day1');
$builder
->add('name', 'text', array('label' => 'Name'))
->add('meetingDay', 'choice', array('label' => 'meeting day', 'choices' => $plDays))
->add('meetings','collection', array('type' => new MeetingType(), 'allow_add' => true))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Viszman\CongregationBundle\Entity\Congregation'
));
}
/**
* #return string
*/
public function getName()
{
return 'viszman_congregationbundle_congregation';
}
}
And when I try do render this form I only get CongregationType and no MeetingType form. part of code responsible for rendering form is:
<h1>Congregation creation</h1>
{{ form_start(form) }}
{{ form_widget(form) }}
<h3>Meetings</h3>
<ul class="tags" data-prototype="{{ form_widget(form.meetings.vars.prototype)|e }}">
{{ form_widget(form.meetings) }}
</ul>
{{ form_end(form) }}
If I remember correctly the entity form field is used to reference an existing entity, not to create a new one.
This is not what you need, what you need is embedded forms, take a look at the symfony book.