[RESOLU] [Symfony2] Form field à sauver et utilisé dans Query Builder

Modérateur PHPfrance
Modérateur PHPfrance | 6373 Messages

24 avr. 2013, 15:58

Hello,

j'ai un petit soucis et je ne sais pas de quel façon je suis censé faire pour le prendre en charge.

J'ai une entité "ClassSession", elle contient une relation vers un Cours, et une relation ManyToMany vers des Etudiants, afin d'indiquer ceux qui étaient présent à la session en question.

Voici l'entité:
    class ClassSession {
        /**
         * @ORM\ManyToMany(targetEntity="Student", inversedBy="classSessions")
         * @ORM\JoinTable(name="class_session_students")
         * @Assert\NotNull
         */
        protected $classSessionStudents;

       /**
         * @ORM\ManyToOne(targetEntity="Course", inversedBy="classSessions")
         * @ORM\JoinColumn(name="fk_course", referencedColumnName="id", nullable=false)
         */
        private $course;
    }
Dans le formulaire associé, j'envoie l'ID du cours lié via l'url afin de n'afficher des cases à cocher que pour les Etudiants inscrits à ce cours, voici le formulaire (un bout):
public function __construct(RegistryInterface $doctrine, $courseId = null, $organizationBranchId = null, Teacher $currentTeacher = null) {
        $this->courseId = $courseId;
        $this->doctrine = $doctrine;
        $this->organizationBranchId = $organizationBranchId;
        $this->currentTeacher = $currentTeacher;
    }

    public function buildForm(FormBuilderInterface $builder, array $options) {
      $builder
        ->add('classSessionStudents', 'entity', array(
                      'class' => 'VirguleMainBundle:Student',
                      'query_builder' => function(EntityRepository $er) use ($courseId) {
                                          return $er->createQueryBuilder('s')
                                              ->innerJoin('s.courses', 'c2', 'WITH', 'c2.id = ?1')
                                              ->setParameters(array('1' => $courseId));
                                          },
                      'expanded' => true,
                      'multiple' => true,
                      'property_path' => 'classSessionStudents',
                      'property' => 'fullname')       
                  )

        ->add('course_id', 'hidden', array(
            'data' => $this->courseId,
            'mapped' => false))
        ;
    }
}
ça, ça fonctionne bien. Le problème c'est au traitement du formulaire soumis, dans le createAction:
public function createAction(Request $request) {
        $entity = new ClassSession();
       
        $organizationBranchId = $this->getSelectedOrganizationBranchId();
        $currentTeacher = $this->getConnectedUser();
       
        $form = $this->createForm(new ClassSessionType($this->getDoctrine(), $courseId, $organizationBranchId, $currentTeacher), $entity);
        $form->bind($request);
}
A l'appel du createForm, je ne peux pas lui passer le $courseId vu qu'il est dans le formulaire... et comme il est nul, le QueryBuilder ne fonctionne pas, du coup toutes mes infos sont sauvés sauf les entrées de la relation ClassSession <-> Student

Quelle est la bonne façon de faire dans ce cas là ? Créer une fois le formulaire, binder avec la requête, puis recréer le formulaire en passant l'ID du cours récupéré du premier ne me paraît pas très élégant.

Pour être honnête j'ai déjà posé la question sur le forum Symfony sans succès: http://forum.symfony-project.org/viewto ... 23&t=69441

J'ai beaucoup cherché mais pas trouvé d'exemples de ce type, je ne sais même pas trop comment formulé la recherche d'ailleurs.

Merci d'avance ! :)

Mammouth du PHP | 568 Messages

24 avr. 2013, 16:30

Salut Ouckileou,

Je n'arrive pas bien à comprendre ce que tu souhaite faire.

Concrètement, tu as une entité "ClassSession" contenant deux propriétés, la salle de "Cours" et la liste des "Student", arrête moi si je me trompe ?
A l'appel du createForm, je ne peux pas lui passer le $courseId vu qu'il est dans le formulaire... et comme il est nul, le QueryBuilder ne fonctionne pas, du coup toutes mes infos sont sauvés sauf les entrées de la relation ClassSession <-> Student
Comment ça $courseId est dans le formulaire ? tu le passe via le construct donc oui et évidement il est null lors de la création d'une ClassSession...je ne comprend pas vraiment le soucis en fait ici, cela me parait normal.

Le but c'est quoi concrètement ? je vois ça en deux étapes mais peut être ais-je tort du à un manque de compréhension de ton besoin.

1/ créé une ClassSession et définir la salle de cours ainsi que la liste des students
2/ définir quels students étaient présent à une ClassSession via un formulaire spécifique (case à cocher) ?

Si tu pouvais m'éclaircir ces points je pourrais peut-être t'orienter, mais a priori c'est plus au niveau de l'organisation des étapes de formulaires que tu coincent.

++

Modérateur PHPfrance
Modérateur PHPfrance | 6373 Messages

24 avr. 2013, 17:18

Ok je vais essayer de préciser, voici mes entités:

