Symfony 5 - DataTransformer with Form Validation Constraint - symfony

I build a FormType in Symfony5 and make use of DataTransformer on 2 fields :
compagnie_princ
compagnie_sec.
DataTransformer basically takes an object ID and renders It to a label.
DataTransformer works fine, when Form is initially rendered on browser, see below capture:
Problem is after validation callbacks are executed, If an error occured, It fails to transform my Id back to a text value.
Code samples (most important parts) :
AccordCommercialFormType.php
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('compagnie_princ', TextType::class,
[
'label' => 'forms.parameter.accord.compagnie_princ',
]
)
->add('compagnie_sec', TextType::class,
[
'label' => 'forms.parameter.accord.compagnie_sec',
]
);
/** ... **/
$builder>addEventListener(
FormEvents::PRE_SET_DATA,
[$this, 'onPreSetData']
)
->addEventListener(
FormEvents::PRE_SUBMIT,
[$this, 'onPreSubmit']
);
$builder->get('compagnie_princ')->addModelTransformer($this->transformer);
$builder->get('compagnie_sec')->addModelTransformer($this->transformer);
}
Events are captured on preSubmit to fetch ID, because fields 'compagnie_princ' and 'compagnie_sec' are autocompleted with AJAX, populating hidden inputs. My guess is something is going wrong on that part.
public function onPreSetData(FormEvent $event): void
{
$form = $event->getForm();
$form->add('compagnie_princ_id', HiddenType::class,
['mapped' => false,
'attr' => ['class' => 'hidden-field'],
'data' => $event->getData()->getCompagniePrinc() ? $event->getData()->getCompagniePrinc()->getId() : null
]
);
$form->add('compagnie_sec_id', HiddenType::class,
['mapped' => false,
'attr' => ['class' => 'hidden-field'],
'data' => $event->getData()->getCompagnieSec() ? $event->getData()->getCompagnieSec()->getId() : null,
]
);
}
public function onPreSubmit(FormEvent $event): void
{
$data = $event->getData();
$data['compagnie_princ'] = (int)$data['compagnie_princ_id'];
$data['compagnie_sec'] = (int)$data['compagnie_sec_id'];
$event->setData($data);
}
CompagnieToIdTransformer.php
class CompagnieToIdTransformer implements DataTransformerInterface
{
public function __construct(private EntityManagerInterface $em){
}
public function transform($compagnie)
{
if (null === $compagnie) {
return '';
}
return $compagnie->getCodeIata();
}
public function reverseTransform($compagnieId):?Compagnie
{
if (!$compagnieId) {
return null;
}
$compagnie = $this->em
->getRepository(Compagnie::class)
->find($compagnieId)
;
if (null === $compagnie) {
throw new TransformationFailedException(sprintf(
'A company with number "%s" does not exist!',
$compagnieId
));
}
return $compagnie;
}
}

Related

Symfony - ManyToMany - replaces instead of adding

