edit Symfony2 big entity in form with tabs - forms

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

Related

Symfony 2.8 - display certain entity value in form field widget template

I am building a custom file upload widget where I display last uploaded filename. I created FormType class and in form/fields.html.twig I added the following:
{% block custom_document_widget %}
{% spaceless %}
{# here I want to include code to display filename #}
{# display file input #}
{% set type = 'file' %}
{{ block('form_widget_simple') }}
{% endspaceless %}
{% endblock %}
I know that the value of a current field can be parsed {{ form.vars.value }}, but in the end the field is file input and does not have the value of filename that was uploaded previously.
To store uploaded filename I have $filename variable in entity and would like to display it in field widget template. How can I approach it?
In the end I had to pass the filename as an option to embedded form that represented my FileType:
$builder
->add('resumeFile', CustomDocsType::class, array(
'required' => false,
'constraints' => array(
new File(array(
'mimeTypes' => array(
'application/pdf',
),
'mimeTypesMessage' => 'mimetype',
)),
),
'filename' => $trainee->getResumeOriginal(),
))
In my CustomDocsType:
class CustomDocsType extends AbstractType
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
parent::buildView($view, $form, $options);
$view->vars = array_merge($view->vars, array(
'filename' => $options['filename']
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'filename' => null
));
}
public function getParent()
{
return FileType::class;
}
}
And now I only had to acces the filename in template:
{{ form.vars.filename }}

Symfony - Add text in generated form

I'd like to do something quite simple, but I can't figure out how to manage it. I have a form:
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
There are several text field in it. I'd like to "insert" some text (like <p>my Text</p>) between two text fields (let's say between name text field and description text field). Form are generated with form builder tool. I've tried something like:
$builder
->add('stuffName') // works well (text field 1)
->add('addedText', 'text', array('label' => 'my Text')) // Trouble here!
->add('stuffDescription'); // works well (text field 2)
But it generates a text field (and not a simple text). I don't care if the text is set in the form builder or directly in twig template... As long as it is between my two text fields. Any idea?
Thanks a lot!
Symfony forms contain only form fields. Any additional content you want has to be added by the template.
This means you'll have to output the form field-by-field. Your form, for example might look like this:
{{ form_start(form) }}
{{ form_row(form.stuffName) }}
<p>Your Text</p>
{{ form_row(form.stuffDescription) }}
{{ form_end(form) }}
For more more information on how you can customize form rendering, please see the forms chapter in the Symfony documentation.
The keyword in this question is generated.
Let's assume, that you build a form generator in Symfony. You have entities like Form, Fields and Fields Items (it's options for select box or buttons for radio button field).
So you have this entities and you create a service to create a form from the data. In the service you build the form ($this->buildedForm - generated form, $page->getFormData() - put the data to the constructed form):
$this->buildedForm = $this->formFactory->create(
'form',
$page->getFormData(),
['action' => '/page/formview/' . $task->getId()]
);
foreach($fields as $field) {
$fieldBuilderMethod = 'construct' . ucfirst($field->getType()) . 'Field';
if (method_exists($this, $fieldBuilderMethod)) {
$this->$fieldBuilderMethod($field);
}
}
return $this->buildedForm;
And you have methods for each type like (examples for Symfony 2):
private function constructInputField(FormField $field)
{
$this->buildedForm->add(
$field->getFieldName(),
'text',
[
'label' => $field->getName(),
]
);
}
private function constructTextareaField(FormField $field)
{
$this->buildedForm->add(
$field->getFieldName(),
'textarea',
[
'label' => $field->getName(),
]
);
}
You can now create your custom form type to paste a text in the generated form (it could be placed in the form folder of your bundle and retrieved with namespace "use"):
private function constructSimpletextField(FormField $field)
{
$this->buildedForm->add(
$field->getFieldName(),
new SimpletextType(),
[
'label' => $field->getName(),
'data' => $field->getPlaceholder(),
]
);
}
What in this custom field?
namespace Myproject\MyBundle\Form\TaskTypes;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SimpletextType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'disabled' => true,
'required' => false,
'mapped' => false,
]);
}
public function getParent()
{
return 'text';
}
public function getName()
{
return 'simpletext';
}
}
And the whole magic comes out in the template. For your custom form type you need to make a custom theme (see https://symfony.com/doc/2.7/form/form_customization.html#form-customization-form-themes). And there:
{% block simpletext_label %}{% endblock %}
{% block simpletext_widget %}
<p>{{ form.vars.data }}</p>
{% endblock %}
{% block simpletext_errors %}{% endblock %}
See, no label, no errors (it just a text) and only text in the field widget. Very handy for generated forms with dynamic template.
EDIT - Symfony 5
In Symfony 5, this solution became simplier. The form customization doesn't changes, and the php code became like this:
namespace Myproject\MyBundle\Form\TaskTypes;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SimpletextType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'disabled' => true,
'required' => false,
'mapped' => false,
]);
}
public function getBlockPrefix(): string
{
return 'simpletext';
}
}
It's used like this :
public function buildForm(FormBuilderInterface $builder, array $options): void {
/* … */
$builder->add('anykey', SimpleTextType::class, [
'data' => "Type your text here",
]);
/* … */
}
Here a sample code which would be self explain
{{ form_start(form, { 'attr': { 'class': 'form-horizontal form-bordered'} }) }}
<div class="form-group">
<div class="col-md-3 ">
{{ form_label(form.User, 'Label text', { 'attr': {'class': 'control-label'} }) }}
</div>
<p>You are free to add whatever you want here</p>
<div class="col-md-9">
{{ form_widget(form.User, { 'attr': {'class': 'form-control'} }) }}
</div>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
In any case, the symfony documentation is pretty clear and well-explain about this point.