Course: c'est un cours dans le planning, ça défini le jour de la semaine, la salle, heure début - heure fin.. Ainsi qu'une relation ManyToMany vers l'entité Student, pour savoir lesquels sont inscrits à ce cours.
Ca donne par exemple: Cours #1 - chaque mardi de 18h30 à 20h, en salle 1. Inscrits: Etudiant 2, Etudiant 3, Etudiant 4, Etudiant 12.

ClassSession: utilisée pour enregistrer un compte-rendu de chaque occurence d'un cours, donc ça contient une relation ManyToOne vers l'entité Course ci-dessus, la date de la session, un petit résumé du travail fait, et une relation ManyToMany vers l'entité Student pour savoir qui y a assisté.
Ca donnera pour le Cours #1:
ClassSession#1 - mardi 10/12/2013: "apprentissage du participe passé", étudiants présents: Etudiant 2, Etudiant 4, Etudiant 12
ClassSession#2 - mardi 17/12/2013: "apprentissage du passé simple", étudiants présents: Etudiant 2, Etudiant 3, Etudiant 4
etc

Dans mon formulaire de saisie, je présente une textarea pour saisir le compte-rendu, et aussi une liste de case à cocher pour sélectionne les étudiants qui ont assisté à la session de cours. Mais je ne veux pas afficher tous les étudiants de l'école, uniquement ceux qui se sont inscrits au cours pour lequel tu vas saisir ton compte-rendu.

Donc j'ai cette action:
    /**
     * Displays a form to create a new ClassSession entity.
     *
     * @Route("/add/course/{course_id}", name="classsession_add")
     * @Template()
     */
    public function newAction($courseId) {
        $entity = new ClassSession();      
        $organizationBranchId = $this->getSelectedOrganizationBranchId();
        $currentTeacher = $this->getConnectedUser();
        $semesterId = $this->getSelectedSemesterId();
        
        $form = $this->createForm(new ClassSessionType($this->getDoctrine(), $courseId, $organizationBranchId, $currentTeacher, $semesterId), $entity);
        
        $em = $this->getDoctrine()->getManager();
        $course = $em->getRepository('VirguleMainBundle:Course')->find($course_id);
        
        return array(
            'entity' => $entity,
            'form' => $form->createView(),
            'course'=> $course
        );
    }
L'ID du cours concerné est récupéré via l'URL, je construit avec le formulaire et le passe au QueryBuilder pour afficher la liste des étudiants inscrits au cours en question, et le met dans un chmp caché afin de le récupérer à la sortie.

Une fois le formulaire soumis, c'est là qu'est mon problème. Je pensais récupérer l'ID du cours depuis mon champ caché comme cela, dans createAction:
$form = $this->createForm(new ClassSessionType($this->getDoctrine(), $courseId, $organizationBranchId, $currentTeacher), $entity);
$form->bind($request);
$courseId = $form->get('course_id')->getData();
Chargé l'objet cours correspondant, stocker la relation, et mes autres données.

Mon problème, c'est que même si les ID des étudiants séléctionnés sont transmis par le formulaire, le fait d'avoir "null" pour $courseId au moment du createForm, fait que d'une façon ou d'une autre, ils ne sont pas sauvegardés. Ma table de jointure n'est pas renseignée. Et effectivement, si dans createAction, juste avant le createForm, je rajoute $courseId = 51, ces étudiants présents seront sauvegardés correctement.

D'où ma question, comment je fais pour gérer cet ID de cours au moment de la création ? Ou plus généralement, comment je fais pour gérer une valeur qui est à la fois utilisée dans mon entité cible, sauvée par le formulaire, et dans un QueryBuilder utilisé pour générer le formulaire.

La solution est peut-être très très simple, mais je ne m'en sors pas :)

Est-ce plus clair ? ;)

Modérateur PHPfrance
Modérateur PHPfrance | 6373 Messages

24 avr. 2013, 17:37

En fait je réalise que je n'ai pas bien posé la question dans mon premier post, c'est ça mon problème de base: les valeurs sélectionnées dans les cases à cocher générée par le QueryBuilder ne sont pas sauvées, vu que à la création du formulaire dans le createAction, le $courseId est null.

Et j'ai du mal à comprendre pourquoi, vu que pour moi le QueryBuilder sert à construire cette liste, mais qu'une fois les valeurs sélectionnées transmises, on s'en fout on en a plus besoin.

Voici un screenshot du formulaire généré si ça aide à la compréhension, URL: demo/web/classsession/add/course/1
Vous n’avez pas les permissions nécessaires pour voir les fichiers joints à ce message.

Modérateur PHPfrance
Modérateur PHPfrance | 6373 Messages

24 avr. 2013, 17:56

Si je fais ça dans createAction ça fonctionne, les étudiants sélectionnés sont enregistrés, mais c'est crado non ?
        $form1 = $this->createForm(new ClassSessionType($this->getDoctrine(), null, $organizationBranchId, $currentTeacher), $entity);
        $form1->bind($request);
        $courseId = $form1->get('course_id')->getData();
        
        $form = $this->createForm(new ClassSessionType($this->getDoctrine(), $courseId, $organizationBranchId, $currentTeacher), $entity);
        $form->bind($request);        