I apologize in advance if my question seems silly to you, I'm a beginner, I've searched but I can't find the answers.
We are in a factory. In this factory, each worker can have several posts, and each posts can contain several workers. So we are in a ManyToMany relationship. The problem is that when I add a worker to a post, he doesn't add to the worker already present in this post, he replaces him! As if a post could only contain one worker.
Can someone tell me what I'm doing wrong or send me precisely the documentation related to this type of problem?
Thanks.
Here is the related code.
(Poste = Post, Operateur = Worker)
in the Post Entity :
/**
* #ORM\ManyToMany(targetEntity=Operateur::class, inversedBy="postes")
* #ORM\JoinTable(name="poste_operateur")
*/
private $operateurs;
/**
* #return Collection|Operateur[]
*/
public function getOperateurs(): Collection
{
return $this->operateurs;
}
public function addOperateur(Operateur $operateur): self
{
if (!$this->operateurs->contains($operateur)) {
$this->operateurs[] = $operateur;
$operateur->addPoste($this);
}
return $this;
}
public function removeOperateur(Operateur $operateur): self
{
$this->operateurs->removeElement($operateur);
$operateur->removePoste($this);
return $this;
}
In the Operateur (worker) entity :
/**
* #ORM\ManyToMany(targetEntity=Poste::class, mappedBy="operateurs")
*/
private $postes;
/**
* #return Collection|Poste[]
*/
public function getPostes(): Collection
{
return $this->postes;
}
public function addPoste(Poste $poste): self
{
if (!$this->postes->contains($poste)) {
$this->postes[] = $poste;
$poste->addOperateur($this);
}
return $this;
}
public function removePoste(Poste $poste): self
{
if ($this->postes->removeElement($poste)) {
$poste->removeOperateur($this);
}
return $this;
}
In the PosteController, method to add an operateur to a post :
/**
* #Route("/{id}/new", name="poste_ope", methods={"GET", "POST"})
*/
public function addOpe(Request $request, Poste $poste): Response
{
$form = $this->createForm(PosteType2::class, $poste);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->getDoctrine()->getManager()->flush();
$this->addFlash(
'success',
"L'opérateur a bien été ajouté au poste {$poste->getNom()} !"
);
return $this->redirectToRoute('operateur_index');
}
return $this->render('poste/addope.html.twig', [
'poste' => $poste,
'form' => $form->createView(),
]);
}
The form in PostType2 :
class PosteType2 extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('operateurs', EntityType::class, [
'class' => Operateur::class,
'label' => 'ajouter un opérateur à ce poste',
'choice_label' => 'nom',
'multiple' => true,
'expanded' => true,
])
->add('save', SubmitType::class, [
'label' => 'Enregistrer',
'attr' => [
'class' => 'btn btn-primary'
]
]);
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Poste::class,
]);
}
}
The problem was in the PosteController, here is the correction :
add an addPost
Here is the documentation who helped me : https://symfony.com/doc/current/doctrine/associations.html#saving-related-entities

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

Calling $builder->getData() from within a nested form always returns NULL

I'm trying to get data stored in a nested form but when calling $builder->getData() I'm always getting NULL.
Does anyone knows what how one should get the data inside a nested form?
Here's the ParentFormType.php:
class ParentFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('files', 'collection', array(
'type' => new FileType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false
);
}
}
FileType.php
class FileType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Each one of bellow calls returns NULL
print_r($builder->getData());
print_r($builder->getForm()->getData());
die();
$builder->add('file', 'file', array(
'required' => false,
'file_path' => 'file',
'label' => 'Select a file to be uploaded',
'constraints' => array(
new File(array(
'maxSize' => '1024k',
))
))
);
}
public function setDefaultOptions( \Symfony\Component\OptionsResolver\OptionsResolverInterface $resolver )
{
return $resolver->setDefaults( array() );
}
public function getName()
{
return 'FileType';
}
}
Thanks!
You need to use the FormEvents::POST_SET_DATA to get the form object :
$builder->addEventListener(FormEvents::POST_SET_DATA, function ($event) {
$builder = $event->getForm(); // The FormBuilder
$entity = $event->getData(); // The Form Object
// Do whatever you want here!
});
It's a (very annoying..) known issue:
https://github.com/symfony/symfony/issues/5694
Since it works fine for simple form but not for compound form. From documentation (see http://symfony.com/doc/master/form/dynamic_form_modification.html), you must do:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$product = $event->getData();
$form = $event->getForm();
// check if the Product object is "new"
// If no data is passed to the form, the data is "null".
// This should be considered a new "Product"
if (!$product || null === $product->getId()) {
$form->add('name', TextType::class);
}
});
The form is built before data is bound (that is, the bound data is not available at the time that AbstractType::buildForm() is called)
If you want to dynamically build your form based on the bound data, you'll need to use events
http://symfony.com/doc/2.3/cookbook/form/dynamic_form_modification.html

Pass/bind data objects to inner/embedded Symfony2 forms

