how to dynamically set cascade validation of form from controller - symfony

My form looks like this :
class CpanelRetailerForm extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('name', 'text', array(
'attr' => array(
'class' => 'text-input',
'size' => '50'
),
'required' => false
))
->add('email', 'email', array(
'attr' => array(
'class' => 'text-input',
'size' => '50'
),
'required' => false
))
->add('addUser', 'checkbox', array(
'label' => 'Add User account',
'required' => false,
'mapped' => false
))
->add('user',new CpanelUserForm());
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'data_class' => 'Acme\TestBundle\Entity\Retailer',
//'cascade_validation' => true
));
}
public function getName() {
return 'retailer';
}
}
I want to dynamically set this line from controller depending on whether addUser field is checked or unchecked.
cascade_validation' => true
Here is my controller code:
$form = $this->createForm(new CpanelRetailerForm(), new Retailer());
$form->
if ($this->getRequest()->isMethod('POST')) {
$form->bind($this->getRequest());
if ($form->get('addUser')->getData()) {
// then set the cascade_validation to true here
}
}
How can I do this inside controller?
My attempt :
added this line in my form class:
$builder->addEventListener(
FormEvents::POST_SUBMIT, function(FormEvent $event) {
$form = $event->getForm();
$addUser = $form->get('addUser')->getData();
$validation = false;
if ($addUser) {
$validation = true;
}
$resolver = new OptionsResolver();
$resolver->setDefaults(array(
'cascade_validation' => $validation
));
$this->setDefaultOptions($resolver);
}
);
This didnot work for me. Although I receive data in $addUser, cascade_validation is not added

How can I do this inside controller?
You can´t! Thats the simple answer. Lets take a look at following simple form class:
class TestType extends AbstractType {
/**
* #var boolean
*/
private $myOption;
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$this->myOption = false;
$builder
->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $event) {
dump('formEvents::PRE_SET_DATA');
})
->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) {
dump('FormEvents::POST_SET_DATA');
})
->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
dump('FormEvents::PRE_SUBMIT');
})
->addEventListener(FormEvents::SUBMIT, function(FormEvent $event) {
dump('FormEvents::SUBMIT');
})
->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) {
dump('formEvents::POST_SUBMIT');
})
->add('name', TextType::class)
->add('send', SubmitType::class);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setRequired(array(
'my_option'
));
$resolver->setDefaults(array(
'my_option' => $this->setMyOption()
));
}
/**
* #return bool
*/
public function setMyOption() {
dump($this->myOption);
return $this->myOption;
}
}
Lets take in how you render and handle a form inside a Controller:
public function formAction(Request $request) {
$form = $this->createForm(TestType::class);
dump('calledCreateForm');
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
dump('finished');
dump($form->getData());
die();
}
return $this->render('#TestPra/Test/test_form.html.twig', array(
'form' => $form->createView()
));
}
After submitting the form you get the following output order:
$this->setMyOption() > null
FormEvents::PRE_SET_DATA
FormEvents::POST_SET_DATA
calledCreateForm
FormEvents::PRE_SUBMIT
FormEvents::SUBMIT
FormEvents::POST_SUBMIT
finished
The first thing allways gets called is configureOptions and because you don´t have any data of the filled form before calling handleRequest there is no way to change the options of the allready created form without manipulating Symfonys form component.

Related

Dependants fields inside collectionType