Mammouth du PHP | 568 Messages

25 avr. 2013, 09:45

Si je fais ça dans createAction ça fonctionne, les étudiants sélectionnés sont enregistrés, mais c'est crado non ?
        $form1 = $this->createForm(new ClassSessionType($this->getDoctrine(), null, $organizationBranchId, $currentTeacher), $entity);
        $form1->bind($request);
        $courseId = $form1->get('course_id')->getData();
        
        $form = $this->createForm(new ClassSessionType($this->getDoctrine(), $courseId, $organizationBranchId, $currentTeacher), $entity);
        $form->bind($request);        
C'est plus que crado :)

Je comprend ton soucis.

Pourquoi tu veux absolument passer par un champ hidden ? De ce que je comprend, ton champ hidden correspond à une entité Course ? et tu te simplifierait énormément la vie en l'utilisant en tant que tel.

Déjà premier point, j'ai perdu le nom du process mais pas grave (si je remet la main dessus, je te mettrais un link), sais-tu que tu récupérer automatique une entity via ta méthode newAction ?

Au lieu de
public function newAction($courseId)
Essaye
public function newAction(Course $course) {
Cela va te permettre de dire à sf2 que l'entité à récupérer est de type Course dont l'id est celui de la requête.

Juste une précision au cas ou.

Pour en revenir à ton cas, maintenant que tu as une entité course, je ferais un truc tout simple. C'est bien une création de session ?

Je ferais un truc tout simple, en omettant volontairement tout le reste
public function newAction(Course $course) {
        $classSession = new ClassSession();
        $classSession->setCourse($course);
       
        $form = $this->createForm(new ClassSessionType(), $classSession);
       
        return array(
            'entity' => $classSession,
            'form' => $form->createView(),
            'course'=> $course
        );
    }
9a te permet de dire que tu créé un formulaire de type ClassSession dont la course est défini.

Point important et je te laisse te référer à la doc, un form à 3 jeux de données, les données sources, les données normalisé et les données de la vue (ou truc dans le genre), en faisant ce que l'on vient de faire on initialise les data sources, ce qui va nous permettre d'utiliser un truc très sympa, c'est la génération de form dynamique.

FORM
public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $subscriber = new AddClassSessionStudentsFieldSubscriber($builder->getFormFactory());
        $builder->addEventSubscriber($subscriber);
    }
