Accessing a form field from a subscriber (of a form event) in Symfony2 - symfony

I'm following the tutorial How to Dynamically Generate Forms Using Form Events. I'm stuck on the creation of AddNameFieldSubscriber:
$subscriber = new AddNameFieldSubscriber($builder->getFormFactory());
My question is simple: how FormFactory can access and modify an arbitrary form field previously created by the $builder? And why we are passing the FormFactory instead of the $builder itself?
Assuming we have just two fields ("name" and "price") in the builder:
class ProductType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$subscriber = new AddProductTypeSubscriber($builder->getFormFactory());
$builder->addEventSubscriber($subscriber);
$builder->add('name');
$builder->add('price');
}
public function getName() { return 'product'; }
}
I'd like to set required = false (just an example) in the subscriber:
class ProductTypeSubscriber implements EventSubscriberInterface
{
private $factory;
public function __construct(FormFactoryInterface $factory)
{
$this->factory = $factory;
}
public static function getSubscribedEvents()
{
return array(FormEvents::PRE_SET_DATA => 'preSetData');
}
public function preSetData(DataEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
if (null === $data) return;
// Access "name" field and set require = false
}
}

I could be wrong about it this, but I don't believe you can change a form's attributes after its been created. However, you can add to the form.
Instead of adding the 'name' field in ProductType::buildForm, you can defer this to the subscriber:
if (!$data->getId()) {
$form->add($this->factory->createNamed('text', 'name', null, array('required' => false)));
} else {
$form->add($this->factory->createNamed('text', 'name'));
}

Related

Merge form CollectionType with existing collection

