symfony3 form, can't get options in buildForm method - php

I don't really know which title to give this thread but, i'm working on a Symfony project in v3.1.6 and using the select2 plugin in field of my form with ajax.
I want to use the event (submit and pre_set_data) of the form component for creation and editing to change the field dynamically like this part of the symfony doc. Everything work fine until when i submit the form and give an error Notice: Undefined variable: options
My form type code here
namespace Xxx\ArticleBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Xxx\ArtistBundle\Repository\ArtistRepository;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Ivory\CKEditorBundle\Form\Type\CKEditorType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Doctrine\ORM\EntityManager;
class ArticleType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
//$em = $options['em']; //this give me same error
$builder
->add('artist', EntityType::class, array(
'class' => 'XxxArtistBundle:Artist',
'choice_label' => 'artist_name',
'multiple' => false,
'expanded' => false))
->add('title', TextType::class)
->add('categories', EntityType::class, array(
'class' => 'XxxArticleBundle:Category',
'choice_label' => 'name',
'multiple' => true,
'expanded' => true))
->add('image', ChoiceType::class, array(
'expanded' => false,
'multiple' => false))
->add('intro', CKEditorType::class)
->add('content', CKEditorType::class);
$formModifier = function (FormInterface $form, $image) {
$listImages = $options['em']->getRepository('XxxAppBundle:Image')->find($image)); //this line give me the error
if (!$listImages) {
return; //i will add a FormError in the field
}
$listImages = array();
die(var_dump($listImages));
$form->add('image', EntityType::class, array(
'class' => 'XxxAppBundle:Image',
'choices' => $listImages,
));
};
$builder->get('image')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$image = $event->getForm()->getData();
//die(var_dump($image)); //select2 field, returned null (but it's not the question
//die(var_dump($options)); returned error 500 Notice: Undefined variable: options
$formModifier($event->getForm()->getParent(), $image);
}
);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Xxx\ArticleBundle\Entity\Article',
));
$resolver->setRequired('em');
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'xxx_articlebundle_article';
}
}
And i also want to say the goal is to have a UI similar w/ Wordpress when create a post from the dashboard, for set an image in an article and when image is selected throw it in a tag to the user and without entityType because it will have thousand so i choose an choiceType to use with the select2 plugin but if someone got a better solution i'm on it.
Thx in advance for the help

When you're using a variable from a parent scope inside a closure, you should pass it to 'use' language construct.
$formModifier = function (FormInterface $form, $image) use ($options) {
$listImages = $options['em']->getRepository('XxxAppBundle:Image')->find($image)); //this line give me the error
if (!$listImages) {
return; //i will add a FormError in the field
}
$listImages = array();
die(var_dump($listImages));
$form->add('image', EntityType::class, array(
'class' => 'XxxAppBundle:Image',
'choices' => $listImages,
));
};

Related

Symfony forms | Dynamic collection type values

