Symfony Multi-step form issue (craue form bundle) - php

My form data returned after the flow has finished is a bit odd. Two of the entity properties (that were populated using an embedded form) are in the array format keyed by the field names and not a string.
Engine and model arrays of the values lol.
Anything I have done to cause this?
Here is my setup.
The flow
<?php
namespace AppBundle\Form;
use AppBundle\Form\Type\CreateVehicleFormType;
use Craue\FormFlowBundle\Form\FormFlow;
use Craue\FormFlowBundle\Form\FormFlowInterface;
/**
* Class CreateVehicleFlow
* #package AppBundle\Form
*/
class CreateVehicleFlow extends FormFlow
{
protected $allowRedirectAfterSubmit = true;
/**
* #return array
*/
protected function loadStepsConfig()
{
return [
// Step 1.
[
'label' => 'Wheels',
'form_type' => CreateVehicleFormType::class,
],
// Step 2.
[
'label' => 'Engine',
'form_type' => CreateVehicleFormType::class,
'skip' => function($estimatedCurrentStepNumber, FormFlowInterface $flow) {
return $estimatedCurrentStepNumber > 1 && !$flow->getFormData()->canHaveEngine();
},
],
// Step 3.
[
'label' => 'Model',
'form_type' => CreateVehicleFormType::class
],
// Step 4.
[
'label' => 'Confirmation',
],
];
}
}
Parent create vehicle form
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class CreateVehicleFormType
* #package AppBundle\Form\Type
*/
class CreateVehicleFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
switch ($options['flow_step']) {
case 1:
$validValues = [2, 4];
$builder->add(
'numberOfWheels',
ChoiceType::class, [
'choices' => array_combine($validValues, $validValues),
'attr' => [
'class' => 'form-control input-lg',
'required' => 'required'
],
]);
break;
case 2:
$builder->add('engine', VehicleEngineFormType::class, []);
break;
case 3:
$builder->add('model', VehicleModelFormType::class, [
'numberOfWheels' => $options['data']->getNumberOfWheels()
]);
break;
}
}
/**
* #return string
*/
public function getBlockPrefix()
{
return 'createVehicle';
}
/**
* #return string
*/
public function getName()
{
return 'create_vehicle';
}
}
Child vehicle engine form
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class CreateVehicleFormType
* #package AppBundle\Form\Type
*/
class VehicleEngineFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('engine', ChoiceType::class, [
'label' => false,
'choices' => [
'Diesel' => 'Diesel',
'Petrol' => 'Petrol',
'Electric' => 'Electric',
'Hybrid' => 'Hybrid'
],
'attr' => [
'class' => 'form-control input-lg',
'required' => 'required'
],
]
);
}
/**
* #return string
*/
public function getName()
{
return 'vehicle_engine';
}
}
Child vehicle model form
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class CreateVehicleFormType
* #package AppBundle\Form\Type
*/
class VehicleModelFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [];
if ($options['numberOfWheels'] === 2) {
$choices['BMW R nineT'] = 'BMW R nineT';
$choices['Ducati Monster 1200S'] = 'Ducati Monster 1200S';
} else {
$choices['Ferrari LaFerrari'] = 'Ferrari LaFerrari';
$choices['Aston Martin One-77'] = 'Aston Martin One-77';
}
$builder->add('model', ChoiceType::class, [
'label' => false,
'choices' => $choices,
'attr' => [
'class' => 'form-control input-lg',
'required' => 'required'
],
]
);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'numberOfWheels' => 2,
]);
}
/**
* #return string
*/
public function getName()
{
return 'vehicle_model';
}
}
And finally my controller including my hack to get them back as string properties
/**
* One form type for the entire flow.
*
* #param Request $request
* #return Response
*
* #Route("/create.vehicle", name="create.vehicle")
*/
public function CreateVehicleAction(Request $request)
{
$formData = new Vehicle();
$flow = $this->get('app.form.flow.create_vehicle'); // must match the flow's service id
$flow->setGenericFormOptions([
'action' => $this->generateUrl('create.vehicle')
]);
$flow->bind($formData);
// form of the current step
$form = $submittedForm = $flow->createForm();
if ($flow->isValid($form)) {
$flow->saveCurrentStepData($form);
if ($flow->nextStep()) {
// form for the next step
$form = $flow->createForm();
} else {
// flow finished
// Bit hacky but not sure hy they are arrays set to the property.
if (is_array($formData->getEngine())) {
$formData->setEngine($formData->getEngine()['engine']);
}
if (is_array($formData->getModel())) {
$formData->setModel($formData->getModel()['model']);
}
$flow->reset();
$this->flashMessage(
'success',
[
'title' => 'Vehicle Created',
'message' => sprintf(
'You have created a %s %d wheeled vehicle.',
$formData->getModel(),
$formData->getNumberOfWheels()
)
]
);
return $this->redirect(
$this->getPresencePath('sign_in.page')
);
}
}
if ($flow->redirectAfterSubmit($submittedForm)) {
$params = $this
->get('craue_formflow_util')
->addRouteParameters(
array_merge(
$request->query->all(),
$request->attributes->get('_route_params')
),
$flow
);
return $this->redirect(
$this->getPresencePath('sign_in.page', $params)
);
}
return $this->render('AppBundle:multi-step-form:create-vehicle.html.twig', array(
'form' => $form->createView(),
'flow' => $flow
));
}
Can anyone spot the issue?

