Symfony 5.1 nested forms with ArrayCollection - php

I have an employee entity, a society entity and a site entity,
A society can have several sites (OneToMany),
The employee has a form. Inside, we have the choice of society.
I would like to nest the Site FormType in that of society.
So when I choose a society under EmployeeForm, it only suggests sites linked to society.
Hope I am clear. I followed the symfony documentation on how to embed an ArrayCollection.
https://symfony.com/doc/5.1/form/form_collections.html
and I looked https://symfony.com/doc/current/form/data_transformers.html
I'm not sure which is the best approach ?
I still get the error
The object of class Doctrine\ORM\PersistentCollection could not be
converted to a string
and I am unable in SocietyForm to isolate the "name" attribute of each Site. I don't want a collection, but a list of name (string).
EmployeeType
class EmployeeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', NULL, [
'label' => 'Nom',
])
->add('firstname', NULL, [
'label' => 'Prénom',
])
->add('societe', EntityType::class, [
'label' => 'Société',
'class' => RefSociety::class,
'choice_label' => 'Name',
])
->add('site', EntityType::class, [
'label' => 'Site',
'class' => RefSociety::class,
'choice_label' => 'refSites'
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Employee::class,
]);
}
}
RefSocietyType
class RefSocietyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('Name', NULL, [
'label' => 'Nom de société'
])
->add('refSites', CollectionType::class, array(
'type' => RefSiteType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false,
))
->add('Adress', NULL, [
'label' => 'Adresse'
])
->add('PostalCode', NULL, [
'label' => 'Code postal'
])
->add('Country', NULL, [
'label' => 'Pays'
])
->add('City', NULL, [
'label' => 'Ville'
])
->add('save', SubmitType::class, [
'attr' => ['class' => 'btn btn-primary btn-block btn-radius'],
'label' => 'Sauvegarder'
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => RefSociety::class,
]);
}
}
RefSiteType
class RefSiteType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => RefSite::class,
]);
}
}
index.html.twig
{ form_start(employeeForm) }}
{{form_row(employeeForm.name, {'attr' : {'class' : ""} }) }}
{{form_row(employeeForm.firstname, {'attr' : {'class' : ""} }) }}
{{form_row(employeeForm.societe) }}
<ul class="sites">
{% for site in employeeForm.refSites %}
<li>{{ form_row(site.name) }}</li>
{% endfor %}
</ul>
{{ form_end(employeeForm) }}

->add('site', EntityType::class, [
'label' => 'Site',
'class' => RefSociety::class,
'choice_label' => 'refSites'
])
'choice_label' => 'refSites' means that the method getRefSites() are called by PropertyAccessor to get a label name, so the method getRefSites returns the collection (because one to many), but the label must be a string.
Maybe should you use 'choice_label' => 'name' ?

Related

EasyAdmin 3 - CollectionField add form entryType options?

in my CRUD Controller I have a CollectionField which has an entryType option referring to a formType
yield CollectionField::new('drinks', 'Boissons')
->setFormTypeOption('by_reference', false)
->setEntryType(CommandProductItemType::class);
CommandProductItemType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('product', EntityType::class, [
'label' => 'Produit',
'class' => Product::class,
'choice_label' => 'namePrice',
])
->add('quantity', IntegerType::class, [
'label' => 'Quantité',
'attr' => [
'min' => 1
]
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => CommandProductItem::class,
'category' => Category::class
]);
}
How can I pass the category option to the CommandProductItemType from the CollectionField item ?

Not all form errors rendered in custom form field

