Symfony 3 - subform and element name - symfony

I want to create form with composition pattern like this:
https://symfony.com/doc/current/form/inherit_data_option.html
I use Symfony 3.
and it's working. I have each element like single object and add this.
but finally my form elements names have name like
form[subform][element]
How to make flat structure without subform in name attribute?
use AppBundle\Base\Form\NickType;
use AppBundle\Base\Form\MailType;
use AppBundle\Base\Form\PassType;
use AppBundle\Base\Form\UserType;
class RegisterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nick', NickType::class)
->add('mail', MailType::class)
->add('password', PassType::class)
->add('repeat_password', PassType::class)
(etc...)
and SINGLE ELEMENT
class NickType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nick', TextType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'inherit_data' => true
));
}
}

You don't need to define a NickType if it only inherits a TextType. You can remove NickType, MailType, etc.
You can just do:
class RegisterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('nick', TextType::class)
;
(etc...)
If you want to reuse a form field, you have to create a Custom Form Field Type:
class NickType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
//...
));
}
public function getParent()
{
return TextType::class;
}
}
You can remove form[] from the element name, but removing this is not really recommended, because when you read the request to populate form data you can identify the form by its form name. (via)
You can set the name of the root form to empty, then your field name
will be just form. Do so via
// the first argument to createNamedBuilder() is the name
$form = $this->get('form.factory')->createNamedBuilder(null, 'form', $defaultData)
->add('from', 'date', array(
'required' => false,
'widget' => 'single_text',
'format' => 'dd.MM.yyyy'
));
(via)

Related

How to manually setup the value of a custom form field type in symfony 4?

I've been messing around with custom form field types in Symfony 4 in order to make a form able to crop a picture (using cropper.js) and get some JSON generated by a WYSIWYG (using Quill.js).
I managed to make my custom form field using the documentation and everything is working correctly when it comes to saving data.
I wanna go further and edit some data previously saved in an entity (named "Article" here), I'm passing my entity as a form option under the "edit" key to manually take care of it in my form class ("BackendNewsAddForm.php").
Unfortunatley the usual $builder->setData() function can only set data of pre-builded field types. So I'm looking for a "Best practice" way to get the value I wanna set to the template of the corresponding form field type.
Solution I though of:
Since the $builder->setData() function can set data of pre-builded fields types inside custom field types, I though of adding an hidden widget to each of my custom field types with the value to set for the edit, and bind that value using javascript inside the template. But it doesn't sound really optimized and it's definitely not the "Best practice" way I'm looking for.
Here is the code I'm working on:
BackendNewsAddForm.php (the form class)
class BackendNewsAddForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, array("label" => "Titre de l'article"))
->add('description', TextareaType::class, array("label" => "Description de l'article pour la page d'accueil"))
->add('thumbnail', CroppedPictureInput::class, array("label" => "Miniature de l'article"))
->add('content', QuillTextArea::class, array("label" => "Contenu de l'article"));
if(isset($options['data']['edit'])){
$article = $options['data']['edit'];
$builder->add('id', HiddenType::class, array("data" => $article->getId()));
//I tried the solution I though of here, it looks weird
$builder->setData(array(
'title' => $article->getTitle(),
'description' => $article->getDescription(),
'thumbnail' => array(
'cropped_picture_img' => array(
"picture_input" => $article->getThumbnail()
),
'cropped_picture' => $article->getThumbnail()
),
'content' => array(
'quill_textarea_value' => $article->getContent()
)
));
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => null,
]);
}
}
CroppedPictureInput.php (the cropped picture field type)
class CroppedPictureInput extends AbstractType
{
public $test = false;
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'compound' => true
]);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('cropped_picture_file', FileType::class, array('label' => false, "attr" => array("accept" => "image/jpeg, image/jpg, image/png")))
->add('cropped_picture_img', PictureType::class)
->add('cropped_picture', HiddenType::class);
s('buildForm');
s($builder->getData());
die();
}
public function getBlockPrefix()
{
return 'cropped_picture';
}
public function getName()
{
return 'cropped_picture';
}
}
PictureType.php (the picture type used in CroppedPictureInput)
class PictureType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'compound' => true,
'img_src' => "",
'img_alt' => ""
]);
}
public function buildForm(FormBuilderInterface $builder, array $options){
$builder->add('picture_input', HiddenType::class);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['img_src'] = $options['img_src'];
$view->vars['img_alt'] = $options['img_alt'];
}
public function getBlockPrefix()
{
return 'picture_type';
}
public function getName()
{
return 'picture_type';
}
}
QuillTextArea.php (the custom textarea type)
class QuillTextArea extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'compound' => true
]);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('quill_textarea', TextareaType::class, array('label' => false))
->add('quill_textarea_value', HiddenType::class);
}
public function getBlockPrefix()
{
return 'quill_textarea';
}
public function getName(){
return 'quill_textarea';
}
}
I didn't added the templates of each of those custom fields types for readability purposes but if needed I can provide them.
Thank you for your time :)

Use query_builder on CollectionType in symfony4 forms?

