Symfony2 and Doctrine2 : populate form with two Entity (a complicated scenario) - php

I have 5 entities:
User,
Person,
UserAffiliation,
PersonAffiliation and
Affiliation
Here is the schema:
Some details:
A WebUser is a Person who is registered to the website. For each Webuser, there is a person ID.
A person can be a web user, an author etc.
Each WebUser has 0 or more affiliations. Those affiliations were created by this WebUser and linked in the able UserAffiliations.
The WebUser can also link the affiliations he createed to a person (if the person is an author) and the Entity PersonAffiliation will be populated.
I am trying now to give the possibility to the webuser to assign an affiliation to an author (person). For that, the I have:
In the Entity Person
#ORM\OneToMany(targetEntity="PersonAffiliation", mappedBy="person", cascade={"persist", "remove"})
protected $affiliations;
In the PersonAffiliation
#ORM\ManyToOne(targetEntity="Person", inversedBy="affiliations")
#ORM\JoinColumn(name="person_id", referencedColumnName="id")
protected $person;
#ORM\ManyToOne(targetEntity="Affiliation", inversedBy="person_affiliations")
#ORM\JoinColumn(name="affiliation_id", referencedColumnName="id")
protected $affiliation;
In the Entity User:
#ORM\OneToMany(targetEntity="UserAffiliation", mappedBy="user")
protected $affiliations;
#ORM\ManyToOne(targetEntity="Person")
#ORM\JoinColumn(name="person_id", referencedColumnName="id")
protected $person;
In the Entity UserAffiliation
#ORM\ManyToOne(targetEntity="User", inversedBy="affiliations")
#ORM\JoinColumn(name="user_id", referencedColumnName="id")
protected $user;
#ORM\ManyToOne(targetEntity="Affiliation", inversedBy="user_affiliations")
#ORM\JoinColumn(name="affiliation_id", referencedColumnName="id")
protected $affiliation;
In the form, I am doing the next:
$builder->add('affiliations', 'entity', array(
'class' => 'SciForumVersion2Bundle:PersonAffiliation',
'query_builder' => function($em) use ($person){
return $em->createQueryBuilder('pa')->where('pa.person_id = :id')->setParameter('id', $person->getId());
},
'property' => 'affiliation',
'multiple' => true,
'expanded' => true,
));
But all this is not working properly as I would like.
Explanation: When I try to add a new affiliation, it is only added for the WebUser and I can't link it through the form to the author (Person).
Do you have an idea on how to resolve this, or maybe a good tutorial?

This should be handled in the Entity1Controller.php:
public function createAction(Request $request)
{
$securityContext = $this->get('security.context');
$em = $this->getDoctrine()->getManager();
$form = $this->createForm(new Entity1Type()
,null,array('attr' => array('securitycontext' => $securityContext)));
$form->bind($request);
if ($form->isValid()){
$data = $form->getData();
$entity1id = $data->getId();
$entity2id = $data->getEntity2Id();
$entity1medicaid=$data->getMedicaidID();
$entity1=$em->getRepostiory('projectBundle:Entity1')->findOneById($entity1id);
$entity2=$em->getRepository('projectprojectBundle:Entity2')->findOneById($entity2id);
if (null === $entity1){
$entity1=new entity1();
$entity1->setEntity2id($entity2id);
$entity1->setID($entity1id);
}
if (null === $entity2){
$entity2=new entity2();
$entity2->setID($entity2id);
}
$em->persist($entity1);
$em->persist($entity2);
$em->flush();
return $this->redirect($this->generateUrl('entity1', array()));
}
return $this->render('Bundle:entity1:new.html.twig', array(
'form' => $form->createView()
,'attr' => array('securitycontext' => $securityContext
)
)
);
}
You may also have to set cascade persist in your association mappings. Entity1.yml:
project\projectBundle\Entity\Entity1:
type: entity
table: entity1
repositoryClass: project\projectBundle\Entity\Entity1Repository
fields:
id:
type: bigint
id: true
generator:
strategy: AUTO
property:
type: string
length: 255
unique: true
manyToMany:
entity2:
targetEntity: entity2
mappedBy: entity1
cascade: ["persist"]
In theory, symfony will make entity2 under the hood, making the second if null clause unnecessary, but that always bothers me, so I prefer to do it explicitly.