I'm building a small tool on symfony 3.4, I'm experiencing two issue with a form that I cannot find a solution for.
For the context, the form that is causing me some difficulties is based on a doctrine entity : Event.
This event reference another entity : a Doctrine (nothing do to with the ORM). A doctrine references multiples Fittings.
For a given Event with a given Doctrine, I want to display a collectiontype built from the doctrine fittings that expose a number meant to be the required number of this fitting for this event.
This lead to 3 entities in my form : the event itself, the doctrine, and a collectiontype of fittingRequirements built on my end.
The right panel content is meant to change each time the doctrine change.
Here is the EventType :
<?php
namespace AppBundle\Form;
use AppBundle\Entity\Doctrine;
use AppBundle\Entity\Event;
use AppBundle\Form\DataTransformer\FittingRequirementTransformer;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class EventType extends AbstractType
{
protected $requirementTransformer;
public function __construct(FittingRequirementTransformer $transformer)
{
$this->requirementTransformer = $transformer;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->setMethod('POST')
->add('name')
->add(
'date',
DateTimeType::class,
[
'widget' => 'single_text',
'format' => 'yyyy-MM-dd HH:mm',
]
)
->add('startLocation')
->add(
'eventType',
ChoiceType::class,
[
'choices' => [
'PvE' => 'PvE',
'PvP' => 'PvP',
'Other' => 'Other',
],
]
)
->add('target')
->add('description')
->add(
'doctrine',
EntityType::class,
[
'class' => 'AppBundle\Entity\Doctrine',
'choice_label' => 'name',
'query_builder' => function (EntityRepository $repository) {
return $repository->createQueryBuilder('d')->orderBy('d.name', 'ASC');
},
'required' => false,
]
);
$formModifier = function (FormInterface $form, Doctrine $doctrine = null, Event $event) {
$eventRequirements = [];
if ($doctrine) {
$doctrineFittings = $doctrine->getFittings();
$doctrineRequirements = $event->getDoctrineFittingRequirements($doctrine);
$eventRequirements = $this->requirementTransformer->dataToForm(
$doctrineFittings,
$doctrineRequirements,
$event
);
}
$form->add(
'eventRequirements',
CollectionType::class,
[
'entry_type' => FittingRequirementType::class,
'label' => false,
'entry_options' => ['label' => false],
'data' => $eventRequirements,
'mapped' => false,
]
);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$formupEvent = $event->getData();
$formModifier($event->getForm(), $formupEvent->getDoctrine(), $formupEvent);
}
);
$builder->get('doctrine')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$eventForm = $event->getForm()->getParent();
$doctrine = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $doctrine, $eventForm->getData());
}
);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => 'AppBundle\Entity\Event',
]
);
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'event';
}
}
I'm building the list of eventFittingRequirements and adding it on PRE_SET_DATA and POST_SUBMIT
As you can see, I use a CollectionType of FittingRequirementType you can see bellow :
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FittingRequirementType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('number', NumberType::class);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\FittingRequirement'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'appbundle_fittingrequirement';
}
}
This is only used to expose the number of required fittings.
All this works well when I display the form, however when I submit the form using javascript to refresh the requirement part, the field are indeed replaced, but the returned form has no value in the inputs.
The $eventRequirements variable from my $formModifier contains a proper set of data, with the number value. However, when I check the XHR with the symfony profiler, the form has no values, even if I select the original doctrine again. I don't understand what is going on and how to fix this.
Thanks for reading
I just found out what was going on and fixed my issue.
My forms are fine, however the handleRequest method clears the unmapped fields I set with my custom fittingRequirement list.
I had to manually submit my form with the clearmissing parameter to false like bellow :
$form->submit($request->request->get($form->getName()), false);

How to allow admin to add extra unmapped field to a form in real time (Symfony3)

I have a Symfony3 project which facilitates multiple admin/tenants to create custom forms for their users. TenantData is the main entity which contains Salutation, firstname and lastname fields. Based on this entity the tenant/admin can create a form TenantDataType. Here is what TenantDataType looks like:
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use AppBundle\Entity\TenantData;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class TenantDataType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('firstname', TextType::class, array());
$builder->add('lastname', TextType::class, array());
$builder->add('title', ChoiceType::class, array(
'choices' => array(
'Ms' => true,
'Mr' => false,
),
));
$builder->add('save', SubmitType::class, array('label' => 'Submit'));
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => TenantData::class,
'csrf_protection' => true,
'allow_extra_fields' => true
));
}
}
Now I want the admin to be able to add an extra field without adding an extra column in the TenantData entity. One approach I know is to create a MetaData table which contains label, datatype, isrequired and ismultiple fields. Based on this Entity I can create a new form by the name MetaDataType. So that the admin can use this form to create an extra field. This basically means that every record of the MetaData table contains the details of an extra field. The problem is how do I use these records to render the extra fields added by the admin/tenant in real time. Since I am creating a new instance of TenantData at the time I am rendering the form and there is no TenantData id at the time which can fetch the related MetaData rows. What relationship do I need to establish to get the desired functionality.
Any ideas?
let's suppose that your formBuilder as you show above with some modification :
TenantDataType
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use AppBundle\Entity\TenantData;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class TenantDataType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('firstname', TextType::class, array());
$builder->add('lastname', TextType::class, array());
if(isset($option['userRole'] && $option['userRole']=="ADMIN_ROLE" ){
$builder->add('your_admin_field', TextType::class, array());
}
$builder->add('title', ChoiceType::class, array(
'choices' => array(
'Ms' => true,
'Mr' => false,
),
));
$builder->add('save', SubmitType::class, array('label' => 'Submit'));
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => TenantData::class,
'csrf_protection' => true,
'allow_extra_fields' => true
));
}
}
let's suppose that a tenant user have : TENANT_ROLE
and admin has : ADMIN_ROLE
Controller
$user_role= // get the user role here . (TENANT_ROLE/ADMIN_ROLE)
$form = $this->createForm(TenantDataType ::class, $tenantObject, array(
'userRole' => $user_role,
);
if you want to access $options array from addEventListener:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$user_role = $event->getForm()->getConfig()->getOptions()['userRole'];
}

