usar tutorial getrepository getdoctrine generate from español create como php forms collections symfony entity

php - tutorial - Colección de entidades Symfony2: ¿cómo agregar/eliminar asociaciones con entidades existentes?



symfony 4 create entity from database (5)

1. La solución alternativa

La solución alternativa sugerida por Jeppe Marianger-Lam es por el momento la única que conozco.

1.1 ¿Por qué dejó de funcionar en mi caso?

Cambié mi RoleNameType (por otras razones) a:

  • ID (oculto)
  • nombre (tipo personalizado - etiqueta)
  • módulo y descripción (oculto, solo lectura)

El problema era que mi etiqueta de tipo personalizado representaba la propiedad NAME como

<span> role name </span>

Y dado que no era "de solo lectura", el componente FORM esperaba obtener NAME en POST.

En cambio, solo ID se PUBLICÓ, y por lo tanto el componente FORMA asumió que NAME es NULL.

Esto lleva a CASO 2 (3.2) -> a crear asociación, pero sobrescribe ROLE NAME con una cadena vacía.

2. Entonces, ¿de qué se trata esta solución?

2.1 Controlador

Esta solución es muy simple.

En su controlador, antes de VALIDAR el formulario, debe buscar los identifi- cadores de entidad publicados y obtener entidades coincidentes, luego configurarlas para su objeto.

// 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, )); }

Y debajo del código de solución en la función 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; }

Para que esto funcione su SETTER (en este caso en la entidad User.php) debe ser:

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; }

3. Pensamientos finales

Aún así, creo que esta solución está haciendo el trabajo que

$form->bindRequest($request);

¡debería hacer! O bien estoy haciendo algo mal, o el tipo de formulario de recopilación de Symfony no está completo.

Hay algunos cambios importantes en el componente del Formulario que vienen en Symfony 2.1, con suerte esto será arreglado.

PD. Si es yo haciendo algo mal ...

... por favor publique la forma en que debería hacerse! Me gustaría ver una solución rápida, fácil y "limpia".

PS2. Agradecimientos especiales a:

Jeppe Marianger-Lam y amigable (de # symfony2 en IRC). Has sido de mucha ayuda. ¡Aclamaciones!

1. Visión general rápida

1.1 Meta

Lo que intento lograr es una herramienta de usuario para crear / editar. Los campos editables son:

  • nombre de usuario (tipo: texto)
  • plainPassword (tipo: contraseña)
  • correo electrónico (tipo: correo electrónico)
  • grupos (tipo: colección)
  • avoRoles (tipo: colección)

Nota: la última propiedad no se llama $ roles porque mi clase User está extendiendo la clase User de FOSUserBundle y la sobreescritura de roles trajo más problemas. Para evitarlos, simplemente decidí almacenar mi colección de roles en $ avoRoles .

1.2 Interfaz de usuario

Mi plantilla consta de 2 secciones:

  1. Formulario de usuario
  2. Tabla que muestra $ userRepository-> findAllRolesExceptOwnedByUser ($ user);

Nota: findAllRolesExceptOwnedByUser () es una función de depósito personalizada, devuelve un subconjunto de todos los roles (los que aún no están asignados a $ user).

1.3 Funcionalidad deseada

1.3.1 Agregar 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 Eliminar roles:

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 Guardar cambios:

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)

Nota: SÓLO se pueden asignar Roles y Grupos existentes al Usuario. Si por algún motivo no se encuentran, el formulario no debe validar.

2. Código

En esta sección presento / o describo brevemente el código detrás de esta acción. Si la descripción no es suficiente y necesita ver el código solo dígamelo y lo pegaré. No estoy pegando todo para evitar spam con código innecesario.

2.1 Clase de usuario

Mi clase de usuario extiende la clase de usuario 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 Clase de rol

La clase My Role amplía la clase 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; } }

2.3 Clase de grupos

Como tengo el mismo problema con los grupos que con los roles, los omito aquí. Si obtengo roles de trabajo, sé que puedo hacer lo mismo con los grupos.

2.4 Controlador

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 repositorios personalizados

No es necesario publicar esto porque funcionan muy bien: devuelven un subconjunto de todos los Roles / Grupos (aquellos no asignados al usuario).

2.6 UserType

Tipo de usuario:

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 RoleNameType

Se supone que esta forma debe representar:

  • ID de rol oculto
  • Nombre de la función (LEER SOLAMENTE)
  • módulo oculto (LEER SOLAMENTE)
  • descripción oculta (LEER SOLAMENTE)
  • botón quitar (x)

El módulo y la descripción se representan como campos ocultos, ya que cuando el administrador elimina un rol de un usuario, ese rol debe ser agregado por jQuery a la tabla de roles, y esta tabla tiene columnas de módulo y descripción.

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 &lt;span&gt; item instead of &lt;input&gt; 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. Problemas actuales / conocidos

3.1 Caso 1: configuración como se cita arriba

La configuración anterior devuelve error:

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

Pero setter para ID no debería ser requerido.

  1. Primero porque no quiero crear un NUEVO papel. Solo quiero crear una relación entre las entidades Role y User existentes.
  2. Incluso si quisiera crear una nueva función, su ID debe generarse automáticamente:

    / **

    • @ORM / Id
    • @ORM / Column (type = "integer")
    • @ORM / generatedValue (strategy = "AUTO") * / protected $ id;

3.2 Caso 2: agregado setter para propiedad de ID en Role entidad

Creo que está mal, pero lo hice solo para estar seguro. Después de agregar este código a la entidad Rol:

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

