Symfony 2 Embedded Form Collection Many to Many - php

I have 2 Entities - User and Group. They have a many-to-many relationship and Group is used to store a users' roles.
I'm trying to make a User edit form by adding a collection, I want to be able to add a new role by selecting it from a dropdown (limited to what's already in the DB)
UserType.php:
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username')
->add('email')
->add('forename')
->add('surname')
->add('isActive')
->add('joinDate', 'date', array('input' => 'datetime', 'format' => 'dd-MM-yyyy'))
->add('lastActive', 'date', array('input' => 'datetime', 'format' => 'dd-MM-yyyy'))
->add('groups', 'collection', array(
'type' => new GroupType(),
'allow_add' => true,
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Sfox\CoreBundle\Entity\User'
));
}
}
and GroupType.php:
class GroupType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('role');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
"data_class" => 'Sfox\CoreBundle\Entity\Group'
));
}
}
This displays the roles in the form in basic text boxes, but if I add an entry to the form, it will cascade persist a new entry into Groups and if I were to edit an entry, it would change the underlying Group data.
I tried making a GroupSelectType.php:
class GroupSelectType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('role', 'entity', array('class'=>'SfoxCoreBundle:Group', 'property'=>'name'));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
"data_class" => 'Sfox\CoreBundle\Entity\Group'
));
}
}
Adding the field as an "entity" type, this displays the correct select box (but with the default values) I cant seem to bind it to the UserType form!
All I want the form to do is modify the underlying 'groups' ArrayCollection in the User entity.
Does anyone know how I can achieve this?

Well I worked out a solution for anyone else struggling with similar problems...
I had to create a custom form type and declare it as a service so I could pass in the Entity Manager. I then needed to make a dataTransformer to change my group objects into an integer for the form
Custom GroupSelectType:
class GroupSelectType extends AbstractType
{
/**
* #var ObjectManager
*/
private $om;
private $choices;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
// Build our choices array from the database
$groups = $om->getRepository('SfoxCoreBundle:Group')->findAll();
foreach ($groups as $group)
{
// choices[key] = label
$this->choices[$group->getId()] = $group->getName() . " [". $group->getRole() ."]";
}
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new GroupToNumberTransformer($this->om);
$builder->addModelTransformer($transformer);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
"choices" => $this->choices,
));
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'group_select';
}
}
In the constructor I'm getting all available groups and putting them into a "choices" array which is passed to the select box as an option.
You'll also notice I'm using a custom data transformer, this is to change the groupId (which is used in the rendering of the form) to a Group entity. I made the GroupSelectType a service as well and passed in the [#doctrine.orm.entity_manager]
services.yml (bundle config):
services:
sfox_core.type.group_select:
class: Sfox\CoreBundle\Form\Type\GroupSelectType
arguments: [#doctrine.orm.entity_manager]
tags:
- { name: form.type, alias: group_select }
GroupToNumberTranformer.php
class GroupToNumberTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* Transforms an object (group) to a string (number).
*
* #param Group|null $group
* #return string
*/
public function transform($group)
{
if (null === $group) {
return "";
}
return $group->getId();
}
/**
* Transforms a string (number) to an object (group).
*
* #param string $number
* #return Group|null
* #throws TransformationFailedException if object (group) is not found.
*/
public function reverseTransform($number)
{
if (!$number) {
return null;
}
$group = $this->om
->getRepository('SfoxCoreBundle:Group')
->findOneBy(array('id' => $number))
;
if (null === $group) {
throw new TransformationFailedException(sprintf(
'Group with ID "%s" does not exist!',
$number
));
}
return $group;
}
}
And my modified UserType.php - Notice I'm using my custom form type "group_select" now as it's running as a service:
class UserType extends AbstractType
{
private $entityManager;
public function __construct($entityManager)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new GroupToNumberTransformer($this->entityManager);
$builder
->add('username')
->add('email')
->add('forename')
->add('surname')
->add('isActive')
->add('joinDate', 'date', array('input' => 'datetime', 'format' => 'dd-MM-yyyy'))
->add('lastActive', 'date', array('input' => 'datetime', 'format' => 'dd-MM-yyyy'));
$builder
->add(
$builder->create('groups', 'collection', array(
'type' => 'group_select',
'allow_add' => true,
'options' => array(
'multiple' => false,
'expanded' => false,
)
))
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Sfox\CoreBundle\Entity\User'
));
}
public function getName()
{
return 'sfox_corebundle_usertype';
}
}

