Symfony2 multiple forms in one template - forms

My app consists of Zones that can have many Devices.
When viewing a Zone, it needs to display a control for each Device in the Zone.
Each Device is completely independent, so embedding the Device forms in a Zone form seems unnecessary - I only want to deal with changes to one device at a time.
Currently I'm creating a form for each device and passing them to the Zone view template:
public function viewAction($zone_id)
{
$zone = $this->getZoneById($zone_id);
$forms = array();
foreach ($zone->getDevices() as $device) {
$forms[] = $this->createForm(new DeviceType(), $device)->createView();
}
return $this->render('AcmeBundle:Zones:view.html.twig', array('zone' => $zone, 'deviceForms' => $forms));
}
And then in the view template, I'm looping through the forms:
{% for form in deviceForms %}
{% include 'AcmeBundle:Devices:control.html.twig'
with {'zone':zone, 'form':form}
%}
{% endfor %}
This seems to be working ok, but I really need to change the template that renders based on the 'type' of Device. What's the cleanest way to do this? I can do something like:
{% if form.vars.data.type == 'foo' %}
{% include 'AcmeBundle:Devices:control-foo.html.twig'
with {'zone':zone, 'form':form}
%}
{% elseif form.vars.data.type == 'bar' %}
{% include 'AcmeBundle:Devices:control-bar.html.twig'
with {'zone':zone, 'form':form}
%}
{% endif %}
but this seems like putting too much logic in the template? It would be better to assign the template to render to the form object somehow, but I've no idea if this is possible?

You must add an option 'template' or whatever in the FormType via the controller,
In the FormType you must declare the default option 'template' and pass it the the form view.
public function viewAction($zone_id)
{
$zone = $this->getZoneById($zone_id);
$forms = array();
//You define a config for each type of device (you should use parameters)
$templates = array(
'foo' => 'AcmeBundle:Devices:control-foo.html.twig',
'bar' => 'AcmeBundle:Devices:control-bar.html.twig',
);
foreach ($zone->getDevices() as $device) {
//define your template here.
$type = $device->getType();
//add a template option in the form.
$options['template'] == $templates[$type];
$forms[] = $this->createForm(new DeviceType(), $device, $options)->createView();
}
return $this->render('AcmeBundle:Zones:view.html.twig', array('zone' => $zone, 'deviceForms' => $forms));
}
Now in the DeviceType you should set the defaults options in the form, they will be merged with options we create in the controller.
public function getDefaultOptions(array $options) {
return array(
//...other options...
//this is the default template of this form
'template' => 'AcmeBundle:Devices:control.html.twig'
);
}
Then set the attribute on the form in the Builder
public function buildForm(FormBuilder $builder, array $options)
{
$builder->setAttribute('template', $options['template']);
//...your fields here...
}
And finally, set the var template in the view.
public function buildView(FormView $view, FormInterface $form)
{
$view->set('template', $form->getAttribute('template'));
}
Now you can read the "template" option in twig, and include the corresponding template
{% for form in deviceForms %}
{% include form.get('template') with {'zone':zone, 'form':form} %}
{% endfor %}
Do not forget to add lines at the beginning of the FormType
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormBuilder;

Related

Symfony 5.1 $form->getData() Always Null (Basic Form)

