То, что я пытаюсь достичь, – это создать / редактировать пользовательский инструмент. Редактируемые поля:
Примечание: последнее свойство не называется $ role, потому что мой класс User расширяет класс пользователей FOSUserBundle и перезаписывает роли, вызвав больше проблем. Чтобы избежать их, я просто решил сохранить свою коллекцию ролей под $ avoRoles .
Мой шаблон состоит из двух разделов:
Примечание: findAllRolesExceptOwnedByUser () – это функция пользовательского репозитория, возвращает подмножество всех ролей (которые еще не назначены $ user).
1.3.1 Добавить роль:
КОГДА пользователь нажимает кнопку «+» (добавить) в таблице «Роли» THEN jquery удаляет эту строку из таблицы Roles И jquery добавляет новый элемент списка в форму пользователя (список avoRoles)
1.3.2 Удаление ролей:
КОГДА пользователь нажимает кнопку «x» (удалить) в форме пользователя (список avoRoles) THEN jQuery удаляет этот элемент списка из пользовательской формы (список avoRoles) И jquery добавляет новую строку в таблицу ролей
1.3.3 Сохранить изменения:
КОГДА пользователь нажимает кнопку «Zapisz» (сохранить) Затем пользовательская форма представляет все поля (имя пользователя, пароль, email, avoRoles, группы) И сохраняет avoRoles как ArrayCollection объектов Role (отношение ManyToMany) И сохраняет группы как ArrayCollection объектов Role (отношение ManyToMany)
Примечание. ТОЛЬКО существующие роли и группы могут быть назначены пользователю. Если по какой-либо причине они не найдены, форма не должна проверяться.
В этом разделе я представляю / или кратко описываю код этого действия. Если описания недостаточно, и вам нужно увидеть код, просто скажите мне, и я вставьте его. В первую очередь, я не собираюсь использовать все это, чтобы избежать спама с ненужным кодом.
Класс 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; } }
Мой класс Role расширяет класс основной роли 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; } }
Поскольку у меня такая же проблема с группами, как с ролями, я пропускаю их здесь. Если я получу роли, я знаю, что могу сделать то же самое с группами.
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'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; } }
Эта форма должна отображать:
Модуль и описание отображаются как скрытые поля, потому что, когда администратор удаляет роль у пользователя, эта роль должна быть добавлена в таблицу jQuery to Roles Table – и в этой таблице есть столбцы Module и 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'); } }
Вышеприведенная конфигурация возвращает ошибку:
Property "id" is not public in class "Avocode\UserBundle\Entity\Role". Maybe you should create the method "setId()"?
Но setter для 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).
Добавление cascade = {"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 и поиск ответа, я просто не мог заставить это работать.
Любая помощь будет оценена. Если вам нужно что-то знать, я опубликую любую часть кода, в которой вы нуждаетесь.
Я пришел к такому же выводу, что что-то не так с компонентом 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'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 // eg 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... } }
Итак, прошел год, и этот вопрос стал довольно популярным. Symfony изменился с тех пор, мои навыки и знания также улучшились, и поэтому мой нынешний подход к этой проблеме.
Я создал набор расширений формы для symfony2 (см. Проект FormExtensionsBundle на github), и они включают тип формы для обработки отношений One / Many ToMany .
При написании данных добавление пользовательского кода в контроллер для обработки коллекций было неприемлемым – расширения форм должны были быть просты в использовании, работать из коробки и облегчать жизнь для нас, разработчиков, а не сложнее. Также .. запомнить .. СУХОЙ!
Поэтому мне пришлось переместить код добавления / удаления в другом месте – и подходящее место для этого было, естественно, EventListener 🙂
Посмотрите файл EventListener / CollectionUploadListener.php, чтобы узнать, как мы это обрабатываем.
PS. Копирование кода здесь не является необходимым, самое главное, что на самом деле это должно быть обработано в EventListener.
Обходное решение, предложенное Jeppe Marianger-Lam, на данный момент является единственным, о котором я знаю.
Я изменил свой RoleNameType (по другим причинам) на:
Проблема заключалась в том, что моя пользовательская метка типа отображала свойство NAME как
<span> имя роли </ 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'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; }
Для этого для работы вашего 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 не является полным.
Есть некоторые существенные изменения в компоненте Form, входящие в symfony 2.1, надеюсь, это будет исправлено.
… пожалуйста, напишите, как это должно быть сделано! Я был бы рад увидеть быстрое, простое и «чистое» решение.
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 (type: integer)
имя пользователя (тип: текст)
plainPassword (тип: пароль)
электронная почта (тип: электронная почта)
ГРУППЫ
id_group (type: integer)
descripcion (тип: текст)
AVOROLES
id_avorole (type: integer)
descripcion (тип: текст)
* USER_GROUP *
id_user_group (тип: целое число)
id_user (type: integer) (это идентификатор объекта пользователя)
id_group (type: integer) (это идентификатор в группе)
* USER_AVOROLES *
id_user_avorole (type: integer)
id_user (type: integer) (это идентификатор объекта пользователя)
id_avorole (type: integer) (это идентификатор объекта avorole)
Например, вы можете сделать что-то вроде этого:
пользователь:
id: 3
имя пользователя: john
plainPassword: johnpw
email: john@email.com
группа:
id_group: 5
descripcion: группа 5
группа пользователей:
id_user_group: 1
id_user: 3
id_group: 5
* этот пользователь может иметь много групп, поэтому в другой строке *