Related

Symfony 4. Why does submitted Form just partially populate the Model?

A bit in the panic - I am generating Symfony form for a complex search, i.e. mapped data to the entity will be used just for a search query building.
I create simple form, model, some extended types from ChoiceType for prepopulation choices by some logic. The form is submitted with GET method.
In the model you find maker and model fields for example. The latter populated on the frontend with AJAX, after maker has been selected. When I do submit the form, and maker and model have non-default value, the handleRequest only populates the maker property of the Model, but the model is left empty. Also the checkboxes are correctly populated if checked. All in all, $form->getData() returns just Maker and checkboxes, other fields are null. $request->query has all parameters.
The data mappers are senseless here. And also there is nothing to transform in the data, the Model is mostly from scalar values. The request contains everything, but it is not handled correctly. I tried to implement ChoiceLoaderInterface, but that doesn't work for me, because during loading choices I have to have access to the options of the form, which I don't (I used this article https://speakerdeck.com/heahdude/symfony-forms-use-cases-and-optimization).
I am using Symfony 4.2.4; PHP 7.2.
Controller's method
/**
* #Route("/search/car", name="car_search", methods={"GET"})
* #param Request $request
*/
public function carSearchAction(Request $request)
{
$carModel = new CarSimpleSearchModel();
$form = $this->createForm(CarSimpleSearchType::class, $carModel);
$form->handleRequest($request);
$form->getData();
.....
}
CarSimpleSearchModel
class CarSimpleSearchModel
{
public $maker;
public $model;
public $priceFrom;
public $priceTo;
public $yearFrom;
public $yearTo;
public $isCompanyOwner;
public $isPrivateOwners;
public $isRoublePrice;
}
CarSimpleSearchType the form
class CarSimpleSearchType extends AbstractType
{
protected $urlGenerator;
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('vehicle_type', HiddenType::class, [
'data' => VehicleTypeType::CAR,
'mapped' => false,
])
->add('maker', CarMakerSelectType::class)
->add('model', CarModelsSelectType::class)
->add(
'priceFrom',
VehiclePriceRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'priceTo',
VehiclePriceRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'yearFrom',
VehicleYearRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add(
'yearTo',
VehicleYearRangeType::class,
[
'vehicle_type' => VehicleTypeType::CAR,
]
)
->add('isCompanyOwner', CheckboxType::class)
->add('isPrivateOwners', CheckboxType::class)
->add('isRoublePrice', CheckboxType::class)
->add('submit', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'data_class' => CarSimpleSearchModel::class,
'compound' => true,
'method' => 'GET',
'required' => false,
'action' => $this->urlGenerator->generate('car_search'),
]
);
}
public function getBlockPrefix()
{
return 'car_search_form';
}
}
CarMakerSelectType field
class CarMakerSelectType extends AbstractType
{
/**
* #var VehicleExtractorService
*/
private $extractor;
/**
* VehicleMakerSelectType constructor.
*
* #param VehicleExtractorService $extractor
*/
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'placeholder' => null,
'vehicle_type' => null,
'choices' => $this->getVariants(),
]
);
}
private function getVariants()
{
$makers = $this->extractor->getMakersByVehicleType(VehicleTypeType::CAR);
$choices = [];
foreach ($makers as $maker) {
$choices[$maker['name']] = $maker['id'];
}
return $choices;
}
}
CarModelSelectType field
class CarModelsSelectType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'disabled' => true,
]
);
}
}
VehiclePriceRangeType field
class VehiclePriceRangeType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'vehicle_type' => null,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
foreach ($this->getRange($options['vehicle_type']) as $value) {
$view->vars['choices'][] = new ChoiceView($value, $value, $value);
}
}
private function getRange(int $vehicleType)
{
return PriceRangeGenerator::generate($this->extractor->getMaxVehiclePrice($vehicleType));
}
}
VehicleYearRangeType field
class VehicleYearRangeType extends AbstractType
{
private $extractor;
public function __construct(VehicleExtractorService $extractorService)
{
$this->extractor = $extractorService;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'vehicle_type' => null,
]
);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
foreach ($this->getRange($options['vehicle_type']) as $value) {
$view->vars['choices'][] = new ChoiceView($value, $value, $value);
}
}
protected function getRange(int $vehicleType): array
{
$yearRange = RangeGenerator::generate(
$this->extractor->getMinYear($vehicleType),
$this->extractor->getMaxYear($vehicleType),
1,
true,
true
);
return $yearRange;
}
}
So, I can use the raw data from the Request and manually validate-populate the model and send to further processing, but I guess that's not the Right Way, and I want to populated the form by the framework. How can I ?..
In my case, I had a dependent EntityType populated by ajax that is initially disabled. Since choices where null, it was returning an InvalidValueException on submission. What I had to do is create an EventListener and add the valid choices for the current 'main' field. This is basically it, more or less adapted to your case.
Original form:
// Setup Fields
$builder
->add('maker', CarMakerSelectType::class)
->add('model', CarModelsSelectType::class, [
'choices' => [],
// I was setting the disabled on a Event::PRE_SET_DATA if previous field was null
// since I could be loading values from the database but I guess you can do it here
'attr' => ['disabled' => 'disabled'],
]
);
$builder->addEventSubscriber(new ModelListener($this->extractor));
Event Subscriber that adds back valid choices:
class ModelListener implements EventSubscriberInterface
{
public function __construct(VehicleExtractorService $extractor)
{
$this->extractor = $extractor;
}
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SUBMIT => 'onPreSubmitData',
];
}
public function onPreSubmitData(FormEvent $event)
{
// At this point you get only the scalar values, Model hasn't been transformed yet
$data = $event->getData();
$form = $event->getForm();
$maker_id = $data['maker'];
$model= $form->get('model');
$options = $model->getConfig()->getOptions();
if (!empty($maker_id)) {
unset($options['attr']['disabled']);
$options['choices'] = $this->extractor->getModelsFor($maker_id);
$form->remove('model');
$form->add('model', CarModelsSelectType::class, $options );
}
}
}
}

