Processing self-referencing, complex data structure with Symfony Forms - symfony

How to process following data structure through Symfony Forms?
An entity which holds collection of an entity which has a relation to itself:
Order
/** #var OrderProduct */
$orderProducts
public function getQuoteProducts()
{
return $this->quoteProducts;
}
public function addOrderProduct(OrderProduct $orderProduct)
{
if (!$this->orderProducts->contains($orderProduct)) {
$this->orderProducts[] = $orderProduct;
$orderProduct->setOrder($this);
}
return $this;
}
public function removeOrderProduct(OrderProduct $orderProduct)
{
if ($this->orderProducts->contains($orderProduct)) {
$this->orderProducts->removeElement($orderProduct);
}
return $this;
}
OrderProduct
/** #var Order */
$order
/** #var OrderProduct */
$relatedOrderProducts
public function addRelatedOrderProduct(OrderProduct $orderProduct)
{
if (!$this->relatedOrderProducts->contains($orderProduct)) {
$this->relatedOrderProducts[] = $orderProduct;
$orderProduct->setMainOrderProduct($this);
}
return $this;
}
public function removeRelatedOrderProduct(OrderProduct $orderProduct)
{
if ($this->relatedOrderProducts->contains($orderProduct)) {
$this->relatedOrderProducts->removeElement($orderProduct);
$orderProduct->setMainOrderProduct(null);
}
return $this;
}
The request:
'order' => [
'some_order_property' => 'value',
// ...
'orderProducts' => [
'some_order_product_property' => 'value',
// ...
'relatedOrderProducts' => [
[
'some_order_product_property' => 'value',
// ...
],
[
'some_order_product_property' => 'value',
// ...
],
]
]
]
Forms:
OrderType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('orderProducts', OrderProductCollectionType::class)
->add(...);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => Order::class]);
}
OrderProductCollectionType
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'type' => OrderProductType::NAME,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'prototype' => true,
]);
}
OrderProductType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('relatedOrderProducts', RelatedOrderProductCollectionType::class)
->add(...);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => OrderProduct::class]);
}
RelatedOrderProductCollectionType
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'type' => RelatedOrderProductType::NAME,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'prototype' => true,
]);
}
RelatedOrderProductType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(...);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => OrderProduct::class]);
}
This setup works, only it doesn't relate OrderProducts from RelatedOrderProductType form to the Order (so they have order_id set to null).
When this relation is forced (eg. with a FormEvent listener), then all the relations are getting deleted on subsequent form submission. That's most likely due to orphanRemoval=true set on the entity.
Probably some details are missing from my example, but me general question is: can this data structure be processed with Symfony Forms? If so, then how?

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

Symfony : embeded subform and createdAt / updatedAt : validation conflict

I have an entity form that embed a collectionType and subforms.
In order to make the required option work, I had to enable Auto_mapping
class ClassePriceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('priceform', PriceformType::class, [
'data_class' => ClassePrice::class,
'block_prefix' => 'mainprice_block'
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ClassePrice::class,
'attr' => array(
'class' => 'fullwidth'
)
]);
}
}
class PriceformType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('pricecover', ChoiceType::class, [
'label' => 'Price type',
'placeholder' => 'Select a price option',
'choices' => [ 'Global' => '1' ,'Step' => '2'],
'required' => true
])
->add('rangesubform', RangesubformType::class, [
'data_class' => ClassePrice::class,
'block_prefix' => 'range_block'
])
->add('pricesubform', PricesubformType::class, [
'data_class' => ClassePrice::class,
'block_prefix' => 'price_block'
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'inherit_data' => true
]);
}
}
class RangesubformType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('rangetype', EntityType::class, [
'class' => Ptur::class,
'label' => 'Step type',
'choice_translation_domain'=> true,
'required' => true
])
->add('rangeformat', EntityType::class, [
'class' => Format::class,
'label' => 'Format',
'required' => true
])
->add('rangemin', IntegerType::class, [
'label' => 'Range min',
'required' => true
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'inherit_data' => true,
'attr' => array(
'class' => 'form-horizontal'
)
]);
}
}
framework:
validation:
email_validation_mode: html5
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
auto_mapping:
App\Entity\: []
enabled:
true
In my entites I also use the createdAt / updatedAt auto generation made by
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity;
use Gedmo\Mapping\Annotation as Gedmo;
The problem is that with auto_mapping activation, I got a validator error when creating a new entity input : createdAt and updatedAt can not be null...
Any idea how to solve that please?
Make sure your object is valid, inside the class:
public function __construct()
{
$this->createdAt = new DateTime();
$this->updatedAt = new DateTime();
}
Set the time like this in your entity:
/**
* #var datetime $created_at
*
* #Orm\Column(type="datetime")
*/
protected $created_at;
/**
* #var datetime $updated_at
*
* #Orm\Column(type="datetime", nullable = true)
*/
protected $updated_at;
/**
* #orm\PrePersist
*/
public function onPrePersist()
{
$this->created_at = new \DateTime("now");
}
/**
* #Orm\PreUpdate
*/
public function onPreUpdate()
{
$this->updated_at = new \DateTime("now");
}