Implementation of a search form with Symfony2 Form Component

Maybe i have a very dumb question, but i'm new in Symfony2 and i was wondering if i can build a Search Form with Symfony Form Component, the same way I do with a Registration Form for example.
My Search form will have a Country select field, a Club select field, a Gender radio buttons field a Level select field and the submit button.
Is it possible to do that with the Form Component or to do something like this is better to just build the search form directly in the view?
I've been searching for information about this, but I didn't find anything.
Here is how my SearchPlayerType.php looks like.
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvents;
use Doctrine\ORM\EntityRepository;
class SearchPlayersType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('country', 'entity', array(
'placeholder' => 'Choose a country',
'class' => 'AppBundle:Country',
'property' => 'name',
'query_builder' => function(EntityRepository $er){
return $er->createQueryBuilder('c')->orderBy('c.name', 'ASC');
},
))
->add('club', 'entity', array(
'placeholder' => 'Choose a club',
'class' => 'AppBundle:Club',
'property' => 'name',
'query_builder' => function(EntityRepository $er){
return $er->createQueryBuilder('c')->orderBy('c.name', 'ASC');
},
))
->add('gender', 'entity', array(
'class' => 'AppBundle:Gender',
'property' => 'name',
'expanded' => true
))
->add('level', 'rating', array(
'label' => 'Playing Level',
'stars' => 5))
;
}
public function getName()
{
return 'SearchPlayer';
}
If it is possible to do it this way I don´t know what Entity my data_class needs to be
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Usuario'
));
}
}
?>
You can go two ways with this: you can either create a form specific model for your search form which will either be a really thin object with public properties; or you can remove the entry for data_class in your form options which will switch your form into returning an array rather than an object (documentation).
The former is the more OO way of doing things and allows you to add validation annotations without embedding those within the form, this way also means you can add getters and setters that transform your search data very easily and not cluttering up your controllers. So your model would look something like:
namespace MyBundle\Form\Model;
class SearchModel
{
public $country;
public $club;
// ...
}
The alternate is just:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
// don't set 'data_class' in here
));
}
Then when you do $form->getData() you'll just get an array back rather than an object.

Symfony2 Form get data value in form type

I want modify a form with a content value :
I try with "PRE_BIND" event but this doesn't work if the form isn't send a first time.
I have this :
<?php
namespace YOU\CommercantBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class LivraisonChoixType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_BIND, function (FormEvent $event) use ($builder)
{
$form = $event->getForm();
$data = $event->getData();
if ((int)$data['pays'] > 0) {
$form->remove('livreur');
$pays = $data['pays'];
$form->add('livreur','entity',array(
'property' =>'name',
'class' => 'YOUAdminBundle:Livreur',
'label' => 'Livreur :',
'query_builder' => function($er) use ($pays){
return $er->createQueryBuilder('c')
->join('c.pays', 'p')
->andWhere('p.id= :pays')
->addOrderBy('c.name', 'ASC')
->setParameter('pays', $pays);
},
)
);
}
});
$builder
->add('pays','pays',array('label'=>'Destination :'))
->add('livreur','entity',array(
'property' =>'name',
'class' => 'YOUAdminBundle:Livreur',
'label' => 'Livreur :',
'query_builder' => function($er) {
return $er->createQueryBuilder('c')
->join('c.pays', 'p')
->andWhere('p.id= :pays')
->addOrderBy('c.name', 'ASC')
->setParameter('pays', 0);
},
)
)
->add('prix','number',array('required'=>true,'label' => 'Frais :'))
->add('prix2','number',array('required'=>false,'label' => 'Frais en second article :'))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'YOU\CommercantBundle\Entity\LivraisonChoix',
));
}
public function getName()
{
return 'you_commercantbundle_livraisonchoixtype';
}
}
Called by this form type :
<?php
namespace YOU\CommercantBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class LivraisonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name','text',array('required'=>true,'label'=>'Nom :'))
->add('choix','collection',array(
'type'=>new LivraisonChoixType(),
'options'=>array('attr'=>array('class'=>'livreur-collection')),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'YOU\CommercantBundle\Entity\Livraison'
));
}
public function getName()
{
return 'you_commercantbundle_livraisontype';
}
}
Anyone know how I can get the value ?
This may not be the best answer but this is what I do when I want to assign values to forms before they are rendered. Basically I would bind the form to an entity as such:
// First create entity that will bind with form
$someEntityInstance = new myEntity();
$someEntityInstance->setPropertyOne(5);
$someEntityInstance->setPropertyTwo('another value');
// Then bind entity to form
$myForm = $this->createForm(new myFormType, $someEntityInstance);
Any properties that are mapped from the form to the entity will have the same value and when rendered in the view, this will show up. Though if I have multiple entities represented in a form, I then create a new thing class called a processor that will have properties mapped and bound to the form that can also change the form field values.
This is the easiest way that I know for changing form field values before rendering them.