Well if this form is binding the collection to a WebUser Entity is because you are passing in to the Form creation in the Controller an object of such class, it means:
$webUser = new WebUser();
$this->createForm(new SubmissionAffiliationFormType(), $webUser);
or you are delegating the decision of which class to use to Symfony Forms by not setting the DefaultOptions and telling explicitly the data_class it must be bind to:
class SubmissionAffiliationFormType extends AbstractType
{
//...
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\DemoBundle\Entity\Person',
);
}
}

Related

Entity deleted when unlinked from relation via Form EntityType Symfony3.4

I'm using Symfony3.4 with softDeletable module in some entities.
I have a ZoneMaterial entity with two arrayCollection of entities :
/**
* #ORM\ManyToMany(targetEntity="EPC", cascade={"persist"}, orphanRemoval=true)
* #ORM\JoinTable(name="app_zone_material_epc",
* joinColumns={#ORM\JoinColumn(name="zoneMaterial_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="epc_id", referencedColumnName="id")}
* )
*/
private $epcs;
/**
* #ORM\ManyToMany(targetEntity="EPI", cascade={"persist"}, orphanRemoval=true)
* #ORM\JoinTable(name="app_zone_material_epi",
* joinColumns={#ORM\JoinColumn(name="zoneMaterial_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="epi_id", referencedColumnName="id")}
* )
*/
private $epis;
[...]
public function addEpc(EPC $e)
{
$this->epcs[] = $e;
}
public function removeEpc(EPC $e)
{
$this->epcs->removeElement($e);
}
public function getEpcs()
{
return $this->epcs;
}
public function addEpi(EPI $e)
{
$this->epis[] = $e;
}
public function removeEpi(EPI $e)
{
$this->epis->removeElement($e);
}
public function getEpis()
{
return $this->epis;
}
I link/unlink them with a form defined as followed :
->add('epcs', EntityType::class, array( 'label' =>'MPC',
'class' => 'AppBundle\Entity\EPC',
'query_builder' => function ($repository) use ($options) {
return $repository
->createQueryBuilder('a')
->orderBy('a.name', 'ASC');
},
'multiple' => true,
'expanded' => true))
->add('epis', EntityType::class, array( 'label' =>'EPI',
'class' => 'AppBundle\Entity\EPI',
'query_builder' => function ($repository) use ($options) {
return $repository
->createQueryBuilder('a')
->orderBy('a.name', 'ASC');
},
'multiple' => true,
'expanded' => true))
And my updateAction function in my controller looks like :
public function updtZoneMaterialAction(Request $request, $id){
$doctrine = $this->getDoctrine();
$entityManager = $doctrine->getManager();
$zoneMaterial = $doctrine->getRepository(ZoneMaterial::class)->find($id);
$form = $this->createForm(ZoneMaterialType::class, $zoneMaterial);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
//Persist informations
$entityManager->persist($zoneMaterial);
$entityManager->flush();
//->redirection
}
//Render view
return $this->render('AppBundle:Zone:updt_zone_material.html.twig', array(
'form' => $form->createView(),
'id' => $id
));
}
The softDeletable extension is installed on my two entities EPI and EPC but it seems that each time I "unlink" an EPI or an EPC from a ZoneMaterial, the concerned entity is remove (with a deletedAt set to new DateTime("NOW")).
each time I unlink an EPI or an EPC from a ZoneMaterial, the concerned entity is removed
This is what you've told doctrine by configuring orphanRemoval=true.
The setting for orphan removal tells Doctrine whether or not it should remove the entities that got unlinked from their aggregate root.
This concept is very powerful and enables a form of programming that considers persistence as an implementation detail.
Rather than manually telling the ORM than an entity must be removed, one can model their domain in such a way that simply removing the entity from the collection is enough to automatically synchronise the database to reflect the state of the model.
If that is not what you want, you should not set orphanRemoval to true.

Symfony form collection not saving reference

