Symfony2 Forms and Polymorphic collections - symfony

Im playing around with Symfony2 and Im abit unsure how Symfony2 handles Polymorphic collections in the View component. It seems that i can create an entity with a collection of AbstractChildren, but not sure how to what to do with it inside a Form Type class.
For example, I have the following entity relationship.
/**
* #ORM\Entity
*/
class Order
{
/**
* #ORM\OneToMany(targetEntity="AbstractOrderItem", mappedBy="order", cascade={"all"}, orphanRemoval=true)
*
* #var AbstractOrderItem $items;
*/
$orderItems;
...
}
/**
* Base class for order items to be added to an Order
*
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({
* "ProductOrderItem" = "ProductOrderItem",
* "SubscriptionOrderItem " = "SubscriptionOrderItem "
* })
*/
class AbstractOrderItem
{
$id;
...
}
/**
* #ORM\Entity
*/
class ProductOrderItem extends AbstractOrderItem
{
$productName;
}
/**
* #ORM\Entity
*/
class SubscriptionOrderItem extends AbstractOrderItem
{
$duration;
$startDate;
...
}
Simple enough, but when im create a form for my order class
class OrderType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('items', 'collection', array('type' => AbstractOrderItemType()));
}
}
I am unsure how to handle this situation where you effectively need a different Form Type for each class of item in the collection?

I recently tackled a similar problem - Symfony itself makes no concessions for polymorphic collections, but it's easy to provide support for them using an EventListener to extend the form.
Below is the content of my EventListener, which uses a similar approach to Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener, the event listener which provides the collection form type's normal functionality:
namespace Acme\VariedCollectionBundle\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class VariedCollectionSubscriber implements EventSubscriberInterface
{
protected $factory;
protected $type;
protected $typeCb;
protected $options;
public function __construct(FormFactoryInterface $factory, $type, $typeCb)
{
$this->factory = $factory;
$this->type = $type;
$this->typeCb = $typeCb;
}
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => 'fixChildTypes'
);
}
public function fixChildTypes(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
// Go with defaults if we have no data
if($data === null || '' === $data)
{
return;
}
// It's possible to use array access/addChild, but it's not a part of the interface
// Instead, we have to remove all children and re-add them to maintain the order
$toAdd = array();
foreach($form as $name => $child)
{
// Store our own copy of the original form order, in case any are missing from the data
$toAdd[$name] = $child->getConfig()->getOptions();
$form->remove($name);
}
// Now that the form is empty, build it up again
foreach($toAdd as $name => $origOptions)
{
// Decide whether to use the default form type or some extension
$datum = $data[$name] ?: null;
$type = $this->type;
if($datum)
{
$calculatedType = call_user_func($this->typeCb, $datum);
if($calculatedType)
{
$type = $calculatedType;
}
}
// And recreate the form field
$form->add($this->factory->createNamed($name, $type, null, $origOptions));
}
}
}
The downside to using this approach is that for it to recognize the types of your polymorphic entities on submit, you must set the data on your form with the relevant entities before binding it, otherwise the listener has no way of ascertaining what type the data really is. You could potentially work around this working with the FormTypeGuesser system, but that was beyond the scope of my solution.
Similarly, while a collection using this system still supports adding/removing rows, it will assume that all new rows are of the base type - if you try to set them up as extended entities, it'll give you an error about the form containing extra fields.
For simplicity's sake, I use a convenience type to encapsulate this functionality - see below for that and an example:
namespace Acme\VariedCollectionBundle\Form\Type;
use Acme\VariedCollectionBundle\EventListener\VariedCollectionSubscriber;
use JMS\DiExtraBundle\Annotation\FormType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;
/**
* #FormType()
*/
class VariedCollectionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Tack on our event subscriber
$builder->addEventSubscriber(new VariedCollectionSubscriber($builder->getFormFactory(), $options['type'], $options['type_cb']));
}
public function getParent()
{
return "collection";
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setRequired(array('type_cb'));
}
public function getName()
{
return "varied_collection";
}
}
Example:
namespace Acme\VariedCollectionBundle\Form;
use Acme\VariedCollectionBundle\Entity\TestModelWithDate;
use Acme\VariedCollectionBundle\Entity\TestModelWithInt;
use JMS\DiExtraBundle\Annotation\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;
/**
* #FormType()
*/
class TestForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$typeCb = function($datum) {
if($datum instanceof TestModelWithInt)
{
return "test_with_int_type";
}
elseif($datum instanceof TestModelWithDate)
{
return "test_with_date_type";
}
else
{
return null; // Returning null tells the varied collection to use the default type - can be omitted, but included here for clarity
}
};
$builder->add('demoCollection', 'varied_collection', array('type_cb' => $typeCb, /* Used for determining the per-item type */
'type' => 'test_type', /* Used as a fallback and for prototypes */
'allow_add' => true,
'allow_remove' => true));
}
public function getName()
{
return "test_form";
}
}