Symfony for propert_path for child form type

I understand if I put
->add('visitor.name', TextType::class, [
'property_path' => 'vistorName'
])
This will be identical to <input name="vistorName"
How can I do similar for child form, to have all the child field as a parent field name without putting in as a child array.
$builder->add('customer', CustomerType::class);
CustomerType
$builder
->add('name', TextType::class)
->add('email', EmailType::class)
->add('phone', PhoneNumberType::class, [
'default_region' => 'GB', // TODO GLOBAL release
'format' => PhoneNumberFormat::NATIONAL
]);
The above form should generate <input name="name"... <input name="email"... and not as following <input name="customer[name]"...
how can I do that?
Following is complete code example
// Cart model
class Cart {
protected $productName;
/** #var Customer */
protected $customer;
}
// Customer model
Class Customer {
protected $name;
protected $email;
protected $phone;
}
//CustomerType form
$builder
->add('name', TextType::class)
->add('email', EmailType::class)
->add('phone', PhoneNumberType::class, [
'default_region' => 'GB', // TODO GLOBAL release
'format' => PhoneNumberFormat::NATIONAL
]);
// main parent form
class MainFormType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('productName', TextType::class)
->add('customer', CustomerType::class); // need property_path for this, so all the inner fields can be used as parent
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Cart::class,
'required' => false
]);
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'cart';
}
}
It works the other way around:
Scenario:
VisitorEntity:
TypeOfVisitorEntity
CustomerEntity
Now you want to update a VisitorEntity customer.name:
class MainFormType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('productName', TextType::class)
->add('customerName', TextType::class, ['property_path'=>'customer.name'])
->add('customerEmail', TextType::class, ['property_path'=>'customer.email'])
->add('customerPhone', TextType::class, ['property_path'=>'customer.phone'])
;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Cart::class,
'required' => false
]);
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'cart';
}
}
property_path
https://symfony.com/doc/3.4/reference/forms/types/form.html#property-path
type: any default: the field's name
Fields display a property value of the form's domain object by default. When the form is submitted, the submitted value is written back into the object.
If you want to override the property that a field reads from and writes to, you can set the property_path option. Its default value is the field's name.
If you wish the field to be ignored when reading or writing to the object you can set the property_path option to false, but using property_path for this purpose is deprecated, you should use the mapped option.