Getting entity in embedded collection of forms

In my edit form I need to get the entity object in embedded form. This is my main edit form:
class OrderCollectionsEditType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('sampleCollections', CollectionType::class, [
'entry_type' => SampleCollectionType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Order::class,
]);
}
}
and the embedded one:
class SampleCollectionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$sampleCollection = $builder->getData();
$builder
->add('methods', EntityType::class, [
'class' => Method::class,
'multiple' => true,
])
{...}
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => SampleCollection::class,
]);
}
}
The form created in controller:
$form = $this->createForm(OrderCollectionsEditType::class, $order);
And the problem is that the $sampleCollection returns NULL, but the form is properly filled by the values. Is there any other way to get the entity object?
The object is passed to the form in the $options['data] property.
Instead of $sampleCollection = $builder->getData(); get it by $sampleCollection = $options['data];
Unfortunately, suggested above $options['data'] doesn't work with CollectionType, there's no 'data' index. After some deeper research I've found the solution, we can use PRE_SET_DATA form event and then get the entity object in listener function.
SOLUTION:
class SampleCollectionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$sampleCollection = $event->getData();
$form = $event->getForm();
$form->add('methods', EntityType::class, [
'class' => Method::class,
'multiple' => true,
]);
}
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => SampleCollection::class,
]);
}
}

Form error mapping on collection

I have a form:
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('autor', AutorType::class)
->add('categories', CollectionType::class, array(
'entry_type' => CategoryType::class,
'error_bubbling' => false,
))
->add('submit', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Article::class,
));
}
}
This embeds to a custom form. my Article entity looks like this:
class Article
{
/**
* #Assert\Type(type="AppBundle\Model\Autor")
* #Assert\Valid()
*/
private $autor;
/**
*
* #Assert\All({
* #Assert\Type(type="AppBundle\Model\Category")
* })
* #Assert\Valid()
*/
private $categories;
}
My problem is a category field error (category name not blank for example); the error is never mapped to the field itself.
With 'error_bubbling' => true,the error is map with ArticleType form.
With 'error_bubbling' => false, the error is map to the collection
CollectionType but never to the CategoryType form or name filed of CategoryType.
I am on Symfony 3.3 and can not use cascade_validation, I use #Assert\Valid() but it don't seem to work as I expected.
Where did I do wrong?
Thanks for your help.
Try using Valid() as form constraint instead of class one. (Be sure to remove class Valid constraint)
Just encountered same thing and after like 20 combinations it was the solution for me. I'm on symfony 3.2 though.
use Symfony\Component\Validator\Constraints\Valid;
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('autor', AutorType::class)
->add('categories', CollectionType::class, array(
'entry_type' => CategoryType::class,
'error_bubbling' => false,
'constraints' => [
new Valid(),
],
))
->add('submit', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Article::class,
));
}
}
Try setting the error_bubbling option to the collection entries. Not the collection itself. Because the error does not happen to the collection but to the category item in the collection.
$builder
->add('autor', AutorType::class)
->add('categories', CollectionType::class, array(
'entry_type' => CategoryType::class,
'entry_options' => [
'error_bubbling' => false,
],
))
->add('submit', SubmitType::class)
;
Or set it as default in your CategoryType:
// AppBundle\Form\Type\CategoryType.php
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Category::class,
'error_bubbling' => false,
// maybe other options
]);
}

how to dynamically set cascade validation of form from controller

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.

Resources