Symfony2-Sammlung von Entitäten - Hinzufügen / Entfernen von Verknüpfungen mit vorhandenen Entitäten

1. Schneller Überblick1.1 Ziel

Was ich versuche zu erreichen, ist ein Benutzer-Tool erstellen / bearbeiten. Bearbeitbare Felder sind:

Benutzername (Typ: Text)plainPassword (Typ: Passwort)email (Typ: email)Gruppen (Typ: Sammlung)avoRoles (Typ: Sammlung)

Hinweis: Die letzte Eigenschaft ist nicht benannt$ rolls Weil meine Benutzerklasse die Benutzerklasse von FOSUserBundle erweitert und das Überschreiben von Rollen mehr Probleme mit sich brachte. Um sie zu vermeiden, habe ich einfach beschlossen, meine Rollensammlung unter zu speichern$ avoRoles.

1.2 Benutzeroberfläche

Meine Vorlage besteht aus 2 Abschnitten:

BenutzerformularTabelle mit $ userRepository-> findAllRolesExceptOwnedByUser ($ user);

Hinweis: findAllRolesExceptOwnedByUser () ist eine benutzerdefinierte Repository-Funktion, die eine Teilmenge aller Rollen zurückgibt (die $ user noch nicht zugewiesen wurden).

1.3 Gewünschte Funktionalität

1.3.1 Rolle hinzufügen:

    WHEN user clicks "+" (add) button in Roles table  
    THEN jquery removes that row from Roles table  
    AND  jquery adds new list item to User form (avoRoles list)

1.3.2 Rollen entfernen:

    WHEN user clicks "x" (remove) button in  User form (avoRoles list)  
    THEN jquery removes that list item from User form (avoRoles list)  
    AND  jquery adds new row to Roles table

1.3.3 Änderungen speichern:

    WHEN user clicks "Zapisz" (save) button  
    THEN user form submits all fields (username, password, email, avoRoles, groups)  
    AND  saves avoRoles as an ArrayCollection of Role entities (ManyToMany relation)  
    AND  saves groups as an ArrayCollection of Role entities (ManyToMany relation)  

Hinweis: Dem Benutzer können NUR vorhandene Rollen und Gruppen zugewiesen werden. Wenn sie aus irgendeinem Grund nicht gefunden werden, sollte das Formular nicht validiert werden.

2. Code

In diesem Abschnitt präsentiere / beschreibe ich kurz den Code hinter dieser Aktion. Wenn die Beschreibung nicht ausreicht und Sie den Code sehen müssen, sagen Sie es mir und ich werde es einfügen. Ich füge nicht alles in erster Linie ein, um zu vermeiden, dass Sie mit unnötigem Code überflutet werden.

2.1 Benutzerklasse

Meine Benutzerklasse erweitert die Benutzerklasse 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;
    }
}
2.2 Rollenklasse

Meine Rollenklasse erweitert die Kernrollenklasse von Symfony Security Component.

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;
    }
}
2.3 Gruppenklasse

Da ich bei Gruppen das gleiche Problem habe wie bei Rollen, überspringe ich sie hier. Wenn ich Rollen zum Laufen bringe, weiß ich, dass ich das auch mit Gruppen machen kann.

2.4 Controller
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,
        ));
    }
}
2.5 Benutzerdefinierte Repositorys

Es ist nicht erforderlich, dies zu posten, da sie einwandfrei funktionieren - sie geben eine Teilmenge aller Rollen / Gruppen zurück (die nicht dem Benutzer zugewiesen sind).

2.6 Benutzertyp

Benutzertyp:

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;
    }
}
2.7 RoleNameType

Dieses Formular soll folgendes darstellen:

versteckte Rollen IDRollenname (NUR LESEN)verstecktes Modul (NUR LESEN)versteckte Beschreibung (NUR LESEN)Entfernen Sie die Taste (x)