In the example you have give, you would have to create different form class for those ProductOrder and SubscriptionOrder
class ProductOrderType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
//Form elements related to Product Order here
}
}
and
class SubsciptionOrderType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
//Form elements related SubscriptionOrder here
}
}
In your OrderType form class you add both these forms, like this
class OrderType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('product',new ProductOrderType())
$builder->add('subscription',new SubsciptionOrderType())
//Form elements related to order here
}
}
Now this adds the two forms SubsciptionOrderType,ProductOrderType to the main form OrderType . So later in the controller if you initialize this form you will get all the fields of the subscription and product forms with that of the OrderType.
I hope this answers your questions if still not clear please go through the documentation for embedding multiple forms here. http://symfony.com/doc/current/cookbook/form/form_collections.html

Related

Where to normalize submitted form values in Symfony forms?

I have a postal code field in my form, which' value should match this regex: /^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/
In my entity I would like all postal codes to have the same format: 4 digits, a space and 2 uppercase letters, so the incoming value needs to be normalized somewhere.
Question: where do I do this conversion? I'm using Symfony's form system and Symfony version 5.4.9.
Entity:
class Address
{
/**
* #ORM\Column(type="string", length=7)
* #Assert\NotBlank
* #Assert\Regex(
* pattern="/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/",
* message="Deze waarde is geen geldige postcode."
* )
*/
private $postcode;
public function setPostcode(string $postcode): self
{
$this->postcode = $postcode;
return $this;
}
// other fields
}
FormType:
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('postcode', null, [
'label' => 'Postcode'
])
// other fields
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Address::class,
]);
}
}
If I understand correctly, the form system sets incoming valid and invalid values from the form directly onto my entity (in the isSubmitted method in the controller), and after that the entity is validated (in the isValid method).
So if I add the normalization in my entity's setter (which is called in isSubmitted), I would have to manually validate the incoming value before I can normalize it, but this duplicates the regex that's later executed by isValid. Same goes if I were to use an event listener or a transformer on the form, so neither of these seem to be a great solution.
How is this usually done?
I ended up using an event listener as that required the least amount of extra code.
Restricted the regex on the entity like this:
class Address
{
/**
* #ORM\Column(type="string", length=7)
* #Assert\NotBlank
* #Assert\Regex(
* pattern="/^[1-9][0-9]{3} [A-Z]{2}$/",
* message="Deze waarde is geen geldige postcode."
* )
*/
private $postcode;
public function setPostcode(string $postcode): self
{
$this->postcode = $postcode;
return $this;
}
// other fields
}
And added an event listener for the PRE_SUBMIT event on the FormType:
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('postcode', null, [
'label' => 'Postcode'
])
// other fields
;
$builder->get('postcode')->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if (!\is_string($data)) {
return;
}
if (preg_match('/^([1-9][0-9]{3})\s?([a-zA-Z]{2})$/', $data, $matches)) {
$event->setData($matches[1] . ' ' . strtoupper($matches[2]));
}
});
}
}
This does change the formatting of the value that is displayed in the form in case of a submit + invalid, but I did not mind that.
If you want to keep the displayed value exactly as the user inputted it, but transform the value in the background before it is set onto the entity, you can use a ModelTransformer instead:
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('postcode', null, [
'label' => 'Postcode'
])
// other fields
;
$builder->get('postcode')
->addModelTransformer(new CallbackTransformer(
function ($modelData) {
return $modelData;
},
function ($normData) {
if (!\is_string($normData)) {
return;
}
if (preg_match('/^([1-9][0-9]{3})\s?([a-zA-Z]{2})$/', $normData, $matches)) {
return $matches[1] . ' ' . strtoupper($matches[2]);
}
return $normData;
}
))
;
}
}
Using a ViewTransformer (->addViewTransformer) with the same code as the ModelTransformer above also changes the formatting of the value that is displayed in the form, but with more code than the event listener.