What I Have ...
Got an OperationForm containing a collection of PaymentEmbeddedForm.
What I intend to do
I want to add some dependant fields in my entries inside the collection of PaymentEmbeddedForm.
The wall i'm hitting.
If some fields are dynamically added inside one of my PaymentEmbeddedForm then when the OperationForm (the root form) is submited, it doesn't recognize the new data, only the field from which the other dependants fields are bound.
Example
OperationFrom
field1
field2
field3 : (Collection of PaymentEmbeddedForm)
Payment1 (PaymentEmbeddedForm)
Payment1.field1
Payment1.field2
Payment1.field3 (dynamically added)
Payment2 (PaymentEmbeddedForm)
Payment2.field1
Payment2.field2
In this case, the data of Payment1.field3 will not be bound at my form ....
What I've tried
Events manipulation inside PaymentEmbeddedForm
If I create an independant PaymentEmbeddedForm, there is no problème, the dependants fields are well bound at my form.
Events manipulation inside OperationForm
When I debug the data of the form with dump($data), I can't see the data of the dependants fields. They are always null at first submition. But once the form has been submitted at least once, the following submition is OK, the data is well bound.
My code
//****************************************
//PaymentEmbeddedForm.php
//****************************************
class PaymentEmbeddedForm extends AbstractType
{
/** #var AccountRepository $accountRepo*/
private $accountRepo;
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** #var Organization $org */
$org = $options['organization'];
$builder
->add('method', EntityType::class, [
'placeholder' => '-- Choississez un moyen de paiement --',
'class' => PaymentMethod::class,
'choice_label' => 'displayName',
'choice_value' => 'name'
])
;
$builder
->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($org) {
return $this->onPreSetData($event, $org);
})
->get('method')->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) use ($org) {
return $this->methodOnPostSubmit($event, $org);
})
;
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'data_class' => Payment::class,
'organization' => false
]);
$resolver->setAllowedTypes('organization', 'App\Entity\Core\Organization');
}
public function onPreSetData(FormEvent $event, Organization $org)
{
/** #var Payment $payment */
$payment = $event->getData();
if (!$payment)
return;
$this->setupSpecificMethodFields($event->getForm(), $payment->getMethod(), $org);
return;
}
public function methodOnPostSubmit(FormEvent $event, ?Organization $org)
{
$form = $event->getForm();
$this->setupSpecificMethodFields(
$form->getParent(),
$form->getData(),
$org
);
}
private function setupSpecificMethodFields(FormInterface $form, ?PaymentMethod $method, ?Organization $org)
{
// $form
// ->remove('account')
// ->remove('amount')
// ->remove('iban')
// ->remove('number')
// ->remove('paidAt')
// ->remove('isPaid')
// ;
if (null === $method) {
return;
}
$this->setupAmountField($form);
$this->setupPaidAtField($form);
$this->setupIsPaidField($form);
if (in_array($method->getName(), ['cash', 'credit_card', 'check', 'transfer', 'direct_debit', 'direct_debit_in_installments', 'interbank_payment_order']))
$this->setupAccountField($form, $method, $org);
if (in_array($method->getName(), ['check', 'voucher']))
$this->setupNumberField($form);
if (in_array($method->getName(), ['direct_debit', 'direct_debit_in_installments']))
$this->setupIbanField($form);
}
private function setupPaidAtField(FormInterface $form): void
{
$form->add('paidAt', DateType::class);
return;
}
private function setupIsPaidField(FormInterface $form): void
{
$form->add('isPaid');
return;
}
private function setupAccountField(FormInterface $form, PaymentMethod $method, Organization $org): void
{
$accountChoices = new ArrayCollection();
switch($method->getName()) {
case 'cash':
$accountChoices = $org->getBankAccounts();
break;
case 'credit_card':
case 'check':
case 'transfer':
case 'direct_debit':
case 'direct_debit_in_installments':
case 'interbank_payment_order':
$accountChoices = $org->getBankAccounts();
break;
}
$form->add('account', AccountType::class, [
'choices' => $accountChoices
]);
}
private function setupAmountField(FormInterface $form): void
{
$form->add('amount', MoneyType::class);
return;
}
private function setupIbanField(FormInterface $form): void
{
$form->add('iban');
}
private function setupNumberField(FormInterface $form): void
{
$form->add('number');
}
}
//****************************************
// OperationFormType.php
//****************************************
class OperationFormType extends AbstractType
{
/** #var AccountRepository $accountRepo*/
private $accountRepo;
public function __construct(EntityManagerInterface $em)
{
$this->accountRepo = $em->getRepository(Account::class);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('amount', MoneyType::class)
->add('description', TextareaType::class)
->add('account', AccountType::class, [
'choices' => $this->accountRepo->getChildren($this->accountRepo->findOneBy([
'code' => $options['type'] == 'income' ? 7 : 6,
'chart' => $options['organization']->getChart()->getId()
]), false, null, 'ASC', false)
])
->add('operateAt', DateType::class)
->add('payments', CollectionType::class, [
'entry_options' => [
'organization' => $options['organization']
],
'entry_type' => PaymentEmbeddedForm::class,
'allow_delete' => true,
'allow_add' => true,
'prototype' => true
])
->add('reset', ResetType::class, [
'label' => 'Réinitialiser'
])
->add('submit', SubmitType::class, [
'label' => 'Enregistrer'
])
;
$builder->addEventListener(FormEvents::PRE_SET_DATA, array($this, 'onPreSetData'));
parent::buildForm($builder, $options);
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'data_class' => OperationFormModel::class,
'organization' => false,
'type' => null
]);
$resolver
->setAllowedTypes('organization', 'App\Entity\Core\Organization')
->setAllowedTypes('type', ['string', 'null'])
->setAllowedValues('type', ['expense', 'income', null])
;
}
public function onPreSetData(FormEvent $event)
{
/** #var OperationFormModel $data */
$data = $event->getData();
if ($data->getPayments()->isEmpty()) {
$newPayment = new Payment();
$newPayment->setPaidAt(new DateTime());
$data->addPayment($newPayment);
}
}
}
Thanks for you're help.
Bdisklaz

