Ce que j'essaie de réaliser, c'est un outil utilisateur de création/modification. Les champs modifiables sont:
Remarque: la dernière propriété n'est pas nommée $ roles parce que ma classe User étend la classe User de FOSUserBundle et le remplacement des rôles a posé plus de problèmes. Pour les éviter, j'ai simplement décidé de stocker ma collection de rôles sous $ avoRoles .
Mon modèle se compose de 2 sections:
Remarque: findAllRolesExceptOwnedByUser () est une fonction de référentiel personnalisée, renvoie un sous-ensemble de tous les rôles (ceux qui ne sont pas encore attribués à $ user).
1.3.1 Ajouter un rôle:
QUAND l'utilisateur clique sur le bouton "+" (ajouter) dans le tableau des rôles PUIS jquery supprime cette ligne de la table des rôles ET jquery ajoute un nouvel élément de liste au formulaire utilisateur (liste avoRoles)
1.3.2 Supprimer des rôles:
QUAND l'utilisateur clique sur le bouton "x" (supprimer) dans le formulaire utilisateur (liste avoRoles) PUIS jquery supprime cet élément de liste du formulaire utilisateur (liste avoRoles) ET jquery ajoute une nouvelle ligne à la table des rôles
1.3.3 Enregistrer les modifications:
QUAND l'utilisateur clique sur le bouton "Zapisz" (enregistrer) PUIS le formulaire utilisateur soumet tous les champs (nom d'utilisateur, mot de passe, e-mail, avoRoles, groupes) ET enregistre les avoRoles en tant qu'ArrayCollection d'entités Role (relation ManyToMany) ET enregistre les groupes en tant qu'entités ArrayCollection of Role (relation ManyToMany)
Remarque: seuls les rôles et groupes existants peuvent être attribués à l'utilisateur. Si pour une raison quelconque ils ne sont pas trouvés, le formulaire ne doit pas être validé.
Dans cette section, je présente/ou décris brièvement le code derrière cette action. Si la description ne suffit pas et que vous avez besoin de voir le code, dites-le moi et je le collerai. Je ne colle pas tout cela en premier lieu pour éviter de vous spammer avec du code inutile.
Ma classe d'utilisateurs étend la classe d'utilisateurs FOSUserBundle.
namespace Avocode\UserBundle\Entity;
use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use Avocode\CommonBundle\Collections\ArrayCollection;
use Symfony\Component\Validator\ExecutionContext;
/**
* @ORM\Entity(repositoryClass="Avocode\UserBundle\Repository\UserRepository")
* @ORM\Table(name="avo_user")
*/
class User extends BaseUser
{
const ROLE_DEFAULT = 'ROLE_USER';
const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\generatedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\ManyToMany(targetEntity="Group")
* @ORM\JoinTable(name="avo_user_avo_group",
* joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="group_id", referencedColumnName="id")}
* )
*/
protected $groups;
/**
* @ORM\ManyToMany(targetEntity="Role")
* @ORM\JoinTable(name="avo_user_avo_role",
* joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")}
* )
*/
protected $avoRoles;
/**
* @ORM\Column(type="datetime", name="created_at")
*/
protected $createdAt;
/**
* User class constructor
*/
public function __construct()
{
parent::__construct();
$this->groups = new ArrayCollection();
$this->avoRoles = new ArrayCollection();
$this->createdAt = new \DateTime();
}
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set user roles
*
* @return User
*/
public function setAvoRoles($avoRoles)
{
$this->getAvoRoles()->clear();
foreach($avoRoles as $role) {
$this->addAvoRole($role);
}
return $this;
}
/**
* Add avoRole
*
* @param Role $avoRole
* @return User
*/
public function addAvoRole(Role $avoRole)
{
if(!$this->getAvoRoles()->contains($avoRole)) {
$this->getAvoRoles()->add($avoRole);
}
return $this;
}
/**
* Get avoRoles
*
* @return ArrayCollection
*/
public function getAvoRoles()
{
return $this->avoRoles;
}
/**
* Set user groups
*
* @return User
*/
public function setGroups($groups)
{
$this->getGroups()->clear();
foreach($groups as $group) {
$this->addGroup($group);
}
return $this;
}
/**
* Get groups granted to the user.
*
* @return Collection
*/
public function getGroups()
{
return $this->groups ?: $this->groups = new ArrayCollection();
}
/**
* Get user creation date
*
* @return DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
}
La classe My Role étend la classe Symfony Security Component Core Role.
namespace Avocode\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Avocode\CommonBundle\Collections\ArrayCollection;
use Symfony\Component\Security\Core\Role\Role as BaseRole;
/**
* @ORM\Entity(repositoryClass="Avocode\UserBundle\Repository\RoleRepository")
* @ORM\Table(name="avo_role")
*/
class Role extends BaseRole
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\generatedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(type="string", unique="TRUE", length=255)
*/
protected $name;
/**
* @ORM\Column(type="string", length=255)
*/
protected $module;
/**
* @ORM\Column(type="text")
*/
protected $description;
/**
* Role class constructor
*/
public function __construct()
{
}
/**
* Returns role name.
*
* @return string
*/
public function __toString()
{
return (string) $this->getName();
}
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* @param string $name
* @return Role
*/
public function setName($name)
{
$name = strtoupper($name);
$this->name = $name;
return $this;
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set module
*
* @param string $module
* @return Role
*/
public function setModule($module)
{
$this->module = $module;
return $this;
}
/**
* Get module
*
* @return string
*/
public function getModule()
{
return $this->module;
}
/**
* Set description
*
* @param text $description
* @return Role
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* @return text
*/
public function getDescription()
{
return $this->description;
}
}
Puisque j'ai le même problème avec les groupes qu'avec les rôles, je les saute ici. Si je fais travailler les rôles, je sais que je peux faire la même chose avec les groupes.
namespace Avocode\UserBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\SecurityContext;
use JMS\SecurityExtraBundle\Annotation\Secure;
use Avocode\UserBundle\Entity\User;
use Avocode\UserBundle\Form\Type\UserType;
class UserManagementController extends Controller
{
/**
* User create
* @Secure(roles="ROLE_USER_ADMIN")
*/
public function createAction(Request $request)
{
$em = $this->getDoctrine()->getEntityManager();
$user = new User();
$form = $this->createForm(new UserType(array('password' => true)), $user);
$roles = $em->getRepository('AvocodeUserBundle:User')
->findAllRolesExceptOwned($user);
$groups = $em->getRepository('AvocodeUserBundle:User')
->findAllGroupsExceptOwned($user);
if($request->getMethod() == 'POST' && $request->request->has('save')) {
$form->bindRequest($request);
if($form->isValid()) {
/* Persist, flush and redirect */
$em->persist($user);
$em->flush();
$this->setFlash('avocode_user_success', 'user.flash.user_created');
$url = $this->container->get('router')->generate('avocode_user_show', array('id' => $user->getId()));
return new RedirectResponse($url);
}
}
return $this->render('AvocodeUserBundle:UserManagement:create.html.twig', array(
'form' => $form->createView(),
'user' => $user,
'roles' => $roles,
'groups' => $groups,
));
}
}
Il n'est pas nécessaire de publier ceci car ils fonctionnent très bien - ils renvoient un sous-ensemble de tous les rôles/groupes (ceux qui ne sont pas attribués à l'utilisateur).
Type d'utilisateur:
namespace Avocode\UserBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class UserType extends AbstractType
{
private $options;
public function __construct(array $options = null)
{
$this->options = $options;
}
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('username', 'text');
// password field should be rendered only for CREATE action
// the same form type will be used for EDIT action
// thats why its optional
if($this->options['password'])
{
$builder->add('plainpassword', 'repeated', array(
'type' => 'text',
'options' => array(
'attr' => array(
'autocomplete' => 'off'
),
),
'first_name' => 'input',
'second_name' => 'confirm',
'invalid_message' => 'repeated.invalid.password',
));
}
$builder->add('email', 'email', array(
'trim' => true,
))
// collection_list is a custom field type
// extending collection field type
//
// the only change is diffrent form name
// (and a custom collection_list_widget)
//
// in short: it's a collection field with custom form_theme
//
->add('groups', 'collection_list', array(
'type' => new GroupNameType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => true,
'error_bubbling' => false,
'prototype' => true,
))
->add('avoRoles', 'collection_list', array(
'type' => new RoleNameType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => true,
'error_bubbling' => false,
'prototype' => true,
));
}
public function getName()
{
return 'avo_user';
}
public function getDefaultOptions(array $options){
$options = array(
'data_class' => 'Avocode\UserBundle\Entity\User',
);
// adding password validation if password field was rendered
if($this->options['password'])
$options['validation_groups'][] = 'password';
return $options;
}
}
Ce formulaire est censé rendre:
Le module et la description sont rendus sous forme de champs masqués, car lorsque l'administrateur supprime un rôle d'un utilisateur, ce rôle doit être ajouté par jQuery à la table des rôles - et cette table contient des colonnes de module et de description.
namespace Avocode\UserBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class RoleNameType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('', 'button', array(
'required' => false,
)) // custom field type rendering the "x" button
->add('id', 'hidden')
->add('name', 'label', array(
'required' => false,
)) // custom field type rendering <span> item instead of <input> item
->add('module', 'hidden', array('read_only' => true))
->add('description', 'hidden', array('read_only' => true))
;
}
public function getName()
{
// no_label is a custom widget that renders field_row without the label
return 'no_label';
}
public function getDefaultOptions(array $options){
return array('data_class' => 'Avocode\UserBundle\Entity\Role');
}
}
La configuration ci-dessus renvoie une erreur:
Property "id" is not public in class "Avocode\UserBundle\Entity\Role". Maybe you should create the method "setId()"?
Mais le setter pour ID ne devrait pas être requis.
Même si je voulais créer un nouveau rôle, son ID devrait être généré automatiquement:
/ **
Je pense que c'est faux, mais je l'ai fait juste pour être sûr. Après avoir ajouté ce code à l'entité Role:
public function setId($id)
{
$this->id = $id;
return $this;
}
Si je crée un nouvel utilisateur et ajoute un rôle, alors ENREGISTRER ... Ce qui se passe est:
Évidemment, ce n'est pas ce que je veux. Je ne veux pas modifier/écraser les rôles. Je veux juste ajouter une relation entre eux et l'utilisateur.
Lorsque j'ai rencontré ce problème pour la première fois, je me suis retrouvé avec une solution de contournement, la même que celle suggérée par Jeppe. Aujourd'hui (pour d'autres raisons), j'ai dû refaire mon formulaire/vue et la solution de contournement a cessé de fonctionner.
Quels changements dans Case3 UserManagementController -> createAction:
// in createAction
// instead of $user = new User
$user = $this->updateUser($request, new User());
//and below updateUser function
/**
* Creates mew iser and sets its properties
* based on request
*
* @return User Returns configured user
*/
protected function updateUser($request, $user)
{
if($request->getMethod() == 'POST')
{
$avo_user = $request->request->get('avo_user');
/**
* Setting and adding/removeing groups for user
*/
$owned_groups = (array_key_exists('groups', $avo_user)) ? $avo_user['groups'] : array();
foreach($owned_groups as $key => $group) {
$owned_groups[$key] = $group['id'];
}
if(count($owned_groups) > 0)
{
$em = $this->getDoctrine()->getEntityManager();
$groups = $em->getRepository('AvocodeUserBundle:Group')->findById($owned_groups);
$user->setGroups($groups);
}
/**
* Setting and adding/removeing roles for user
*/
$owned_roles = (array_key_exists('avoRoles', $avo_user)) ? $avo_user['avoRoles'] : array();
foreach($owned_roles as $key => $role) {
$owned_roles[$key] = $role['id'];
}
if(count($owned_roles) > 0)
{
$em = $this->getDoctrine()->getEntityManager();
$roles = $em->getRepository('AvocodeUserBundle:Role')->findById($owned_roles);
$user->setAvoRoles($roles);
}
/**
* Setting other properties
*/
$user->setUsername($avo_user['username']);
$user->setEmail($avo_user['email']);
if($request->request->has('generate_password'))
$user->setPlainPassword($user->generateRandomPassword());
}
return $user;
}
Malheureusement, cela ne change rien. Les résultats sont soit CASE1 (sans ID setter) soit CASE2 (avec ID setter).
Ajout de cascade = {"persist", "remove"} au mappage.
/**
* @ORM\ManyToMany(targetEntity="Group", cascade={"persist", "remove"})
* @ORM\JoinTable(name="avo_user_avo_group",
* joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="group_id", referencedColumnName="id")}
* )
*/
protected $groups;
/**
* @ORM\ManyToMany(targetEntity="Role", cascade={"persist", "remove"})
* @ORM\JoinTable(name="avo_user_avo_role",
* joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")}
* )
*/
protected $avoRoles;
Et changer by_reference en false dans FormType:
// ...
->add('avoRoles', 'collection_list', array(
'type' => new RoleNameType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'error_bubbling' => false,
'prototype' => true,
));
// ...
Et garder le code de contournement suggéré dans 3.3 a changé quelque chose:
Alors .. ça a changé quelque chose mais dans la mauvaise direction.
J'ai essayé de nombreuses approches différentes (ci-dessus ne sont que les plus récentes) et après des heures passées à étudier le code, à chercher sur Google et à chercher la réponse, je n'ai tout simplement pas pu faire fonctionner cela.
Toute aide sera fortement appréciée. Si vous avez besoin de savoir quelque chose, je publierai la partie du code dont vous avez besoin.
Une année s'est donc écoulée et cette question est devenue très populaire. Symfony a changé depuis, mes compétences et mes connaissances se sont également améliorées, tout comme mon approche actuelle de ce problème.
J'ai créé un ensemble d'extensions de formulaire pour symfony2 (voir FormExtensionsBundle projet sur github) et elles incluent un type de formulaire pour gérer One/Many ToMany relations.
Lors de l'écriture de ces derniers, l'ajout de code personnalisé à votre contrôleur pour gérer les collections était inacceptable - les extensions de formulaire étaient censées être faciles à utiliser, fonctionnelles et faciliter la vie des développeurs, pas plus difficiles. Aussi .. rappelez-vous .. SEC!
J'ai donc dû déplacer le code d'association ajouter/supprimer ailleurs - et le bon endroit pour le faire était naturellement un EventListener :)
Jetez un œil au fichier EventListener/CollectionUploadListener.php pour voir comment nous gérons cela maintenant.
PS. Copier le code ici n'est pas nécessaire, la chose la plus importante est que des choses comme ça devraient être gérées dans EventListener.
Je suis arrivé à la même conclusion qu'il y a quelque chose qui ne va pas avec le composant Form et je ne vois pas de moyen facile de le corriger. Cependant, j'ai trouvé une solution de contournement légèrement moins lourde qui est complètement générique; il n'a aucune connaissance codée en dur des entités/attributs, il corrigera donc toute collection qu'il rencontrera:
Cela ne vous oblige pas à apporter des modifications à votre entité.
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Form\Form;
# In your controller. Or possibly defined within a service if used in many controllers
/**
* Ensure that any removed items collections actually get removed
*
* @param \Symfony\Component\Form\Form $form
*/
protected function cleanupCollections(Form $form)
{
$children = $form->getChildren();
foreach ($children as $childForm) {
$data = $childForm->getData();
if ($data instanceof Collection) {
// Get the child form objects and compare the data of each child against the object's current collection
$proxies = $childForm->getChildren();
foreach ($proxies as $proxy) {
$entity = $proxy->getData();
if (!$data->contains($entity)) {
// Entity has been removed from the collection
// DELETE THE ENTITY HERE
// e.g. doctrine:
// $em = $this->getDoctrine()->getEntityManager();
// $em->remove($entity);
}
}
}
}
}
cleanupCollections()
avant de persister# in your controller action...
if($request->getMethod() == 'POST') {
$form->bindRequest($request);
if($form->isValid()) {
// 'Clean' all collections within the form before persisting
$this->cleanupCollections($form);
$em->persist($user);
$em->flush();
// further actions. return response...
}
}
La solution de contournement suggérée par Jeppe Marianger-Lam est à l'heure actuelle la seule qui fonctionne à ma connaissance.
J'ai changé mon RoleNameType (pour d'autres raisons) en:
Le problème est que mon étiquette de type personnalisée a rendu la propriété NAME sous la forme
<span> nom du rôle </span>
Et comme il n'était pas "en lecture seule", le composant FORM s'attendait à obtenir NAME dans POST.
Au lieu de cela, seul l'ID a été POSTé et le composant FORM a donc supposé que NAME est NULL.
Cela a conduit à CASE 2 (3.2) -> créer une association, mais en écrasant ROLE NAME avec une chaîne vide.
Cette solution de contournement est très simple.
Dans votre contrôleur, avant de VALIDER le formulaire, vous devez récupérer les identifiants d'entité publiés et obtenir les entités correspondantes, puis les définir sur votre objet.
// example action
public function createAction(Request $request)
{
$em = $this->getDoctrine()->getEntityManager();
// the workaround code is in updateUser function
$user = $this->updateUser($request, new User());
$form = $this->createForm(new UserType(), $user);
if($request->getMethod() == 'POST') {
$form->bindRequest($request);
if($form->isValid()) {
/* Persist, flush and redirect */
$em->persist($user);
$em->flush();
$this->setFlash('avocode_user_success', 'user.flash.user_created');
$url = $this->container->get('router')->generate('avocode_user_show', array('id' => $user->getId()));
return new RedirectResponse($url);
}
}
return $this->render('AvocodeUserBundle:UserManagement:create.html.twig', array(
'form' => $form->createView(),
'user' => $user,
));
}
Et en dessous du code de contournement dans la fonction updateUser:
protected function updateUser($request, $user)
{
if($request->getMethod() == 'POST')
{
// getting POSTed values
$avo_user = $request->request->get('avo_user');
// if no roles are posted, then $owned_roles should be an empty array (to avoid errors)
$owned_roles = (array_key_exists('avoRoles', $avo_user)) ? $avo_user['avoRoles'] : array();
// foreach posted ROLE, get it's ID
foreach($owned_roles as $key => $role) {
$owned_roles[$key] = $role['id'];
}
// FIND all roles with matching ID's
if(count($owned_roles) > 0)
{
$em = $this->getDoctrine()->getEntityManager();
$roles = $em->getRepository('AvocodeUserBundle:Role')->findById($owned_roles);
// and create association
$user->setAvoRoles($roles);
}
return $user;
}
Pour que cela fonctionne, votre SETTER (dans ce cas dans l'entité User.php) doit être:
public function setAvoRoles($avoRoles)
{
// first - clearing all associations
// this way if entity was not found in POST
// then association will be removed
$this->getAvoRoles()->clear();
// adding association only for POSTed entities
foreach($avoRoles as $role) {
$this->addAvoRole($role);
}
return $this;
}
Pourtant, je pense que cette solution de contournement fait le travail qui
$form->bindRequest($request);
devrait faire! Soit je fais quelque chose de mal, soit le type de formulaire Collection de symfony n'est pas complet.
Il y a des changements majeurs dans le composant Form à venir dans symfony 2.1, j'espère que ce sera corrigé.
... veuillez poster la façon dont cela devrait être fait! Je serais heureux de voir une solution rapide, facile et "propre".
Jeppe Marianger-Lam et convivial (à partir de # symfony2 sur IRC). Vous avez été très utile. À votre santé!
C'est ce que j'ai fait auparavant - je ne sais pas si c'est la "bonne" façon de le faire, mais cela fonctionne.
Lorsque vous obtenez les résultats du formulaire soumis (c'est-à-dire juste avant ou juste après if($form->isValid())
), demandez simplement la liste des rôles, puis supprimez-les tous de l'entité (en enregistrant la liste en tant que variable). Avec cette liste, parcourez-les tout simplement, demandez au référentiel l'entité de rôle qui correspond aux ID et ajoutez-les à votre entité utilisateur avant de persist
et flush
.
Je viens de parcourir la documentation Symfony2 parce que je me souvenais de quelque chose à propos de prototype
pour les collections de formulaires, et cela s'est révélé: http://symfony.com/doc/current/cookbook/form/form_collections.html - Il contient des exemples sur la façon de gérer correctement l'ajout et la suppression javascript des types de collection dans les formulaires. Essayez peut-être d'abord cette approche, puis essayez ce que j'ai mentionné ci-dessus si vous ne pouvez pas le faire fonctionner :)
Vous avez besoin de plus d'entités:
[~ # ~] utilisateur [~ # ~]
id_user (type: entier)
nom d'utilisateur (type: texte)
plainPassword (type: mot de passe)
email (type: email)
GROUPES
id_group (type: entier)
description (type: texte)
AVOROLES
id_avorole (type: entier)
description (type: texte)
* SER_GROUP *
id_user_group (type: entier)
id_user (type: entier) (il s'agit de l'id sur l'entité utilisateur)
id_group (type: entier) (c'est l'id sur l'entité de groupe)
* SER_AVOROLES *
id_user_avorole (type: entier)
id_user (type: entier) (il s'agit de l'id sur l'entité utilisateur)
id_avorole (type: entier) (c'est l'identifiant sur l'entité avorole)
Vous pouvez par exemple avoir quelque chose comme ceci:
utilisateur:
id: 3
nom d'utilisateur: john
plainPassword: johnpw
e-mail: [email protected]
groupe:
id_group: 5
description: groupe 5
groupe_utilisateurs:
id_user_group: 1
id_user: 3
id_group: 5
* cet utilisateur peut avoir plusieurs groupes donc sur une autre ligne *