FIELD SUBSCRIBER (cf. http://symfony.com/doc/current/cookbook ... ation.html)

A gérer dans le PRE_SET_DATA (voir si le PRE_BIND est nécessaire)
/**
     * @param \Symfony\Component\Form\FormEvent $event
     */
    public function preSetData(FormEvent $event)
    {
        /** @var ClassSession $data */
        $data = $event->getData();
        $form = $event->getForm();

        if (null === $data) {
            return;
        }

//ici ton objet $data fait référence à l'entité ClassSession, ce qui te permet de récupérer la course et son id (vu qu'on a fait l'initialisation dans la méthode newAction avec "$classSession->setCourse($course);")
//je passe par la récup afin de passer la valeur à la fonction anonyme plus bas - ma version de PHP ne gère pas sinon
        $courseId = $data->getCourse()->getId();
if($courseId) {
        $form->add($this->factory->createNamed('classSessionStudents', 'entity', $data['courseId'], array(
             'class' => 'VirguleMainBundle:Student',
                      'query_builder' => function(EntityRepository $er) use ($courseId) {
                                          return $er->createQueryBuilder('s')
                                              ->innerJoin('s.courses', 'c2', 'WITH', 'c2.id = ?1')
                                              ->setParameters(array('1' => $courseId));
                                          },
                      'expanded' => true,
                      'multiple' => true,
                      'property_path' => 'classSessionStudents',
                      'property' => 'fullname'
        )));
}
Et a priori, tu n'a rien de plus à faire.

EDIT: a priori, il va peut-être te falloir laissé le champ hidden, et dans ce cas, je te conseille de passer par un DataTransformer qui te permet de transformer un id en entité et vice versa pour les form. bref de la lecture déjà et du code a adapter je pense.

A tester.

Modérateur PHPfrance
Modérateur PHPfrance | 6373 Messages

25 avr. 2013, 15:44

Déjà premier point, j'ai perdu le nom du process mais pas grave (si je remet la main dessus, je te mettrais un link), sais-tu que tu récupérer automatique une entity via ta méthode newAction ?
Non je ne le savais pas, merci.

Pour le reste je vais prendre le temps de lire et de faire des essais. J'étais déjà tombé sur les DataTransformer mais je ne voyais pas en quoi ça pouvait m'aider, c'est parfois difficile d'appliquer la doc à ses propres problèmes.

Un très grand merci :)

Mammouth du PHP | 568 Messages

25 avr. 2013, 16:00

Pas de soucis.

C'est juste des pistes, pour le datatransformer, je ne pense pas que tu en ai vraiment besoin, regarde plutôt les fieldsubscriber.

Si tu galère trop on peut se faire une session skype pour en discuter si tu veux.

Modérateur PHPfrance
Modérateur PHPfrance | 6373 Messages

03 mai 2013, 13:52

J'ai commencé à bosser dessus, j'ai déjà un problème avec la création de mes checkboxes avec l'event, dès que je passe en multiple = true j'ai cette erreur :
Expected argument of type "Doctrine\Common\Collections\Collection", "integer" given
En attendant je mets en multiple = false, et si je comprends bien la doc j'ai besoin du preBindData pour récupérer le cours transmis par le formulaire.
<?php

namespace Virgule\Bundle\MainBundle\Form\EventListener;

use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class AddClassSessionStudentsFieldSubscriber implements EventSubscriberInterface {

    /**
     * @var FormFactoryInterface
     */
    private $factory;

    /**
     * @var EntityManager
     */
    private $om;
    
    /**
     * @param factory FormFactoryInterface
     */
    public function __construct(FormFactoryInterface $factory) {
        $this->factory = $factory;
    }

    public static function getSubscribedEvents() {
        return array(
            FormEvents::PRE_BIND => 'preBind',
            FormEvents::PRE_SET_DATA => 'preSetData',
        );
    }

    /**
     * @param \Symfony\Component\Form\FormEvent $event
     */
    public function preSetData(FormEvent $event) {
        /** @var ClassSession $data */
        $data = $event->getData();
        $form = $event->getForm();

        if (null === $data) {
            return;
        }

        //ici ton objet $data fait référence à l'entité ClassSession, ce qui te permet de récupérer la course et son id (vu qu'on a fait l'initialisation dans la méthode newAction avec "$classSession->setCourse($course);")
        //je passe par la récup afin de passer la valeur à la fonction anonyme plus bas - ma version de PHP ne gère pas sinon
        $courseId = $data->getCourse()->getId();
        if ($courseId) {
            $field = $this->factory->createNamed('classSessionStudents', 'entity', $data->getCourse()->getId(), array(
                        'class' => 'VirguleMainBundle:Student',
                        'query_builder' => function(EntityRepository $er) use ($courseId) {
                            return $er->createQueryBuilder('s')
                                            ->add('orderBy', 's.lastname ASC, s.firstname ASC')
                                            ->innerJoin('s.courses', 'c2', 'WITH', 'c2.id = ?1')
                                            ->setParameter('1', $courseId);
                        },
                        'expanded' => false,
                        'multiple' => false,
                        'property_path' => 'classSessionStudents',
                        'property' => 'fullname'
                    ));
            $form->add($field);
                          
            $form->add('course_id', 'hidden', array(
                    'data' => $courseId,
                    'mapped' => false))
                ;
        }
    }
    
    public function preBind(FormEvent $event) {
        $data = $event->getData();
    }
}
Mais après avoir soumis le formulaire, je passe aussi dans le preSetData et donc cela fait une erreur:
Call to a member function getId() on a non-object in /var/www/html/Virgule/src/Virgule/Bundle/MainBundle/Form/EventListener/AddClassSessionStudentsFieldSubscriber.php line 51
Car $data est un objet ClassSession vide:
FatalErrorException: Error: Call to a member function getId() on a non-object in /var/www/html/Virgule/src/Virgule/Bundle/MainBundle/Form/EventListener/AddClassSessionStudentsFieldSubscriber.php line 51
Du coup j'ai l'impression d'en être au même point qu'avant, avec plus de fichiers et plus de code :P

Dans ton exemple tu as mis $data['courseId'], c'est une erreur où c'est censé marcher ? Car $data contient ma classSession donc je ne peux pas y accéder en tant que tableau.

Mammouth du PHP | 568 Messages

03 mai 2013, 14:31

Dans ton exemple tu as mis $data['courseId'], c'est une erreur où c'est censé marcher ? Car $data contient ma classSession donc je ne peux pas y accéder en tant que tableau.

Erreur de ma part, désolé.

Tu pourrais mettre tous les fichiers utilisés ? Ce sera plus simple pour faire un point d'ensemble.

Modérateur PHPfrance
Modérateur PHPfrance | 6373 Messages

03 mai 2013, 14:41

Voici:

ClassSessionController:
    /**
     * Displays a form to create a new ClassSession entity.
     *
     * @Route("/add", name="classsession_new")
     * @Route("/add/course/{id}", name="classsession_add")
     * @Template()
     */
    public function newAction(Course $course) {       
        $classSession = new ClassSession();
        $classSession->setCourse($course);
       
        $organizationBranchId = $this->getSelectedOrganizationBranchId();
        $currentTeacher = $this->getConnectedUser();
        $semesterId = $this->getSelectedSemesterId();
        
        $form = $this->createForm(new ClassSessionType($this->getDoctrine(), $organizationBranchId, $currentTeacher, $semesterId), $classSession, array('em' => $this->getDoctrine()->getManager()));
       
        return array(
            'entity' => $classSession,
            'form' => $form->createView(),
            'course'=> $course
        );
    }


    /**
     * Creates a new ClassSession entity.
     *
     * @Route("/create", name="classsession_create")
     * @Method("POST")
     * @Template("VirguleMainBundle:ClassSession:new.html.twig")
     */
    public function createAction(Request $request) {
        $entity = new ClassSession();      
        //$form = $this->initClassSessionForm($entity, $courseId);
        
        $organizationBranchId = $this->getSelectedOrganizationBranchId();
        $currentTeacher = $this->getConnectedUser();
                
        $form = $this->createForm(new ClassSessionType($this->getDoctrine(), $organizationBranchId, $currentTeacher), $entity, array(
    'em' => $this->getDoctrine()->getManager(),
));
        $form->bind($request);   
        
        $entity->setReportDate(new \Datetime('now'));
            
        $connectedUser = $this->getConnectedUser();
        $entity->setReportTeacher($connectedUser);
        if ($form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $em->persist($entity);           
            $em->flush();

            return $this->redirect($this->generateUrl('classsession_index'));
        }
        
        $this->logDebug("Students saved: " . count($entity->getClassSessionStudents()));
        foreach ($entity->getClassSessionStudents() as $student) {
            $this->logDebug($student->getId());
        }
        
        return array(
            'course_id' => $courseId,
            'course' => $course,
            'entity' => $entity,
            'form' => $form->createView(),
        );
    }
ClassSessionType:
<?php

namespace Virgule\Bundle\MainBundle\Form;

use Symfony\Bridge\Doctrine\RegistryInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

use Virgule\Bundle\MainBundle\Entity\Teacher;
use Virgule\Bundle\MainBundle\Form\EventListener\AddClassSessionStudentsFieldSubscriber;
use Virgule\Bundle\MainBundle\Form\DataTransformer\CourseToIdTransformer;

class ClassSessionType extends AbstractType {
        
    private $organizationBranchId;
    
    private $doctrine;
    
    private $currentTeacher;

    public function __construct(RegistryInterface $doctrine, $organizationBranchId = null, Teacher $currentTeacher = null) {
        $this->doctrine = $doctrine;
        $this->organizationBranchId = $organizationBranchId;
        $this->currentTeacher = $currentTeacher;
    }

    public function buildForm(FormBuilderInterface $builder, array $options) {    
        $now = new \DateTime('now');
        $sNow = $now->format('d/m/Y');
        
        $builder
                ->add('sessionDate', 'date', array(
                    'widget' => 'single_text',
                    'format' => 'dd/MM/yyyy',
                    'attr' => array('class' => 'date', 'value' => $sNow)))
                ->add('summary')          
                ->add('sessionTeacher', 'entity', array(
                        'class' => 'VirguleMainBundle:Teacher', 
                        'query_builder' => $this->getTeachers($this->organizationBranchId),
                        'multiple' => false,
                        'expanded' => false,
                        'property_path' => 'sessionTeacher',
                        'attr' => array('class' => 'small-select'),
                        'preferred_choices' => array($this->currentTeacher))
                    )
                ->add('course_id', 'hidden', array(
                    'data' => $data->getCourse()->getId(),
                    'mapped' => false))
                ;
                
         // this assumes that the entity manager was passed in as an option
        $entityManager = $options['em'];
        $transformer = new CourseToIdTransformer($entityManager);
        
        $subscriber = new AddClassSessionStudentsFieldSubscriber($builder->getFormFactory());
        $builder->addEventSubscriber($subscriber);
    }
    
    private function getTeachers($organizationBranchId) {
        $qb = $this->doctrine->getRepository('VirguleMainBundle:Teacher')->getAvailableTeachersQueryBuilder($organizationBranchId);
        return $qb;
    }
    
    private function getStudents($courseId) {
        $qb = $this->doctrine->getRepository('VirguleMainBundle:Student')->getQueryBuilderForStudentEnrolledInCourses(Array($courseId));
        return $qb;
    }
    
    public function setDefaultOptions(OptionsResolverInterface $resolver) {
        $resolver->setDefaults(array(
            'data_class' => 'Virgule\Bundle\MainBundle\Entity\ClassSession',
        ));
        
        
        $resolver->setRequired(array(
            'em',
        ));

        $resolver->setAllowedTypes(array(
            'em' => 'Doctrine\Common\Persistence\ObjectManager',
        ));
    }

    public function getName() {
        return 'virgule_bundle_mainbundle_classsessiontype';
    }

}
Dans celui-ci, je ne sais pas comment accéder à l'objet ClassSession passé en paramètre du Form, ici:
 $form = $this->createForm(new ClassSessionType($this->getDoctrine(), $organizationBranchId, $currentTeacher, $semesterId), $classSession, array('em' => $this->getDoctrine()->getManager()));
Afin de récupérer l'ID du cours pour mon champ caché.

Et donc le field subscriber:
<?php

namespace Virgule\Bundle\MainBundle\Form\EventListener;

use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class AddClassSessionStudentsFieldSubscriber implements EventSubscriberInterface {

    /**
     * @var FormFactoryInterface
     */
    private $factory;

    /**
     * @var EntityManager
     */
    private $om;
    
    /**
     * @param factory FormFactoryInterface
     */
    public function __construct(FormFactoryInterface $factory) {
        $this->factory = $factory;
    }

    public static function getSubscribedEvents() {
        return array(
            FormEvents::PRE_BIND => 'preBind',
            FormEvents::PRE_SET_DATA => 'preSetData',
        );
    }

    /**
     * @param \Symfony\Component\Form\FormEvent $event
     */
    public function preSetData(FormEvent $event) {
        /** @var ClassSession $data */
        $data = $event->getData();
        $form = $event->getForm();

        if (null === $data) {
            return;
        }

        //ici ton objet $data fait référence à l'entité ClassSession, ce qui te permet de récupérer la course et son id (vu qu'on a fait l'initialisation dans la méthode newAction avec "$classSession->setCourse($course);")
        //je passe par la récup afin de passer la valeur à la fonction anonyme plus bas - ma version de PHP ne gère pas sinon
        $courseId = $data->getCourse()->getId();
        if ($courseId) {
            $field = $this->factory->createNamed('classSessionStudents', 'entity', $data->getCourse()->getId(), array(
                        'class' => 'VirguleMainBundle:Student',
                        'query_builder' => function(EntityRepository $er) use ($courseId) {
                            return $er->createQueryBuilder('s')
                                            ->add('orderBy', 's.lastname ASC, s.firstname ASC')
                                            ->innerJoin('s.courses', 'c2', 'WITH', 'c2.id = ?1')
                                            ->setParameter('1', $courseId);
                        },
                        'expanded' => false,
                        'multiple' => false,
                        'property_path' => 'classSessionStudents',
                        'property' => 'fullname'
                    ));
            $form->add($field);
                          
            $form->add('course_id', 'hidden', array(
                    'data' => $courseId,
                    'mapped' => false))
                ;
        }
    }
    
    public function preBind(FormEvent $event) {
        $data = $event->getData();
    }
}
Le field transformer mais que je n'utilise pas encore:
<?php

namespace Virgule\Bundle\MainBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\TaskBundle\Entity\Issue;

/**
 * Description of CourseToNumberTransformer
 *
 * @author guillaume
 */
class CourseToIdTransformer implements DataTransformerInterface {

    /**
     * @var ObjectManager
     */
    private $om;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om) {
        $this->om = $om;
    }

    /**
     * Transforms an object (issue) to a string (number).
     *
     * @param  Issue|null $issue
     * @return string
     */
    public function transform($course) {
        if (null === $course) {
            return "";
        }

        return $course->getId();
    }

    /**
     * Transforms a string (id) to an object (course).
     *
     * @param  string $id
     *
     * @return Course|null
     *
     * @throws TransformationFailedException if object (course) is not found.
     */
    public function reverseTransform($id) {
        if (!$id) {
            return null;
        }

        $issue = $this->om
                ->getRepository('VirguleMainBundle:Course')
                ->findOneBy(array('id' => $id))
        ;

        if (null === $course) {
            throw new TransformationFailedException(sprintf(
                            'A course with ID "%s" does not exist!', $id
            ));
        }

        return $course;
    }

}