FormType - ManyToMany on himself

I try to do a Parents, Children relation on an Entity, but I've a problem when I submit the form. (It's a ManyToMany on himself)
To do it, I've a ManyToMany relation on my entity like it:
class Strain
{
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Strain", inversedBy="children")
*/
private $parents;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Strain", mappedBy="parents")
*/
private $children;
public function __construct()
{
$this->parents = new ArrayCollection();
$this->children = new ArrayCollection();
}
public function addParent(Strain $strain)
{
$this->parents->add($strain);
}
public function removeParent(Strain $strain)
{
$this->parents->removeElement($strain);
}
public function getParents()
{
return $this->parents;
}
public function getChildren()
{
return $this->children;
}
I think it's okay, I've the foreign keys, an intermediate table strain_strain, with 2 columns: strain_source and strain_target.
My FormTypes:
class StrainType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('parents', CollectionType::class, array(
'entry_type' => StrainParentType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required' => false,
))
;
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Strain'
));
}
And the second:
class StrainParentType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('gmoStrain', EntityType::class, array(
'class' => 'AppBundle\Entity\Strain',
'choice_label' => 'systematicName',
'placeholder' => '-- select a parent --',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('strain')
->orderBy('strain.systematicName', 'ASC');
}
))
;
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Strain'
));
}
When I send the form, symfony return this error:
Neither the property "strain" nor one of the methods "getStrain()", "strain()", "isStrain()", "hasStrain()", "__get()" exist and have public access in class "AppBundle\Entity\Strain".
If someone have an idea :/ Because I don't know how to do it.
EDIT:
The problem is in the FormType, because I need a Collection of EntityType, I've do 2 FormType, but I can do it in on FormType and use entry_options to define config of EntityType, like this:
class StrainType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('parents', CollectionType::class, array(
'entry_type' => EntityType::class,
'entry_options' => array(
'class' => 'AppBundle\Entity\Strain',
'choice_label' => 'systematicName',
'placeholder' => '-- select a parent --',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('strain')
->orderBy('strain.systematicName', 'ASC');
}
),
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'required' => false,
))
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Strain'
));
}

Get parent object within child formType event