setting 'by reference' to false, on symfony form throws 'Could not determine access type'

I'm trying to create a form for the creation of a product in sylius. I want to add a collection of "PackItem".
However,only the last item is added and when I add "by_reference" => false I've got this issue
Could not determine access type for property "products".
This is my code
#ProductTypeExtension.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** #var PackItem $packItem */
$packItem = new PackItem();
$packItem->setParent($builder->getData());
$builder
->add('products', CollectionType::class, [
'entry_type' => PackItemType::class,
'allow_add' => true,
'allow_delete' => true,
'entry_options' => [
'data' => $packItem
],
'by_reference' => false,
]);
}
/**
* {#inheritdoc}
*/
public function getExtendedType()
{
return ProductType::class;
}
PackItemType.php
#PackItemType.php
final class PackItemType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('child', 'entity', [
'label' => 'winzana.ui.token',
'class' => Product::class,
'query_builder' => function(EntityRepository $er) {
$qr = $er->createQueryBuilder('t')
->leftJoin('t.products', 'p')
->having('COUNT(p.parent) = 0')
->groupBy('t.id')
->orderBy('t.code', 'ASC')
;
return $qr;
}
])
->add('quantity', IntegerType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => PackItem::class
]);
}
Product :
class Product extends BaseProduct
{
/**
* #ORM\OneToMany(targetEntity="XXXX\PackBundle\Entity\PackItem", mappedBy="parent", cascade={"persist"})
* #var ArrayCollection|PackItem $products
*/
private $products;
Thank you for your time
You can try to initialise your products in the __construct() method of your class product
public function __construct()
{
$this->products= new ArrayCollection();
}
if it does not correct the problem, then check if you set correctly the getProducts(), setProducts() and addProduct().
You can check this page for information,
http://symfony.com/doc/current/best_practices/business-logic.html#doctrine-mapping-information
regards.
The problem was solved by this change
/**
* #param ArrayCollection|PackItem[] $products
*/
public function setProducts($products)
{
$this->products = $products;
}
I don't use the setter so I didn't made it however by_references needs it.
Now I've got an other problem, only the last Item is saved.

How to get the data from a field displayed with form events in symfony?

I am using Symfony 3.0
I have a country->region->locality->medical center form type. All these fields are interdependent using ajax.
I made four Event subscribers, each of them have onPreSetData and onPreSubmit.
Everything is working well until the moment when I wish to process the submited data in my controller.
Being not mapped fields I though I could access the submited value with
$form->get("field_name")->getData();
But this is null for these fields.
How can I access the values submited in these fields?
This is how I added the fields:
->add('medical_center', MedicalCenterType::class, array(
'mapped' => false
));
Medical Center Type:
class MedicalCenterType extends AbstractType
{
/** #var EntityManager */
protected $em;
/**
* #param mixed $em
* #return $this
*/
public function setEntityManager($em)
{
$this->em = $em;
return $this;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addEventSubscriber(new AddCountryFieldSubscriber($this->em, 38))
->addEventSubscriber(new AddRegionFieldSubscriber($this->em, 38))
->addEventSubscriber(new AddLocalityFieldSubscriber($this->em))
->addEventSubscriber(new AddMedicalCenterFieldSubscriber($this->em))
;
}
}
And the subscriber:
class AddMedicalCenterFieldSubscriber implements EventSubscriberInterface
{
/** #var EntityManager */
protected $em;
/** #var int */
protected $locality;
/** #var int */
public $medical_center;
public function __construct(EntityManager $em, $locality = null)
{
$this->em = $em;
$this->locality = $locality;
}
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => 'onPreSetData',
FormEvents::PRE_SUBMIT => 'onPreSubmit'
);
}
public function onPreSetData(FormEvent $event)
{
$this->formModifier($event);
}
public function onPreSubmit(FormEvent $event)
{
$data = $event->getData();
$this->locality = $data['locality'];
$this->formModifier($event);
}
private function formModifier(FormEvent $event)
{
$form = $event->getForm();
$form->add('medical_center', EntityType::class, array(
'placeholder' => 'Select a locality first',
'class' => 'WebsiteBundle:MedicalCenters',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('mc')
->where('mc.localities = :locality')
->orderBy('mc.name')
->setParameter('locality', $this->locality);
},
'constraints' => new NotBlank(),
'choice_label' => 'name',
'mapped'=>false
));
}
}
I found the solution, it was:
$form->get('medical_center')->get('medical_center')->getData();

