Kolekcja obiektów Symfony2 - jak dodać / usunąć powiązanie z istniejącymi podmiotami?

1. Szybki przegląd1.1 Cel

To, co próbuję osiągnąć, to narzędzie do tworzenia / edycji użytkownika. Edytowalne pola to:

nazwa użytkownika (typ: tekst)plainPassword (typ: hasło)email (wpisz: email)grupy (typ: kolekcja)avoRoles (typ: kolekcja)

Uwaga: ostatnia właściwość nie ma nazwy$ role ponieważ moja klasa użytkownika rozszerza klasę użytkownika FOSUserBundle, a role nadpisywania przyniosły więcej problemów. Aby ich uniknąć, po prostu postanowiłem zapisać moją kolekcję ról pod$ avoRoles.

1.2 Interfejs użytkownika

Mój szablon składa się z 2 sekcji:

Formularz użytkownikaTabela wyświetlająca $ userRepository-> findAllRolesExceptOwnedByUser ($ user);

Uwaga: findAllRolesExceptOwnedByUser () jest niestandardową funkcją repozytorium, zwraca podzbiór wszystkich ról (tych, które nie zostały jeszcze przypisane do użytkownika $).

1.3 Pożądana funkcjonalność

1.3.1 Dodaj rolę:

    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 Usuń role:

    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 Zapisz zmiany:

    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)  

Uwaga: TYLKO istniejące role i grupy mogą być przypisane do użytkownika. Jeśli z jakiegoś powodu nie zostaną znalezione, formularz nie powinien zostać zatwierdzony.

2. Kod

W tej sekcji przedstawię / lub krótko opiszę kod tej akcji. Jeśli opis nie wystarczy i musisz zobaczyć kod, po prostu powiedz mi, że go wkleję. Nie wklejam tego wszystkiego w pierwszej kolejności, aby uniknąć spamowania niepotrzebnym kodem.

2.1 Klasa użytkownika

Moja klasa użytkownika rozszerza klasę użytkowników 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 Klasa ról

Moja klasa ról rozszerza klasę roli składnika zabezpieczeń Symfony.

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 Klasa grup

Ponieważ mam ten sam problem z grupami, co z rolami, pomijam je tutaj. Jeśli dostanę role, wiem, że mogę zrobić to samo z grupami.

2.4 Kontroler
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 Repozytoria niestandardowe

Nie ma potrzeby publikowania tego, ponieważ działają dobrze - zwracają podzbiór wszystkich ról / grup (tych, które nie są przypisane do użytkownika).

2.6 Typ użytkownika

Typ użytkownika:

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 Typ RoleName

Ten formularz ma renderować:

ukryty identyfikator roliNazwa roli (TYLKO ODCZYT)ukryty moduł (TYLKO PRZECZYTAJ)ukryty opis (TYLKO PRZECZYTAJ)usuń przycisk (x)

Moduł i opis są renderowane jako ukryte pola, ponieważ administrator usuwa rolę od użytkownika, tę rolę powinien dodać jQuery do tabeli ról - a ta tabela zawiera kolumny modułu i opisu.

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. Aktualne / znane problemy3.1 Przypadek 1: konfiguracja cytowana powyżej

Powyższa konfiguracja zwraca błąd:

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

Jednak ustawianie ID nie powinno być wymagane.

Po pierwsze, ponieważ nie chcę tworzyć nowej roli. Chcę tylko utworzyć relację między istniejącymi obiektami roli i użytkownika.

Nawet jeśli chciałbym utworzyć nową rolę, jej identyfikator powinien zostać wygenerowany automatycznie:

/ **

@ORM Id@ORM Kolumna (type = "integer")@ORM generatedValue (strategy = "AUTO") * / protected $ id;3.2 Przypadek 2: dodano setter dla właściwości ID w obiekcie Rola

Myślę, że to źle, ale zrobiłem to tylko po to, żeby mieć pewność. Po dodaniu tego kodu do elementu Rola:

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

Jeśli utworzę nowego użytkownika i dodam rolę, następnie ZAPISZ ... Co się dzieje:

Nowy użytkownik jest tworzonyNowy użytkownik ma rolę z przypisanym żądanym identyfikatorem (yay!)ale nazwa roli jest nadpisywana pustym łańcuchem (bummer!)

Oczywiście to nie to, czego chcę. Nie chcę edytować / nadpisywać ról. Chcę tylko dodać relację między nimi a użytkownikiem.

3.3 Przypadek 3: Obejście zaproponowane przez Jeppe

Kiedy pierwszy raz spotkałem się z tym problemem, skończyło się na obejściu tego samego, co sugerował Jeppe. Dzisiaj (z innych powodów) musiałem przerobić mój formularz / widok i obejście przestało działać.

Jakie zmiany w 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;
    }

Niestety nic to nie zmienia .. wyniki są albo CASE1 (bez ustawiania ID) lub CASE2 (z ustawiaczem ID).

3.4 Przypadek 4: zgodnie z sugestiami użytkowników

Dodawanie kaskady = {"persist", "remove"} do mapowania.

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

I zmienia sięby_reference dofałszywy w FormType:

// ...

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

// ...

A utrzymanie kodu obejścia sugerowanego w 3.3 zmieniło coś:

Powiązanie między użytkownikiem a rolą byłonie utworzony.. ale nazwa roli została zastąpiona pustym łańcuchem (jak w 3.2)

Więc ... to coś zmieniło, ale w złym kierunku.

4. Wersje4.1 Symfony2 v2.0.154.2 Doctrine2 v2.1.74.3 Wersja FOSUserBundle:6fb81861d84d460f1d070ceb8ec180aac841f7fa5. Podsumowanie

Próbowałem wielu różnych podejść (powyżej są tylko najnowsze) i po godzinach spędzonych na studiowaniu kodu, wyszukiwaniu google i szukaniu odpowiedzi po prostu nie mogłem tego zrobić.

Każda pomoc zostanie bardzo doceniona. Jeśli musisz wiedzieć cokolwiek, opublikuję każdą część kodu, której potrzebujesz.

questionAnswers(5)

yourAnswerToTheQuestion