1. Краткое описание
1.1 Цель
То, что я пытаюсь достичь, - это создать/изменить пользовательский инструмент. Редактируемые поля:
- имя пользователя (тип: текст)
- plainPassword (тип: пароль)
- email (введите: email)
- группы (тип: коллекция)
- avoRoles (тип: коллекция)
Примечание: последнее свойство не названо $role, потому что мой класс User расширяет класс пользователей FOSUserBundle и перезаписывает роли, вызвав больше проблем. Чтобы избежать их, я просто решил сохранить свою коллекцию ролей под $avoRoles.
1.2 Пользовательский интерфейс
Мой шаблон состоит из двух разделов:
- Форма пользователя
- Таблица, отображающая $userRepository- > findAllRolesExceptOwnedByUser ($ user);
Примечание: findAllRolesExceptOwnedByUser() - это функция пользовательского репозитория, возвращает подмножество всех ролей (которые еще не назначены $user).
1.3 Желаемая функциональность
1.3.1 Добавить роль:
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 Удаление ролей:
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 Сохранить изменения:
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)
Примечание. Только ТОЛЬКО существующие роли и группы могут быть назначены пользователю. Если по какой-либо причине они не найдены, форма не должна проверяться.
2. Код
В этом разделе я представляю/или кратко описываю код этого действия. Если описания недостаточно, и вам нужно увидеть код, просто скажите мне, и я вставьте его. Я не собираюсь использовать все это в первую очередь, чтобы не рассылать вам ненужный код.
2.1 Пользовательский класс
Класс My User расширяет класс пользователей 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 Класс ролей
Класс My Role расширяет класс ролей Symfony Security Component Core.
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 Класс групп
Поскольку у меня такая же проблема с группами, как с ролями, я пропускаю их здесь. Если я получаю роли, я знаю, что могу сделать то же самое с группами.
2.4 Контроллер
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 Пользовательские репозитории
Не стоит публиковать это, так как они работают очень хорошо - они возвращают подмножество всех Ролей/групп (не назначенных пользователю).
2.6 UserType
UserType:
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 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
Эта форма должна отображать:
- скрытый идентификатор ролей
- Имя роли (ТОЛЬКО ЧИТАНО)
- скрытый модуль (ТОЛЬКО ЧИТАЙТЕ)
- скрытое описание (ТОЛЬКО ЧИТАНО) Кнопка
- remove (x)
Модуль и описание отображаются как скрытые поля, потому что, когда администратор удаляет роль пользователя, эта роль должна быть добавлена командой jQuery в таблицу ролей, и в этой таблице есть столбцы "Модуль" и "Описание".
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. Текущие/известные проблемы
3.1 Случай 1: конфигурация, указанная выше
Вышеприведенная конфигурация возвращает ошибку:
Property "id" is not public in class "Avocode\UserBundle\Entity\Role". Maybe you should create the method "setId()"?
Но параметр set для ID не требуется.
- Сначала я не хочу создавать новую роль. Я хочу просто создать связь между существующими объектами Role и User.
-
Даже если бы я хотел создать новую роль, его идентификатор должен быть сгенерирован автоматически:
/**
- @ORM\Id
- @ORM\Колонка (тип = "целое число" )
- @ORM\GeneratedValue (стратегия = "AUTO" ) */ protected $id;
3.2 Случай 2: добавлен сеттер для свойства ID в объекте Role
Я думаю, что это неправильно, но я сделал это, чтобы быть уверенным. После добавления этого кода в объект Role:
public function setId($id)
{
$this->id = $id;
return $this;
}
Если я создаю нового пользователя и добавлю роль, тогда СОХРАНИТЬ... Что происходит:
- Создан новый пользователь.
- Новый пользователь имеет роль с назначенным идентификатором (yay!)
- , но имя этой роли перезаписывается пустой строкой (bummer!)
Очевидно, это не то, что я хочу. Я не хочу редактировать/перезаписывать роли. Я просто хочу добавить связь между ними и пользователем.
3.3 Случай 3: Обходное решение, предложенное Jeppe
Когда я впервые столкнулся с этой проблемой, у меня получилось обходное решение, то же самое, что предложил Джеппе. Сегодня (по другим причинам) мне пришлось переделать свою форму/представление, и обходное решение перестало работать.
Какие изменения в 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;
}
К сожалению, это ничего не меняет. Результаты являются либо CASE1 (без идентификатора), либо CASE2 (с установщиком ID).
3.4 Случай 4: как было предложено пользователем
Добавление каскада = { "persist", "remove" } к отображению.
/**
* @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;
И изменив by_reference на false в FormType:
// ...
->add('avoRoles', 'collection_list', array(
'type' => new RoleNameType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'error_bubbling' => false,
'prototype' => true,
));
// ...
И поддерживая обходной код, предложенный в 3.3, что-то изменил:
- Ассоциация между пользователем и ролью была не создана
- .. но имя сущности роли было перезаписано пустой строкой (как в 3.2)
Итак, он что-то изменил, но в неправильном направлении.
4. Версии
4.1 Symfony2 v2.0.15
4.2 Doctrine2 v2.1.7
4.3 Версия FOSUserBundle: 6fb81861d84d460f1d070ceb8ec180aac841f7fa
5. Резюме
Я пробовал много разных подходов (выше только самые последние), и после нескольких часов, потраченных на изучение кода, google'ing и поиск ответа, я просто не мог заставить это работать.
Любая помощь будет принята с благодарностью. Если вам нужно что-то знать, я отправлю вам любую часть кода.