I would need some help about management of CollectionType. In order to make my question as clear as possible, I will change my situation to fit the official Symfony documentation, with Tasks and Tags.
What I would like :
A existing task have alrady some tags assigned to it
I want to submit a list of tags with an additional field (value)
If the submitted tags are already assigned to the task->tags collection, I want to update them
It they are not, I want to add them to the collection with the submitted values
Existing tags, no part of the form, must be kept
Here is the problem :
All task tags are always overwritten by submitted data, including bedore the handleRequest method is called in the controller.
Therefore, I can't even compare the existing data using the repository, since this one already contains the collection sent by the form, even at the top of the update function in the controller.
Entity wize, this is a ManyToMany relation with an additional field (called value), so in reality, 2 OneToMany relations. Here are the code :
Entity "Task"
class Task
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\OneToMany(mappedBy: 'task', targetEntity: TaskTags::class, orphanRemoval: false, cascade: ['persist'])]
private Collection $TaskTags;
/**
* #return Collection<int, TaskTags>
*/
public function getTaskTags(): Collection
{
return $this->TaskTags;
}
public function addTaskTag(TaskTags $TaskTag): self
{
// I have voluntarily remove the presence condition during my tests
$this->TaskTags->add($TaskTag);
$TaskTag->setTask($this);
return $this;
}
public function removeTaskTag(TaskTags $TaskTag): self
{
if ($this->TaskTags->removeElement($TaskTag)) {
// set the owning side to null (unless already changed)
if ($TaskTag->getTask() === $this) {
$TaskTag->setTask(null);
}
}
return $this;
}
}
Entity "Tag"
class Tag
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\OneToMany(mappedBy: 'tag', targetEntity: TaskTags::class, orphanRemoval: false)]
private Collection $TaskTags;
/**
* #return Collection<int, TaskTags>
*/
public function getTaskTags(): Collection
{
return $this->TaskTags;
}
public function addTaskTag(TaskTags $TaskTag): self
{
$this->TaskTags->add($TaskTag);
$TaskTag->setTag($this);
return $this;
}
public function removeTaskTag(TaskTags $TaskTag): self
{
if ($this->TaskTags->removeElement($TaskTag)) {
// set the owning side to null (unless already changed)
if ($TaskTag->getTag() === $this) {
$TaskTag->setTag(null);
}
}
return $this;
}
}
Entity "TaskTags"
class TaskTags
{
#[ORM\Id]
#[ORM\ManyToOne(inversedBy: 'TaskTags')]
#[ORM\JoinColumn(nullable: false)]
private Task $task;
#[ORM\Id]
#[ORM\ManyToOne(inversedBy: 'TaskTags')]
#[ORM\JoinColumn(nullable: false)]
private Tag $tag;
// The addional field
#[ORM\Column(nullable: true)]
private ?int $value = null;
public function getTask(): ?Task
{
return $this->task;
}
public function setTask(?Task $task): self
{
if(null !== $task) {
$this->task = $task;
}
return $this;
}
public function getTag(): ?Tag
{
return $this->tag;
}
public function setTag(?Tag $tag): self
{
if(null !== $tag) {
$this->tag = $tag;
}
return $this;
}
public function getValue(): ?string
{
return $this->value;
}
public function setValue(?string $value): self
{
$this->value = $value;
return $this;
}
}
FormType "TaskFormType"
class TaskFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
...
->add('TaskTags', CollectionType::class, [
'by_reference' => false,
'entry_type' => TaskTagsFormType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Task::class,
'csrf_protection' => false
]);
}
}
FormType "TaskTagsFormType"
class TaskTagsFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('task')
->add('tag')
->add('value')
;
}
Controller
#[Route('/tasks/edit/{id}/tags', name: 'app_edit_task')]
public function editasktags(Request $request, EntityManagerInterface $em, TaskTagsRepository $TaskTagsRepo): Response
{
...
// Create an ArrayCollection of the current tags assigned to the task
$task = $this->getTask();
// when displaying the form (method GET), this collection shows correctly the tags already assigned to the task
// when the form is submitted, it immediately becomes the collection sent by the form
$ExistingTaskTags = $TaskTagsRepo->findByTask($task);
$form = $this->createForm(TaskFormType::class, $task);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// here begins all I have tried ... I was trying to compare the value in the DB and in the form, but because of the repo being overwritten I can't
$task = $form->getData();
$SubmittedTaskTags = $userForm->getTaskTags();
$CalculatedTaskTags = new ArrayCollection();
foreach ($ExistingTaskTags as $ExistingTaskTag) {
foreach ($SubmittedTaskTags as $SubmittedTaskTag) {
if ($ExistingTaskTag->getTag()->getId() !== $SubmittedTaskTag->getTag()->getId()) {
// The existing tag is not the same as submitted, keeping it as it in a new collection
$CalculatedTaskTags->add($ExistingTaskTag);
} else {
// The submitted tag is equal to the one in DB, so adding the submitted one
$SubmittedTaskTag->setTask($task);
$CalculatedTaskTags->add($SubmittedTaskTag);
}
}
}
$em->persist($task);
$em->flush();
}
return $this->render('task/edittasktags.twig.html', [
'form' => $form,
'task' => $this->getTask()
]);
}
My main issue is that I am not able to get the existing data one the form has been submitted, in order to perform a "merge"
I have tried so many things.
One I did not, and I'd like to avoid : sending the existing collection as hidden fields.
I don't like this at all since if the data have been modified in the meantime, we are sending outdated data, which could be a mess in multi tab usage.
Thank you in advance for your help, I understand this topic is not that easy.
NB : the code I sent it not my real code / entity. I've re written according to the Symfony doc case, so there could be some typo here and there, apologize.
Solution found.
I added 'mapped' => false in the FormType.
And I was able to retrieve the form data using
$SubmittedTags = $form->get('TaskTags')->getData();
The repository was not overwritten by the submitted collection.

Symfony entity form with dynamic fields from another entity

Question:
Consider following Order form:
META DATA:
Title: [________]
REQUIREMENTS:
What size? [_] Small [_] Medium [_] Large
What shape? [_] Circle [_] Square [_] Triangle
.
.
.
How can I generate the form?
Constraints:
size & shape & ... should be retrieved form another entity named: Requirement.
What I think:
Order
Class Order
{
private $title;
//OneToMany(targetEntity="Requirement", mappedBy="order")
private $requirements;
public function __construct()
{
$this->requirements = new ArrayCollection();
}
}
Requirement
Class Requirement
{
private $title;
//ManyToOne(targetEntity="Order", inversedBy="requirements")
private $order;
//OneToMany(targetEntity="Selection", mappedBy="selections")
private $selections;
public function __construct()
{
$this->selections = new ArrayCollection();
}
}
Selection
Class Selection
{
private $title;
//ManyToOne(targetEntity="Requirement", inversedBy="selections")
private $requirement;
}
OrderType
class OrderType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'allow_extra_fields' => true
));
$resolver->setRequired('em');
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$em = $options['em'];
$requirements = $em->getRepository(Requirement::class)->findAll();
$form = $builder
->add('title', TextType::class, array());
foreach ($requirements as $requirement) {
$args['label'] = $requirement->getTitle();
$args['expanded'] = true;
$args['multiple'] = true;
$args['mapped'] = false;
$args['class'] = Selection::class;
$args['choice_label'] = 'title';
$args['choice_value'] = 'id';
$args['query_builder'] = call_user_func(function (EntityRepository $er, $requirement) {
return $er->createQueryBuilder('s')
->where('s.requirement = :requirement')
->setParameter('requirement', $requirement)
},$em->getRepository($args['class']), $requirement);
$form
->add($requirement->getTitle(), EntityType::class, $args);
}
$form
->add('submit', SubmitType::class, array(();
return $form;
}
}
Problem:
This works nice and I can persist new Orders. The problem is with editing the Order, as
$form->createView()
while filling order title correctly, would not update the selections (checkboxes). I don't even know if this is the right way of doing this on Symfony. I don't know how can I refill selected checkboxes while rendering the edit entity form.
Notice:
I don't think if Symfony Form Collections is a good idea, as the requirements are changing in time and could not be hardcoded as FormTypes, thus should be stored in database.