I have entity which has multiple Photos:
/**
* related images
* #ORM\OneToMany(targetEntity="Photo", mappedBy="entity",cascade={"persist"})
* #ORM\OrderBy({"uploaded_at" = "ASC"})
*/
private $photos;
Photos have ManyToOne relation with entity
/**
* #ORM\ManyToOne(targetEntity="Acme\AppBundle\Entity\Entity", inversedBy="photos")
* #ORM\JoinColumn(name="entity_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $entity;
all setters and getter are set I'm foliving symfony collection documentation: http://symfony.com/doc/current/reference/forms/types/collection.html
FormType:
->add('photos', 'collection', array(
'type' => new PhotoFormType(),
'allow_add' => true,
'by_reference' => false,
'allow_delete' => true,
'prototype' => true
))
PhotoType:
$builder
->add('title', null, ['label' => 'front.photo.title', 'required' => true])
->add('image', 'file', array('required' => false))
;
For upload I'm using vichUploadableBundle, Images are save just fine, but entity_id is not save and has null. I don't know what I did miss here.
Following would be best solution on this issue so far investigate or research with symfony form component.
FormType:
->add("photos",'collection', array(
'type' => new PhotoFormType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
))
Entity class
public function addPhoto(Photo $photo)
{
$photo->setEntity($this);
$this->photos->add($photo);
}
public function removePhoto(Photo $photo)
{
$this->photos->removeElement($photo);
}
Best practice is not to use loop to bind reference entity manually . Remember by_reference must be false. like 'by_reference' => false.
Run into the same issue, still I remember there is better solution.
You need to specify add and remove functions in the entity with collection.
class Entity
{
// ...
public function addPhoto(Photo $photo)
{
$this->photos->add($photo);
$photo->setEntity($this);
}
public function removePhoto(Photo $photo)
{
$this->photos->removeElement($photo);
}
}
So in such a case you wouldn't need a loop in the controller.
Also if
orphanRemoval=true
is set, no problems with delete.
I've went to this also. I think the main problem is that even the main entity has cascade={"persist"} , the child entites do not get the ID when you are creating a new entry.
So what I did, that is kind of a hack, but works fine is this.
// $em->persist($entity); After persisting entity:
foreach ($entity->getPhotos() as $photo) {
$photo->setEntity($entity);
}
Basically persisting the ID in the childs after their father is created.
But on another point, at least how I understand Doctrine, please correct me if I'm wrong. Try to add an orphanRemoval / fetch additional properties:
FATHER Entity has:
/**
* Related images.
* #ORM\OneToMany(targetEntity="Photo", mappedBy="entity", cascade={"persist"}, orphanRemoval=true, fetch="EAGER")
* #ORM\OrderBy({"uploaded_at" = "ASC"})
*/
private $photos;
Photo Entity is persisted so I add to controller handler to set for every photo Entity. Don't know if it's right solution but it's working.
/** #var Photo $photo */
foreach ($entity->getPhotos() as $photo){
$photo->setEntity($entity);
$em->persist($photo);
}

Editing form for relationship n:m with extra attributes between two entities