How to get Entity in custom Form type in symfony EasyAdmin

I'm trying to add custom type to JSONB field as described in documentation:
form:
fields:
- { property: 'attr', type: 'App\Form\Type\AttrType'}
And class realization:
class AttrType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title',TextType::class, array('label' => 'title'));
}
}
And it's work fine.
But I have JSONB column and I don't know how many fields are stored and their types.
QUESTION: How to get Entity in buildForm for acess attr. Needed data exist in $builder and $options I can see in var_dump().
Simplified desired result:
public function buildForm(FormBuilderInterface $builder, array $options)
{
foreach($builder->getData()->getAttr() as $key=>$value){
$builder->add($key,TextType::class, array('data' => $value));
}
}
I myself have been searching for a solution and waiting for a long time and we are not alone. It appears EasyCorp/EasyAdminBundle gave up on this.
While this may not be the answer you're looking for, so far the only solution I've found is to read the request in your custom form type; same as you would in the controller. This necessitates that the data is in the request URI somehow otherwise it won't work.
Example URI: /path/to/action/[ID] or /path/to/action/99 where '99' is the ID of the entity you're looking for.
use App\Repository\SomeRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class AttrType extends AbstractType
{
private ?Request $request = null;
private SomeRepository $repository;
public function __construct(RequestStack $requestStack, SomeRepository $repository)
{
$this->repository = $repository;
if ($req = $requestStack->getCurrentRequest()) {
$this->request = $req;
}
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if ($this->request && $id = (int) $this->request->get('id')) {
$parentFormData = $this->repository->find($id);
}
}
}
This of course requires a second lookup of the same data so it's far from elegant but it has worked in some situations for me and allowed me to keep my parent form type clean.
Hope it helps.
EDIT:
You can get the parent form data in buildView() as well but that usually never sufficed for me, hence the above solution. For anyone who is unaware of this, here it is:
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
class AttrType extends AbstractType
{
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$parentData = $form->getParent()->getData();
}
}

Extending EntityType to allow extra choices set with AJAX calls

