I'm working on a Symfony project, so here's is my problem:
I have got an Entity and for that Entity I have got two forms in the same page(one for insert and the other to update the inserted).
So I made that.
ActionMethod
public function adminTareasAction(Request $request) {
$newTareaForm = $this->createForm(TareaType::class, null, array("formType" => "newTarea"));
$editTareasForm = $this->createForm(TareaType::class, null, array("formType" => "editTareas"));
if($request->isMethod("POST")) {
if(!is_null($request->request->get('newTarea'))) {
$newTareaForm->handleRequest($request);
if($newTareaForm->isSubmitted() && $newTareaForm->isValid()) {
$newTarea = $newTareaForm->getData();
$dataManager = $this->getDoctrine()->getManager();
$dataManager->persist($newTarea);
$dataManager->flush();
return $this->redirectToRoute("admin_tareas");
}
}
elseif(!is_null($request->request->get('editTareas'))) {
$editTareasForm->handleRequest($request);
if($editTareasForm->isSubmitted() && $editTareasForm->isValid()) {
$newTarea = $newTareaForm->getData();
$dataManager = $this->getDoctrine()->getManager();
$dataManager->persist($newTarea);
$dataManager->flush();
return $this->redirectToRoute("admin_tareas");
}
}
}
$tareas = $this->getDoctrine()->getRepository('FabricacionBundle:Tarea')->findAll();
if(!$tareas) {
$tareas = "No hay Tareas";
}
return $this->render('UsersBundle:Admin:adminTareas.html.twig', array("newTareaForm" => $newTareaForm->createView(), "editTareasForm" => $editTareasForm->createView(), "tareas" => $tareas));
}
Type Class
class TareaType extends AbstractType {
private $formType;
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$this->formType = $options["formType"];
if($this->formType == "newTarea") {
//var_dump();
$builder
->add('tareaName', TextType::class)
->add('tareaOrden', IntegerType::class)
->add('submitNewTarea', SubmitType::class);
}
elseif($this->formType == "editTareas") {
$builder
->add('newName', CollectionType::class, array("entry_type" => TextType::class, "allow_add" => true))
->add('newOrden', CollectionType::class, array("entry_type" => IntegerType::class, "allow_add" => true))
->add('deleteTarea', CollectionType::class, array("entry_type" => CheckboxType::class, "allow_add" => true))
->add('submitTarea', SubmitType::class);
}
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'FabricacionBundle\Entity\Tarea',
"formType" => null
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix() {
/*if($this->formType == "newTarea") {
return $this->formType;
}
elseif($this->formType == "editTareas") {
return $this->formType;
}*/
return 'FabricacionBundle_tarea';
}
}
The only thing I need is to change the form name and then handle them in the controller by name.
Just remove getBlockPrefix().You are accessing you're field type by its FQCN :
$newTareaForm = $this->createForm(TareaType::class, null, array("formType" => "newTarea"));
so you don't need to have an alias for it (wich is deprecated).
Well, reading the Form classes I get what I have to do.
Just this to create the form:
$newTareaForm = $this->container->get('form.factory')->createNamedBuilder("newTarea", TareaType::class, null, array("formType" => "newTarea"))->getForm();
$editTareasForm = $this->container->get('form.factory')->createNamedBuilder("editTareas", TareaType::class, null, array("formType" => "editTareas"))->getForm();
Related
A bit in the panic - I am generating Symfony form for a complex search, i.e. mapped data to the entity will be used just for a search query building.
I create simple form, model, some extended types from ChoiceType for prepopulation choices by some logic. The form is submitted with GET method.
In the model you find maker and model fields for example. The latter populated on the frontend with AJAX, after maker has been selected. When I do submit the form, and maker and model have non-default value, the handleRequest only populates the maker property of the Model, but the model is left empty. Also the checkboxes are correctly populated if checked. All in all, $form->getData() returns just Maker and checkboxes, other fields are null. $request->query has all parameters.
The data mappers are senseless here. And also there is nothing to transform in the data, the Model is mostly from scalar values. The request contains everything, but it is not handled correctly. I tried to implement ChoiceLoaderInterface, but that doesn't work for me, because during loading choices I have to have access to the options of the form, which I don't (I used this article https://speakerdeck.com/heahdude/symfony-forms-use-cases-and-optimization).
I am using Symfony 4.2.4; PHP 7.2.
Controller's method
/**
* #Route("/search/car", name="car_search", methods={"GET"})
* #param Request $request
*/
public function carSearchAction(Request $request)
{
$carModel = new CarSimpleSearchModel();
$form = $this->createForm(CarSimpleSearchType::class, $carModel);
$form->handleRequest($request);
$form->getData();
.....
}
CarSimpleSearchModel
class CarSimpleSearchModel
{
public $maker;
public $model;
public $priceFrom;
public $priceTo;
public $yearFrom;
public $yearTo;
public $isCompanyOwner;
public $isPrivateOwners;
public $isRoublePrice;
}
CarSimpleSearchType the form
class CarSimpleSearchType extends AbstractType
{
protected $urlGenerator;
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('vehicle_type', HiddenType::class, [
'data' => VehicleTypeType::CAR,
'mapped' => false,
])
->add('maker', CarMakerSelectType::class)
->add('model', CarModelsSelectType::class)
->add(
'priceFrom',
VehiclePriceRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'priceTo',
VehiclePriceRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'yearFrom',
VehicleYearRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'yearTo',
VehicleYearRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add('isCompanyOwner', CheckboxType::class)
->add('isPrivateOwners', CheckboxType::class)
->add('isRoublePrice', CheckboxType::class)
->add('submit', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => CarSimpleSearchModel::class,
'compound' => true,
'method' => 'GET',
'required' => false,
'action' => $this->urlGenerator->generate('car_search'),
]
);
}
public function getBlockPrefix()
{
return 'car_search_form';
}
}
CarMakerSelectType field
class CarMakerSelectType extends AbstractType
{
/**
* #var VehicleExtractorService
*/
private $extractor;
/**
* VehicleMakerSelectType constructor.
*
* #param VehicleExtractorService $extractor
*/
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'placeholder' => null,
'vehicle_type' => null,
'choices' => $this->getVariants(),
]
);
}
private function getVariants()
{
$makers = $this->extractor->getMakersByVehicleType(VehicleTypeType::CAR);
$choices = [];
foreach ($makers as $maker) {
$choices[$maker['name']] = $maker['id'];
}
return $choices;
}
}
CarModelSelectType field
class CarModelsSelectType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'disabled' => true,
]
);
}
}
VehiclePriceRangeType field
class VehiclePriceRangeType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'vehicle_type' => null,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
foreach ($this->getRange($options['vehicle_type']) as $value) {
$view->vars['choices'][] = new ChoiceView($value, $value, $value);
}
}
private function getRange(int $vehicleType)
{
return PriceRangeGenerator::generate($this->extractor->getMaxVehiclePrice($vehicleType));
}
}
VehicleYearRangeType field
class VehicleYearRangeType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractorService)
{
$this->extractor = $extractorService;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'vehicle_type' => null,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
foreach ($this->getRange($options['vehicle_type']) as $value) {
$view->vars['choices'][] = new ChoiceView($value, $value, $value);
}
}
protected function getRange(int $vehicleType): array
{
$yearRange = RangeGenerator::generate(
$this->extractor->getMinYear($vehicleType),
$this->extractor->getMaxYear($vehicleType),
1,
true,
true
);
return $yearRange;
}
}
So, I can use the raw data from the Request and manually validate-populate the model and send to further processing, but I guess that's not the Right Way, and I want to populated the form by the framework. How can I ?..
In my case, I had a dependent EntityType populated by ajax that is initially disabled. Since choices where null, it was returning an InvalidValueException on submission. What I had to do is create an EventListener and add the valid choices for the current 'main' field. This is basically it, more or less adapted to your case.
Original form:
// Setup Fields
$builder
->add('maker', CarMakerSelectType::class)
->add('model', CarModelsSelectType::class, [
'choices' => [],
// I was setting the disabled on a Event::PRE_SET_DATA if previous field was null
// since I could be loading values from the database but I guess you can do it here
'attr' => ['disabled' => 'disabled'],
]
);
$builder->addEventSubscriber(new ModelListener($this->extractor));
Event Subscriber that adds back valid choices:
class ModelListener implements EventSubscriberInterface
{
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SUBMIT => 'onPreSubmitData',
];
}
public function onPreSubmitData(FormEvent $event)
{
// At this point you get only the scalar values, Model hasn't been transformed yet
$data = $event->getData();
$form = $event->getForm();
$maker_id = $data['maker'];
$model= $form->get('model');
$options = $model->getConfig()->getOptions();
if (!empty($maker_id)) {
unset($options['attr']['disabled']);
$options['choices'] = $this->extractor->getModelsFor($maker_id);
$form->remove('model');
$form->add('model', CarModelsSelectType::class, $options );
}
}
}
}
I faced up with some non-ordinary situation for me.
1) I have a dependent list that rendering by Symfony FormType like this:
2) Location and Instruction fields are depend from Company field.
3) When I change Company field (onchange event js) then goes ajax request that retrieves data from the database and build a dropdown list.
4) But when form is submitted I have an error:
Please help me to resolve this. Thanks in advance.
My formType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('hours')
->add('messageText', null, ['required' => false])
->add('company', null, [
'choice_label' => 'name',
'placeholder' => 'Please select company',
'required' => true
])
->add('procedure', TextType::class, ['attr' => ['placeholder' => 'Please type code or description'] ])
->add('instruction', ChoiceType::class, ['mapped' => false])
->add('location', ChoiceType::class, ['mapped' => false])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => InstructionNotification::class
));
}
Action from controller:
/**
* #Route("/admin/api/instructions", name="admin_api_instructions")
* #param Request $request
* #return JsonResponse
*/
public function getInstructionsByCompanyId(Request $request)
{
$id = $request->get('id');
if (!$id) {
return new JsonResponse('No data', 404);
}
$instructions = $this->getDoctrine()->getRepository('OctaneBundle:Instruction')->findInstructionsByCompanyId($id);
return new JsonResponse($instructions);
}
findInstructionsByCompanyId($id):
public function findInstructionsByCompanyId($id)
{
$qb = $this->createQueryBuilder('i');
if ($id) {
$qb
->where('i.company = :id')
->setParameter('id', $id);
}
return $qb->getQuery()->getResult();
}
response from api (i.e.: admin/api/instructions?id=1):
[{"id":2,"label":"First instruction"},{"id":3,"label":"First instruction"}]
If you need any additional information please leave comments below. Thanks
Symfony's Validator expects that your submitted form will have a submitted instruction and location value that exists in the list you provided when creating your form in form type class. Since you are not providing any options for instructions and locations, you are getting a validation error.
In order to bypass this error you should use Symfony's Form Events in your buildForm function in your form type like this:
$builder->get('company')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) {
$company = $event->getForm()->getData();
$form = $event->getForm()->getParent();
$form->add('location', EntityType::class, array(
'class' => Location::class,
'query_builder' => function (EntityRepository $repo) use ($company) {
return $repo->createQueryBuilder('location')
->where('location.company = :company')
->setParameter('company', $company->getId());
}
));
$form->add('instruction', EntityType::class, array(
'class' => Instruction::class,
'query_builder' => function (EntityRepository $repo) use ($company) {
return $repo->createQueryBuilder('instruction')
->where('instruction.company = :company')
->setParameter('company', $company->getId());
}
));
}
);
Thanks for the answer but I found out more elegant solution for my case. So,
my formType now:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
...
->add('instruction', FormattedSelectType::class, ['class' => Instruction::class])
->add('location', FormattedSelectType::class, ['class' => Location::class])
;
}
FormattedSelectType:
class FormattedSelectType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'choice_label' => function (FormattedLabelInterface $entity) {
return $entity->getFormattedLabel();
}
));
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return EntityType::class;
}
}
Etities Location and Instruction entities implement JsonSerializable and custom FormattedLabelInterface interface and have the next methods:
/**
* #return string
*/
public function getFormattedLabel()
{
return sprintf(self::LABEL_FORMATTED, $this->zip, $this->city, $this->name, $this->address, $this->phone);
}
/**
* #return array|mixed
*/
public function jsonSerialize()
{
return [
'id' => $this->id,
'label' => $this->getFormattedLabel()
];
}
My application show this error
Type error: Too few arguments to function AppBundle\Form\ActualiteType::__construct(), 0 passed in /Applications/MAMP/htdocs/SyndicNous/vendor/symfony/symfony/src/Symfony/Component/Form/FormRegistry.php on line 90 and exactly 2 expected
My formType
class ActualiteType extends AbstractType
{
/**
* #var bool $admin
*/
private $admin;
/**
* #var User $user
*/
private $user;
/**
* ActualiteType constructor.
* #param bool|false $admin
*/
public function __construct($admin = false, $user)
{
$this->admin = $admin;
$this->user = $user;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$categories = array(
'Travaux' => 'Travaux',
'Voisinage' => 'Voisinage',
);
$builder
->add('title')
->add('category')
->add('content')
->add('datePublish')
->add('category', ChoiceType::class, array(
'choices' => $categories
)
);
if ($this->user->getResidence() != null) {
$builder->add('residence', EntityType::class, array(
'class' => 'AppBundle:Residence',
'choices' => $this->user->getResidence(),
));
} else {
$builder->add('residence', 'entity', array(
'class' => 'AppBundle:Residence',
'choice_label' => 'name'
));
};
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Actualite'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'appbundle_actualite';
}
}
Do you have any idea where the problem would come from? Thank you
I do not understand what you're trying to do. You do not need to use the constructor to pass parameters to your formType. There is the second parameter of the buildForm method for this ($options).
In your controller, create your form like this :
$form = $this->createForm(ActualiteType::class, $actualite, [
'admin' => $admin,
'user' => $user
]);
And modify your formType like that :
class ActualiteType extends AbstractType {
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$admin = $options['admin']; // Not used ?
$user = $options['user'];
$categories = array(
'Travaux' => 'Travaux',
'Voisinage' => 'Voisinage',
);
$builder->add('title')
->add('category')
->add('content')
->add('datePublish')
->add('category', ChoiceType::class, array(
'choices' => $categories
)
);
if ($user->getResidence() != null) {
$builder->add('residence', EntityType::class, array(
'class' => 'AppBundle:Residence',
'choices' => $user->getResidence(),
));
} else {
$builder->add('residence', 'entity', array(
'class' => 'AppBundle:Residence',
'choice_label' => 'name'
));
};
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Actualite',
'admin' => null, // Default
'user' => null // Default
));
}
}
Do not forget to put the default values of your options in configureOptions method.
I have 2 Entities - User and Group. They have a many-to-many relationship and Group is used to store a users' roles.
I'm trying to make a User edit form by adding a collection, I want to be able to add a new role by selecting it from a dropdown (limited to what's already in the DB)
UserType.php:
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username')
->add('email')
->add('forename')
->add('surname')
->add('isActive')
->add('joinDate', 'date', array('input' => 'datetime', 'format' => 'dd-MM-yyyy'))
->add('lastActive', 'date', array('input' => 'datetime', 'format' => 'dd-MM-yyyy'))
->add('groups', 'collection', array(
'type' => new GroupType(),
'allow_add' => true,
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Sfox\CoreBundle\Entity\User'
));
}
}
and GroupType.php:
class GroupType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('role');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
"data_class" => 'Sfox\CoreBundle\Entity\Group'
));
}
}
This displays the roles in the form in basic text boxes, but if I add an entry to the form, it will cascade persist a new entry into Groups and if I were to edit an entry, it would change the underlying Group data.
I tried making a GroupSelectType.php:
class GroupSelectType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('role', 'entity', array('class'=>'SfoxCoreBundle:Group', 'property'=>'name'));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
"data_class" => 'Sfox\CoreBundle\Entity\Group'
));
}
}
Adding the field as an "entity" type, this displays the correct select box (but with the default values) I cant seem to bind it to the UserType form!
All I want the form to do is modify the underlying 'groups' ArrayCollection in the User entity.
Does anyone know how I can achieve this?
Well I worked out a solution for anyone else struggling with similar problems...
I had to create a custom form type and declare it as a service so I could pass in the Entity Manager. I then needed to make a dataTransformer to change my group objects into an integer for the form
Custom GroupSelectType:
class GroupSelectType extends AbstractType
{
/**
* #var ObjectManager
*/
private $om;
private $choices;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
// Build our choices array from the database
$groups = $om->getRepository('SfoxCoreBundle:Group')->findAll();
foreach ($groups as $group)
{
// choices[key] = label
$this->choices[$group->getId()] = $group->getName() . " [". $group->getRole() ."]";
}
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new GroupToNumberTransformer($this->om);
$builder->addModelTransformer($transformer);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
"choices" => $this->choices,
));
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'group_select';
}
}
In the constructor I'm getting all available groups and putting them into a "choices" array which is passed to the select box as an option.
You'll also notice I'm using a custom data transformer, this is to change the groupId (which is used in the rendering of the form) to a Group entity. I made the GroupSelectType a service as well and passed in the [#doctrine.orm.entity_manager]
services.yml (bundle config):
services:
sfox_core.type.group_select:
class: Sfox\CoreBundle\Form\Type\GroupSelectType
arguments: [#doctrine.orm.entity_manager]
tags:
- { name: form.type, alias: group_select }
GroupToNumberTranformer.php
class GroupToNumberTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* Transforms an object (group) to a string (number).
*
* #param Group|null $group
* #return string
*/
public function transform($group)
{
if (null === $group) {
return "";
}
return $group->getId();
}
/**
* Transforms a string (number) to an object (group).
*
* #param string $number
* #return Group|null
* #throws TransformationFailedException if object (group) is not found.
*/
public function reverseTransform($number)
{
if (!$number) {
return null;
}
$group = $this->om
->getRepository('SfoxCoreBundle:Group')
->findOneBy(array('id' => $number))
;
if (null === $group) {
throw new TransformationFailedException(sprintf(
'Group with ID "%s" does not exist!',
$number
));
}
return $group;
}
}
And my modified UserType.php - Notice I'm using my custom form type "group_select" now as it's running as a service:
class UserType extends AbstractType
{
private $entityManager;
public function __construct($entityManager)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new GroupToNumberTransformer($this->entityManager);
$builder
->add('username')
->add('email')
->add('forename')
->add('surname')
->add('isActive')
->add('joinDate', 'date', array('input' => 'datetime', 'format' => 'dd-MM-yyyy'))
->add('lastActive', 'date', array('input' => 'datetime', 'format' => 'dd-MM-yyyy'));
$builder
->add(
$builder->create('groups', 'collection', array(
'type' => 'group_select',
'allow_add' => true,
'options' => array(
'multiple' => false,
'expanded' => false,
)
))
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Sfox\CoreBundle\Entity\User'
));
}
public function getName()
{
return 'sfox_corebundle_usertype';
}
}
I have a form "AddressType" with an event listener that works as expected when instantiated alone. Here is the code.
namespace Nc\ClientsBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Nc\ClientsBundle\Form\EventListener\AddCityFieldSubscriber;
class AddressType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('street')
->add('number', 'text')
->add('complement')
->add('district')
->add('state', 'entity', array(
'class' => 'ClientsBundle:State',
'property' => 'name',
));
$subscriber = new AddCityFieldSubscriber($builder->getFormFactory());
$builder->addEventSubscriber($subscriber);
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Nc\ClientsBundle\Entity\Address',
);
}
public function getName()
{
return 'addresstype';
}
}
<?php
namespace Nc\ClientsBundle\Form\EventListener;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
use Doctrine\ORM\EntityRepository;
class AddCityFieldSubscriber implements EventSubscriberInterface
{
private $factory;
public function __construct(FormFactoryInterface $factory)
{
$this->factory = $factory;
}
public static function getSubscribedEvents()
{
// Tells the dispatcher that we want to listen on the form.pre_set_data
// event and that the preSetData method should be called.
return array(FormEvents::PRE_SET_DATA => 'preSetData');
}
public function preSetData(DataEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
// During form creation setData() is called with null as an argument
// by the FormBuilder constructor. We're only concerned with when
// setData is called with an actual Entity object in it (whether new,
// or fetched with Doctrine). This if statement let's us skip right
// over the null condition.
if (null === $data) {
return;
}
if (!$data->getCity()) {
$form->add($this->factory->createNamed('city_selector', 'city', null, array(
'choices' => array('' => '-- Selecione um estado --'),
'required' => true,
'expanded' => false,
'multiple' => false,
)));
} else {
$city = $data->getCity();
$form->add($this->factory->createNamed('city_selector', 'city', null, array(
'choices' => array($city->getId() => $city->getName()),
'required' => true,
'expanded' => false,
'multiple' => false,
)));
}
}
}
<?php
namespace Nc\ClientsBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Nc\ClientsBundle\Form\DataTransformer\CityToIdTransformer;
class CitySelectorType extends AbstractType
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function buildForm(FormBuilder $builder, array $options)
{
$transformer = new CityToIdTransformer($this->om);
$builder->prependNormTransformer($transformer);
}
public function getDefaultOptions(array $options)
{
return array(
'invalid_message' => 'Selecione um estado e uma cidade.',
);
}
public function getParent(array $options)
{
return 'choice';
}
public function getName()
{
return 'city_selector';
}
}
<?php
namespace Nc\ClientsBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Nc\ClientsBundle\Entity\City;
class CityToIdTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* Transforms an object (city) to a integer (id).
*
* #param City|null $city
* #return string
*/
public function transform($city)
{
if ($city === null) {
return '';
}
return $city->getId();
}
/**
* Transforms a string (id) to an object (city).
*
* #param string $id
* #return City|null
* #throws TransformationFailedException if object (city) is not found.
*/
public function reverseTransform($id)
{
if (!$id) {
return null;
}
$city = $this->om
->getRepository('ClientsBundle:City')
->findOneBy(array('id' => $id))
;
if (null === $city) {
throw new TransformationFailedException(sprintf(
'An city with id "%s" does not exist!',
$id
));
}
return $city;
}
}
What happens is that when I try to embed this form into the "ClientType" form, the "city" field is not rendered.
namespace Nc\ClientsBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class ClientType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name');
$builder->add('type', 'choice', array(
'choices' => array('1' => 'Comum', '2' => 'Parceiro'),
'expanded' => true,
'multiple' => false,
'required' => true,
));
$builder->add('contactInfo', new ContactInfoType(), array('label' => ' '));
$builder->add('address', new AddressType(), array('label' => ' '));
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Nc\ClientsBundle\Entity\Client',
);
}
public function getName()
{
return 'clienttype';
}
}
When you try to embed city field and entity does not have city value filled, method $form->getData() returns null, so city field won't render at this case, because "if" statement always returns false (preSetData function in the subscriber class)
First of all we gotta check if city is filled, if so add corresponding form field, otherwise add blank field (or whatever).
Here are some code snippets:
Add city field if its value is filled
public function preSetData(DataEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
if (!($data instanceof Address) || !$data->getCity()) {
return;
}
$city = $data->getCity();
$form->add($this->factory->createNamed('city_selector', 'city', null, array(
'choices' => array($city->getId() => $city->getName()),
'required' => true,
'expanded' => false,
'multiple' => false,
)));
}
Add blank city field if its value is not filled (postSetData triggers just after from data is set)
public function postSetData(DataEvent $event)
{
$form = $event->getForm();
if (!$form->has('city_selector')) {
$form->add($this->factory->createNamed('city_selector', 'city', null, array(
'choices' => array('' => '-- Selecione um estado --'),
'required' => true,
'expanded' => false,
'multiple' => false,
)));
}
}
We also have to register postSetData event
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::POST_SET_DATA => 'postSetData'
);
}