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.
Related
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 :)
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]);
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)
I have a form responsible of creating and updating users. A user can (or not) have an address (OneToOne unidirectional relation from user).
When I create a user, no problem.
When I update a user, usually no problem.
Problems come up when i update a user which already has an address and try to unset all the address fields. There is then a validation error.
The wanted behavior would be to have the user->address relation set to null (and delete the previously set address on the DB).
There is a cascade_validation, the addess field in form (nested form) is set to not be required and the user entity allow the address to be null.
UPDATE
Relevant entities and forms :
User entity (Getters & Setters are classical, Symfony generated):
class User
{
[...]
/**
* #var \Address
*
* #ORM\OneToOne(targetEntity="Address", cascade="persist")
* #ORM\JoinColumn(
* name="address_id", referencedColumnName="id"
* )
*/
private $address;
[...]
}
The address entity is classical, no bidirectionnal relation to user.
User form
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
[...]
->add('address', new AddressType(), array('required' => false))
[...]
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Xentia\FuneoBundle\Entity\User',
'cascade_validation' => true
));
}
public function getName()
{
return 'user';
}
}
The address nested form is classical
As you can see, the is a quite classical and straightforward code. The only particular case is that address is optional. Leading to an validation error only in the case that the address was previously set (and, thus, exist in the DB and as a not null relation with the user) and the user want to unset it (all address fields are left empty).
It seems that if the related address has not an actual instance it can still be optional. But, if an instance of the address exist and is linked with the user, it can not be optional anymore.
UPDATE 2
namespace Xentia\FuneoBundle\Form\Type;
use Doctrine\Common\Util\Debug;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add("suggestLocality", null, array(
'mapped' => false,
'label' => 'Locality'
))
->add("suggestStreet", null, array(
'mapped' => false,
'label' => 'Street'
))
->add('street')
->add('locality')
->add('postalCode')
->add('country', null, array(
'label' => false,
))
->add('latitude', 'hidden')
->add('longitude', 'hidden');
$builder->addEventListener(FormEvents::PRE_SUBMIT,
function(FormEvent $event) {
$address = $event->getData();
if (!empty($address)) {
$addressLocality = $address['locality'];
if (empty($addressLocality)) {
$event->setData(null);
}
}
}
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Xentia\FuneoBundle\Entity\Address',
'validation_groups' => array('Default'),
));
}
public function getName()
{
return 'address';
}
}
Try setting orphanRemoval on your relation
/** #OneToOne(targetEntity="...", orphanRemoval=true) */
$address
EDIT
I see now, you have placed the wrong listener. First of all it should be POST_SUBMIT, PRE_SUBMIT is to process request data and modify form. On POST SUBMIT you can modify the object.
My form uses the uploads collection type. Each element of the collection is of UploadType:
class MultiUploadType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('uploads', 'collection', array(
'type' => new UploadType(), // This should be validated
'allow_add' => true,
));
$builder->add('Save', 'submit');
}
}
Using javascript I'm able to add new uploads, but validation doesn't work. I've read many questions here (here, here or here) but I can't find a solution yet.
This is how the upload type looks like, while validation is defined using YAML, as the form has a corresponding entity of type Upload (file can't be blank):
class UploadType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('file', 'file');
$builder->add('description', 'textarea');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'required' => false,
'data_class' => 'App\Entity\Upload'
));
}
}
Validation code:
App\Entity\Upload:
properties:
file:
- NotBlank:
message: Occorre selezionare un file.
- File: ~
From comments disscusion:
Yes, basically each form should have a data class. It has not to be a entity, a simple model class is enough. So you can apply validation to it. To validate embed forms the Valid assert is required and for collections the same but with the option traverse: true.