Symfony 2.7 Form Entity type render multiple properties in form

I had this working previously but it stopped working with Symfony 2.7
What I want is to render an expanded/multiple entity choice list such that I display multiple custom properties. The goal is to list the choices as:
{name} - {description} More info
So I created a custom form type with "entity" as parent so I could customize the form rendering
<?php
namespace Study\MainBundle\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;
class ScholarshipEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->setAttribute('dataType', $options['dataType']);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'required' => false,
'dataType' => 'entity'
));
}
public function getAllowedOptionValues(array $options)
{
return array('required' => array(false));
}
public function getParent()
{
return 'entity';
}
public function getName()
{
return 'scholarship_entity';
}
}
I render the type as follows (it was just based off of the Twitter Bootstrap bundle template):
{% block scholarship_entity_widget %}
{% spaceless %}
{% if expanded %}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default(''))}) %}
{% set label_attr = label_attr|merge({'class': (label_attr.class ~ ' ' ~ (widget_type != '' ? (multiple ? 'checkbox' : 'radio') ~ '-' ~ widget_type : ''))}) %}
{% if expanded %}
{% set attr = attr|merge({'class': attr.class|default(horizontal_input_wrapper_class)}) %}
{% endif %}
{% for child in form %}
{% if widget_type != 'inline' %}
<div class="{{ multiple ? 'checkbox' : 'radio' }}">
{% endif %}
<label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>
{{ form_widget(child, {'horizontal_label_class': horizontal_label_class, 'horizontal_input_wrapper_class': horizontal_input_wrapper_class, 'attr': {'class': attr.widget_class|default('')}}) }}
{{ child.vars.label.name|trans({}, translation_domain) }}
- {{ child.vars.label.description }}
More Information
</label>
{% if widget_type != 'inline' %}
</div>
{% endif %}
{% endfor %}
{{ block('form_message') }}
{% if expanded %}
{% endif %}
{% else %}
{# not being used, just default #}
{{ block('choice_widget_collapsed') }}
{% endif %}
{% endspaceless %}
{% endblock %}
Finally, I use my custom type in another form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('scholarships', new ScholarshipEntityType(), array(
'class' => 'StudyMainBundle:Scholarship',
'query_builder' => function(EntityRepository $er) use ($options) {
return $er->findAllByOfferingQueryBuilder($options['offering']);
},
'choice_label' => 'entity',
'multiple' => true,
'expanded' => true,
'label' => 'financial.scholarships'
))
;
}
The "property" I'm rendering is just the entity itself:
/**
* Scholarship
*
* #ORM\Table(name="scholarship")
* #ORM\Entity(repositoryClass="Study\MainBundle\Repository\ScholarshipRepository")
*/
class Scholarship
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
// ...
/**
* Get the Entity object for form rendering
*
* #return \Study\MainBundle\Entity\Scholarship
*/
public function getEntity()
{
return $this;
}
}
Unfortunately, it looks like my trick which was passing the entire Entity to Twig and letting me access properties is no longer working. There is some change where the label is rendered as a string (I changed 'property' to 'choice_label' above for 2.7, if that matters).
Error:
Catchable Fatal Error: Object of class Study\MainBundle\Entity\Scholarship could not be converted to string
Stack Trace:
1. in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php at line 251 +
2. at ErrorHandler ->handleError ('4096', 'Object of class Study\MainBundle\Entity\Scholarship could not be converted to string', '/var/project/vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php', '251', array('choice' => object(Scholarship), 'key' => '0', 'label' => object(Closure), 'values' => array('2'), 'index' => array('Symfony\Bridge\Doctrine\Form\Type\DoctrineType', 'createChoiceName'), 'attr' => null, 'isPreferred' => array(), 'preferredViews' => array(), 'otherViews' => array(), 'value' => '2', 'nextIndex' => '2'))
in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php at line 251 +
3. at DefaultChoiceListFactory ::addChoiceView (object(Scholarship), '0', object(Closure), array('2'), array('Symfony\Bridge\Doctrine\Form\Type\DoctrineType', 'createChoiceName'), null, array(), array(), array())
in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php at line 185
Is there another way to achieve this?
I was thinking about the following (but don't know exactly how to do these or if it's worth looking into any of them):
transformers
custom type that derives from Choice and does what I want (maybe from a bundle)
using the choice list factory somehow
passing the entity as some additional field instead of the label (maybe the new 'choice_attr'?)
If I understood correctly the problem, you should implement the __toString() function in your entity, that will format the string you want to print in the Choice list for you entity.
For example:
function __toString() {
return sprintf("%s - %s", $this->type, $this->description);
}
Try to use the method AbstractType::buildView(FormView, FormInterface, array). There you can access the variables that get passed to the template.
I used it for a DaterangeType to declare separate ids and names for two date fields:
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['full_name_0'] = $view->vars['full_name'] . '[0]';
$view->vars['full_name_1'] = $view->vars['full_name'] . '[1]';
$view->vars['id_0'] = $view->vars['id'] . '_0';
$view->vars['id_1'] = $view->vars['id'] . '_1';
}
You can then access these values as standard twig variables.

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).