i have the following form where i would like to pass some objects to the inner forms in order to populate them with data when being edited:
public function __construct( $em, $id )
{
$this->_em = $em;
}
public function buildForm( \Symfony\Component\Form\FormBuilderInterface $builder, array $options )
{
$builder->add( 'accessInfo', new AccessInfoType( $this->_em, $options[ 'entities' ][ 'user' ] ) , array(
'attr' => array( 'class' => 'input-medium' ),
'required' => false,
'label' => false
)
);
$builder->add( 'profileInfo', new ProfileInfoType( $this->_em, $options[ 'entities' ][ 'profile' ] ) , array(
'required' => false,
'label' => false
)
);
}
public function setDefaultOptions( \Symfony\Component\OptionsResolver\OptionsResolverInterface $resolver )
{
$resolver->setDefaults( $this->getDefaultOptions( array() ) );
return $resolver->setDefaults( array( ) );
}
/**
* {#inheritDoc}
*/
public function getDefaultOptions( array $options )
{
$options = parent::getDefaultOptions( $options );
$options[ 'entities' ] = array();
return $options;
}
public function getName()
{
return 'UserType';
}
which i instantiate with the following code:
$form = $this->createForm( new UserType( $em ), null, array( 'entities' => array( 'user' => $userObj, 'profile' => $profileObj ) ) );
Once i get, via the constructor, the object containing the needed data does anyone know how could i bind that object to the form?
class ProfileInfoType extends AbstractType
{
private $_em;
public function __construct( $em, $dataObj )
{
$this->_em = $em;
$this->_dataObj = $dataObj;
}
Thanks in advanced!
I was having the same issue and fixed this with inherit_data
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'inherit_data' => true,
));
}
See also http://symfony.com/doc/current/cookbook/form/inherit_data_option.html
Inside your controller yo should get the request data
$request = $this->getRequest();
or request it through the method parameters
public function newAction(Request $request)
and then bind it to the form
$form->bind($request);
For further details have a look at http://symfony.com/doc/2.1/book/forms.html#handling-form-submissions
this works well add an attr for use the html attribute 'value' depends of the form type, maybe this can help you.
Twig
{{ form_label(blogpostform.title) }}
{{ form_widget(blogpostform.title, {'attr': {'value': titleView }}) }}
{{ form_errors(blogpostform.title) }}

How do you default to the empty_value choice in a custom symfony form field type?

I have a created a custom form field dropdown list for filtering by year. One of the things I want to do is to allow the user to filter by all years, which is the default option. I am adding this as an empty_value. However, when I render the form, it defaults on the first item that's not the empty value. The empty value is there, just above it in the list. How do I make the page default to, in my case 'All' when the page initially loads? Code is below.
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class YearType extends AbstractType
{
private $yearChoices;
public function __construct()
{
$thisYear = date('Y');
$startYear = '2012';
$this->yearChoices = range($thisYear, $startYear);
}
public function getDefaultOptions(array $options)
{
return array(
'empty_value' => 'All',
'choices' => $this->yearChoices,
);
}
public function getParent(array $options)
{
return 'choice';
}
public function getName()
{
return 'year';
}
}
I'm rendering my form in twig with a simple {{ form_widget(filter_form) }}
Try adding empty_data option to null, so it comes first. I have many fields of this type and it's working, for example:
class GenderType extends \Symfony\Component\Form\AbstractType
{
public function getDefaultOptions(array $options)
{
return array(
'empty_data' => null,
'empty_value' => "Non specificato",
'choices' => array('m' => 'Uomo', 'f' => 'Donna'),
'required' => false,
);
}
public function getParent(array $options) { return 'choice'; }
public function getName() { return 'gender'; }
}
EDIT: Another possibility (i suppose) would be setting preferred_choices. This way you'll get "All" option to the top. But i don't know if it can work with null empty_data, but you can change empty_data to whatever you want:
public function getDefaultOptions(array $options)
{
return array(
'empty_value' => 'All',
'empty_data' => null,
'choices' => $this->yearChoices,
'preferred_choices' => array(null) // Match empty_data
);
}
When I've needed a simple cities dropwdown from database without using relations, I've ended up using this config for city field (adding null as first element of choices array), as empty_data param didn't do work for me:
$builder->add('city',
ChoiceType::class,
[
'label' => 'ui.city',
'choices' => array_merge([null], $this->cityRepository->findAll()),
'choice_label' => static function (?City $city) {
return null === $city ? '' : $city->getName();
},
'choice_value' => static function(?City $city) {
return null === $city ? null : $city->getId();
},
]);

Resources