I have this mapping betwenn two entities:
class Orders {
// Other attributes
/**
* #ORM\OneToMany(targetEntity="OrderHasProduct", mappedBy="order")
*/
protected $orderProducts;
// Other set/get methods
public function getOrderProducts()
{
return $this->orderProducts;
}
}
class Product {
// Other attributes
/**
* #ORM\OneToMany(targetEntity="\Tanane\FrontendBundle\Entity\OrderHasProduct", mappedBy="product")
*/
protected $orderProducts;
// Other set/get methods
public function getOrderProducts()
{
return $this->orderProducts;
}
}
And of course since many Orders can have many products but also there is an extra attribute this other entity is needed:
class OrderHasProduct
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="\Tanane\FrontendBundle\Entity\Orders")
* #ORM\JoinColumn(name="general_orders_id", referencedColumnName="id")
*/
protected $order;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="\Tanane\ProductBundle\Entity\Product")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
protected $product;
/**
* #ORM\Column(type="integer", nullable=false)
*/
protected $amount;
public function setOrder(\Tanane\FrontendBundle\Entity\Orders $order)
{
$this->order = $order;
}
public function getOrder()
{
return $this->order;
}
public function setProduct(\Tanane\ProductBundle\Entity\Product $product)
{
$this->product = $product;
}
public function getProduct()
{
return $this->product;
}
public function setAmount($amount)
{
$this->amount = $amount;
}
public function getAmount()
{
return $this->amount;
}
}
When I edit a order I should able to add/remove the products on that order but I don't know how to achieve this. I knew that I must use a form collection but how? I mean a collection should be embed as follow:
$builder->add('product', 'collection', array(
'type' => new OrderHasProductType(),
'allow_add' => true,
'allow_delete' => true
));
When I should create a new OrderHasProductType form and I think I understand until this point but my question now is, what happens to the ID of the order? What is the proper way to handle an embedded form a relationship n:m with extra parameters?
Can any give me some code example to order my ideas?
Extra resources
Orders Entity Complete Source
Product Entity Complete Source
Orders Form Type Complete Source
OrderHasProduct Form Type Complete Source
I think your situation is slightly complicated by having not a standard Doctrine many-to-many relationship with two Entities, but two separate one-to-many and many-to-one relationships, with three Entities.
Normally, with a full many-to-many, the process is to have, for example, an OrderType form, containing a Collection field full of ProductTypes representing the Products assigned to the Order.
('allow_add' => true means that if Symfony sees an entry with no ID it expects it to be a brand new item added via Javascript, and is happy to call the form Valid and add the new item to the Entity. 'allow_delete' => true conversely means that if one of the items is missing then Symfony will remove it from the Entity.)
However, you have one further level of Entities, it goes Order->OrderHasProduct->Product. So logically your OrderType form contains a Collection of OrderHasProductType forms (as you've put above), which in turn contains a ProductType form.
So your OrderType becomes more like this:
$builder->add('orderHasProducts', 'collection', array(
'type' => new OrderHasProductType(),
'allow_add' => true,
'allow_delete' => true
));
And you also have another level for the Products:
OrderHasProductType
$builder->add('product', 'collection', array(
'type' => new ProductType(),
'allow_add' => true,
'allow_delete' => true
));
And a ProductType as well:
$builder->add('product', 'entity', array(
'class' => 'ProductBundle:Product'
));
Symfony should be happy to map your Entities to the correct level of Types. In your View you will need to include Javascript which will understand that adding a Product to an Order also involves the ProductHasOrder level - best to put some data in and see how Symfony turns that into a form, and then mimic the structure in the Javascript.

Embeded Form Collection doesn't remove item Symfony2

I have a strange problem. I'm working on a Symfony 2 based web app and I embedding a from collection to another one based on the Symfony Cookbook entry (This: http://symfony.com/doc/2.3/cookbook/form/form_collections.html ).
Recently I changed my bundle's mapping data to annotation from yaml.
The problem starts here. Persisting new elements works fine, but I can't remove anything.
I have a Page entity and an Image entity and they have a many to many relation. I embed and image upload form collection to the Page's form.
The old mapping was invalid, the new one is valid but doesn't work.
Here are my old and new mappings:
old yml:
page:
manyToMany:
images:
targetEntity: Image
inversedBy: page
cascade: [persist, remove]
orderBy:
image_order: DESC
no mapping on the image
New mapping:
Page:
/**
* #ORM\ManyToMany(targetEntity="Image", inversedBy="pages", cascade={"persist", "remove"})
* #ORM\JoinTable(name="page_image",
* joinColumns={#ORM\JoinColumn(name="page_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="image_id", referencedColumnName="id")}
* )
* #ORM\OrderBy({"image_order" = "DESC"})
*/
protected $images;
Image:
/**
* #ORM\ManyToMany(targetEntity="Page", mappedBy="images")
*/
protected $pages;
The action that handles the edit form submission:
/**
* Edits an existing Page entity.
*
* #Route("/{id}/update", name="page_update")
* #Method("post")
* #Template("AdamantiumBackendBundle:Page:edit.html.twig")
*/
public function updateAction($id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('AdamantiumBackendBundle:Page')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Page entity.');
}
$editForm = $this->createForm(new PageType(), $entity);
$deleteForm = $this->createDeleteForm($id);
$request = $this->getRequest();
$editForm->handleRequest($request);
if ($editForm->isValid()) {
$em->persist($entity);
$em->flush();
apc_clear_cache('user');
return $this->redirect($this->generateUrl('page_edit', array('id' => $id)));
}
return $this->render('AdamantiumBackendBundle:Page:edit.html.twig',array(
'entity' => $entity,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
));
}
The embeded collection from the Page form:
->add('images', 'collection', array('type' => new ImageType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'options' => array('data_class' => 'Adamantium\BackendBundle\Entity\Image'),
))
I have the add and remove Images methods, I have getters and setters. (and as I said earlier everything worked fine with the invalid yml mapping)
So please tell me what am I missing or what do I do wrong.
Thanks!!