Si creo un nuevo usuario y agrego un rol, entonces GUARDAR ... Lo que sucede es:

  1. Se crea un nuevo usuario
  2. El nuevo usuario tiene un rol con el ID deseado asignado (¡yay!)
  3. pero el nombre de esa función se sobrescribe con una cadena vacía (¡bummer!)

Obviamente, eso no es lo que quiero. No quiero editar / sobrescribir roles. Solo quiero agregar una relación entre ellos y el Usuario.

3.3 Caso 3: solución propuesta por Jeppe

Cuando encontré este problema por primera vez, terminé con una solución, la misma que sugirió Jeppe. Hoy (por otras razones) tuve que rehacer mi formulario / vista y la solución dejó de funcionar.

Qué cambios en 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; }

Lamentablemente, esto no cambia nada ... los resultados son CASE1 (sin ID setter) o CASE2 (con ID setter).

3.4 Caso 4: según lo sugerido por userfriendly

Añadiendo cascade = {"persist", "remove"} al mapeo.

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

Y cambiando por referencia a falso en FormType:

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

Y mantener el código de solución alternativa sugerido en 3.3 cambió algo:

  1. La asociación entre usuario y rol no fue creada
  2. .. pero el nombre de la entidad Rol fue sobrescrito por una cadena vacía (como en 3.2)

Entonces ... cambió algo pero en la dirección incorrecta.

4. Versiones

4.1 Symfony2 v2.0.15

4.2 Doctrine2 v2.1.7

4.3 Versión de 6fb81861d84d460f1d070ceb8ec180aac841f7fa : 6fb81861d84d460f1d070ceb8ec180aac841f7fa

5. Resumen

He intentado muchos enfoques diferentes (los anteriores son solo los más recientes) y después de horas dedicadas a estudiar código, buscar en Google y buscar la respuesta, no pude hacer que esto funcionara.

Cualquier ayuda será apreciada. Si necesita saber algo, publicaré cualquier parte del código que necesite.


Así que ha pasado un año y esta pregunta se ha vuelto bastante popular. Symfony ha cambiado desde entonces, mis habilidades y conocimiento también han mejorado, y también mi enfoque actual con respecto a este problema.

Creé un conjunto de extensiones de formulario para Symfony2 (vea el proyecto FormExtensionsBundle en github) e incluyen un tipo de formulario para manejar las relaciones One / Many ToMany .

Mientras escribía esto, agregar código personalizado a su controlador para manejar colecciones era inaceptable: las extensiones de formulario se suponía que eran fáciles de usar, funcionaban listas para usar y facilitaban la vida de los desarrolladores de nosotros, no más. Además ... recuerda ... ¡SECO!

Así que tuve que mover el código de agregar / quitar asociaciones a otro lugar, y el lugar correcto para hacerlo fue naturalmente un EventListener :)

Eche un vistazo al archivo EventListener/CollectionUploadListener.php para ver cómo lo manejamos ahora.

PD. Copiar el código aquí es innecesario, lo más importante es que cosas así deberían manejarse en el EventListener.


Esto es lo que he hecho antes: no sé si es la manera "correcta" de hacerlo, pero funciona.

Cuando obtiene los resultados del formulario enviado (es decir, justo antes o después de if($form->isValid()) ), simplemente solicite la lista de roles y luego elimínelos de la entidad (guardando la lista como variable). Con esta lista, simplemente recorra todos ellos, solicite al repositorio la entidad de función que coincida con los ID, y agréguelos a su entidad de usuario antes de persist y flush .

Solo busqué en la documentación de Symfony2 porque recordé algo sobre prototype para colecciones de formularios, y esto apareció: http://symfony.com/doc/current/cookbook/form/form_collections.html - Tiene ejemplos de cómo tratar correctamente con javascript agrega y elimina tipos de colección en formularios. Tal vez intente este enfoque primero, y luego intente lo que mencioné anteriormente si no puede hacerlo funcionar :)


Llegué a la misma conclusión de que hay algo mal con el componente Formulario y no veo una manera fácil de solucionarlo. Sin embargo, he encontrado una solución alternativa menos engorrosa que es completamente genérica; no tiene ningún conocimiento codificado de las entidades / atributos, por lo que corregirá cualquier colección que encuentre:

Método de solución más simple y genérico

Esto no requiere que realice ningún cambio en su entidad.

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); } } } } }

Llame al nuevo método cleanupCollections() antes de persistir

# 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... } }


Necesitas algunas entidades más:
USUARIO
id_user (tipo: entero)
nombre de usuario (tipo: texto)
plainPassword (tipo: contraseña)
correo electrónico (tipo: correo electrónico)

GRUPOS
id_group (tipo: entero)
descripcion (tipo: texto)

AVOROLES
id_avorole (tipo: entero)
descripcion (tipo: texto)

* USER_GROUP *
id_user_group (type: integer)
id_user (type: integer) (este es el id en la entidad de usuario)
id_group (type: integer) (este es el id en la entidad del grupo)

* USER_AVOROLES *
id_user_avorole (type: integer)
id_user (type: integer) (este es el id en la entidad de usuario)
id_avorole (type: integer) (este es el id en la entidad avorole)

Puedes tener, por ejemplo, algo como esto:
usuario:
id: 3
nombre de usuario: john
plainPassword: johnpw
correo electrónico: [email protected]


grupo:
id_group: 5
descripcion: grupo 5


grupo de usuario:
id_user_group: 1
id_user: 3
id_group: 5
* este usuario puede tener muchos grupos, por lo que en otra fila *