Binding request to a form does not work for collection type

I don't use any ORM for entities. This form is built for sending email only.
In my Controller I have this:
$builder = $this->myHelper
->createBuilder('form', null)
->add('my_group', 'collection', array(
'type' => 'text',
'label' => 'mylabel'
));
$builder->get('my_group')->add('first_node', 'text');
$builder->get('my_group')->add('second_node', 'text');
return $builder->getForm();
The form is rendered OK - with additional input fields as expected. But when it comes to binding request to form in my post-data handling action - my_group field comes empty (even due to fact that this field is posted in 'form' array):
// var_dump($request->get('form'));die;
array
'my_group' =>
array
'first_node' => string 'asdasd' (length=3)
'second_node' => string 'asdasda' (length=3)
When I bind request to form, I have null in my_group collection field (all other inputs are OK).
$form->bindRequest($request);
$formData = $form->getData();
var_dump($formData);die; // Outputs my_group => null
What am I doing wrong?
UPDATE 1
Part of Twig template (requested by Max):
{% for field in form.children if 'hidden' not in field.vars.types %}
...
{% elseif 'collection' in field.vars.types %}
<th>{{ form_label(field) }}</th>
<td>
{% for collection_field in field %}
{{ form_widget(collection_field) }}
{% endfor %}
</td>
{% else %}
...
{% endfor %}
You need to create:
form class
template of a form (with macro)
jQuery script to manipulate the data in the form
Check this tutorial :)
http://toni.uebernickel.info/2012/03/15/an-example-of-symfony2-collectiontype-form-field-prototype.html
My problem was solved, when I defined FormType for needed collection:
class MyType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('first_node', 'text');
$builder->add('second_node', 'text');
}
public function getDefaultOptions(array $options)
{
return array();
}
public function getName()
{
return 'mytype';
}
}
Then I used it in builder:
$builder = $this->myHelper
->createBuilder('form', null)
->add('my_group', new MyType(), array(
'label' => 'mylabel'
));
return $builder->getForm();
After this data was bound to form correctly.