I try to create a Symfony Custom type extending the core "entity" type.
But I want to use it with Select2 version 4.0.0 (ajax now works with "select" html element and not with hidden "input" like before).
This type should create an empty select instead of the full list of entities by the extended "entity" type.
This works by setting the option (see configureOption):
'choices'=>array()
By editing the object attached to the form it should populate the select with the current data of the object. I solved this problem but just for the view with the following buildView method ...
Select2 recognize the content of the html "select", and does its work with ajax.
But when the form is posted back, Symfony doesn't recognize the selected choices, (because there were not allowed ?)
Symfony\Component\Form\Exception\TransformationFailedException
Unable to reverse value for property path "user": The choice "28" does not exist or is not unique
I tried several methods using EventListeners or Subscribers but I can't find a working configuration.
With Select2 3.5.* I solved the problem with form events and overriding the hidden formtype, but here extending the entitytype is much more difficult.
How can I build my type to let it manage the reverse transformation of my entites ?
Custom type :
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
class AjaxEntityType extends AbstractType
{
protected $router;
public function __construct($router)
{
$this->router = $router;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->setAttribute('attr',array_merge($options['attr'],array('class'=>'select2','data-ajax--url'=>$this->router->generate($options['route']))));
}
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['attr'] = $form->getConfig()->getAttribute('attr');
$choices = array();
$data=$form->getData();
if($data instanceOf \Doctrine\ORM\PersistentCollection){$data = $data->toArray();}
$values='';
if($data != null){
if(is_array($data)){
foreach($data as $entity){
$choices[] = new ChoiceView($entity->getAjaxName(),$entity->getId(),$entity,array('selected'=>true));
}
}
else{
$choices[] = new ChoiceView($data->getAjaxName(),$data->getId(),$data,array('selected'=>true));
}
}
$view->vars['choices']=$choices;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired(array('route'));
$resolver->setDefaults(array('choices'=>array(),'choices_as_value'=>true));
}
public function getParent() {
return 'entity';
}
public function getName() {
return 'ajax_entity';
}
}
Parent form
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AlarmsType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name','text',array('required'=>false))
->add('user','ajax_entity',array("class"=>"AppBundle:Users","route"=>"ajax_users"))
->add('submit','submit');
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array('data_class' => 'AppBundle\Entity\Alarms','validation_groups'=>array('Default','form_user')));
}
/**
* #return string
*/
public function getName()
{
return 'alarms';
}
}
Problem solved.
The solution is to recreate the form field with 'choices'=>$selectedChoices in both PRE_SET_DATA and PRE_SUBMIT FormEvents.
Selected choices can be retrived from the event with $event->getData()
Have a look on the bundle I created, it implements this method :
Alsatian/FormBundle - ExtensibleSubscriber
Here is my working code which adds to users (EntityType) related to tag (TagType) ability to fill with options from AJAX calls (jQuery Select2).
class TagType extends AbstractType
{
//...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$modifyForm = function ($form, $users) {
$form->add('users', EntityType::class, [
'class' => User::class,
'multiple' => true,
'expanded' => false,
'choices' => $users,
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($modifyForm) {
$modifyForm($event->getForm(), $event->getData()->getUsers());
}
);
$userRepo = $this->userRepo; // constructor injection
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($modifyForm, $userRepo) {
$userIds = $event->getData()['users'] ?? null;
$users = $userIds ? $userRepo->createQueryBuilder('user')
->where('user.id IN (:userIds)')->setParameter('userIds', $userIds)
->getQuery()->getResult() : [];
$modifyForm($event->getForm(), $users);
}
);
}
//...
}
here's my approach based on Your bundle just for entity type in one formtype.
Usage is
MyType extends ExtensibleEntityType
(dont forget parent calls on build form and configure options)
and the class itself
abstract class ExtensibleEntityType extends AbstractType
{
/**
* #var EntityManagerInterface
*/
private EntityManagerInterface $entityManager;
/**
* ExtensibleEntityType constructor.
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function getParent()
{
return EntityType::class;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->addEventListener(FormEvents::PRE_SET_DATA, [$this, 'preSetData']);
$builder->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'preSubmit'], 50);
}
/**
* #param FormEvent $event
*/
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
$parent = $event->getForm()->getParent();
$options = $form->getConfig()->getOptions();
if (!$options['pre_set_called']) {
$options['pre_set_called'] = true;
$options['choices'] = $this->getChoices($options, $event->getData());
$parent->add($form->getName(), get_class($this), $options);
}
}
/**
* #param FormEvent $event
*/
public function preSubmit(FormEvent $event)
{
$form = $event->getForm();
$parent = $event->getForm()->getParent();
$options = $form->getConfig()->getOptions();
if (!$options['pre_submit_called']) {
$options['pre_submit_called'] = true;
$options['choices'] = $this->getChoices($options, $event->getData());
$parent->add($form->getName(), get_class($this), $options);
$newForm = $parent->get($form->getName());
$newForm->submit($event->getData());
}
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'multiple' => true,
'expanded' => true,
'choices' => [],
'required' => false,
'pre_set_called' => false,
'pre_submit_called' => false,
'validation_groups' => false,
]);
}
/**
* #param array $options
* #param $data
* #return mixed
*/
public function getChoices(array $options, $data)
{
if ($data instanceof PersistentCollection) {
return $data->toArray();
}
return $this->entityManager->getRepository($options['class'])->findBy(['id' => $data]);
}
}