Symfony2 pass option to abstract FormType

I've been stuck on this for quite some time now. I read quite some answers here on SO but I couldn't get it running.
My controller contains
$series = $this->getDoctrine()->getRepository('DemoDemoBundle:Series')->createQueryBuilder('series')
->where('series.type = 1')
->getQuery()->getResult();
$magazine = new Magazine();
$content = array(
'series' => $series
);
$form = $this->createForm(new MagazineType($this->get('translator'), $content), $magazine);
$form->handleRequest($request);
I would like to pass the variable $content to the form so that 'series' can be used in some selectboxes. MagazineType extends PublicationType. The same goes for the underlying entities. Here's how MagazineType looks:
class MagazineType extends PublicationType
{
function __construct($translator, array $series = null)
{
parent::__construct($translator, $series); //$series);
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// TODO: Refactor constant out
parent::buildForm($builder, $options)
// ->add('dateSubmitted')
// ->add('userSubmitted')
// ->add('file')
->add('issueNumber')
->add('save', 'submit');
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
array(
'data_class' => 'Demo\DemoBundle\Entity\Magazine',
'series' => $this->translator->trans('addPublication.noSeries')
)
);
}
and PublicationType
class PublicationType extends AbstractType
{
protected $series, $translator;
public function __construct($translator, $series)
{
$this->translator = $translator;
$this->series = $series;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options){
$builder
->add('name', 'text', array(
'label' => $this->translator->trans('addPublication.name')
))
// ->add('dateSubmitted')
// ->add('userSubmitted')
// ->add('file')
->add('series', 'choice', $this->series);//$formOptions['series']);
;
return $builder;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Demo\DemoBundle\Entity\Publication',
'series' => $this->translator->trans('addPublication.noSeries')
));
}
Maybe I'm accessing the series variable the wrong way, but I'm not even getting any further than constructing MagazineType (option series not allowed) even though I've set it in DefaultOptions as stated on http://symfony.com/doc/current/components/options_resolver.html#configure-allowed-types. I'm probably missing something here though similar questions, since I'm stuck on this the entire day.
Thx for any help!

Categories