Related

Symfony: Expected argument of type "?Doctrine\Common\Collections\Collection", "array" given at property path

i have a entity called DynamicForm, which looks like this:
<?php
namespace App\Entity\Product\DynamicForm;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Entity;
/**
* Class DynamicForm
* #package App\Entity\Product
*
* #Entity
* #ORM\Table(name="product_dynamic_form")
*/
class DynamicForm
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="id", type="integer", unique=true, nullable=false)
*/
private ?int $id = null;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Product\DynamicForm\Component\Text", mappedBy="dynamicForm")
* #ORM\JoinColumn(name="component_text_id", referencedColumnName="id")
*/
private ?Collection $textComponents;
/**
* #return Collection|null
*/
public function getTextComponents(): ?Collection
{
return $this->textComponents;
}
/**
* #param Collection|null $textComponents
*
* #return DynamicForm
*/
public function setTextComponents(?Collection $textComponents): DynamicForm
{
$this->textComponents = $textComponents;
return $this;
}
}
Also i created a related type for it - DynamicFormType:
<?php
namespace App\Type\Product\DynamicForm;
use App\Entity\Product\DynamicForm\DynamicForm;
use App\Type\Product\DynamicForm\Component\TextType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DynamicFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('textComponents', CollectionType::class, [
'entry_type' => TextType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'label' => ' '
])
->add('submit', SubmitType::class, [
'label' => 'form.basic.save'
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', DynamicForm::class);
}
}
The TextType entry type class from the namespace App\Type\Product\DynamicForm\Component\TextType looks like this:
<?php
namespace App\Type\Product\DynamicForm\Component;
use App\Entity\Product\DynamicForm\Component\Text;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type as FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class TextType extends AbstractType
{
private TranslatorInterface $translator;
/**
* TextType constructor.
*
* #param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('label', FormType\TextType::class, [
'required' => true,
'translation_domain' => 'wedding',
'label' => 'setting.form.dynamic_by_user.query_to_guest'
])
->add('type', FormType\ChoiceType::class, [
'required' => true,
'translation_domain' => 'wedding',
'label' => 'setting.form.dynamic_by_user.select_type',
'attr' => [
'class' => 'enriched',
'data-search-placeholder' => $this->translator->trans('select.search'),
'data-search-no-results-text' => $this->translator->trans('select.search_no_results_found')
],
'choice_translation_domain' => 'wedding',
'choices' => [
'setting.form.dynamic_by_user.type_text' => Text::TYPE_TEXT_FIELD,
'setting.form.dynamic_by_user.type_textarea' => Text::TYPE_TEXT_AREA,
'setting.form.dynamic_by_user.type_email' => Text::TYPE_EMAIL_FIELD,
'setting.form.dynamic_by_user.type_number' => Text::TYPE_NUMBER_FIELD,
]
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', Text::class);
}
}
While i try to transmit the form, the request in the controller contains the form data as you can see it, in the following screenshot:
My problem is now that i always get the following error (while handling the request for the form via ->handleRequest($request) on created form in controller):
Expected argument of type "?Doctrine\Common\Collections\Collection", "array" given at property path "textComponents".
I have such collection settings also in other classes, but without problems - I don't know any further, can anyone please assist me or see the error?
(I am using Symfony version 5.2.9, if u need any further info just ask for it - I will give it to you as soon as possible)
Trying to add a constructor in your entity
public function __construct()
{
$this->textComponents = new ArrayCollection();
}
Add addTextComponent and removeTextComponent methods intead of setTextComponents
public function addTextComponent(Text $textComponent): self
{
$textComponent->setDynamicForm($this);
$this->textComponents->add($textComponent);
return $this;
}
public function removeTextComponent(Text $textComponent): self
{
$this->textComponents->removeElement($textComponent);
return $this;
}
Add 'by_reference' => false in the textComponents form params
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('textComponents', CollectionType::class, [
'entry_type' => TextType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'label' => ' ',
'by_reference' => false,
])
->add('submit', SubmitType::class, [
'label' => 'form.basic.save'
]);
}
I was able to fix this by doing this, that allows us to pass a Collection or an empty array or an array of your entity which I think is Text:
/**
* #param Collection|Text[] $textComponents
*
* #return DynamicForm
*/
public function setTextComponents($textComponents): DynamicForm
{
$this->textComponents = $textComponents;
return $this;
}

Symfony FormType construct error

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.

The Symfony FormType contains 1 abstract method after update to 2.8

I just recently updated to 2.8 and now I get the following error when calling the create function of the Form Factory.
Error: Class Symfony\Component\Form\Extension\Core\Type\FormType contains 1
abstract method and must therefore be declared abstract or implement the
remaining methods (Symfony\Component\Form\FormTypeInterface::setDefaultOptions)
The call of the FormFactory looks like this:
$this->formFactory->create(
get_class(new ProductType()),
$product,
[
'method' => 'POST',
'type' => $type,
'locales' => $context->shop->getLocales(),
'product' => $product,
'numberDataTransformer' => $this->numberTransformerFactory->createFromLocale(
$context->user->getLocale(),
$context->shop->getDefaultLocale()
),
'priceType' => $context->shop->getConfiguration()->getProductConfiguration()->getPricesBackend(),
'isShortDescriptionEnabled' => $context->shop->getConfiguration()->getProductConfiguration()->isShortDescriptionEnabled()
]);
I tried several ways to pass the ProductType to the function, but none seems to work. I always get one out of two results: Either the result is that the type cannot be found or the error is returned that the FormType does not implement setDefaultOptions.
What did I miss?
EDIT:
Here are some additional code:
The declaration of the formFactory parameter:
public function __construct(Request $request, FormFactoryInterface $formFactory)
{
$this->request = $request;
$this->formFactory = $formFactory;
}
The ProductType class
<?php
namespace CustomNamespace\BackendBundle\Product\Form;
use CustomNamespace\BackendBundle\Common\NumberDataTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use CustomNamespace\BackendBundle\Product\Form\ImageType;
use ShopwareEasy\BackendBundle\Product\Form\AttachmentType;
use Symfony\Component\Validator\Exception\InvalidOptionsException;
/**
* Form element type for products.
*/
class ProductType extends AbstractType
{
/**
* #var string
*/
private $method;
/**
* #var string
*/
private $type;
/**
* #var array
*/
private $locales;
/**
* #var Product
*/
private $product;
/**
* #var \CustomNamespace\BackendBundle\Common\NumberDataTransformer
*/
private $numberDataTransformer;
/**
* #var string
*/
private $priceType;
/**
* #var bool
*/
private $isShortDescriptionEnabled;
/**
* #param FormBuilderInterface $builder
* #param array $options
* #return void
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->setMethod($this->method);
$regionType = new RegionShippingTimeType();
if ($this->type == 'download') {
$regionType->enableForDownloadableProduct();
}
$builder->add('regions', 'collection', array(
'type' => $regionType,
'label' => false,
'options' => array(
'required' => false,
'attr' => array('class' => 'email-box')
),
));
$builder->add('vendor', 'text', ['label' => 'form_product_vendor']);
if ($this->type == 'normal') {
$builder->add(
'mainVariant',
new MainVariantNormalType(
$this->method,
$this->locales,
$this->product,
$this->numberDataTransformer,
$this->priceType,
$this->isShortDescriptionEnabled
),
['error_bubbling' => false, 'label' => false]
);
} elseif ($this->type == 'download') {
$builder->add(
'mainVariant',
new MainVariantDownloadType(
$this->method,
$this->locales,
$this->product,
$this->numberDataTransformer,
$this->priceType,
$this->isShortDescriptionEnabled
),
['error_bubbling' => false, 'label' => false]
);
} elseif ($this->type == 'variant') {
$builder->add(
'mainVariant',
new MainVariantVariantType(
$this->method,
$this->locales,
$this->product,
$this->numberDataTransformer,
$this->priceType,
$this->isShortDescriptionEnabled
),
['error_bubbling' => false, 'label' => false]
);
}
if ($this->method == 'PUT') {
$builder->add(
'images',
new ImageType(),
['error_bubbling' => true, 'label' => false]
);
$builder->add(
'attachments',
new AttachmentType(),
['error_bubbling' => true, 'label' => false]
);
}
}
/**
* #param \Symfony\Component\OptionsResolver\OptionsResolverInterface $resolver
*/
public function configureOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
array(
'data_class' => 'CustomNamespace\\BackendBundle\\Product\\Product',
'csrf_protection' => false,
'error_bubbling' => false,
'cascade_validation' => true,
'method' => 'POST',
'type' => 'normal'
)
);
parent::configureOptions($resolver);
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
/** #var OptionResolver $resolver */
$this->configureOptions($resolver);
}
public function getName() {
return get_class($this);
}
}
The problem was produced by files that were not updated at all.. The implementation of setDefaultOptions should still exist in the Symfony 2.8 classes - but they didn't.
After destroying my vagrant and recreating it again, everything worked just fine.
But thanks everyone for the help!