How to pass arguments to Datatransformer when using a custom field type

I need to transform a user name to a user entity, and I'm using this transformation in many form,every form need some specific test to the returned user entity.
In the registration form a user can enter a referral code (user name) or leave this empty, the referral code must have the role ROLE_PARTNER.
In the dashboard a partner can append some user to his account: in this case the user name can't be empty, the user must have the role ROLE_CLIENT and the partner can't enter his own user name this is my data transformer class
class UserToNameTransformer implements DataTransformerInterface
{
private $om;
private $role;
private $acceptEmpty;
private $logged;
public function __construct(ObjectManager $om,$role,$acceptEmpty,$logged)
{
$this->om = $om;
$this->role=$role;
$this->acceptEmpty=$acceptEmpty;
$this->logged=$logged;
}
public function transform($user)
{
if (null === $user) {
return "";
}
return $user->getUsername();
}
public function reverseTransform($username)
{
if (!$username) {
if ($this->acceptEmpty)
return null;
else
throw new TransformationFailedException(sprintf(
'user name can\'t be empty!'
));
}
$user = $this->om
->getRepository('FMFmBundle:User')
->findOneBy(array('username' => $username))
;
if (null === $user) {
throw new TransformationFailedException(sprintf(
'user with user name "%s" not found!',
$username
));
}
else if (!$user->hasRole($this->role)){
throw new TransformationFailedException(sprintf(
'user name "%s" is not valid',
$username
));
}
else if($this->logged==true){
$activeUser=$this->get('security.context')->getToken()->getUser();
if($user===$activeUser)
throw new TransformationFailedException(sprintf(
'you can\'t enter you user name'
));
}
else
return $user;
}
}
Form type
this form work fine because I'm not using the custom field
class ActivatePartnerType extends AbstractType
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user','text', array(
'invalid_message' => 'That is not a valid username',
'label' => 'Partner Username :',
))
->add('next','submit')
;
$builder->get('user')
->addModelTransformer(new UserToNameTransformer($this->entityManager,'ROLE_PARTNER',false,true));
}
public function getName()
{
return 'fm_fmbundle_activate_partner';
}
}
The custom field type
class UserSelectorType extends AbstractType
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new UserToNameTransformer($this->entityManager,**{others parametres}**);
$builder->addModelTransformer($transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'invalid_message' => 'The selected user does not exist'
));
}
public function getParent()
{
return 'text';
}
public function getName()
{
return 'user_selector';
}
}
the custom field service
FmarketUserSelector:
class: FM\FmBundle\Form\UserSelectorType
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: form.type, alias: user_selector }
the form type
class ActivatePartnerType extends AbstractType
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user','user_selector')
->add('next','submit')
;
}
public function getName()
{
return 'fm_fmbundle_activate_partner';
}
}
Look at this pages:
Introduction to Parameters
Service Container
In your case, it would look like this:
FmarketUserSelector:
class: FM\FmBundle\Form\UserSelectorType
arguments: ["#doctrine.orm.entity_manager", "ROLE_PARTNER", false, true]
tags:
- { name: form.type, alias: user_selector }
but looks like, you're passing dynamic values to your form type, that's why this doesn't suit your needs I think..
In your case you can call your form type as a regular class call, like this:
...
$builder
->add('user', new UserSelectorType(ARG1, ARG2, ...))
->add('next','submit')
;
...
may be this answer can also help you a bit..
Here, is how I use the $options array
class UserSelectorType extends AbstractType
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new UserToNameTransformer($this->entityManager,$options['role'],$options['accept_empty'],$options['logged']);
$builder->addModelTransformer($transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'invalid_message' => 'The selected user does not exist',
'role'=>'ROLE_PARTNER',
'accept_empty'=>true,
'logged'=>false
));
}
public function getParent()
{
return 'text';
}
public function getName()
{
return 'user_selector';
}
}
In the form type
$builder->add('referral','user_selector',array(
'role'=>'ROLE_PARTNER',
'accept_empty'=>false,
'logged'=>false
));