This Symfony form question has been asked 100 times (and I've read ALL of the responses), but none are working for me. I have a class (Employer), a form (Preview.html.twig), and a controller (DefaultController.php). No matter what I try, I still get null values for the form fields. The form displays properly and I'm not saving to a database (I just want to dump the variables, then I'll move on to db action). This has consumed weeks of my life and any assistance is sincerely appreciated.
The Default Controller (DefaultController.php)
<?
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use App\Entity\Employer;
use App\Form\EmployerType;
class DefaultController extends AbstractController
{ /**
* #Route("/preview", name="preview")
*/
public function preview(Request $request)
{
$employer = new Employer();
$form = $this->createForm(EmployerType::class, $employer, ['csrf_protection' => false]);
$form->handleRequest($request);
//the next two lines were added to force the form to submit, which it wasn't doing prior to
if ($request->isMethod('POST')) {
$form->submit($request->request->get($form->getName()));
if ($form->isSubmitted() && $form->isValid()) {
$employer = $form->getData();
dump($form); /****** ALL ENTRIES FROM THIS DUMP ARE NULL. *****/
exit; /***Added to capture dump ******/
return $this->redirectToRoute('homepage'); /** Works when the exit is removed ***/
}
}
return $this->render('preview.html.twig',
['form'=> $form->createView()]
);
}}
The Employer Class (Employer.php)
namespace App\Entity;
class Employer
{
protected $companyName;
protected $companyAddress;
public function setCompanyName($companyName)
{ $this->companyName = trim($companyName); }
public function getCompanyName()
{ return $this->companyName; }
public function setCompanyAddress($companyAddress)
{ $this->companyAddress = trim($companyAddress); }
public function getCompanyAddress()
{ return $this->companyAddress; }
}
Form Builder (EmployerType.php)
<?php
namespace App\Form;
use App\Entity\Employer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class EmployerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('companyName', TextType::class, ['label' => 'Company Name', 'required' => false])
->add('companyAddress', TextType::class, ['label' => 'Address', 'required' => false])
->add('submit',SubmitType::class, ['label' => 'Submit & Preview'])
->getForm() //I've added and removed this line multiple times. Not sure if its needed.
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Employer::class,
]);
}
}
For Display (Preview.html.twig) ** Displays Form Correctly **
{{ form(form) }}
A few rabbit holes:
The site is running on localhost (Symfony, Apache, MySQL).
The form input is sent via POST.
The Default Controller redirects after the Submit; the "exit" was added to pause my code.
The form is not embedded. I've scaled back the entire project because I thought the embedded form was the issue.
I changed the method to PUT and can see the form values appended to the URL, but $employer = $form->getData() still populates $employer with null values.
I tried to get individual form fields upon submit using $form->get('companyName')->getData(); The data remains null.
I'm out of ideas on how to save the form data to the Employer object.
You must delete getForm() in EmployeType.
In DefaultController, delete the line that contains form->submit(). Here the employee that you initialized is the form which fills it automatically. To retrieve your employee, you no longer need to do $form->getData(). The employee is already full. You can check with dd($employer) instead of $employer = $form->getData()
I gave up and created a fresh instance of Symfony using Composer. Starting over led me to the issue. Something (I'm not yet confident of what) in my custom twig file was causing the problem. I'll update everyone once I figure it out.
Final Findings:
The name attribute for my form inputs were incorrect. The controller was expecting named input in the form of:
formName[formField] //Ex: employer[companyName]
and mine were the standard type generated by Twig (formName_formField)
The addition of:
<p>form.vars: </p>
{{ dump(form.vars) }}
in my Twig file led me to the answer. I modified the input using a custom form theme by first adding the following line to twig.yaml:
form_themes: ['custom_bootstrap.html.twig']
Then, in the custom file, I created a new instance for each type of input I use to override the defaults. For example, for checkboxes my code is:
{% use "form_div_layout.html.twig" %}
{%- block checkbox_widget -%}
<input type="checkbox" id="{{ form.vars.name }}" name="{{ full_name }}"
{%- if disabled %} disabled="disabled"{% endif -%}
{%- if required %} required="required"{% endif -%}
{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %}
{{ block('attributes') }}
/>
{%- endblock checkbox_widget -%}
You should be able to add the name values directly to your twig template without using a custom file. I really hope this helps someone save time.

Value goes in the database, but doesn't show up in the view - Symfony3

I have a project related to restaurants.
I have an entity restaurant with several fields, and a foreign key related to another entity called people
Once I created the restaurant page with its form, and can view the restaurant in its view (show.html.twig), I should be able to click on a link that lets me add a value for how many people can eat there
This should open a new page, with a little form where I can add this value. Once submitted, I should be redirected to the restaurant page (show.html.twig) and then see the value which I just entered.
The FormType I created to add the number of people
class PeopleType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('value', NumberType::class, array(
'label' => 'How many people',
'required' => false,
))
->add('save', SubmitType::class, array(
'label' => 'submit'
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => People::class,
));
}
}
and I created a specific controller for that
class PeopleController extends Controller
{
public function PeopleAction(Request $request, Restaurant $restaurant)
{
$people = new People();
$form = $this->createForm(PeopleType::class, $people);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($people);
$em->flush();
return $this->redirectToRoute('admin_restaurant_show', array('id' => $restaurant->getId()));
}
return $this->render('admin/restaurant/people.html.twig', array(
'restaurant' => $restaurant,
'form' => $form->createView()
));
}
}
and the route I created
admin_restaurant_people:
path: /{id}/people
defaults: { _controller: "AdminBundle:Retrocession:people" }
in my restaurant view(show.html.twig) where I already added a restaurant with a form. I have the link to the route going to my PeopleType form view
Add people
So once on this page I can add my value, and then redirect to the restaurant page show.html.twig when clicking on submit
And then to be able to display the value in the restaurant view, I added a twig field to be able to show it
<p>People</p>
{% for value in restaurant.people.values %}
<p>{{ value }}</p>
{% endfor %}
But then the value that was entered in the form, doesn'tshow up in the view. It is right in the database, but the view itself doesn't let me see it even with the twig.
here is my database with the People entity
I think I missed something somewhere. Can you help me find the problem?
Thank you
So in fact, you have an array. To resolve your issue, you can try to display your data in for loop
{% for value in restaurant.people.values %}
{{ value }}
{% endfor %}
I think you are expecting a single value. If that's the case, you should check your entities relations to figure out why you are getting an array.
To display only the first value, here is a ugly workaround with slice
{% for value in restaurant.people.values[:1] %}
{{ value }}
{% endfor %}
If you want sum all the values, you try that :
{% set sum = 0 %}
{% for value in restaurant.people.values[:1] %}
{% set sum = sum + value %}
{% endfor %}
At least, the above solutions will suppress your error.