A Symfony 4 application has a User entity using Joined InheritanceType with types Admin, Staff and Volunteer. The Staff entity has a OneToOne relationship with an Organization entity. So the Organization form type includes a child staff, which has a custom form type NewUserType. (Relevant excerpts appear below.) If an empty OrganizationType form is submitted all of the expected form errors are rendered EXCEPT the name and email errors. The password form error field of NewUserType IS rendered. Symfony's profiler does not show errors for name or email. error_bubbling in NewUserType makes no difference. #Assert\Valid on the staff property of Organization entity makes no difference.
registerOrganization:
public function registerOrganiztion(Request $request) {
$form = $this->createForm(OrganizationType::class);
$templates = [
'Registration/organization.html.twig',
'Registration/new_user.html.twig',
'Registration/focuses.html.twig',
];
...
return $this->render('Default/formTemplates.html.twig', [
'form' => $form->createView(),
'headerText' => 'Add an organization',
'userHeader' => 'Staff Member',
'orgHeader' => 'Organization',
'focusHeader' => "Organization's Focus",
'templates' => $templates,
]);
}
new_user.html.twig:
{% if form.staff is defined %}{% set user = form.staff %}{% else %}{% set user = form %}{% endif %}
<div class="text-center text-bold">
{{ userHeader }}
</div>
{{ form_row(user.fname) }}
{{ form_row(user.sname) }}
{{ form_row(user.email) }}
{{ form_row(user.plainPassword.first) }}
{{ form_row(user.plainPassword.second) }}
OrganizationType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
...
->add('staff', NewUserType::class)
}
NewUserType:
class NewUserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('sname', null, [
'attr' => [
'class' => 'mb-2',
'size' => '15',
],
'label' => 'Last name: ',
'label_attr' => ['class' => 'mr-2'],
])
->add('fname', null, [
'attr' => [
'class' => 'mb-2',
'size' => '15',
],
'label' => 'First name: ',
'label_attr' => ['class' => 'mr-2'],
])
->add('email', null, [
'attr' => [
'class' => 'mb-2',
'size' => '15',
],
'label' => 'Email: ',
'label_attr' => ['class' => 'mr-2'],
])
->add('plainPassword', RepeatedType::class, array(
'type' => PasswordType::class,
'mapped' => false,
'constraints' => [new NotBlank(['message' => "Password may not empty"])],
'invalid_message' => 'Passwords do not match',
'first_options' => [
'attr' => [
'class' => 'mb-2',
'size' => '15',
],
'label' => 'Password:',
'label_attr' => ['class' => 'mr-2'],
'required' => true,
],
'second_options' => [
'attr' => [
'class' => 'mb-2',
'size' => '15',
],
'label' => 'Confirm:',
'label_attr' => ['class' => 'mr-2'],
'required' => true,
],
))
;
if (Volunteer::class === $options['data_class']) {
$builder
->add('focuses', FocusFieldType::class)
->add('skills', SkillFieldType::class)
;
}
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => Staff::class,
'required' => false,
'error_bubbling' => true,
]);
}
}
Organization entity
/**
* Organization
*
* #ORM\Table(name="organization")
* #ORM\Entity
*/
class Organization
{
...
/**
* #ORM\OneToOne(targetEntity="Staff", inversedBy="organization")
* #ORM\JoinColumn(name="staff_id", referencedColumnName="id")
* #Assert\Valid
*/
protected $staff;
...
}
Staff entity
/**
* Staff
*
* #ORM\Table(name="staff")
* #ORM\Entity
*/
class Staff extends User
{
...
/**
* #ORM\OneToOne(targetEntity="Organization", mappedBy="staff")
*/
protected $organization;
public function getOrganization()
{
return $this->organization;
}
public function setOrganization(Organization $organization = null)
{
$this->organization = $organization;
return $this;
}
}
While I don't know why the name & email form errors are not rendered, they can be forced by adding the constraint to the form type. Just need then to remove the #Assert\... from those fields in the User entity.

How can i create one form who contains multiple forms as fields using collectionType in symfony 4?

