Getting entity in embedded collection of forms - symfony

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,
]);
}
}

Related

Validations Groups doesn't working with forms

I'm trying to setup groups validation on my symfony project.
When I update an entity, I only need to validate some fields. When I create an entity, I only need to validate some other fields.
Service:
$form = $this->formFactory->createNamed('form', FormType::class, $entity, ['validation_groups' => ['update']]);
Form:
class FormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user', EntityType::class, [
'class' => User::class,
'validation_groups' => ['create']
])
->add('number', EntityType::class, [
'class' => Numbers::class,
'validation_groups' => ['create', 'update']
])
->add('phone', TextType::class, [
'validation_groups' => ['create', 'update']
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Phones::class,
'allow_extra_fields' => true,
'validation_groups' => ['create', 'update'],
'cascade_validation' => true,
]);
}
}
But, when I submit my form, the "user" field still validated.
{"form":{"user":748,"number":"9.2","phone":"0x xx xx xx xx"}}
{"id":957,"error":"Expected argument of type \"App\\Entity\\User\", \"null\" given at property path \"user\"."}
You don't need to specifify validation_groups on fields.
Groups passed into form options are applied to all fields by default.
# Service 1
// Only 'create' group will be validated
$this->formFactory->createNamed('form', FormType::class, $entity, ['validation_groups' => ['create']]);
# Service 2
// Only 'create' and 'update' group will be validated
$this->formFactory->createNamed('form', FormType::class, $entity, ['validation_groups' => ['create', 'update']]);
# Form
class FormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user', EntityType::class, [
'class' => User::class,
// 'validation_groups' => ['create']
])
->add('number', EntityType::class, [
'class' => Numbers::class,
// 'validation_groups' => ['create', 'update']
])
->add('phone', TextType::class, [
// 'validation_groups' => ['create', 'update']
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Phones::class,
'allow_extra_fields' => true,
// 'validation_groups' => ['create', 'update'],
'cascade_validation' => true,
]);
}
}
However, this doesn't seem like form validation error (violation).
Expected argument of type \"App\\Entity\\User\", \"null\" given at property path \"user\" means, that value passed into 'user' field is null (not set).
You should omit 'user' field when it form is used for 'creation'.
Solution 1:
// add option
$resolver->setDefaults([
'data_class' => Phones::class,
// ...
'addUserField' => null,
]);
// in service pass this option
$this->formFactory->createNamed('form', FormType::class, $entity, [
'validation_groups' => ['create'],
'addUserField' => true
]);
// use condition in form to add field only if option is passed with true value
if($options['addUserField'] === true) {
$builder->add('user', EntityType::class, [class' => User::class]);
}
$builder
->add('number'/** ... */)
->add('phone'/** ... */)
;
Solution 2:
Create 2 form types FormTypeCreate and FormTypeUpdate. Skip 'user' field in FormTypeUpdate. You could also extend FormTypeCreate from FormTypeUpdate.
class FormTypeUpdate extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('number', EntityType::class, [
'class' => Numbers::class,
])
->add('phone', TextType::class)
;
}
//...
}
//
class FormTypeCreate extends FormTypeUpdate
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('user', EntityType::class, [
'class' => User::class,
])
parent::buildForm($builder, $options); // adds 'number' and 'phone' fields
}
//...
}

symfony modify choices in collection after set data

I'm trying to build a form, for user to enter his answers to an exam.
Form consists of Collection of answers looking like this:
class UserExamFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('answers', CollectionType::class,
[
'entry_type' => UserExamAnswerType::class,
'allow_delete' => false,
'allow_add' => false,
])->add('submit', SubmitType::class,
[
'label' => 'Zapisz',
'attr' => [
'class' => 'btn-success',
],
]
)->add('finish', SubmitType::class,
[
'label' => 'Zapisz i zakończ',
'attr' => [
'class' => 'btn-danger confirmation',
'data-message' => 'Czy na pewno chcesz zakończyć test? Nie będzie możliwości poprawy odpowiedzi.'
],
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => UserExam::class,
]);
}
}
class UserExamAnswerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// dump($builder->getData()); === null
$builder
->add('answer', EntityType::class,
[
'class' => Answer::class,
'choice_value' => 'id',
'expanded' => true,
'query_builder' => function (EntityRepository $entityRepository) {
return $entityRepository->createQueryBuilder('a');
}
]);
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) {
dump($event->getData()); // !== null
//modify choices
});
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ExamAnswer::class,
]);
}
}
How to modify choices in the commented line?
I'd like the choices to be affected by the object passed into the form.
In the line dump($event->getData()); // !== null I get exactly the object I need to determine choices but I haven't found any info on it - only dynamically adding a field.
I finally made it by adding an element right before Symfony sets the data on form.
Maybe someone will profit from this:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$event->getForm()
->add('answer', EntityType::class,
[
'class' => Answer::class,
'choice_value' => 'id',
'expanded' => true,
'choices' => $event->getData()->getQuestion()->getAnswers()
]);
});
}

Processing self-referencing, complex data structure with Symfony Forms

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?

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

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

Resources