Accessing the original untransformed data after applying a DataTransformer?

Would be possibile to get the original data type after applying the ArrayToStringTransformer to the form field? I can't find any help in Symfony2 documentation here.
That is, i need the original array data type in my Twing template. {{ value }} contains only the already transformed data.
class SMSType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('recipient', 'text', array('property_path' => false));
->add('tags', 'text');
$builder->get('tags')
->appendClientTransformer(new ArrayToStringTransformer());
}
public function getDefaultOptions(array $options)
{
return array('required' => false);
}
public function getName() { return 'sms'; }
}
The transform is just an array explode/implode:
class ArrayToStringTransformer implements DataTransformerInterface
{
public function transform($val)
{
if (null === $val) return '';
return implode(',', $val);
}
public function reverseTransform($val)
{
if (!$val) return null;
return explode(',', $val);
}
}
Nope. The transformed value is what ends up being passed to your template as part of form. I suppose you could explicitly pass the original tags directly to your template.

Is it possible to have collection field in Symfony2 form with different choices?

I have a collection field with elements of type choice in my Symfony form. Each element should have different list o choices. How can I arrange this in Symfony2? I can't use choices option because every element will have the same choices. I have seen the choice_list option which takes an object that can produce the list of options, but I don't see how it could produce a different choices for different elements in collection.
Any idea how to deal with that?
I think you need form event : http://symfony.com/doc/current/cookbook/form/dynamic_form_generation.html.
To change the default way the collection is made.
The main form is simple:
namespace Acme\Bundle\AcmeBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Acme\Bundle\AcmeBundle\Form\DescriptorDumpFieldsType;
class TranscodingType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('descriptorDumpFields', 'collection', array('type' => new DescriptorDumpFieldsType()));
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\Bundle\AcmeBundle\Entity\Descriptor',
);
}
public function getName()
{
return 'descriptor';
}
}
Just a simple form with a collection of sub forms.
The second one use a form subscriber who handle the form creation. (using form events)
So the first form is created normaly and add many DescriptorDumpFieldsType wich are dynamicly created.
namespace Acme\Bundle\AcmeBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormTypeInterface;
use Acme\Bundle\AcmeBundle\Form\EventListener\TranscodingSubscriber;
class DescriptorDumpFieldsType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$subscriber = new TranscodingSubscriber($builder->getFormFactory());
$builder->addEventSubscriber($subscriber);
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\Bundle\AcmeBundle\Entity\DescriptorDumpField',
);
}
public function getName()
{
return 'desc_dump_field';
}
}
The form subscriber :
namespace Acme\Bundle\AcmeBundle\Form\EventListener;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
use Acme\Bundle\AcmeBundle\Entity\DumpField;
use Acme\Bundle\AcmeBundle\Form\Transcoding\DataTransformer\JsonToHumanDateTransformer;
class TranscodingSubscriber implements EventSubscriberInterface
{
private $factory;
public function __construct(FormFactoryInterface $factory)
{
$this->factory = $factory;
}
public static function getSubscribedEvents()
{
return array(FormEvents::SET_DATA => 'setData');
}
public function setData(DataEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
if (!is_null($data)) {
$this->buildForm($data, $form);
}
}
protected function buildForm($data, $form)
{
switch ($data->getDumpField()->getType()) {
case DumpField::TYPE_ENUM:
$type = 'enum'.ucfirst($data->getDumpField()->getKey());
$class = 'dump_field_'.strtolower($data->getDumpField()->getKey());
$form->add($this->factory->createNamed('collection', 'transcodings', null, array('required' => false, 'type' => $type, 'label' => $data->getDumpField()->getKey(), 'attr' => array('class' => $class))));
break;
case DumpField::TYPE_DATE:
$transformer = new JsonToHumanDateTransformer();
$class = 'dump_field_'.strtolower($data->getDumpField()->getKey());
$builder = $this->factory->createNamedBuilder('human_date', 'params', null, array('label' => $data->getDumpField()->getKey(), 'attr' => array('class' => $class)));
$builder->prependNormTransformer($transformer);
$form->add($builder->getForm());
break;
}
}
}
So you can customize the way you want, each sub-form of the collection in buildForm.

Resources