In a symfony 4 form, I need to use something like a query_builder option that is available on EntityType but from a CollectionType. There is a similar question here with no good answers.
In my project, each Site entity has many Goal. Each Goal has a numeric goal and a specific date. I'd like to edit the goals of a site for a specific date only. The problem is that a CollectionType form pulls all goals to show in the form, but I only want to pull the goals for a given date. How? There is no query_builder on a CollectionType like there is on an EntityType. I could change the getter in my Site entity, but I don't know how to pass the needed date to my getter.
For now my work-around is to render the entire form (with ALL associated goals for a given site), and then use some javascript to hide all goals except those with the date to edit. This works, but it's a terrible solution for sites with lots of goals spanning a range of dates.
My Site entity (only relevant code is shown):
class Site
{
public function __construct()
{
$this->goals = new ArrayCollection();
}
/** #ORM\OneToMany(targetEntity="App\Entity\Goal", mappedBy="site") */
private $goals;
public function getGoals()
{
return $this->goals;
}
}
and my related Goal entity:
class Goal
{
/** #ORM\Column(type="date") */
private $goalDate;
/** #ORM\Column(type="integer") */
private $goal;
/** #ORM\ManyToOne(targetEntity="App\Entity\Site", inversedBy="goals") */
private $site;
// ...
}
My forms:
class SiteGoalsAdminForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('goals', CollectionType::class, [
'entry_type' => GoalsEmbeddedForm::class,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Site::class
]);
}
}
and the individual goal form:
class GoalsEmbeddedForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('goal', IntegerType::class)
->add('goalDate', DateType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Goal::class,
]);
}
}
Using Form Events, while avoiding the allow_add and allow_delete options for the CollectionType form might land you in the right neighbourhood:
First - let's assume we're filtering by year, for ease of example, and that the year is being scooped up from a ?y=2018 style of querystring. We'll pass that info down to the form builder:
<?php
// Inside a *Action method of a controller
public function index(Request $request): Response
{
// ...
$filteredYear = $request->get('y');
$form = $this->createForm(SiteGoalsAdminForm::class, $site, ['year_filter' => $filteredYear]);
// ...
}
This implies we should be updating the default options for the SiteGoalsAdminForm class:
<?php
// SiteGoalsAdminForm.php
// ...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Site::class,
'year_filter' => 2018
]);
}
// ...
Then, in the buildForm method of that same class, we could access the Site object and remove Goals from it where the year of the goalDate did not fall inside the form's
<?php
// SiteGoalsAdminForm.php
namespace App\Form;
// ... other `use` statements, plus:
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class SiteGoalsAdminForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($options) {
$form = $event->getForm();
/** #var Site */
$site = $event->getData();
$goals = $site->getGoals();
foreach ($goals as $g) {
if ($g->getGoalDate()->format('Y') !== (string) $options['year_filter']) {
$site->removeGoal($g);
}
}
$form->add('goals', CollectionType::class, [
'entry_type' => GoalsEmbeddedForm::class,
]);
}
);
}
// ...
}
Not a query_builder exactly, but functionally similar.
Filter the results using the entity manager in the controller that you want to set on the collection type.
$goals = $entityManager->getRepository(Goals::class)->findBy(['year' => 2020]);
$form = $this->createForm(SiteGoalsType::class, $site, [
'goals' => $goals
]);
Then configure the SiteGoalsType::class to accept new option goals.
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Site::class,
]);
$resolver->setRequired(['goals']);
}
In the buildForm method of SiteGoalsType::class Set the data to the collection type field from the options.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('goals', Type\CollectionType::class, [
'entry_type' => GoalsEmbeddedType::class,
'data' => $options['goals'],
'mapped` => false
]);
}
Make sure the add the 'mapped' => false to your collection type field else it may lead to removing the records that didn't falls in the filter we have written in the controller.
$goals = $entityManager->getRepository(Goals::class)->findBy(['year' => 2020]);

Symfony form to create a collection of entities sharing inputs

I need to make a form to create a collection of the same entity, but I do not want to repeat all fields for each entry of the collection (only fields which will have different values).
Imagine a Product entity:
class Product
{
private $category;
private $name;
private $price;
}
I would like a form to create multiple Product entities of the same category at the same time.
So the form should have one category field, and a collection of name and price.
My form will look something like:
class ProductCollectionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('category')
->add('products', CollectionType::class, array(
'entry_type' => ProductType::class,
'allow_add' => true,
'mapped' => false,
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Product::class,
));
}
}
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('price')
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Product::class,
));
}
}
Anyone know if there is a way to automatically fill all entities in the collection type with the parent fields? (only category in this example)
Thanks!
I would solve this by having form(s) for the Product(s) which do not require a value for the category member. Then create a single unmapped field to receive a category value. Once back in the controller after submission, apply the unmapped category value to each of the Product entities, and then process/persist/etc.
Checkout the Symfony Forms page, search for "mapped" and you'll see how to add an unmapped field to a form.

Why does data_class leads to LogicException?

I have a form type (field_type) which extends text and have a data_class. Passing an entity instance to the form via event listener leads to a LogicException:
The form's view data is expected to be an instance of class Entity,
but is a(n) string. You can avoid this error by setting the
"data_class" option to null or by adding a view transformer that
transforms a(n) string to an instance of Entity.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('field', 'field_type', $opts);
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) use($options)
{
...
$form = $event->getForm();
$form->get('field')->setData($entity);
});
}
$entity is an instance of the data_class. The form type has a view data transformer, too.
Field type:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer($this->viewTransformer, true);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Entity',
'invalid_message' => 'The given id is invalid!',
'required' => true
));
}
public function getParent()
{
return 'text';
}
Everything works fine except the part of the data_class. If I remove the data_class it works.
Why do I need to remove this part?

Symfony 2 Form: how to bind data

How to bind data which is gained by using call:
$attributes = $em->getRepository('\OBB\Entity\Attribute')->findAllWithAllRelations($id);
to a Symfony 2 Form
Because according to a manual you need to have a method defined in Entity which is bound to a form.
You should add a form type for editing an individual attribute. This could look something like:
namespace OBB\Form;
class AttributeType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name');
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'OBB\Entity\Attribute',
);
}
public function getName()
{
return 'obb_attribute';
}
}
Then you can use a collection form to edit a collection of them simultaneously.
$form = $this->createForm('collection', $attributes, array(
'type' => new AttributeType(),
));

Resources