I want to create multiples forms in one page with one submit buttons with symfony. I checked the documentation and i saw i will need to make a form with a collectiontype field but i don't know how to proceed. I have two forms in two page, the first page the user tell how many forms the next page will have . I already create my first form and its working fine, i have the data of the first form but now how i can create the second page that contain multiple forms depending of the submited data.
Here is my first form :
class OrdersType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add ('tickets', CollectionType::class, [
'entry_type' => TicketsType::class,
'allow_add' => true
])
->add('numberOfTickets', ChoiceType::class, [
'attr' => [
'class' => 'form-control'
],
'choices' => [
'1' => 1,
'2' => 2,
'3' => 3,
'4' => 4,
'5' => 5,
]
])
->add('date', DateType::class, [
'attr' => [
'class' => 'form-control'
],
'widget' => 'single_text'
])
->add('type', ChoiceTYpe::class, [
'attr' => [
'class' => 'form-control'
],
'choices' => [
'Journée' => 1,
'Demi-journée(à partir de 14 heures)' => 0,
]
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Orders::class,
]);
}
}
And my second form (the one i want to duplicate in my second page) :
class TicketsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('category', CheckboxType::class, [
'attr' => [
'class' => 'form-control'
]
])
->add('firstname', TextType::class, [
'attr' => [
'class' => 'form-control'
]
])
->add('lastname', TextType::class, [
'attr' => [
'class' => 'form-control'
]
])
->add('country', TextType::class, [
'attr' => [
'class' => 'form-control'
]
])
->add('dateOfBirth', DateType::class, [
'attr' => [
'class' => 'form-control'
],
'widget' => 'single_text'
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Tickets::class,
]);
}
}
and my controller :
/**
* #Route("/", name="home")
*/
public function home(Request $request, SessionInterface $session)
{
$Orders = new Orders();
$form = $this->createForm(OrdersType::class, $Orders);
$form->handleRequest($request);
if($form->isSubmitted()){
$formData = $form->getData();
$session->set('orders', $formData);
return $this->redirectToRoute('ticket');
}
$repo = $this->getDoctrine()->getRepository(Tickets::class);
$tickets = $repo->findAll();
return $this->render('louvre/home.html.twig', [
'form' => $form->createView()
]);
}
/**
* #Route("/ticket", name="ticket")
*/
public function ticket(Request $request, SessionInterface $session)
{
$data = $session->get('orders');
$Orders = new Orders()
for ($i=0; $i<$data->getNumberOfTickets() ;$i++){
echo 'test';
$tickets = new Tickets();;
$form = $this->createForm(OrdersType::class, $Orders);
$form->handleRequest($request);
};
$repo = $this->getDoctrine()->getRepository(Tickets::class);
$tickets = $repo->findAll();
return $this->render('louvre/ticket.html.twig', [
'form' => $form->createView()
]);
}
Here's an example from symfony 3.4 that shows two different forms on the same page with a single submit button. Note that the head of household is a Member entity and the household is a Household entity:
$form = $this->createForm(HouseholdType::class, $household);
$formHead = $this->createForm(MemberType::class, $head);
$form->handleRequest($request);
$formHead->handleRequest($request);
if ($form->isSubmitted() && $form->isValid() && $formHead->isSubmitted() && $formHead->isValid()) {
//relevant logic
}
return $this->render(
'Household/new.html.twig',
array(
'formType' => 'New Household',
'form' => $form->createView(),
'formHead' => $formHead->createView(),
'title' => 'New Household',
)
);
So the solution was to make the CollectionType not in my form but in my controller, so I deleted:
->add(
'tickets',
CollectionType::class,
[
'entry_type' => TicketsType::class,
'allow_add' => true
]
)
Then when I want to make a CollectionType of my form I write this in my controller:
$form = $this->createForm(
CollectionType::class,
$tickets,
['entry_type' => TicketsType::class]
);

Render a field from a collectionType form in twig with symfony 4