?>
Donc voilà, de ce que j'ai compris de tes explications, le point de la doc qui se rapporte à mon problème serait celui-là: http://symfony.com/doc/current/cookbook ... tted-forms

Je me trompe ?

Mammouth du PHP | 568 Messages

03 mai 2013, 14:56

Oui c'est bien le point abordé dans la doc.

Par contre, le fait d'initialiser la course dans ton controller en faisant:
$classSession = new ClassSession();
$classSession->setCourse($course);
Cela te permet de complètement supprimer ton champ hidden course_id, tu en avais besoin avant car tu ne faisait pas l'initialisation.

Ensuite ta classe permettant de transformer un ID en Entity n'est plus utile non plus dans ce cas.

Donc pour reprendre (il vaut mieux y aller par étape), ton controller semble ok.

ClassSessionType:
public function buildForm(FormBuilderInterface $builder, array $options) {    
        $now = new \DateTime('now');
        $sNow = $now->format('d/m/Y');
       
        $builder
                ->add('sessionDate', 'date', array(
                    'widget' => 'single_text',
                    'format' => 'dd/MM/yyyy',
                    'attr' => array('class' => 'date', 'value' => $sNow)))
                ->add('summary')          
                ->add('sessionTeacher', 'entity', array(
                        'class' => 'VirguleMainBundle:Teacher',
                        'query_builder' => $this->getTeachers($this->organizationBranchId),
                        'multiple' => false,
                        'expanded' => false,
                        'property_path' => 'sessionTeacher',
                        'attr' => array('class' => 'small-select'),
                        'preferred_choices' => array($this->currentTeacher))
                    )
                /*->add('course_id', 'hidden', array(
                    'data' => $data->getCourse()->getId(),
                    'mapped' => false))*/ --------------------------------------> plus besoin de ce champ, l'id de la course est stocker dans l'entité
                ;
               
         // this assumes that the entity manager was passed in as an option
        //$entityManager = $options['em'];
        //$transformer = new CourseToIdTransformer($entityManager);
/*--------------------------------------> idem plus besoin pour le moment */
       
        $subscriber = new AddClassSessionStudentsFieldSubscriber($builder->getFormFactory());
        $builder->addEventSubscriber($subscriber);
    }