Add a default role during user registration with FOSUserBundle

Version : Symfony 2.2
I'm trying to add a default role when a user register on my website. I use FOSUserBundle and i see that when a user register the role field is empty in a database.
I begin with this huge bundle and it's not very easy to understand. So i read all the documentation and i'm not sur what to do.
For now, i create an Event to add this role dynamically, but it doesn't work (i have no error but my database is still empty) I'm not even sur this is the good way to do that ?
My Event :
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class AddDefaultRoleListener implements EventSubscriberInterface {
private $container;
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* {#inheritDoc}
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => 'onAddDefaultRoleSuccess',
);
}
public function onAddDefaultRoleSuccess(FormEvent $event)
{
$doctrine = $this->container->get('doctrine');
$em = $doctrine->getManager();
$user = $event->getForm()->getData();
$user->addRole('ROLE_USER');
//$user->setRoles(array('ROLE_USER'));
$em->persist($user);
}
}
As you see i create a simple event which listen on REGISTRATION_SUCCESS, but nothing seems to work. It's my first try with Events and services. So if someone has an advice, i'll take it :)
The recommended way to do it as indicated by a main contributor to the FOSUserBundle (in the comment here linked) is to register an Event Listener on the REGISTRATION_SUCCESS event and use the $event->getForm()->getData() to access the user and modify it.
Following those guidelines, I created the following listener (which works!):
<?php
// src/Acme/DemoBundle/EventListener/RegistrationListener.php
namespace Acme\DemoBundle\EventListener;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Listener responsible for adding the default user role at registration
*/
class RegistrationListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => 'onRegistrationSuccess',
);
}
public function onRegistrationSuccess(FormEvent $event)
{
$rolesArr = array('ROLE_USER');
/** #var $user \FOS\UserBundle\Model\UserInterface */
$user = $event->getForm()->getData();
$user->setRoles($rolesArr);
}
}
Also, the service needs to be registered as follows:
// src/Acme/DemoBundle/Resources/config/services.yml
services:
demo_user.registration_listener:
class: Acme\DemoBundle\EventListener\RegistrationListener
arguments: []
tags:
- { name: kernel.event_subscriber }
Notice that adding a default role in the User class __construct() may have some issues as indicated in this other answer.
What i have done is override the entity constructor:
Here a piece of my Entity/User.php
public function __construct()
{
parent::__construct();
// your own logic
$this->roles = array('ROLE_USER');
}
This is the lazy way. If you want the right and better way see the #RayOnAir answer
I think #RayOnAir solution is right way of doing this. But it will not work due to FOS default role handling
to make possible to persist default role in database one need to override User::setRoles() method (add it to your User entity):
/**
* Overriding Fos User class due to impossible to set default role ROLE_USER
* #see User at line 138
* #link https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Model/User.php#L138
* {#inheritdoc}
*/
public function addRole($role)
{
$role = strtoupper($role);
if (!in_array($role, $this->roles, true)) {
$this->roles[] = $role;
}
return $this;
}
Tested under:
Symfony version 2.3.6,
FOSUserBundle 2.0.x-dev
You can add an Event Subscriber to a Form Class and use the form event "formEvents::POST_SUBMIT"
<?php
//src/yourNS/UserBundle/Form/Type/RegistrationFormType.php
use Symfony\Component\Form\FormBuilderInterface;
use FOS\UserBundle\Form\Type\RegistrationFormType as BaseType;
use yourNS\UserBundle\Form\EventListener\AddRoleFieldSubscriber;
class RegistrationFormType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
// add your custom field
$builder->add('firstName')
->add('lastName')
->add('address')
//...
//...
->add('phone');
$builder->addEventSubscriber(new AddRoleFieldSubscriber());
}
public function getName()
{
return 'yourNS_user_registration';
}
}
Now the logic for adding the role field resides in it own subscriber class
<?php
//src/yourNS/UserBundle/Form/EventListener/AddRoleFieldSubscriber.php
namespace yourNS\UserBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class AddRoleFieldSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(FormEvents::POST_SUBMIT => 'setRole');
}
public function setRole(FormEvent $event)
{
$aRoles = array('ROLE_USER');
/** #var $user \FOS\UserBundle\Model\UserInterface */
$user = $event->getForm()->getData();
$user->setRoles($aRoles);
}
}
Ok now it's working with that :
public function onAddDefaultRoleSuccess(FilterUserResponseEvent $event)
{
$doctrine = $this->container->get('doctrine');
$em = $doctrine->getManager();
$user = $event->getUser();
$user->addRole('ROLE_BLOGGER');
$em->persist($user);
$em->flush();
}
I change my listener and know use REGISTRATION_COMPLETED. If someone has a better idea to do that, don't hesitate :)
public function __construct()
{
parent::__construct();
$this->setRoles(["ROLE_WHATEVER"]);
}