I want to use a form_widget to render a field for a collectionType form. Here is my controller :
/**
* #Route("/ticket", name="ticket")
*/
public function ticket(Request $request)
{
$data = $request->getSession()->get('orders');
$number = $data->getNumberOfTickets();
for ($i=1; $i<=$number ;$i++){
$tickets[] = new Tickets();
}
$form = $this->createForm(CollectionType::class, $tickets, ['entry_type' => TicketsType::class] );
$form->handleRequest($request);
dump($request);
return $this->render('louvre/ticket.html.twig', [
'tickets' =>$tickets,
'form' => $form->createView()
]);
}
and when i try :
{{ form_widget(tickets.firstname)}}
or
{{ form_widget(form.firstname)}}
or
{{ form_widget(form.tickets.firstname)}}
I have an error :
Neither the property "firstname" nor one of the methods "firstname()", "getfirstname()"/"isfirstname()"/"hasfirstname()" or "__call()" exist and have public access in class "Symfony\Component\Form\FormView".
Here is my form :
class TicketsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('category', CheckboxType::class, [
'attr' => [
'class' => 'form-control'
]
])
->add('firstname', TextType::class, [
'attr' => [
'class' => 'form-control'
]
])
->add('lastname', TextType::class, [
'attr' => [
'class' => 'form-control'
]
])
->add('country', TextType::class, [
'attr' => [
'class' => 'form-control'
]
])
->add('dateOfBirth', DateType::class, [
'attr' => [
'class' => 'form-control'
],
'widget' => 'single_text',
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Tickets::class,
]);
}
}
To render a field I just need to use the prototype in twig:
{{ form_widget(form.vars.prototype.firstname) }}
and add this in my form method, in my controller:
'allow_add' => true
You might want to add the code of the entity to your question: Tickets.php
In Tickets.php you probably define the class Tickets and in that class you probably don't have any of the methods listed in the error message. Adding this method with the exact name should help:
public function getfirstname() {
return $this->firstname;
}
About the twig code: you might want to add some more to the question. For example, do you have {{ form_start(form) }} or something else in the beginning?
Then as it is a CollectionType, you probably want to render some input field for each of the members in the collection. Maybe something like this:
{% for ticket in form.tickets %}
<div class="ticket">{{ form_widget(ticket.firstname) }}</div>
{% endfor %}

Symfony form validation error with customType ManyToMany

I'm running Symfony 3.3 with a form type configuration like this.
class UserFinancialTransactionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('billingAddresses', AddressType::class, array(
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => User::class,
'translation_domain' => 'forms',
));
}
}
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$countryChoices = $options['country_choices'];
$builder
->add('firstName', TextType::class, array(
'label' => 'address.first_name',
'required' => false,
))
->add('lastName', TextType::class, array(
'label' => 'address.last_name',
'required' => false,
))
->add('line1', TextType::class, array(
'label' => 'address.line1',
))
->add('line2', TextType::class, array(
'label' => 'address.line2'
))
->add('city', TextType::class, array(
'label' => 'address.city'
))
->add('postalCode', TextType::class, array(
'label' => 'address.postal_code'
))
->add('country', EntityType::class, array(
'label' => 'address.country',
'class' => 'AppBundle:Country',
'choices' => $countryChoices,
'choice_label' => 'shortDe',
'query_builder' => function (EntityRepository $entityRepository) {
return $entityRepository->createQueryBuilder('c')
->orderBy('c.shortDe', 'ASC');
},
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Address::class,
'translation_domain' => 'forms',
'country_choices' => null,
));
}
}
BillingAddresses is a child of user (many-to-many-relation). In my form I want to have only one BillingAddress and want to have it without javascript, this is the reason why I did not use CollectionType.
To get this working I added a setter Method to Entity\User for "billingAddresses":
/**
* Set billingAddresses
*
* #param \AppBundle\Entity\Address $billingAddress
*
* #return User
*
*/
public function setBillingAddresses(\AppBundle\Entity\Address $billingAddress)
{
if($this->billingAddresses !== NULL and $this->billingAddresses->contains($billingAddress)){
return false;
}
$this->addBillingAddress($billingAddress);
return $this;
}
In twig I render the fields like this:
{{ form_row(form.payment.user.billingAddresses) }}
When I submit the form, I got errors for all address form fields (e. g. for user.billingAddresses[0].line1) in symfony profiler and the also in {{ form_errors(form) }}, but my form fields of billingAddress are not labeled as having an error (normally error message of each field is returned directly by form field and not in {{ form_errors(form) }} and in twig form field template form.vars.valid is not set to "false").
So how could I got correct form handling with many-to-many property with my custom form type (AddressType)?
Thank you and best!

Categories