FieldSubscriber
public function preSetData(FormEvent $event) {
        /** @var ClassSession $data */
        $data = $event->getData();
        $form = $event->getForm();

        if (null === $data) {
            return;
        }

        //ici ton objet $data fait référence à l'entité ClassSession, ce qui te permet de récupérer la course et son id (vu qu'on a fait l'initialisation dans la méthode newAction avec "$classSession->setCourse($course);")
        //je passe par la récup afin de passer la valeur à la fonction anonyme plus bas - ma version de PHP ne gère pas sinon
        $courseId = $data->getCourse()->getId();
        if ($courseId) {
            $field = $this->factory->createNamed('classSessionStudents', 'entity', $data->getCourse()->getId(), array(
                        'class' => 'VirguleMainBundle:Student',
                        'query_builder' => function(EntityRepository $er) use ($courseId) {
                            return $er->createQueryBuilder('s')
                                            ->add('orderBy', 's.lastname ASC, s.firstname ASC')
                                            ->innerJoin('s.courses', 'c2', 'WITH', 'c2.id = ?1')
                                            ->setParameter('1', $courseId);
                        },
                        'expanded' => false,
                        'multiple' => false,
                        'property_path' => 'classSessionStudents',
                        'property' => 'fullname'
                    ));
            $form->add($field);
                         
            /*$form->add('course_id', 'hidden', array(
                    'data' => $courseId,
                    'mapped' => false))
                ;*/  --------------------------------------> plus besoin de ce champ, l'id de la course est stocker dans l'entité
        }
    }