I've created two form types as below
EmailOptInType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
$_1 = $event->getData();
$_2 = $event->getForm()->getData();
$_3 = $event->getForm()->getParent()->getData();
$form = $event->getForm();
});
}
/**
* #return string
*/
public function getParent()
{
return 'checkbox';
}
And SubscribeType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', 'text', array(
'label' => 'Sign up to news',
'required' => true,
'attr' => array(
'placeholder' => 'Enter your email to subscribe',
),
))
->add('email_opt_in', 'newsletter_opt_in', array(
'data' => true
));
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults(array(
'data_class' => 'Bundle\Entity\Customer',
));
}
As you can see the subscribeType form includes the opt in form. Is it possible to fetch the parent forms data within the event listener of the child form EmailOptInType?
In the snippet above $_1 returns a boolean value of true which represents the checked checkbox, $_2 returns the same and $_3 returns an empty customer object.
Is it possible to get the customer object that has just been created / submitted?
You can pass the customer object an a form option when building the form (eg. in the controller):
$customer = $myRepository->find($customerID);
$form = $this->createForm(SubscribeType::class, $customer, ['customer' => $customer]);
The new SubscribeType.php:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', 'text', array(
'label' => 'Sign up to news',
'required' => true,
'attr' => array(
'placeholder' => 'Enter your email to subscribe',
),
'customer' => $options['customer']
))
->add('email_opt_in', 'newsletter_opt_in', array(
'data' => true
));
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults(array(
'data_class' => 'Bundle\Entity\Customer',
'customer' => null
));
}
And the new EmailOptInType.php:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$customer = $options['customer'];
$builder
->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) use ($customer) {
//do thing with $customer
$_1 = $event->getData();
$_2 = $event->getForm()->getData();
$_3 = $event->getForm()->getParent()->getData();
$form = $event->getForm();
});
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults(array(
//[...]
'customer' => null
));
}

Symfony 2 - Gedmo Uploadable error durng edition

I have everything configured as in documentation and everything works perfect if I upload all files.
But when I want change only other elements in form without changing photo i have received following error message:
You must pass an instance of FileInfoInterface or a valid array for entity of class
public function updateAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('CmUserBundle:User')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find User entity.');
}
$deleteForm = $this->createDeleteForm($id);
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
$container = $this->container;
if ($editForm->isValid()) {
$uploadableManager = $this->get('stof_doctrine_extensions.uploadable.manager');
$uploadableManager->markEntityToUpload($entity, $entity->getPath());
$em->flush();
return $this->redirect($this->generateUrl('user_edit', array('id' => $id)));
}
return array(
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
);
}
Form clas:
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class UserType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', null, array('attr' => array('class' => 'form-control')))
->add('username', null, array('attr' => array('class' => 'form-control')))
->add('path', 'file', array(
'data_class' => null
))
->add('firstName', null, array('attr' => array('class' => 'form-control')))
->add('lastName', null, array('attr' => array('class' => 'form-control')))
->add('localization', null, array('attr' => array('class' => 'form-control')))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'CmUserBundle\Entity\User',
));
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_user';
}
}
How to prevent from update entity by empty input fields? during editForm->handleRequest($request);
Any ideas?
Try $form->submit($request->request->all(), false) instead of $form->handleRequest($request). This will prevent from clearing entity's properties which are not present in incoming POST data.

Symfony2/ Use custom repository method in a formType