Modul und Beschreibung werden als ausgeblendete Felder gerendert, da wenn der Administrator eine Rolle von einem Benutzer entfernt, diese Rolle von jQuery zur Rollentabelle hinzugefügt werden sollte. Diese Tabelle enthält die Spalten "Modul" und "Beschreibung".

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');
    }
}
3. Aktuelle / bekannte Probleme3.1 Fall 1: Konfiguration wie oben angegeben

Die obige Konfiguration gibt einen Fehler zurück:

Property "id" is not public in class "Avocode\UserBundle\Entity\Role". Maybe you should create the method "setId()"?

Der ID-Setter sollte jedoch nicht erforderlich sein.

Zunächst, weil ich keine NEUE Rolle erstellen möchte. Ich möchte lediglich eine Beziehung zwischen vorhandenen Rollen- und Benutzerentitäten herstellen.

Auch wenn ich eine neue Rolle erstellen wollte, sollte die ID automatisch generiert werden:

/ **

@ORM \ Id@ORM \ Column (Typ = "Ganzzahl")@ORM \ generatedValue (strategy = "AUTO") * / protected $ id;3.2 Fall 2: Setter für ID-Eigenschaft in Rollenentität hinzugefügt

Ich denke, es ist falsch, aber ich habe es getan, nur um sicherzugehen. Nach dem Hinzufügen dieses Codes zur Rollenentität:

public function setId($id)
{
    $this->id = $id;
    return $this;
}

Wenn ich einen neuen Benutzer erstelle und eine Rolle hinzufüge, dann SPEICHERN ... Was passiert ist:

Neuer Benutzer wird angelegtNeuer Benutzer hat eine Rolle mit der gewünschten ID zugewiesen (yay!)Der Name dieser Rolle wird jedoch mit einer leeren Zeichenfolge überschrieben (Mist!)

Offensichtlich ist das nicht das, was ich will. Ich möchte keine Rollen bearbeiten / überschreiben. Ich möchte nur eine Beziehung zwischen ihnen und dem Benutzer hinzufügen.

3.3 Fall 3: Von Jeppe vorgeschlagene Problemumgehung

Als ich zum ersten Mal auf dieses Problem stieß, kam es zu einer Problemumgehung, die auch Jeppe vorschlug. Heute (aus anderen Gründen) musste ich mein Formular / meine Ansicht neu erstellen und die Problemumgehung funktionierte nicht mehr.

Was ändert sich in 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;
    }

Leider ändert dies nichts. Die Ergebnisse sind entweder CASE1 (ohne ID-Setter) oder CASE2 (mit ID-Setter).

3.4 Fall 4: wie von benutzerfreundlich vorgeschlagen

Hinzufügen von cascade = {"persist", "remove"} zur Zuordnung.

/**
 * @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;

Und sich änderndby_reference zufalsch in FormType:

// ...

                ->add('avoRoles', 'collection_list', array(
                        'type' => new RoleNameType(),
                        'allow_add' => true,
                        'allow_delete' => true,
                        'by_reference' => false,
                        'error_bubbling' => false,
                        'prototype' => true,
                     ));

// ...

Das Beibehalten des in 3.3 vorgeschlagenen Problemumgehungscodes hat etwas geändert:

Zuordnung zwischen Benutzer und Rolle warnicht erstellt.. aber der Name der Rollenentität wurde durch eine leere Zeichenkette überschrieben (wie in 3.2)

Also .. es hat sich etwas geändert, aber in die falsche Richtung.

4. Versionen4.1 Symfony2 v2.0.154.2 Doctrine2 v2.1.74.3 FOSUserBundle Version:6fb81861d84d460f1d070ceb8ec180aac841f7fa5. Zusammenfassung

Ich habe viele verschiedene Ansätze ausprobiert (oben nur die neuesten) und nach Stunden, in denen ich Code studiert, googelt und nach der Antwort gesucht habe, konnte ich es einfach nicht zum Laufen bringen.

Jede Hilfe wird sehr geschätzt. Wenn Sie etwas wissen müssen, werde ich jeden Teil des Codes posten, den Sie benötigen.

Antworten auf die Frage(5)

Ihre Antwort auf die Frage