Form collection of imbricated form without entity never validates

I am currently working on a "Filters" form to add the possibility for the users to apply filters on item lists. The issue I am facing is that once the form is submitted, the controller considers that the form is empty and invalid.
Dumping what's returned by $form->getData() shows the following:
array(1) { ["filters"]=> array(0) { } }
There is neither errors nor warnings in the logs. The GUI returns an error on the field of the filter:
This value is not valid.
However if I modify the Twig widget to change the select's id to anything else, I no longer get the invalid value but the form's data still is an empty array.
Here's the layout of this project:
a FormType containing one select input, and one text input,
another FormType that implements the former in a collection,
The controller, which instantiates and use the form,
a Twig view of the 2nd FormType,
and the final Twig page
FilterSearchType.php
namespace NetDev\CoreBundle\Form\Helpers;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormBuilderInterface;
use NetDev\CoreBundle\Form\Helpers\FilterType;
class FilterSearchType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('filters', 'collection', array('type' => new FilterType($options['entity']), 'allow_add' => true,
'allow_delete' => true, 'by_reference' => false))
->add('search', 'submit');
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('filters' => [],
'entity' => null));
}
public function getName() {
return 'search_filter';
}
}
FilterType.php
<?php
namespace NetDev\CoreBundle\Form\Helpers;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormBuilderInterface;
class FilterType extends AbstractType {
public function __construct($entity) {
$this->model = $entity;
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
/*getFilters returns something like that:
* ['Column A' => 'column_a', 'Column B' => 'column_b', ...]
*/
->add('column_name', 'choice', array('choices' => $this->model->getFilters(true)))
->add('search_value', 'text')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(['column_name' => '',
'search_value' => '']);
}
public function getName() {
return 'netdev_filter';
}
}
Here is how the form is given to Twig from the controller:
RoutesController.php
class RoutesController extends Controller {
public function indexAction($page = 1, Request $request) {
$em = $this->getDoctrine()->getManager();
$orderBy = $request->query->get('orderBy');
$orderType = $request->query->get('orderType');
$form = $this->createForm(new RouteManagerType($em), null, array('orderBy' => $orderBy,
'orderType' => $orderType));
$filterForm = $this->createForm(new FilterSearchType(), null, array('entity' => new Route()));
if ($request->isMethod('POST') && $filterForm->handleRequest($request)->isValid()) {
// Never reached, $filterForm is always marked as invalid
$formData = $filterForm->getData();
var_dump($formData);
exit();
if (!empty($formData['filters']) && count($formData['filters'])) {
if (empty($formData['action'])) $formData['action'] = 'filter';
$form = $this->createForm(new RouteManagerType($em), null,
array('orderBy' => $orderBy,
'orderType' => $orderType,
'filters' => $formData['filters']));
}
}
Twig widget: filter_search.html.twig
{% block netdev_filter_widget %}
{% spaceless %}
{{ form_errors(form) }}
<div class="form-group">
<div class="input-group">
<select {{ block('widget_attributes') }} class="form-control" id="search_column">
{% for group_label, choice in form.column_name.vars.choices %}
<option value="{{ choice.value }}" {{ block('attributes') }}>{{ choice.label }}</option>
{% endfor %}
</select>
<div class="input-group-addon">contains</div>
<input type="text" class="form-control" id="search_criteria" placeholder="search"/>
</div>
</div>
{% endspaceless %}
{% endblock %}
I've dumped pretty much everything I could and nothing was really interesting. I am not even sure that the Kernel understands / correctly "links" the submit the user has performed and the form the controller created.
Any help on that would be greatly appreciated.
OK, so the issue is that if you do the rendering yourself, you should very well be aware of the fact that the names as rendered in HTML are exactly what the backend expects, otherwise you'll get issues like these.
The best way to tackle that is take the default form rendering as a starting point and don't do any custom HTML until you're absolutely sure you need custom templating. When you do, inspect the default templates to see how the names of the elements are built, and follow the exact same naming, or even better, reuse base templates wherever possible (either by extending blocks and calling parent() and/or using the block function).

symfony 2 form display default value

I'm working with Symfony 2.0.14 and I would like to display the default value in my form template.
Well a FormType is bound to an entity, when I want to add extra field, I know the option property_path = false allow to add non-entity fields, right ?
When I m in the opposite case, I want to set an entity field without a form field.
Ok I just have to give a default entity to "createForm".
Howewver how can I render it in my template form ?
Controller code :
public function newAction(Request $request)
{
$game = new Game();
$local = new Role();
$visitor = new Role();
$local->setType('LOCAL');
$visitor->setType('VISITOR');
$game->addRole($local);
$game->addRole($visitor);
$form = $this->createForm(new GameType(), $game);
GameType code :
public function buildForm(FormBuilder $builder, array $options){
$builder->add('teams', 'collection', array( 'type' => new RoleType()));
}
RoleType code :
public function buildForm(FormBuilder $builder, array $options){
$builder->add('type', 'text'); // <= I would like read only for end-User
$builder->add('score', 'integer');
form template :
{% for role in form.teams %}
<li>
<div class="role-team">
{{ role.type }} {# WRONG way, how to do ? #}
{{ form_row(role.score) }}
</div>
</li>
{% endfor %}
If you want just to display your entity field value (without passing the entire entity to the view) you can print it with:
{{ form.vars.value.type }}
(assuming your role entity has type property).
EDIT: i realized you're inside the loop. Try figuring out the right property path using:
{% for role in form.teams %}
{% debug role %}
{% endfor %}

edit Symfony2 big entity in form with tabs

I'm building form using Sf2's form builder.
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('firstName')
->add('lastName')...
The Entity has a lot of fields and I'd like to put them in jQuery UI Tabs. But in twig template I'd like to use single command
<form action="#" method="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}
<input type="submit" value="Save"/>
</form>
What is best solution?
edit **
To be more conrete: I have 4 fields: firstName, lastName, birthDate, deathDate. I want first 2 fields to be on first tab and the last 2 fields to be on second tab. I want to keep way of rendering the form as mentioned earlier.
I though of a solution to create my own fields not conneceted to underlaying object which will render required html tags (h3, div, etc).
I defined my own field called 'Tab' and add it when new tab should appear.
<?php
//\src\Alden\xyzBundle\Form\Type\TabsType.php
namespace Alden\BonBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\CallbackValidator;
use Symfony\Component\Form\FormValidatorInterface;
use Symfony\Component\Form\Form;
class TabsType extends AbstractType {
public function buildForm(FormBuilder $builder, array $options)
{
$builder->setAttribute('starting', $options['starting']);
$builder->setAttribute('ending', $options['ending']);
$builder->setAttribute('header', $options['header']);
}
public function buildView(FormView $view, FormInterface $form)
{
$parent = $form->getParent();
if (is_null($parent->getParent()))
{
$tabs = $this->findTabs($parent);
}
else
{
$tabs = array();
}
$view->set('starting', $form->getAttribute('starting'));
$view->set('ending', $form->getAttribute('ending'));
$view->set('header', $form->getAttribute('header'));
$view->set('tabs', $tabs);
}
public function getDefaultOptions(array $options)
{
return array(
'property_path' => false,
'starting' => true,
'ending' => true,
'header' => false,
);
}
public function getName()
{
return 'tabs';
}
public function getParent(array $options)
{
return 'field';
}
private function findTabs(Form $form)
{
$prefix = $form->getName();
$tabs = array();
foreach ($form->getChildren() as $child)
{
foreach ($child->getTypes() as $type)
/* #var $child \Symfony\Component\Form\Form */
{
if (get_class($type) == __NAMESPACE__ . '\TabsType')
{
if ($child->getAttribute('starting'))
{
$tabs[$prefix . '_' . $child->getName()] = $child->getAttribute('label');
}
}
}
}
return $tabs;
}
}
?>
and Twig
{# \src\Alden\xyzBundle\Resources\views\Form\fields.html.twig #}
{% block tabs_row %}
{% if header %}
<ul>
{% for tid, t in tabs %}
<li>
{{ t }}
</li>
{% endfor %}
</ul>
{% endif %}
{% if ending %}
</div>
{% endif %}
{% if starting %}
<div id="{{ id }}">
{% endif %}
{% endblock %}
and usage in form builder:
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('tabs_head', new TabsType(), array(
'ending' => false,
'starting' => false,
'header' => true
))
->add('tab_1', new TabsType(), array(
'ending' => false,
'label' => 'Podstawowe'
))
->add('firstName', null, array(
'label' => 'ImiÄ™'
))
->add('lastName', null, array(
'label' => 'Nazwisko'
))
->add('tab_contact', new TabsType(), array(
'label' => 'Kontakt'
))
->add('address', new AddressType(), array(
'label' => 'Adres zameldowania'
))
->add('tabs_end', new TabsType(), array(
'starting' => false
))
;
}
If you want a form to act like a form wizard you could look at look at the multi-step form bundle
It's pretty nice, you can for example, define step one as filling in software details and then on step2, fill out version details. or whatever you want.
Features
navigation (next, back, start over)
step descriptions
skipping of specified steps
different validation group for each step
dynamic step navigation
And here is a live demo
But in twig template I'd like to use single command
Do you mean to render the fields?
{{ form_rest(form) }}
renders all unrendered forms