I am new with Symfony2, hopefully I am clear enough.
I have a repository:
class districtRepository extends EntityRepository
{
public function findAllFromCity($idCity)
{
return $this->createQueryBuilder('d')
->where('d.city = :city')
->setParameter('city', $idCity)
->orderBy('d.name', 'ASC');
->getQuery()
->getResult();
}
}
And a form type
class searchPropertyType extends AbstractType
{
public function getDefaultOptions(array $options)
{
// return array('validation_constraint' => $collectionConstraint
return array ('required'=>false, 'csrf_protection' => true);
}
public function buildForm(FormBuilder $builder, array $options)
{
$em = $this->getDoctrine()->getEntityManager();
$builder
->add('keywords')
->add('disctrict')
->add('price_min')
->add('price_max')
->add('type')
->add('date_from' , 'date', array('widget' => 'single_text'))
->add('date_to' , 'date', array('widget' => 'single_text'))
;
}
public function getName()
{
return 'searchProperty';
}
}
How do I simply use findAllFromCity() to get a list of option in ->add('disctrict') ??
I know the Query Builder solution, but it makes me repeating my code.
I've read about the service container solution. Is is applicabe in my case? Can you show me how or put me on good tracks??
Indeed, only way to access your 'findAllFromCity' method in the 'searchPropertyType' class is to inject the Doctrine registry.
In your form type class:
use Symfony\Bridge\Doctrine\RegistryInterface;
class searchPropertyType extends AbstractType
{
private $doctrine;
public function __construct(RegistryInterface $doctrine)
{
$this->doctrine = $doctrine;
}
In your services.xml file:
<service id="my_project.form.type.search_property" class="{mynamespace}\searchPropertyType" public="false">
<tag name="form.type" />
<argument type="service" id="doctrine" />
</service>
In order to use this this method, you'll have to use the 'choice' type with 'choices' or 'choice_list' option.
In your form type class:
public function buildForm(FormBuilder $builder, array $options)
{
$builder
// ...
->add('disctrict', 'choice', $this->getDistrictChoices($options['city_id']))
// ...
;
}
private function getDistrictChoices()
{
$choices = array();
$districts = $this->doctrine->getRepository('MyBundle:District')->findAllFromCity($cityId);
foreach ($dictricts as $dictrict) {
$choices[$district->getId()] = $district->getName();
}
return $choices;
}
Of course, this is an example and it needs to be adapted.
And remember: class name fisrt letters are always in upper case.
Thanks to Simon, I have updated as follow and it is now working!
searchController:
public function basicsearchAction(Request $request)
{
$form = $this->createForm(new searchPropertyType($this->getDoctrine()));
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
}
searchPropertyType
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('keywords')
->add('district', 'choice', array('choices'=>$this->getDistrictChoices(), 'multiple'=>true) )
->add('price_min')
->add('price_max')
->add('type', 'entity', array('class'=>'FlatShanghaidefaultBundle:propertytype',
'property'=>'name',
'multiple'=>true
))
->add('date_from' , 'date', array('widget' => 'single_text'))
->add('date_to' , 'date', array('widget' => 'single_text'))
;
}
private function getDistrictChoices()
{
$choices = array();
$districts = $this->doctrine->getRepository('FlatShanghaidefaultBundle:district')->findByDefaultCity();
foreach ($districts as $district) {
$choices[$district->getId()] = $district->getName();
}
return $choices;
}
service.yml
services:
search_property:
class: FlatShanghai\propertyBundle\Form\searchPropertyType
arguments: [doctrine]
I was also concerned about repeated code. Not only for my query builder, but also didn't want to write a custom choice mapping such as "getDistrictChoices". I chose to abstract my query inside my EntityRepository to make it available in my form type as well.
Here's what I did.
My form:
class UserEditType extends AbstractType
{
/**
* #var Session $session
*/
private $session;
public function __construct(Session $session){
$this->session = $session;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('firstName')
->add('middleName')
->add('lastName')
->add('username')
->add('userStatus', EntityType::class, [
'class' => UserStatus::class,
'property' => 'status',
'query_builder' => function(UserStatusRepository $er) {
return $er->createQueryBuilder('s')
->orderBy('s.status');
},
])
->add('agency', EntityType::class, [
'class' => WorkflowGroup::class,
'property' => 'groupName',
'query_builder' => function(WorkflowGroupRepository $er) {
return $er->createSelectAllQB($this->session->get(AbstractController::SESSION_KEY_AGENCY_ID));
},
])
->add('phoneNumber')
->add('email', EmailType::class)
->add('activationDate', DateType::class, [
'widget' => 'single_text',
'format' => 'M/d/yyyy',
'attr' => [
'data-provide' => 'datepicker',
'data-date-format' => 'mm/dd/yyyy',
'data-date-show-on-focus' => 'false',
'data-date-auto-format' => 'true'
]
])
->add('expirationDate', DateTimeType::class, [ 'widget' => 'single_text', 'format' => 'yyyy-MM-dd HH:mm' ]);
;
}
...
}
My Repository:
class WorkflowGroupRepository extends EntityRepository
{
public function createSelectAllQB($agencyId)
{
return $this->createQueryBuilder('w')
->addOrderBy('w.groupName', 'ASC')
->andWhere('w.agencyId = :agencyId')
->setParameter('agencyId', $agencyId);
}
public function selectAll($agencyId)
{
return $this->createSelectAllQB($agencyId)->getQuery()->execute();
}
}

Resources