Cross reference in Symfony2 form not work as expected

This is my first question here, so please excuse any mistakes - I'll try to avoid them the next time. ;-)
I've written a custom RegistrationFormType for the FOSUserBundle. This form handles - in addition to the default fields of the bundle - a PlayerType. This PlayerType itself again contains a PlayerSkillsType. Here the classes:
class RegistrationFormType extends BaseType
{
public function buildForm(FormBuilder $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add('player', new PlayerType());
}
public function getName()
{
return 'signup_form';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\AcmeBundle\Entity\User',
);
}
}
class PlayerType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('firstname');
$builder->add('lastname');
$builder->add('age');
$builder->add('playerSkills', new PlayerSkillsType());
}
public function getName()
{
return 'player_form';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\AcmeBundle\Entity\Player',
);
}
}
class PlayerSkillsType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('tackling');
$builder->add('passing');
$builder->add('shooting');
}
public function getName()
{
return 'playerSkills_form';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\AcmeBundle\Entity\PlayerSkills',
);
}
}
/**
* #ORM\Entity
*/
class Player
{
/**
* #ORM\OneToOne(targetEntity="PlayerSkills", cascade={"persist"})
*
* #var PlayerSkills
*/
private $playerSkills;
}
/**
* #ORM\Entity
*/
class PlayerSkills
{
/**
* #ORM\OneToOne(targetEntity="Player", cascade={"persist"})
*
* #var Player
*/
private $player;
}
(I've left out getters and setters and unimportant properties and methods.)
This is working fine so far, the form is shown and persisted. Now, my problem is, that after persisting the data, the PlayerSkills entity in the data is missing the reference back to the Player entity.
I think it's something that I need to tell the PlayerSkillsType that it shall also add the reference in the form builder..? Or maybe this is issue in the Doctrine annotations?
Any hint is very appreciated! :-)
The problem could come from the initialization of your data and/or doctrine mapping.
The form will create the data_class if none is passed using $form->setData.
When you submit the form and bind data, It will call $player>setPlayerSkills($playerSkill),
but it won't call $playerSkill->setPlayer($player);
Depending of the owning side of your oneToOne association, you should call one of the two methods so that Doctrine will be aware of this association ( http://docs.doctrine-project.org/projects/doctrine-orm/en/2.0.x/reference/association-mapping.html#owning-side-and-inverse-side ).
Try to modify your annotation mapping in PlayerSkills to introduce the inversedBy information also ( http://docs.doctrine-project.org/projects/doctrine-orm/en/2.0.x/reference/association-mapping.html#one-to-one-bidirectional ).
It should be something like this:
/**
* #ORM\OneToOne(targetEntity="Player", mappedBy="playerSkills", cascade={"persist"})
*
* #var Player
*/
private $player;
Same thing for the Player class:
/**
* #ORM\OneToOne(targetEntity="PlayerSkills", inversedBy="player" cascade={"persist"})
*
* #var PlayerSkills
*/
private $playerSkills;
Last, you can code your methods to automatically synchronize inverse side, as explained here: http://docs.doctrine-project.org/projects/doctrine-orm/en/2.0.x/reference/association-mapping.html#picking-owning-and-inverse-side .

Resources