Symfony2 form validator groups without entities

I'm using Symfony2 form component to build and validate forms. Now I need to setup validator groups based on a single field value, and unfortunately it seems that every example out there is based on entities - which im not using for several reasons.
Example:
If task is empty, all constraint validators should be removed, but if not, it should use the default set of validators (or a validator group).
In other words, what I'm trying to achieve is making subforms optional, but still be validated if a key field is populated.
Can someone possible give me an example how to configure it?
<?php
namespace CoreBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Validator\Constraints as Assert;
use CoreBundle\Form\Type\ItemGroupOption;
class ItemGroup extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title', 'text', array(
'label' => 'Titel',
'attr' => array('class' => 'span10 option_rename'),
'required' => false
));
$builder->add('max_selections', 'integer', array(
'label' => 'Max tilvalg',
'constraints' => array(new Assert\Type('int', array('groups' => array('TitleProvided')))),
'attr' => array('data-default' => 0)
));
$builder->add('allow_multiple', 'choice', array(
'label' => 'Tillad flere valg',
'constraints' => array(new Assert\Choice(array(0,1))),
'choices' => array(0 => 'Nej', 1 => 'Ja')
));
$builder->add('enable_price', 'choice', array(
'label' => 'Benyt pris',
'constraints' => array(new Assert\Choice(array(0,1))),
'choices' => array(0 => 'Nej', 1 => 'Ja'),
'attr' => array('class' => 'option_price')
));
$builder->add('required', 'choice', array(
'label' => 'Valg påkrævet',
'constraints' => array(new Assert\Choice(array(0,1))),
'choices' => array(0 => 'Nej', 1 => 'Ja')
));
$builder->add('options', 'collection', array(
'type' => new ItemGroupOption(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
)
);
$builder->add('sort', 'hidden');
}
public function getName()
{
return 'item_group';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
global $app;
$resolver->setDefaults(array(
'validation_groups' => function(FormInterface $form) use ($app) {
// Get submitted data
$data = $form->getData();
if (count($app['validator']->validateValue($data['title'], new Assert\NotBlank())) == 0) {
return array('TitleProvided');
} else {
return false;
}
},
));
}
}
If you are using 2.1 you may want to have a look at "Groups based on submitted data".
Update
Example using the demo contact page /demo/contact in the default AcmeDemoBundle provided with Symfony Standard Edition :
The form type with conditional validation groups :
namespace Acme\DemoBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Validator\Constraints as Assert;
class ContactType extends AbstractType
{
// Inject the validator so we can use it in the closure
/**
* #var Validator
*/
private $validator;
/**
* #param Validator $validator
*/
public function __construct(Validator $validator)
{
$this->validator = $validator;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('email', 'email');
$builder->add('message', 'textarea', array(
// Added a constraint that will be applied if an email is provided
'constraints' => new Assert\NotBlank(array('groups' => array('EmailProvided'))),
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
// This is needed as the closure doesn't have access to $this
$validator = $this->validator;
$resolver->setDefaults(array(
'validation_groups' => function(FormInterface $form) use ($validator) {
// Get submitted data
$data = $form->getData();
$email = $data['email'];
// If email field is filled it will not be blank
// Then we add a validation group so we can also check message field
if (count($validator->validateValue($email, new Assert\NotBlank())) == 0) {
return array('EmailProvided');
}
},
));
}
public function getName()
{
return 'contact';
}
}
Don't forget to inject the validator service in the form type :
<?php
namespace Acme\DemoBundle\Controller;
//...
class DemoController extends Controller
{
// ...
public function contactAction()
{
$form = $this->get('form.factory')->create(new ContactType($this->get('validator')));
// ...
}
}
As you can see validation of the message field will be triggered only if the email field is filled.
Use a diff tool to catch the differences.

Categories