symfony2 form collection This form should not contain extra fields error

I have a symfony form which I use either for creating new entities of for parametring existing ones. Parametring works fine.
If creating new entities, I use a presetdata event listener to not render the fields so only the prototype is rendered.
In that case addind a new row works fine only if no entities exist in my database. if there is already one row, it throws the
This form should not contain extra fields
error. I have set true to allow_add and allow_delete. Any Idea about what the issue is?
My controller:
/**
* #Route("/create", name="worker_create")
* #Method({"POST","GET"})
* #Template(":FoodAnalytics/Worker:create_worker.html.twig")
* #param Request $request
* #return array|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function createWorkerAction(Request $request)
{
$formManager = $this->get('form_manager');
$createWorkerForm = $formManager->createForm(new UserUserWorkersType(false, '_create'), $this->getUser(), 'POST' , 'worker_create',null,'Enregistrer');
if ($request->getMethod() == 'POST')
{
if ($formManager->handleRequestAndFlush($createWorkerForm, $this->getUser()))
{
$formManager->addSuccess('Les associés ont été rajoutés.');
return $this->redirect($this->generateUrl('my_teams'));
}
return $this->render(
':FoodAnalytics/Team:my_teams.html.twig',
array(
'create_worker_form' => $createWorkerForm->createView(),
'tab' => 'create_worker'
));
}
return array(
'create_worker_form' => $createWorkerForm->createView()
);
}
My first form:
<?php
//src/AppBundle/Form/User/UserUserWorkersType.php
namespace AppBundle\Form\User\UserCollections;
use AppBundle\Form\FoodAnalytics\WorkerType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class UserUserWorkersType extends AbstractType
{
private $displayExisting;
private $suffix;
public function __construct($displayExisting = true, $suffix)
{
$this->displayExisting = $displayExisting;
$this->suffix = $suffix;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('workers', 'collection', array(
'type' => new WorkerType($this->displayExisting),
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'label' => 'Associés',
))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
array(
'data_class' => 'AppBundle\Entity\User\User',
)
);
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_user_user_workers'.$this->suffix;
}
}
My collection form:
<?php
namespace AppBundle\Form\FoodAnalytics;
use AppBundle\Entity\FoodAnalytics\Worker;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class WorkerType extends AbstractType
{
private $displayExisting;
public function __construct($displayExisting = true)
{
$this->displayExisting = $displayExisting;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event)
{
/** #var $worker Worker */
$worker = $event->getData();
$form = $event->getForm();
if (!$worker || ($worker && $this->displayExisting))
{
$form
->add('title','choice',array(
'choices' => ['M.' => 'M.', 'Mme' => 'Mme'],
'label' => 'Titre',
))
->add('firstName','text',array(
'label' => 'Prénom',
))
->add('lastName','text',array(
'label' => 'Nom',
))
->add('position','text',array(
'label' => 'Poste occupé',
))
->add('totalCost','number',array(
'label' => 'Coût total mensuel (€)',
'required' => false,
'attr' => array(
'data-toggle' => "tooltip",
'data-placement' => "top",
'title' => "Indiquez ici le coût total que représente le salarié par mois (salaires brut, primes, taxes, coûts cachés, etc). Cette donnée est totalement confidentielle.",
)))
->add('weeklyHours','number',array(
'label' => "Durée de travail par semaine (h)",
'required' => false,
'attr' => array(
'data-toggle' => "tooltip",
'data-placement' => "top",
'title' => "Indiquez ici la durée de travail hebdomadaire de votre associé. Cette donnée est totalement confidentielle.",
)));
}
});
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\FoodAnalytics\Worker'
));
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_foodanalytics_worker';
}
}

Symfony2 Form - Embed forms and event listeners

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'
);
}

Categories