I have some nested forms with CollectionType and seems that the data from the constructor is not passed to the 2nd nesting level.
I simplified my form classes, just with I think's important (if you want more info jut tell me in the comments).
The bottom level form is entirely generated depending on the Activity entity class:
class ActivityServiceCreationType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) use ($options, $router) {
$activity = $event->getData();
dump($activity); //JUST TO TEST
$form = $event->getForm();
... //$form->add of all necessary fields
}
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Activity',
...
);
}
}
Over the ActivityServiceCreationType I have the next form that is just a collection of the previous one:
class ActivityServiceCreationMultipleType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('activities', CustomCollectionType::class, [
'entry_type' => ActivityServiceCreationType::class,
'entry_options' => $options,
'mapped' => true,
'allow_add' => true,
'show_add_link' => true,
])
;
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) use ($options) {
$data = $event->getData();
dump($data); //To test the data arriving to this form
});
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => null,
...
));
}
}
Then I have the "main" form, with I create from the controller:
class ActivityServiceCreationCollectionType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('selectAll', CheckboxType::class, [...])
...
;
$builder->add('multipleActivities', CustomCollectionType::class, [
'entry_type' => ActivityServiceCreationMultipleType::class,
'entry_options' => [
"router" => $options["router"],
"em" => $options['em'],
"basePeriod" => $options['basePeriod'],
'fit' => $options['fit'],
'periods' => $options['periods'],
'activities' => $options['activities']
],
'mapped' => true
])
;
}
From the controller I want to set the Activity objects to the ActivityServiceCreationType form, so the fields can be created. And I'm doing it like this:
$form = $this->createForm(ActivityServiceCreationCollectionType::class,
["multipleActivities" => ["activities" => $activities]],
[
"router" => $this->get("router"),
"em" => $this->getEm(),
"periods" => $periods,
"basePeriod" => $basePeriod,
'fit' => $fit
]);
As you can see the data for the form is:
["multipleActivities" => ["activities" => $activities]]
The results for the dumps that I put in the code is the following:
For the first dump, in the ActivityServiceCreationMultipleType I get an ArrayCollection of Activities
witch is what is expected, no problem here,
But in the second dump, in the ActivityServiceCreationType, I'm getting null. Here what I expected is an Activity entity for each one of the forms in the collection, right?
Can you tell me where I'm wrong?
--
Edited to add more info:
I've been trying to know when the data is "lost" and added some code to the event in the collection type.
In the ActivityServiceCreationMultipleType changed the POST_SET_DATA this way:
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) use ($options) {
$data = $event->getData();
dump($data);
$form = $event->getForm();
$form->get('activities')->setData($data);
});
Now the dump() in the ActivityServiceCreationMultipleType (that you see in the last code snippet) shows the array of activities. Directly the activities.
And the dump() in ActivityServiceCreationType is executed 36 times (one for each activity) with null...
Don't know why it seems to be passing the data to the last embedded form, but the event can not get it.
EDIT
The configurateOptions of ActivityServiceCreationCollectionType must be:
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => null,
'multipleActivities' => null
);
}
The configureOptions of ActivityServiceCreationMultipleType.
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => null,
'entry_options' => null,
'router" => null,
'em" => null,
'basePeriod" => null,
'fit' => null,
'periods'=>null,
'activities' => null
));
}
The configureOptions of ActivityServiceCreationType.
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Activity',
'entry_options' => null,
'router" => null,
'em" => null,
'basePeriod" => null,
'fit' => null,
'periods'=>null,
'activities' => null
));
}
In conclusion, you must always indicate every external property that you want to pass to the form in configureOptions
Related
(Symfony 5.2, doctrine, PHP 8.0)
Take a look at this sample code:
more described here: https://symfony.com/doc/current/form/form_collections.html
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('description');
$builder->add('tags', CollectionType::class, [
'entry_type' => TagType::class,
'entry_options' => ['label' => false],
'prototype' => true,
'allow_add' => true,
'allow_delete' => true,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Task::class,
]);
}
}
class TagType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('name');
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$tag = $event->getData();
$form = $event->getForm();
dd($tag); // empty because of prototype: true (as I said need it for adding new t
});
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Tag::class,
]);
}
}
TABLES:
TASK: id, name
TAG: id, task_id, name
my problem:
I would like to set - HTML attribute disabled for input name that meets the condition.
I need to get Task ID inside TagType so I can check if this id is used in another table and if so - disable changing tag name.
I tried multiple things but nothing worked for me.
The main problem is its a prototype (need it for adding/removing) and I don't receive tag id inside $tag = $event->getData(); in TagType so I can't get task id.
any idea? I didn't found the same problem on the internet and I'm not able to figure it out alone.
Something you can try is to use the PRE_SET_DATA event on the task form side to get the ID, and pass that ID to your tag form with the entry_options. Something like (not tested, but you'll get the idea) :
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('description');
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$task = $event->getData();
$form = $event->getForm();
$form->add('tags', CollectionType::class, [
'entry_type' => TagType::class,
'entry_options' => [
'label' => false,
'task_id' => !empty($task) ? $task->getId() : null,
],
'prototype' => true,
'allow_add' => true,
'allow_delete' => true,
]);
});
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Task::class,
]);
}
}
class TagType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// $options['task_id']
// Use your taskId here. The value can be null, on the task creation form.
$builder->add('name');
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Tag::class,
'task_id' => null,
]);
}
}
I am using React on the frontend to send data to the controller which uses Symfony Form for validation. The information sent from React is what I expect but I am getting an error that says "This form should not contain extra fields".
I want to create a new Booking using this form, which has a Reservation. A Reservation has many (or none) reservationTripAddOns (TripAddOns). I have figured out that the problem is caused when I select trip add on((TripAddOn::class) reservationTripAddOn) that I want linked to reservation. When I do not select any add on, it works.
Data passed from React when no add ons are selected and works:
{
"reservations”:
[
{
"trip":277,
"date":"10/27/2017”,
"guests":2,
"reservationTripAddOns":[]
}
]
}
Data passed from React when add ons are selected and does not work:
{
"reservations”:
[
{
"trip":277,
"date":"10/26/2017”,
"guests":2,
"reservationTripAddOns”:
[
{
"id":34,
"name":"Additional one guest”,
"price":100,
"trip”:null
}
]
}
]
}
BookingController:
public function newAction(Request $request, GuideProfile $guideProfile)
{
$reservation = new Reservation();
$booking = new Booking();
$booking->addReservation($reservation);
$form = $this->createForm(BookingType::class, $booking, ['guide_profile' => $guideProfile]);
$form->submit(json_decode($request->getContent(), true));
if ($form->isValid()) {
$context = new SerializationContext();
$context->setSerializeNull(true);
$context->setGroups(['Session']);
$this->get('session')->set(
'booking', $this->get('serializer')->serialize($booking, 'json', $context)
);
return $this->createApiResponse($booking, ['Default'], 201);
}
return $this->createApiErrorResponse($form->getErrors(true));
}
BookingType:
class BookingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('reservations', CollectionType::class, [
'entry_type' => ReservationType::class,
'entry_options' => [
'data_class' => Reservation::class,
'guide_profile' => $options['guide_profile'],
],
'allow_add' => true,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Booking::class,
'guide_profile' => null,
]);
}
}
ReservationType:
class ReservationType extends AbstractType
{
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$guideProfileId = $options['guide_profile'] instanceof GuideProfile ? $options['guide_profile']->getId() : 0;
$builder
->add('trip', EntityType::class, [
'class' => Trip::class,
'query_builder' => function (EntityRepository $er) use ($guideProfileId) {
return $er->createQueryBuilder('t')
->where('t.guideProfile = :guide_profile_id')
->orderBy('t.title', 'ASC')
->setParameter('guide_profile_id', $guideProfileId);
},
'choice_label' => 'title',
'placeholder' => 'Select a Trip',
])
->add('date', DateType::class, [
'format' => 'MM/dd/yyyy',
'html5' => false,
'widget' => 'single_text',
])
->add('reservationTripAddOns', 'collection', [
'entry_type' => TripAddOnType::class,
'entry_options' => [
'data_class' => TripAddOn::class,
'mapped' => false,
]
])
->addEventListener(FormEvents::PRE_SET_DATA, [$this, 'addGuests'])
;
}
public function addGuests(FormEvent $event)
{
//assume everything works here
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Reservation::class,
'guide_profile' => null,
]);
}
TripAddOnType:
class TripAddOnType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class)
->add('price', TextType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\TripAddOn'
]);
}
Is it possible to map this data by SF2 form
[
'name' => 'XL',
'dimensions' => [
'width' => 50,
'height' => 20,
'length' => 20,
]
]
to the entity
Box[name, width, height, length]
Something like:
$builder->add('dimensions.width', 'text', [
'property_path' => 'width'
])
Thanks!
Short version no. The dimensions array key above would have to be an embedded form, however you could set the data class to be the same object which would (probably) work. E.g.
class DimensionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('width')->add('height')->add('length');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([ 'data_class' => 'MyClass' ]);
}
}
class MyFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name')->add('dimension', new DimensionType(), [ 'mapped' => false, 'data' => $options['data'] ]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([ 'data_class' => 'MyClass' ]);
}
}
So you have two form types, one of which is embedded in the other. For MyFormType, the dimension field isn't mapped to MyClass, however it has the same data_class option which means when the form resolves it should set the width, height and length properties on the object.
I haven't tested this but in theory this should work. Ideally you'd change the submitted data to match your object (as Symfony forms are only views of your objects) but this might be a decent patch.
I have a trouble with a formtype with mapped=false.
In controller, I called the form with:
$form = $this->createForm(new JurisdictionUserNewType(), $jurisdiction_user);
This is my JurisdictionUserNewType:
class JurisdictionUserNewType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new CapitalLetterToLowerCaseTransformer();
$builder
->add('name', 'text')
->add($builder->create('email', 'email')
->addModelTransformer($transformer))
->add('securityUser', new SecurityUserType(), array('mapped' => false))
->add('save', 'submit');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Radmas\Open010Bundle\Document\JurisdictionUser'
));
}
public function getName()
{
return 'jurisdictionUserNew';
}
}
This is my SecurityUserType :
class SecurityUserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('first_name', null, ['label' => 'profile.edit.labels.first_name', 'icon_class' => 'fa fa-user'])
->add('last_name', null, ['label' => 'profile.edit.labels.last_name', 'icon_class' => 'fa fa-user'])
->add('nickname', null, ['label' => 'profile.edit.labels.nickname',
'attr' => [ 'help_text' => 'profile.edit.labels.nickname_help'], 'icon_class' => 'fa fa-globe']);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Radmas\SecurityBundle\Document\SecurityUser'
));
}
public function getName()
{
return 'securityUser';
}
}
When I put data in the form, I get the object jurisdictionUser in the modalview but I dont get object securityUser.
If you set 'mapped' => false on any field, you're saying that that field isn't related to your entity, so you don't get it when you retrieve the entity from the submitted form.
You can get it anyway as a single field from the form, as:
$form->handleRequest($request);
if ($form->isValid()) {
$entity = $form->getData();
$securityUser = $form->get('securityUser')->getData();
}
I have 2 entities, User and Account. An Account can be own by multiple users and a User can own multiple Account. So I have this entity UserHasAccount that is handling this relation, plus some few fields that have personalised values for each User - Account relation, such as position, showInMenu, showInSideBar.
In the settings of the account, when a user is logged in I want to display those options but only for the current logged user.
So I end up with 2 form type:
AccountEditType:
class AccountEditType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', null, array(
'label' => 'account.form.name',
'translation_domain' => 'AcmeAccountBundle',
))
->add('timezone', 'genemu_jqueryselect2_timezone', array(
'label' => 'account.form.timezone',
'translation_domain' => 'AcmeAccountBundle',
))
->add('accountUser', new UserAccountEditType($options['user']))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\AccountBundle\Entity\Account',
'user' => null,
'validation_groups' => array('AcmeUpdateAccount'),
'cascade_validation' => true,
));
}
public function getName()
{
return 'acme_account_edit_settings';
}
}
UserAccountEditType:
class UserAccountEditType extends AbstractType
{
protected $user;
public function __construct($user)
{
$this->user = $user;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('showInMenu', null, array(
'label' => 'account.user_account.form.show_in_menu',
'translation_domain' => 'AcmeUserBundle',
'required' => false,
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\UserBundle\Entity\UserHasAccount',
));
}
public function getName()
{
return 'acme_general_account_user';
}
}
The problem is that Account.accountUser is a collection of user and so I cannot use UserAccountEditType which is mapping only one relation (not the collection). If I leave it like this I have the following error:
The form's view data is expected to be an instance of class Acme\UserBundle\Entity\UserHasAccount, but is an instance of class Doctrine\ORM\PersistentCollection
How can I possibly select one of the relation in the form type?
Cheers,
Maxime