Symfony2 collection of Entities - how to add/remove association with existing entities?

1. Quick overview
1.1 Goal
What I'm trying to achieve is a create/edit user tool. Editable fields are:
username (type: text)
plainPassword (type: password)
email (type: email)
groups (type: collection)
avoRoles (type: collection)
Note: the last property is not named $roles becouse my User class is extending FOSUserBundle's User class and overwriting roles brought more problems. To avoid them I simply decided to store my collection of roles under $avoRoles.
1.2 User Interface
My template consists of 2 sections:
User form
Table displaying $userRepository->findAllRolesExceptOwnedByUser($user);
Note: findAllRolesExceptOwnedByUser() is a custom repository function, returns a subset of all roles (those not yet assigned to $user).
1.3 Desired functionality
1.3.1 Add role:
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 Remove 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 Save changes:
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)
Note: ONLY existing Roles and Groups can be assigned to User. If for any reason they are not found the form should not validate.
2. Code
In this section I present/or shortly describe code behind this action. If description is not enough and you need to see the code just tell me and I'll paste it. I'm not pasteing it all in the first place to avoid spamming you with unnecessary code.
2.1 User class
My User class extends FOSUserBundle user class.
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 Role class
My Role class extends Symfony Security Component Core Role class.
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 Groups class
Since I've got the same problem with groups as with roles, I'm skipping them here. If I get roles working I know I can do the same with groups.
2.4 Controller
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 Custom repositories
It is not neccesary to post this since they work just fine - they return a subset of all Roles/Groups (those not assigned to user).
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'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
This form is supposed to render:
hidden Role ID
Role name (READ ONLY)
hidden module (READ ONLY)
hidden description (READ ONLY)
remove (x) button
Module and description are rendered as hidden fields, becouse when Admin removes a role from a User, that role should be added by jQuery to Roles Table - and this table has Module and Description columns.
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. Current/known Problems
3.1 Case 1: configuration as quoted above
The above configuration returns error:
Property "id" is not public in class "Avocode\UserBundle\Entity\Role". Maybe you should create the method "setId()"?
But setter for ID should not be required.
First becouse I don't want to create a NEW role. I want just to create a relation between existing Role and User entities.
Even if I did want to create a new Role, it's ID should be auto-generated:
/**
#ORM\Id
#ORM\Column(type="integer")
#ORM\generatedValue(strategy="AUTO")
*/
protected $id;
3.2 Case 2: added setter for ID property in Role entity
I think it's wrong, but I did it just to be sure. After adding this code to Role entity:
public function setId($id)
{
$this->id = $id;
return $this;
}
If I create new user and add a role, then SAVE... What happens is:
New user is created
New user has role with the desired ID assigned (yay!)
but that role's name is overwritten with empty string (bummer!)
Obviously, thats not what I want. I don't want to edit/overwrite roles. I just want to add a relation between them and the User.
3.3 Case 3: Workaround suggested by Jeppe
When I first encountered this problem I ended up with a workaround, the same that Jeppe suggested. Today (for other reasons) I had to remake my form/view and the workaround stopped working.
What changes in 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;
}
Unfortunately this does not change anything.. the results are either CASE1 (with no ID setter) or CASE2 (with ID setter).
3.4 Case 4: as suggested by userfriendly
Adding cascade={"persist", "remove"} to mapping.
/**
* #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;
And changeing by_reference to false in FormType:
// ...
->add('avoRoles', 'collection_list', array(
'type' => new RoleNameType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'error_bubbling' => false,
'prototype' => true,
));
// ...
And keeping workaround code suggested in 3.3 did change something:
Association between user and role was not created
.. but Role entity's name was overwritten by empty string (like in 3.2)
So.. it did change something but in the wrong direction.
4. Versions
4.1 Symfony2 v2.0.15
4.2 Doctrine2 v2.1.7
4.3 FOSUserBundle version: 6fb81861d84d460f1d070ceb8ec180aac841f7fa
5. Summary
I've tried many diffrent approaches (above are only the most recent ones) and after hours spent on studying code, google'ing and looking for the answer I just couldn't get this working.
Any help will be greatly appreciated. If you need to know anything I'll post whatever part of code you need.
I've come to the same conclusion that there's something wrong with the Form component and can't see an easy way to fix it. However, I've come up with a slightly less cumbersome workaround solution that is completely generic; it doesn't have any hard-coded knowledge of entities/attributes so will fix any collection it comes across:
Simpler, generic workaround method
This doesn't require you to make any changes to your entity.
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);
}
}
}
}
}
Call the new cleanupCollections() method before persisting
# 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...
}
}
So a year has passed, and this question has become quite popular. Symfony has changed since, my skills and knowledge have also improved, and so has my current approach to this problem.
I've created a set of form extensions for symfony2 (see FormExtensionsBundle project on github) and they include a form type for handleing One/Many ToMany relationships.
While writing these, adding custom code to your controller to handle collections was unacceptable - the form extensions were supposed to be easy to use, work out-of-the-box and make life easier on us developers, not harder. Also.. remember.. DRY!
So I had to move the add/remove associations code somewhere else - and the right place to do it was naturally an EventListener :)
Have a look at the EventListener/CollectionUploadListener.php file to see how we handle this now.
PS. Copying the code here is unnecessary, the most important thing is that stuff like that should actually be handled in the EventListener.
1. The workaround solution
The workaround solution suggested by Jeppe Marianger-Lam is at the moment the only one working I know of.
1.1 Why did it stop working in my case?
I changed my RoleNameType (for other reasons) to:
ID (hidden)
name (custom type - label)
module & description (hidden, read-only)
The problem was my custom type label rendered NAME property as
<span> role name </span>
And since it was not "read only" the FORM component expected to get NAME in POST.
Instead only ID was POSTed, and thus FORM component assumed NAME is NULL.
This lead to CASE 2 (3.2) -> creating association, but overwriting ROLE NAME with an empty string.
2. So, what exacly is this workaround about?
2.1 Controller
This workaround is very simple.
In your controller, before you VALIDATE the form, you have to fetch the posted entity identyficators and get matching entities, then set them to your object.
// 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,
));
}
And below the workaround code in updateUser function:
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;
}
For this to work your SETTER (in this case in User.php entity) must be:
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. Final thoughts
Still, I think this workaround is doing the job that
$form->bindRequest($request);
should do! It's either me doing something wrong, or symfony's Collection form type is not complete.
There are some major changes in Form component comeing in symfony 2.1, hopefully this will be fixed.
PS. If it's me doing something wrong...
... please post the way it should be done! I'd be glad to see a quick, easy and "clean" solution.
PS2. Special thanks to:
Jeppe Marianger-Lam and userfriendly (from #symfony2 on IRC). You've been very helpful. Cheers!
This is what I have done before - I don't know if it's the 'right' way to do it, but it works.
When you get the results from the submitted form (i.e., just before or right after if($form->isValid())), simply ask the list of the roles, then remove them all from the entity (saving the list as a variable). With this list, simply loop through them all, ask the repository for the role entity that matches the ID's, and add these to your user entity before you persist and flush.
I just searched through the Symfony2 documentation because I remembered something about prototype for form collections, and this turned up: http://symfony.com/doc/current/cookbook/form/form_collections.html - It has examples of how to deal correctly with javascript add and remove of collection types in forms. Perhaps try this approach first, and then try what I mentioned above afterwards if you cannot get it to work :)
You need some more entities:
USER
id_user (type: integer)
username (type: text)
plainPassword (type: password)
email (type: email)
GROUPS
id_group (type: integer)
descripcion (type: text)
AVOROLES
id_avorole (type: integer)
descripcion (type: text)
*USER_GROUP*
id_user_group (type:integer)
id_user (type:integer) (this is the id on the user entity)
id_group (type:integer) (this is the id on the group entity)
*USER_AVOROLES*
id_user_avorole (type:integer)
id_user (type:integer) (this is the id on the user entity)
id_avorole (type:integer) (this is the id on the avorole entity)
You can have for example something like this:
user:
id: 3
username: john
plainPassword: johnpw
email: john#email.com
group:
id_group: 5
descripcion: group 5
user_group:
id_user_group: 1
id_user: 3
id_group: 5
*this user can have many groups so in another row *

Categories