То, что я пытаюсь достичь, - это создать/изменить пользовательский инструмент. Редактируемые поля:
Примечание: последнее свойство не названо $role, потому что мой класс User расширяет класс пользователей FOSUserBundle и перезаписывает роли, вызвав больше проблем. Чтобы избежать их, я просто решил сохранить свою коллекцию ролей под $avoRoles.
Мой шаблон состоит из двух разделов:
Примечание: findAllRolesExceptOwnedByUser() - это функция пользовательского репозитория, возвращает подмножество всех ролей (которые еще не назначены $user).
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)
Примечание. Только ТОЛЬКО существующие роли и группы могут быть назначены пользователю. Если по какой-либо причине они не найдены, форма не должна проверяться.
В этом разделе я представляю/или кратко описываю код этого действия. Если описания недостаточно, и вам нужно увидеть код, просто скажите мне, и я вставьте его. Я не собираюсь использовать все это в первую очередь, чтобы не рассылать вам ненужный код.
Класс 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;
}
}
Класс 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;
}
}
Поскольку у меня такая же проблема с группами, как с ролями, я пропускаю их здесь. Если я получаю роли, я знаю, что могу сделать то же самое с группами.
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,
));
}
}
Не стоит публиковать это, так как они работают очень хорошо - они возвращают подмножество всех Ролей/групп (не назначенных пользователю).
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;
}
}
Эта форма должна отображать:
Модуль и описание отображаются как скрытые поля, потому что, когда администратор удаляет роль пользователя, эта роль должна быть добавлена командой 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');
}
}
Вышеприведенная конфигурация возвращает ошибку:
Property "id" is not public in class "Avocode\UserBundle\Entity\Role". Maybe you should create the method "setId()"?
Но параметр set для ID не требуется.
Даже если бы я хотел создать новую роль, его идентификатор должен быть сгенерирован автоматически:
/**
Я думаю, что это неправильно, но я сделал это, чтобы быть уверенным. После добавления этого кода в объект Role:
public function setId($id)
{
$this->id = $id;
return $this;
}
Если я создаю нового пользователя и добавлю роль, тогда СОХРАНИТЬ... Что происходит:
Очевидно, это не то, что я хочу. Я не хочу редактировать/перезаписывать роли. Я просто хочу добавить связь между ними и пользователем.
Когда я впервые столкнулся с этой проблемой, у меня получилось обходное решение, то же самое, что предложил Джеппе. Сегодня (по другим причинам) мне пришлось переделать свою форму/представление, и обходное решение перестало работать.
Какие изменения в 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).
Добавление каскада = { "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, что-то изменил:
Итак, он что-то изменил, но в неправильном направлении.
Я пробовал много разных подходов (выше только самые последние), и после нескольких часов, потраченных на изучение кода, google'ing и поиск ответа, я просто не мог заставить это работать.
Любая помощь будет принята с благодарностью. Если вам нужно что-то знать, я отправлю вам любую часть кода.
Итак, прошел год, и этот вопрос стал довольно популярен. Symfony изменился с тех пор, мои навыки и знания также улучшились, и поэтому мой нынешний подход к этой проблеме.
Я создал набор расширений формы для symfony2 (см. FormExtensionsBundle проект на github), и они включают в себя тип формы для обработки One/Many ToMany.
При написании этих данных добавление пользовательского кода в контроллер для обработки коллекций было неприемлемым - расширение формы должно было быть простым в использовании, работать готово и облегчать жизнь для нас, разработчиков, а не сложнее. Также.. запомнить.. СУХОЙ!
Поэтому мне пришлось переместить код добавления/удаления в другом месте - и правильным местом для этого было, естественно, EventListener:)
Посмотрите на EventListener/CollectionUploadListener.php, чтобы посмотреть, как мы это делаем сейчас.
PS. Копирование кода здесь не является необходимым, самое главное, что на самом деле это должно быть обработано в EventListener.
Я пришел к тому же выводу, что что-то не так с компонентом Form и не может найти простой способ его исправить. Тем не менее, я придумал несколько менее громоздкое решение обходного решения, которое является полностью общим; он не имеет жестко закодированного знания сущностей/атрибутов, поэтому исправляет любую коллекцию, с которой он сталкивается:
Это не требует внесения каких-либо изменений в вашу сущность.
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 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()
до сохранения# 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...
}
}
Обходное решение, предложенное Jeppe Marianger-Lam, на данный момент является единственным, о котором я знаю.
Я изменил свой RoleNameType (по другим причинам) на:
Проблема заключалась в том, что мой пользовательский тип метки отображал свойство NAME как
<span> role name </span>
И поскольку он не был "только для чтения", компонент FORM должен был получить NAME в POST.
Вместо этого только ID был POSTED, и, таким образом, FORM-компонент, предположительно, NAME равен NULL.
Это приводит к созданию ассоциации CASE 2 (3.2) → , но перезаписывает ROLE NAME с пустой строкой.
Это обходное решение очень просто.
В вашем контроллере перед VALIDATE формой вы должны получить опубликованные идентификаторы сущностей и получить соответствующие объекты, а затем установить их в свой объект.
// 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,
));
}
И под обходным кодом в функции 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 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;
}
Для этого для работы вашего SETTER (в данном случае в объекте User.php) должно быть:
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;
}
Тем не менее, я думаю, что это обходное решение делает работу, которая
$form->bindRequest($request);
должен делать! Это либо я делаю что-то неправильно, либо тип формы коллекции symfony не завершен.
В Symform 2.1 есть существенные изменения в компоненте Form, надеюсь, это будет исправлено.
..., пожалуйста, напишите, как это должно быть сделано! Я был бы рад увидеть быстрое, простое и "чистое" решение.
Jeppe Marianger-Lam и дружелюбный пользователь (от # symfony2 на IRC). Вы очень помогли. Ура!
Это то, что я сделал раньше - я не знаю, правильно ли это сделать, но он работает.
Когда вы получите результаты из представленной формы (то есть как раз перед, так и сразу после if($form->isValid())
), просто спросите список ролей, затем удалите их все из сущности (сохраняя список как переменную). В этом списке просто пропустите их все, спросите репозиторий для объекта роли, который соответствует идентификаторам, и добавьте их в свой пользовательский объект перед persist
и flush
.
Я только что просмотрел документацию Symfony2, потому что вспомнил что-то о prototype
для коллекций форм, и это оказалось: http://symfony.com/doc/current/cookbook/form/form_collections.html - В нем есть примеры того, как правильно обрабатывать javascript для добавления и удаления типов коллекций в формах. Возможно, сначала попробуйте этот подход, а затем попробуйте то, что я упомянул выше, если вы не можете заставить его работать:)
Вам нужно еще несколько объектов:
USER
id_user (тип: целое)
имя пользователя (тип: текст)
plainPassword (тип: пароль)
email (введите: email)
ГРУППЫ
id_group (тип: целое)
descripcion (тип: текст)
AVOROLES
id_avorole (тип: целое)
descripcion (тип: текст)
* user_group *
id_user_group (тип: целое)
id_user (type: integer) (это идентификатор пользователя)
id_group (type: integer) (это идентификатор объекта группы)
* USER_AVOROLES *
id_user_avorole (тип: целое)
id_user (type: integer) (это идентификатор пользователя)
id_avorole (type: integer) (это идентификатор объекта avorole)
У вас может быть, например, что-то вроде этого:
Пользователь:
id: 3
имя пользователя: john
plainPassword: johnpw
email: [email protected]
группы:
id_group: 5
descripcion: группа 5
user_group:
id_user_group: 1
id_user: 3
id_group: 5
* этот пользователь может иметь много групп, поэтому в другой строке *
RoleNameType
: «Рендеринг пользовательского типа поля вместо элемента».