私が達成しようとしているのは、ユーザーの作成/編集ツールです。編集可能なフィールドは次のとおりです。
注:最後のプロパティには名前がありません$ rolesそれらを避けるために、ロールのコレクションを$ avoRolesの下に保存することにしました。
マイテンプレート は2つのセクションで構成されます。
注:findAllRolesExceptOwnedByUser()はカスタムリポジトリ関数であり、すべてのロール(まだ$ userに割り当てられていないロール)のサブセットを返します。
1.3.1役割の追加:
いつ ユーザーが役割テーブルの[+](追加)ボタンをクリックします その後 jqueryは、Rolesテーブルからその行を削除します そして jqueryは、ユーザーリスト(avoRolesリスト)に新しいリストアイテムを追加します
1.3.2ロールの削除:
いつ ユーザーがユーザーフォームの[x](削除)ボタンをクリックする(avoRolesリスト) その後 jqueryは、ユーザーフォーム(avoRolesリスト) からそのリストアイテムを削除します そして jqueryは、ロールテーブルに新しい行を追加します
1.3.3変更を保存します。
いつ ユーザーが[Zapisz](保存)ボタンをクリックします その後 ユーザーフォームはすべてのフィールド(ユーザー名、パスワード、メール、avoRoles、グループ)を送信します そして avoRolesをRoleエンティティのArrayCollection(ManyToMany関係)として保存します そして グループをロールエンティティのArrayCollectionとして保存します(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;
}
}
My Roleクラスは、Symfony Security Component Core Roleクラスを拡張します。
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,
));
}
}
うまく機能するため、これを投稿する必要はありません-すべてのロール/グループ(ユーザーに割り当てられていないもの)のサブセットを返します。
ユーザータイプ:
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によってロールテーブルに追加されるためです。このテーブルにはモジュールと説明の列があります。
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()"?
ただし、IDのセッターは必要ありません。
新しいロールを作成したい場合でも、そのIDは自動生成される必要があります。
/ **
それは間違っていると思いますが、念のためにやったのです。このコードを役割エンティティに追加した後:
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(IDセッターなし)または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;
そして、FormTypeでby_referenceをfalseに変更します:
// ...
->add('avoRoles', 'collection_list', array(
'type' => new RoleNameType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'error_bubbling' => false,
'prototype' => true,
));
// ...
3.3で提案されている回避策のコードを保持することで、何かが変更されました。
だから..それは何かを変えましたが、間違った方向に。
私は多くの異なるアプローチを試しましたが(上記は最新のもののみです)、コードを勉強し、グーグルで検索し、答えを探すことに何時間も費やした後、私はこれを動作させることができませんでした。
どんな助けも大歓迎です。何かを知る必要がある場合は、必要なコードの部分を投稿します。
それで一年が経ち、この質問は非常に一般的になりました。それ以来、symfonyは変化し、私のスキルと知識も向上し、この問題に対する私の現在のアプローチも変化しました。
Symfony2の一連のフォーム拡張機能を作成しました(githubの FormExtensionsBundle プロジェクトを参照)。One/ ManyToMany関係を処理するためのフォームタイプが含まれています。
これらを書いている間、コレクションを処理するためにコントローラーにカスタムコードを追加することは受け入れられません-フォームの拡張機能は使いやすく、すぐに使用でき、開発者の負担を軽減するものでした。また、覚えておいてください。
そのため、アソシエーションの追加/削除コードを他の場所に移動する必要がありました-それを行う適切な場所は、当然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'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
// 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>ロール名</ span>
また、「読み取り専用」ではなかったため、FORMコンポーネントはPOSTでNAMEを取得することが期待されていました。
代わりに、IDのみがPOSTされたため、FORMコンポーネントはNAMEがNULLであると想定しました。
これにより、ケース2(3.2)->関連付けが作成されますが、ROLE NAMEは空の文字列で上書きされます。
この回避策は非常に簡単です。
コントローラーで、フォームを検証する前に、投稿されたエンティティー識別子を取得し、一致するエンティティーを取得してから、オブジェクトに設定する必要があります。
// 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のCollectionフォームタイプが完全ではありません。
Formコンポーネントにいくつかの大きな変更があります symfony 2.1で導入される予定です。うまくいけば修正されるでしょう。
...それが行われるべき方法を投稿してください!迅速で簡単な「クリーン」なソリューションを見つけてうれしいです。
Jeppe Marianger-Lamとユーザーフレンドリー(IRCの#symfony2から)。とても助かりました。乾杯!
これは私が以前にやったことです-それがそれを行うための「正しい」方法であるかどうかはわかりませんが、動作します。
提出されたフォームから結果を取得するとき(つまり、if($form->isValid())
の直前または直後)、ロールのリストを確認し、それらをすべてエンティティから削除します(リストを変数として保存します)。このリストを使用して、すべてをループし、IDに一致するロールエンティティをリポジトリに要求し、persist
およびflush
の前にこれらをユーザーエンティティに追加します。
フォームコレクションのprototype
について何か覚えていたため、Symfony2のドキュメントを検索したところ、次のようになりました。 http://symfony.com/doc/current/cookbook/form/form_collections.html -フォームのコレクションタイプのjavascriptの追加と削除を正しく処理する方法の例があります。おそらく最初にこのアプローチを試して、次にそれを動作させることができない場合は、上で述べたものを試してください:)
さらにエンティティが必要です:
[〜#〜] user [〜#〜]
id_user(タイプ:整数)
ユーザー名(タイプ:テキスト)
plainPassword(タイプ:パスワード)
電子メール(タイプ:電子メール)
[〜#〜] groups [〜#〜]
id_group(タイプ:整数)
説明(タイプ:テキスト)
[〜#〜] avoroles [〜#〜](
id_avorole(タイプ:整数)
説明(タイプ:テキスト)
* SER_GROUP *
id_user_group(タイプ:整数)
id_user(type:integer)(これはユーザーエンティティのIDです)
id_group(type:integer)(これはグループエンティティのIDです)
* SER_AVOROLES *
id_user_avorole(タイプ:整数)
id_user(type:integer)(これはユーザーエンティティのIDです)
id_avorole(type:integer)(これはavoroleエンティティのIDです)
たとえば、次のようなものを使用できます。
ユーザー:
id:3
ユーザー名:john
plainPassword:johnpw
email:[email protected]
グループ:
id_group:5
説明:グループ5
ユーザー・グループ:
id_user_group:1
id_user:3
id_group:5
*このユーザーは多くのグループを持つことができるため、別の行に*