Voila déjà deux trois points que je regarderais en priorité.

Je reste dispo si tu as besoin.

++

Modérateur PHPfrance
Modérateur PHPfrance | 6373 Messages

03 mai 2013, 15:08

Ok merci, donc j'ai viré tout ça. A l'exception du problème avec mon champ quand "multiple = true", mon formulaire s'affiche correctement.

Mon problème est désormais à la réception du formulaire, sans champ hidden, et sans ma classSession correctement initialisée, comment puis-je savoir sur quel cours porte mon compte-rendu ?

Dans mon createAction j'initialise avec un nouvel objet, donc je ne récupère pas mon cours dans le preSetData, mais en même temps il me faut bien un ClassSession pour initialiser le formulaire:
    public function createAction(Request $request) {
        $entity = new ClassSession();      // ========> Good ?
        
        $organizationBranchId = $this->getSelectedOrganizationBranchId();
        $currentTeacher = $this->getConnectedUser();
                
        $form = $this->createForm(new ClassSessionType($this->getDoctrine(), $organizationBranchId, $currentTeacher), $entity, array(
    'em' => $this->getDoctrine()->getManager(),
J'ai beau relire la doc et tes messages, j'avoue que je ne comprends pas trop :)

Modérateur PHPfrance
Modérateur PHPfrance | 6373 Messages

06 mai 2013, 15:55

J'ai réussi à faire marcher le truc mais je suis pas convaincu que c'est ce que tu avais en tête ;)

Le controller:
    /**
     * Displays a form to create a new ClassSession entity.
     *
     * @Route("/add", name="classsession_new")
     * @Route("/add/course/{id}", name="classsession_add")
     * @Template()
     */
    public function newAction(Course $course) {       
        $classSession = new ClassSession();
        $classSession->setCourse($course);
       
        $organizationBranchId = $this->getSelectedOrganizationBranchId();
        $currentTeacher = $this->getConnectedUser();
        $semesterId = $this->getSelectedSemesterId();
        
        $form = $this->createForm(new ClassSessionType($this->getDoctrine(), $organizationBranchId, $currentTeacher, $semesterId), $classSession, array('em' => $this->getDoctrine()->getManager()));
       
        return array(
            'entity' => $classSession,
            'form' => $form->createView(),
            'course'=> $course
        );
    }


    /**
     * Creates a new ClassSession entity.
     *
     * @Route("/create", name="classsession_create")
     * @Method("POST")
     * @Template("VirguleMainBundle:ClassSession:new.html.twig")
     */
    public function createAction(Request $request) {
        $entity = new ClassSession();
        $organizationBranchId = $this->getSelectedOrganizationBranchId();
        $currentTeacher = $this->getConnectedUser();
                
        $form = $this->createForm(new ClassSessionType($this->getDoctrine(), $organizationBranchId, $currentTeacher), $entity, array('em' => $this->getDoctrine()->getManager()));
        $form->bind($request);   
        
        $entity->setReportDate(new \Datetime('now'));
        
        $courseId = $form->get('course_id')->getData();
        $course = $this->getCourseRepository()->find($courseId);
        $entity->setCourse($course);
            
        $connectedUser = $this->getConnectedUser();
        $entity->setReportTeacher($connectedUser);
        if ($form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $em->persist($entity);           
            $em->flush();

            return $this->redirect($this->generateUrl('classsession_index'));
        }
        
        $this->logDebug("Students saved: " . count($entity->getClassSessionStudents()));
        foreach ($entity->getClassSessionStudents() as $student) {
            $this->logDebug($student->getId());
        }
        
        return array(
            'entity' => $entity,
            'form' => $form->createView(),
        );
    }
Le formulaire:
public function buildForm(FormBuilderInterface $builder, array $options) {    
        $now = new \DateTime('now');
        $sNow = $now->format('d/m/Y');
        
        $builder
                ->add('sessionDate', 'date', array(
                    'widget' => 'single_text',
                    'format' => 'dd/MM/yyyy',
                    'attr' => array('class' => 'date', 'value' => $sNow)))
                ->add('summary')          
                ->add('sessionTeacher', 'entity', array(
                        'class' => 'VirguleMainBundle:Teacher', 
                        'query_builder' => $this->getTeachers($this->organizationBranchId),
                        'multiple' => false,
                        'expanded' => false,
                        'property_path' => 'sessionTeacher',
                        'attr' => array('class' => 'small-select'),
                        'preferred_choices' => array($this->currentTeacher))
                    );
        
        $subscriber = new AddClassSessionStudentsFieldSubscriber($builder->getFormFactory(), $options['em']);
        $builder->addEventSubscriber($subscriber);
    }
Le field subscriber:
<?php

namespace Virgule\Bundle\MainBundle\Form\EventListener;

use \Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class AddClassSessionStudentsFieldSubscriber implements EventSubscriberInterface {

    /**
     * @var FormFactoryInterface
     */
    private $factory;

    /**
     * @var EntityManager
     */
    private $em;

    /**
     * @param factory FormFactoryInterface
     */
    public function __construct(FormFactoryInterface $factory, EntityManager $em) {
        $this->factory = $factory;
        $this->em = $em;
    }

    public static function getSubscribedEvents() {
        return array(
            FormEvents::PRE_BIND => 'preBind',
            FormEvents::PRE_SET_DATA => 'preSetData',
        );
    }

    /**
     * @param \Symfony\Component\Form\FormEvent $event
     */
    public function preSetData(FormEvent $event) {
        /** @var ClassSession $data */
        $data = $event->getData();
        $form = $event->getForm();

        if (null === $data || null === $data->getCourse()) {
            return;
        }

        $courseId = $data->getCourse()->getId();
        $this->customizeForm($form, $courseId);
    }

    public function preBind(FormEvent $event) {
        $data = $event->getData();
        $courseId = $data['course_id'];
        $form = $event->getForm();

        $this->customizeForm($form, $courseId);
    }

    protected function customizeForm($form, $courseId) {
        if ($courseId) {
            $field = $this->factory->createNamed('classSessionStudents', 'entity', new ArrayCollection(), array(
                'class' => 'VirguleMainBundle:Student',
                'query_builder' => function(EntityRepository $er) use ($courseId) {
                    return $er->createQueryBuilder('s')
                                    ->add('orderBy', 's.lastname ASC, s.firstname ASC')
                                    ->innerJoin('s.courses', 'c2', 'WITH', 'c2.id = ?1')
                                    ->setParameter('1', $courseId);
                },
                'expanded' => true,
                'multiple' => true,
                'property_path' => 'classSessionStudents',
                'property' => 'fullname'
                    ));
            $form->add($field);
                    
            $form->add('course_id', 'hidden', array(
                'data' => $courseId,
                'mapped' => false))
            ;
        }
    }

}
Dans ce dernier je passe un ArrayCollection au createNamed afin de pouvoir passer mon champ en multiple. Je sais pas trop si le "preBind" sert à quelque chose, je récupère toujours l'objet cours dans mon controller, dans createAction. Je n'ai pas non plus compris comment j'aurais pu me passer du champ "hidden".

Je serais ravi d'avoir un avis sur tout ça :)

Mammouth du PHP | 568 Messages

07 mai 2013, 10:01

Ça me parait pas trop mal.

Juste deux points à discuter,

- as-tu vraiment besoin du champs hidden "course_id" ? Tu en fait quoi, tu t'en sers ou ?
- pas bien compris ton histoire de ArrayCollection ?

Voila un exemple de code me permettant d'afficher un select lorsque mon entité ne possède un pas d'ID, cela me permet de définir le type seulement en création.
/**
     * @param FormEvent $event
     */
    public function preSetData(FormEvent $event)
    {
        $data = $event->getData();
        $form = $event->getForm();

        if (null === $data) {
            return;
        }

        if(!$data->getId())
        {
            $form->add($this->factory->createNamed('typeDepense', 'entity', null, array(
                'label' => 'Type de dépense',
                'class' => 'InExtensoNotesFraisBundle:TypeDepense',
                'query_builder' => function(TypeDepenseRepository $er) {
                    return $er->createDefaultJoinQueryBuilder()
                        ->addOrderBy('td.label');
                },
                'property' => 'label',
                'required' => true,
                'multiple' => true,
                'empty_value' => '',
                'attr' => array(
                    'placeholder' => 'Sélectionner'
                )
            )));
        }
    }
J'ai tester le multiple et je n'ai pas de problème...