I am facing a really weird situation a user with ROLE_ADMIN gets logged out as soon as I change the name and even if i dont change anything and press save button it gets logged out. if i change the user's role directly in database to ROLE_USER the same code works fine and user is not logged out.
Following is the controller that takes care of the profile update
/**
* #Route("/profile", name="profile")
*/
public function profileAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$userInfo = $this->getUser();
//create the form object
$profileForm = $this->createForm(UserType::class, $userInfo);
$profileForm->handleRequest($request);
//check data validity
if($profileForm->isValid()){
$em->persist($userInfo);
$em->flush();
$this->get('session')->getFlashBag()->add(
'success',
'Your profile information has been updated'
);
return $this->render('AppBundle:admin/user:admin-edit.html.twig',array(
'edit_form' => $profileForm->createView()
));
}
// render registration form
return $this->render('AppBundle:admin/user:admin-edit.html.twig',array(
'edit_form' => $profileForm->createView()
));
}
}
This is my security.yml
security:
encoders:
# Our user class and the algorithm we'll use to encode passwords
# http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password
AppBundle\Entity\User: bcrypt
providers:
# Simple example of loading users via Doctrine
# To load users from somewhere else: http://symfony.com/doc/current/cookbook/security/custom_provider.html
database_users:
entity: { class: AppBundle:User, property: username }
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
http_basic: ~
anonymous: ~
logout: ~
guard:
authenticators:
- app.form_login_authenticator
- app.facebook_authenticator
# by default, use the start() function from FormLoginAuthenticator
entry_point: app.form_login_authenticator
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, roles: ROLE_ADMIN }
- { path: ^/user, roles: ROLE_USER }
UPDATE 1
This is my UserType
namespace AppBundle\Form;
use AppBundle\Form\EventListener\AddDepartmentDegreeCourseFieldSubscriber;
use AppBundle\Form\EventListener\AddProfileFieldSubscriber;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserType extends AbstractType
{
private $addDepartmentDegreeCourseFieldSubscriber;
private $addProfileFieldSubscriver;
function __construct(AddDepartmentDegreeCourseFieldSubscriber $subscriber, AddProfileFieldSubscriber $fields)
{
$this->addDepartmentDegreeCourseFieldSubscriber = $subscriber;
$this->addProfileFieldSubscriver = $fields;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventSubscriber($this->addProfileFieldSubscriver);
$builder->addEventSubscriber($this->addDepartmentDegreeCourseFieldSubscriber);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\User'
));
}
}
This is my AddProfileFieldSubscriber
namespace AppBundle\Form\EventListener;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Validator\Constraints\NotBlank;
class AddProfileFieldSubscriber implements EventSubscriberInterface
{
protected $authorizationChecker;
function __construct(AuthorizationChecker $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}
public static function getSubscribedEvents()
{
// Tells the dispatcher that you want to listen on the form.pre_set_data
// event and that the preSetData method should be called.
return array(FormEvents::PRE_SET_DATA => 'preSetData');
}
public function preSetData(FormEvent $event)
{
$user = $event->getData();
$form = $event->getForm();
if($user){
$form->add('firstName', TextType::class);
$form->add('lastName', TextType::class);
$form->add('password', PasswordType::class, array(
'mapped' => false
));
$form->add('profileImage', FileType::class, array(
'data_class' => null
));
if (in_array("ROLE_USER", $user->getRoles())) {
$form->add('contactNumber', TextType::class);
$form->add('gender', ChoiceType::class, array(
'choices' => array(
'Male' => 'm',
'Female' => 'f'
),
'placeholder' => 'provide_gender'
));
$form->add('college', EntityType::class, array(
'placeholder' => 'provide_college',
'class' => 'AppBundle\Entity\College')
);
$form->add('interest', EntityType::class, array(
'class' => 'AppBundle\Entity\Interest',
'multiple' => true,
'expanded' => false,
'by_reference' => false,
)
);
}
if($this->authorizationChecker->isGranted('ROLE_ADMIN') ) {
$form->add('isActive', ChoiceType::class, array(
'choices' => array(
'account_active' => '1',
'account_inactive' => '0'
),
'placeholder' => 'provide_status'
));
}
}
//if the selected user has role_user only then display the following fields in edit profile view
else {
$form->add('username', EmailType::class);
$form->add('password', PasswordType::class, array(
'constraints' => array(new NotBlank(array(
'message' => 'user.password.not_blank'
)
),),
));
}
}
}
This is AddDepartmentDegreeCourseFieldSubscriber
namespace AppBundle\Form\EventListener;
use AppBundle\Entity\Degree;
use AppBundle\Entity\Department;
use Doctrine\ORM\EntityManager;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormInterface;
class AddDepartmentDegreeCourseFieldSubscriber implements EventSubscriberInterface
{
protected $em;
function __construct(EntityManager $em)
{
$this->em = $em;
}
public static function getSubscribedEvents()
{
// Tells the dispatcher that you want to listen on the form.pre_set_data
// event and that the preSetData method should be called.
return array(
FormEvents::PRE_SET_DATA => 'onPreSetData',
FormEvents::PRE_SUBMIT => 'onPreSubmit'
);
}
protected function addElements(FormInterface $form, Department $departments = null, Degree $degree = null)
{
// Add the department element
$form->add('department', EntityType::class, array(
'data' => $departments,
'placeholder' => 'provide_department',
'class' => 'AppBundle\Entity\Department')
);
// Degree are empty, unless we actually supplied a department
$degree = array();
if ($departments) {
// Fetch the courses from specified degree
$repo = $this->em->getRepository('AppBundle:Degree');
$degree = $repo->findByDepartment($departments, array('name' => 'asc'));
}
// Add the province element
$form->add('degree', EntityType::class, array(
'placeholder' => 'provide_degree',
'class' => 'AppBundle\Entity\Degree',
'choices' => $degree)
);
// Cities are empty, unless we actually supplied a province
$courses = array();
if ($degree) {
// Fetch the cities from specified province
$repo = $this->em->getRepository('AppBundle:Course');
$courses = $repo->findByDegree($degree, array('name' => 'asc'));
}
// Add the Course element
$form->add('course', EntityType::class, array(
'class' => 'AppBundle\Entity\Course',
'choices' => $courses,
));
}
function onPreSubmit(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
if (isset($data['degree'])) {
// Note that the data is not yet hydrated into the entity.
$degree = $this->em->getRepository('AppBundle:Degree')->find($data['degree']);
$department = $this->em->getRepository('AppBundle:Department')->find($data['department']);
$this->addElements($form, $department, $degree);
}
}
function onPreSetData(FormEvent $event) {
//echo "before submit";die;
$user = $event->getData();
$form = $event->getForm();
if($user){
//if the selected user has role_user only then display the following fields in edit profile view
if (in_array("ROLE_USER", $user->getRoles())) {
$degree = ( !empty($user) && !empty($user->getCourse())) ? $user->getCourse()->getDegree() : null;
$departments = ( !empty($user) && !empty($user->getCourse())) ? $user->getCourse()->getDegree()->getDepartment() : null;
$this->addElements($form, $departments, $degree);
}
}
}
}
At this point I am really clueless what could be causing this, I will really appreciate any help here...
There's quite a few things wrong in what you've provided. Elements of which may contribute to the behaviour you're seeing.
Security rules
In security.yml, you have this line:
- { path: ^/admin/, roles: ROLE_ADMIN }
Which means that any use without ROLE_ADMIN is going to get shot back to the login screen if they access this pattern.
Meanwhile, in your controller, you're always directing the user back to an Admin based route:
/*
* #Route("admin/profile", name="admin_profile")
*/
Which means, no matter what they do, they're always sent to an admin pattern. This means that if they change their role, they'll be kicked by the firewall.
Controller
In your controller you're binding the user entity to a formtype, fine. But then the way you're coding implies that you don't understand how this works.
When you call handleRequest, Symfony pulls the data from the form (if its been submitted), and 'merges' it with the entity that you passed to it. This means that you don't have to call any setters on the object, as all that's already been done for you.
User entity
What you should have on your User entity, is a PlainPassword field, and a Password field. The form only maps to the PlainPassword field.. then, in your controller, take the PlainPassword value, encode it, set it to the entity's Password field, and make sure you clear the PlainPassword value (you dont want to store that). If you've implemented UserInterface (which you should have) on your custom user entity, you should have a method called eraseCredentials, this is what this is for. Here is an example.
The upshot of this is that when you're checking for something, like say, a new password, you only have to do this:
if($profileForm->isValid()){
// $userInfo is populated by handleRequest
if ($userInfo->getPlainPassword()) {
// do your encoding/erasing credentials here
}
// ...
}
What I would also suggest, is that you write a proper UserManager class to handle most of those things. It centralises things and makes it easier to debug. Heres an example
If you use something such as as I've written in the example, it also means that when you want to update a users info, you only have to call $userManager->updateUser($user) and it'll do all the donkey work for you.
Related
I'm using Symfony 3.3 and im getting this TransformationFailedException Error, when i load my profile page:
Unable to transform value for property path "postalcode": Expected a numeric.
The postalcode value for this user in the database is:
'34125abc'
The postalcode attribute defined in UserProfile Entity:
/**
* #ORM\Column(type="string")
*/
private $postalcode;
My ProfileController:
class ProfileController extends Controller{
/**
* #Route("/edit_profile", name="edit_profile")
*/
public function profileAction(Request $request){
$profile = $this->getDoctrine()->getManager()->getRepository('AppBundle:UserProfile')->findOneBy(['user_id' => $this->getUser()->getUserId()]);
// If no UserProfile exists, create a UserProfile Object to insert it into database after POST
if(null === $profile){
$profile = new UserProfile();
$profile->setUserId($this->getUser()->getUserId());
}
$form = $this->createForm(EditProfileFormType::class);
$form->setData($profile);
// only handles data on POST
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
$result = $this->forward('AppBundle:API\User\Profile:update_profile', array(
'profile' => $profile
));
if(200 === $result->getStatusCode()){
$this->addFlash('success', "Profile successfully created!");
}
}
return $this->render(':User/Profile:edit_profile.html.twig', [
'EditProfileForm' => $form->createView(),
]);
}
}
My EditProfileFormType:
class EditProfileFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', ChoiceType::class, array(
'choices' => array(
'Mr' => 'Mr',
'Mrs' => 'Mrs'
)
))
->add('firstName')
->add('lastName')
->add('street')
->add('postalcode', NumberType::class)
->add('city')
->add('telephone')
->add('mobile')
->add('company')
->add('birthday' , BirthdayType::class)
->add('callback', CheckboxType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\UserProfile',
'validation_groups' => array('edit_profile')
]);
}
public function getBlockPrefix()
{
return 'app_bundle_edit_profile_form_type';
}
}
So the problem here seems to be the not numeric string value in the databse
'34125abc'
which is stored in the $profile entity object and is passed to the form by $form->setData($profile); So when the data is being set, the error is thrown because of the Numbertype in this line ->add('postalcode', NumberType::class). Is there a way to pass the postalcode value to the form, even if it's not numeric and only check the Numbertype, when the form is submitted? Because i don't need validation, when I pass data to the form. Just when, it's submitted.
The solution was quite simple, but hard to find out.. I changed
->add('postalcode', NumberType::class)
to
->add('postalcode', TextType::class)
in my EditProfileFormType.php. Why? Because the form builder just need to know the type of the field in the database. It's shouldn't care in this case if it is numeric or not, because thats the task of the model restriction. In this case it is a string, so it is Texttype in a form. All the form type's are applied, when the form is set, but the validation groups are just validated, when the form is submitted! That's exactly they way it should be!
I'm using the select2 plugin with ajax to have a dynamic field on my form, but when i submit the it return me an error "This value is not valid", which is normal cause i use the ChoiceType with an empty array() in the choices options on creation. According to this part of the symfony doc, the form event is my savior, so trying to use it but it look like something wrong with my code and can't really see what.
So My Question Is :
HOW to pass the choices possibility to the field, for the form to be valid.
My form Type
class ArticleType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//My other field
//My functions to add the field with the possible choices
$formModifier = function (FormInterface $form, $imageValue) use ($options) {
if ($imageValue !== null) {
$listImages = $this->getChoiceValue($imageValue, $options);
if (!$listImages) {
$form->get('image')->addError(new FormError(
'Nous n\'avons pas pu trouver l\'image, veuiller choisir une autre'
));
}
} else {
$listImages = array();
}
//die(var_dump($listImages)); //Array of Image
$form->add('image', ChoiceType::class, array(
'attr' => array(
'id' => 'image'),
'expanded' => false,
'multiple' => false,
'choices' => $listImages));
};
$formModifierSubmit = function (FormInterface $form, $imageValue) use ($options) {
if ($imageValue !== null) {
$listImages = $this->getChoiceValue($imageValue, $options);
if (!$listImages) {
$form->get('image')->addError(new FormError(
'Nous n\'avons pas pu trouver l\'image, veuiller choisir une autre'
));
}
} else {
$form->get('image')->addError(new FormError(
'Veuillez choisir une image s.v.p.'
));
}
//die(var_dump($listImages)); //Array of Image object
$config = $form->get('image')->getConfig();
$opts = $config->getOptions();
$chcs = array('choices' => $listImages);
//die(var_dump($chcs)); //output an array with a 'choices' keys with array value
array_replace($opts, $chcs); //not work
//array_merge($opts, $chcs); //not work
//die(var_dump($opts)); //replacements/merge are not made
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
// this would be the entity Article
$data = $event->getData();
$formModifier($event->getForm(), $data->getImage());
}
);
//$builder->get('image')->addEventListener( //give error cause the field image don't exist
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($formModifierSubmit) {
$imageVal = $event->getData();
//die(var_dump($imageVal)); //return all the submitted data field in an array
//But when change this event to Submit it return the Article model populated by the submitted data, EXCEPT the image field which have null as value
$formModifierSubmit($event->getForm(), $imageVal['image']);
}
);
}
public function getChoiceValue($imageValue, $options)
{
$listImages = $options['em']->getRepository('AlmotivAppBundle:Image')->findBy(array(
'id' => $imageValue
));
return $listImages; //array of Image object
}
[...]
}
For Info
My image field is not depending on any other field like the doc example, so i need to populate the choices options on PRE_SUBMIT event to give the possible choice.
And also image have a ManyToOne relation in my Article entity
class Article implements HighlightableModelInterface
{
//some properties
/**
* #ORM\ManyToOne(targetEntity="Image\Entity\Path", cascade={"persist"})
* #Assert\Valid()
*/
private $image;
}
If i'm in the bad way let me know cause i'm out of idea now, i try much thing, like
array_replace with the options in the configuration of the field but didn't wrong.
make an ajax request to the url of the form action url : $form.attr('action'), i think it will load the choices option with the possible of <option> but my select is still returned with none <option>.
and much more (can't remmenber).
And also i'm using the v3.1 of the framework with the v4.0.3 of the select2 plugin, if need more info just ask and thx for reading and trying help.
Edit
Just add some info to be more clear
You making things way too complicated. In your documentation example they add eventListener for already existing form field ('sport') and you are adding it to only later added field which does not exist (your 'image' field and 'position' field from the documentation example).
You should use EntityType and if you need (which I'm not if sure you are) filter your images using query_builder option, for validation add constraints (example with controller).
class ArticleType extends AbstractType {
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// $builder
// My other field
$imageFieldFunction = $this->getImageFieldFunction();
$builder->addEventListener(FormEvents::PRE_SET_DATA, $imageFieldFunction);
$builder->addEventListener(FormEvents::PRE_SUBMIT, $imageFieldFunction);
}
private function getImageFieldFunction()
{
return function(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
//when your data_class is Article
$image = $data->getImage();//depending on your Article class
/*if you are using data_class => null
$image = $data['image'];
*/
$imageId = $image ? $image->getId() : 0;
$builder->add('image', EntityType::class , array(
'class' => 'AlmotivAppBundle:Image',
'attr' => array(
'id' => 'image'
) ,
'expanded' => false,
'multiple' => false,
'constraints' => new NotBlank(),
'query_builder' => function (EntityRepository $er) use ($imageId) {
return $er->createQueryBuilder('i')
->where('i.id = :image_id')
->setParameter('image_id', $imageId);
}
));
}
}
}
In Symfony 2.8 I've got Movie entity with actors field, which is ArrayCollection of entity Actor (ManyToMany) and I wanted the field to be ajax-loaded Select2.
When I don't use Ajax, the form is:
->add('actors', EntityType::class, array(
'class' => Actor::class,
'label' => "Actors of the work",
'multiple' => true,
'attr' => array(
'class' => "select2-select",
),
))
It works, and this is what profiler displays after form submit: http://i.imgur.com/54iXbZy.png
Actors' amount grown up and I wanted to load them with Ajax autocompleter on Select2. I changed form to ChoiceType:
->add('actors', ChoiceType::class, array(
'multiple' => true,
'attr' => array(
'class' => "select2-ajax",
'data-entity' => "actor",
),
))
//...
$builder->get('actors')
->addModelTransformer(new ActorToNumberModelTransformer($this->manager));
I made DataTransformer:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Persistence\ObjectManager;
use CompanyName\Common\CommonBundle\Entity\Actor;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class ActorToNumberModelTransformer implements DataTransformerInterface
{
private $manager;
public function __construct(ObjectManager $objectManager)
{
$this->manager = $objectManager;
}
public function transform($actors)
{
if(null === $actors)
return array();
$actorIds = array();
$actorsArray = $actors->toArray();
foreach($actorsArray as $actor)
$actorIds[] = $actor->getId();
return $actorIds;
}
public function reverseTransform($actorIds)
{
if($actorIds === null)
return new ArrayCollection();
$actors = new ArrayCollection();
$actorIdArray = $actorIds->toArray();
foreach($actorIdArray as $actorId)
{
$actor = $this->manager->getRepository('CommonBundle:Actor')->find($actorId);
if(null === $actor)
throw new TransformationFailedException(sprintf('An actor with id "%s" does not exist!', $actorId));
$actors->add($actor);
}
return $actors;
}
}
And registered form:
common.form.type.movie:
class: CompanyName\Common\CommonBundle\Form\Type\MovieType
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: form.type }
But seems like the reverseTransform() is never called. I even put die() at the beginning of it - nothing happened. This is, what profiler displays after form submit: http://i.imgur.com/qkjLLot.png
I tried to add also ViewTransformer (code here: pastebin -> 52LizvhF - I don't want to paste more and I can't post more than 2 links), with the same result, except that reverseTransform() is being called and returns what it should return.
I know that this is an old question, but I was having a very similar problem. It turned out that I had to explicitly set the compound option to false.
That is to say, for the third parameter to the add() method, you need to add 'compound => false'.
Situation
I have an entity Item that can has a collection of Items (preconditions) with the same class Item. I used this documentation for making a form with the collection type. I want to make almost the same like in the documentation. But my situation is:
My collecting items are self-referenced to the main item
I want to add already existing items from an existing list, not creating new ones
With prototyping, i'm adding a new choosed collection item with JS with the same HTML structure like the default repopulation from symfony's form does. But naturally with the replaced choosed relation ID.
Persisting the relations is working, but only with a manual hack, and with missing datas into the $form object.
Problems
When i add an relation, with js prototyping and post the data, a new entity with the correct Item class is added in the ArrayCollection. This new entity returns with the posted ID as key as ArrayCollection item, but an empty entity itself as value. (New one is with key 1 below)
#collection: ArrayCollection {#829 ▼
-elements: array:2 [▼
5 => Item {#834 ▼
#id: 5
-title: "foo"
+preconditions: PersistentCollection {#836 ▶}
}
1 => Item {#890 ▼
#id: null
-title: null
+preconditions: ArrayCollection {#886 ▶}
}
]
}
Problem 1: This results on persisting that an unknown new entity want to be inserted (Because entity with ID null doesn't exists in the DB), and then with invalid null datas. It must not be inserted because its actually already existing.
Problem 2: Before persist i must modify the ArrayCollection property by hand, invalid entities i have to refill. So the entries are valid for relation persisting.
Problem 3: When i modify the ArrayCollection this way, i need this datas to be accessable also in twig. And an update into $form will result an Exception that i can’t modify the $form after it was submitted. Then i saw here the suggestion of an event listener, but even with an event listener i cant’ update the $form because either $form is not prepared or is too late to update.
Questions
Is following the right way to perform my goal, or does exist a better variant?
How can the new entities in the ArrayCollection be already be filled correctly after a post?
Code
Controller:
public function newesiAction($id, Request $request)
{
$em = $this->getDoctrine()->getManager();
// For repopulating an existing entity
$item = 0 < $id ? $em->getRepository('AppBundle:Item')->find($id) : new Item();
$form = $this->createForm(new ItemType($this->container), $item);
$form->add('submit', 'submit', array('label' => 'speichern'));
$form->handleRequest($request);
/* After handleRequest, the posted (also new) precondition entities are given. But not as expected. So we update the array collection.
* Not as expected meant: New relation entities becomes new entities with NULL as id. So entities with NULL as id wantet to be INSERTED, what is not our goal.
*/
foreach($item->preconditions as $precondition_entity_id => $precondition_entity) {
$precondition_entity_inner_id = $precondition_entity->getId();
if(null === $precondition_entity_inner_id) {
$item->preconditions[$precondition_entity_id] = $em->getRepository('AppBundle:Item')->find($precondition_entity_id);
}
}
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($item);
$em->flush();
}
return $this->render('AppBundle:Item:edit.html.twig', array(
'form' => $form->createView(),
));
}
ItemType:
<?php
// src/AppBundle/Form/Type/ItemType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
class ItemType extends AbstractType
{
public function __construct($container) {
$this->container = $container;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title');
$builder->add('preconditions', 'collection', array(
'type' => new PreconditionType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
));
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
// $em = $this->container->get('doctrine')->getManager();
// $form = $event->getForm();
// // Replace the new related entity that have NULL in its ID, with an fully loaded entity
// $entity = $event->getData();
// foreach($entity->preconditions as $precondition_entity_id_posted => $possible_empty_entity) {
// $precondition_entity_id_loaded = $possible_empty_entity->getId();
// if(null === $precondition_entity_id_loaded) {
// $entity->preconditions[$precondition_entity_id_posted] = $em->getRepository('AppBundle:Item')->find($precondition_entity_id_posted);
// }
// }
// $form->get('preconditions')->setData($entity->preconditions);
});
$builder->add('available_items', 'entity', array(
'class' => 'AppBundle\Entity\Item',
'multiple' => true,
'expanded' => false,
'mapped' => false
));
$builder->add('add_item_precondition', 'button', array(
'attr' => array(
'onclick' => "addPreconditionForm('" . $this->getName() . "_available_items')"
)
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Item',
));
}
public function getName()
{
return 'item';
}
}
?>
Precondition type:
<?php
// src/AppBundle/Form/Type/PreconditionType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PreconditionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('relation', 'hidden', array(
'mapped' => false,
'required' => false
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Item'
));
}
public function getName()
{
return 'precondition';
}
}
?>
I had the possibility to discuss this with an employee of sensiolabs. Its officially not possible to automate this with the built-in tools, you must loop the collection entries and re-get the incomplete entities.
So the answers to my questions:
Yes, it is the correct way go get the missing entities
No, its not possible to get those automatically
I have a form which contains three objects:
$builder
->add('customer', new CustomerType())
->add('shippingAddress', new AddressType())
->add('billingAddress', new AddressType())
->add('sameAsShipping', 'checkbox', ['mapped' => false])
;
Each of the embedded forms has their own validation constraints and they work. In my main form, I have cascade_validation => true so that all of the embedded form validation constraints are applied. This also works.
I am having trouble 'disabling' the validation on the billingAddress form if the sameAsShipping checkbox is enabled. I can't make the validation in the AddressType form conditional because it always needs to be enforced for the shippingAddress form.
I've solved this same problem by using validation groups.
First, this is important: use the validation_groups option in your AddressType to set the validation groups of every constraint of each field in the type:
<?php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Form\FormBuilderInterface;
class AddressType extends \Symfony\Component\Form\AbstractType
{
function buildForm(FormBuilderInterface $builder, array $options)
{
$groups = $options['validation_groups'];
$builder->add('firstName', 'text', ['constraints' => new Assert\NotBlank(['groups' => $groups])]);
$builder->add('lastName', 'text', ['constraints' => new Assert\NotBlank(['groups' => $groups])]);
}
}
Then, in the parent form pass different validation groups to the two fields:
<?php
$formBuilder = $this->get('form.factory')
->createNamedBuilder('checkout', 'form', null, [
'cascade_validation' => true,
])
->add('billingAddress', 'address', [
'validation_groups' => 'billingAddress'
])
->add('shippingAddress', 'address', [
'validation_groups' => 'shippingAddress'
]);
Then, determine determine your validation groups by looking at the value of the checkbox.
if ($request->request->get('sameAsShipping')) {
$checkoutValidationGroups = ['Default', 'billingAddress'];
} else {
$checkoutValidationGroups = ['Default', 'billingAddress', 'shippingAddress'];
}
You can then validate only either the billingAddress or the shippingAddress, or both using the validation group mechanism.
I chose to use a button:
$formBuilder->add('submitButton', 'submit', ['validation_groups' => $checkoutValidationGroups]);
Create a form model (I use it in nearly every form, but this code here is not tested):
/**
* #Assert\GroupSequenceProvider()
*/
class YourForm implements GroupSequenceProviderInterface {
/**
* #Assert\Valid()
*/
private $customer;
/**
* #Assert\Valid()
*/
private $shippingAddress;
/**
* #Assert\Valid(groups={'BillingAddressRequired'})
*/
private $billingAddress;
private $billingSameAsShipping;
public function getGroupSequence() {
$groups = ['YourForm'];
if(!$this->billingSameAsShipping) {
$groups[] = 'BillingAddressRequired';
}
return $groups;
}
}
Try to use meaningful names. sameAsShipping is hard to understand. Read the if-condition in getGroupSequence: if not billing (address) same as shipping (address) then billing address required.
That's all, clear code in my opinion.