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

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 *

Related

Symfony3 fosuserbundle form for admin with group

i use Symfony3.3 and want to make a form for the admin administration. The user should be have a group and the group sould have the roles for the backend access.
The form for groups (name and roles) i finished and the form for the admins (name, passwort...) is finish too.
The admin will be find and have the group. If i load the admin it have the arraycollection with the groups.
Here my classes
admin:
class Admin extends BaseUser
{
/**
* #ORM\Id()
* #ORM\Column(name="idAdmin", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=255, options={"default":NULL})
*/
protected $style;
/**
* #ORM\ManyToMany(targetEntity="AdminBundle\Entity\AdminGroup")
* #ORM\JoinTable(
* name="admin_has_group",
* joinColumns={
* #ORM\JoinColumn(name="idAdmin", referencedColumnName="idAdmin")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="idGroup", referencedColumnName="idGroup")
* }
* )
*/
protected $groups;
/**
* #return string
*/
public function getStyle()
{
return $this->style;
}
/**
* #param string $style
*/
public function setStyle($style)
{
$this->style = $style;
return $this;
}
public function setGroups($groups)
{
$this->groups = $groups;
return $this;
}
public function getGroups()
{
return $this->groups;
}
}
groups
class AdminGroup extends BaseGroup
{
/**
* #var int
* #ORM\Id
* #ORM\Column(name="idGroup", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* Group constructor.
*
* #param string $name
* #param array $roles
*/
public function __construct($name = '', $roles = array())
{
parent::__construct($name, $roles);
}
}
form generation
$admin = $this->getDoctrine()->getRepository(Admin::class)->find(1);
$admingroupList = $this->getDoctrine()->getRepository(AdminGroup::class)->findAll();
$form = $this->createFormBuilder($admin)
->add("username", TextType::class)
->add('plainPassword', PasswordType::class, $passwordSettings)
->add(
'groups', ChoiceType::class, [
'required' => false,
'multiple' => true,
'choices' => $admingroupList,
])
->add('save', SubmitType::class, ['label' => 'Save'])->getForm()->createView();
save form
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
$em->persist($user);
$em->flush();
....
}
The first problem is that i have in the overview only display the id's in the select.
The second problem is that i submit the form (with selected groups) symfony crashed with the following message
Call to a member function contains() on array
I try to convert the grouplist to an normal array they will be crashed at the save the data
Expected argument of type "FOS\UserBundle\Model\GroupInterface", "integer" given
I dont know that i sould do to make a simple symfony form with the admin data and group selection... i dont find any example for a form with fosuserbundle...
Have someone an idea what i can to without manipulate the fosuserbundle entites or the symfonycode?
If you need more source, tell me with part :)
Editing 10.12.17
I try to convert the ChoosenArray into this format
$list = [
'user' => 0,
'admin' => 1
];
but than it will be broken at
$form->handleRequest($request);
with the error:
Expected argument of type "FOS\UserBundle\Model\GroupInterface", "integer" given
I do not think the data returned from
$admingroupList = $this->getDoctrine()->getRepository(AdminGroup::class)->findAll();
Will work as you want it too. choices wants something like this
'choices' => [
'Admin' => 'admin',
'User' => 'user'
]
Where the key of the array is the name the user sees, and the value of the array is the value used in the <option>.
You probably need to manipulate the $admingroupList array to mimic the demo array above. Or write your own query in the AdminGroup Repo to return a pre-formatted array for use with a Symfony form.

How to avoid "Entities passed to the choice field must be managed. Maybe persist them in the entity manager?"

Generated Entities from existing database
Generated CRUD controller
But it does not work with exception message:
Entities passed to the choice field must be managed. Maybe persist them in the entity manager?
Entity
/**
* Question
*
* #ORM\Table(name="question", indexes={#ORM\Index(name="question_category_id", columns={"question_category_id"})})
* #ORM\Entity
*/
class Question
{
//...
/**
* #var \AppBundle\Entity\QuestionCategory
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\QuestionCategory")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="question_category_id", referencedColumnName="id")
* })
*/
private $questionCategory;
public function __construct()
{
$this->questionCategory = new QuestionCategory();
}
//...
}
Form
class QuestionType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('questionCategory');
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Question'
));
}
}
Controller
class QuestionController extends Controller
{
//...
/**
* Creates a new Question entity.
* #Route("/new", name="question_new")
* #Method({"GET", "POST"})
*/
public function newAction(Request $request)
{
$question = new Question();
$form = $this->createForm('AppBundle\Form\QuestionType', $question);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($question);
$em->flush();
return $this->redirectToRoute('question_show', array('id' => $question->getId()));
}
return $this->render('question/new.html.twig', array(
'question' => $question,
'form' => $form->createView(),
));
}
//...
}
Deep debugging gives nothing to me. How to fix it?
Test repository to reproduce error: https://github.com/sectus/question.test.local
Beware, this error can also be raised when using 'by_reference' => false, in your form type with relations that are not ManyToMany.
An unfortunate copy/paste put me in this situation.
According to the code shown on your GitHub project, the Question entity has the following constructor:
public function __construct()
{
$this->questionCategory = new QuestionCategory();
}
When you create an entity form field, it can only contain values that are managed by doctrine, but your new questionCategory is not managed.
Usually, the best solution is just to not fill this entity field in the constructor, but only in those places you strictly need it. When building a form, Synfony will fill it for you after submitting and calling $form->handleRequest().
So, in your case, just remove the Question entity's constructor.
After that, you'll also need to implement the __toString() method in QuestionCategory entity:
public function __toString(){
return 'whatever you neet to see the type`;
}
In my case I was having this issue because I was using EntityType instead of ChoiceType, to build the selectList.
EntityType display only data from database, instead ChoiceType can display "not managed" objects.
I hope this will help.
This error means the attribute questionCategory which is a relationship, is not managed by the EntityManager. For this to be done automatically, add a cascade-persist in your Doctrine Mapping for questionCategory attribute:
Entity
/**
* Question
*
* #ORM\Table(name="question")
* #ORM\Entity
*/
class Question
{
//...
/**
* #ORM\ManyToOne(
* targetEntity="QualityBundle\Entity\QuestionCategory",
* cascade={"persist"}
* )
*/
private $questionCategory;
//...
}
This way, when you call $em->persist($question);, the QuestionCategory linked to your Question will automatically be persisted as well.
There are different reasons that create this issue.
In my case, I fixed it changing
by_reference' => false
to
by_reference' => true
in the CRUD controller.
In my case, it just was because by my fault I was using the QueryManager instead of EntityManager to find the entity in my controller.

Symfony entity field : manyToMany with multiple = false - field not populated correctly

I am using symfony2 with doctrine 2.
I have a many to many relationship between two entities :
/**
* #ORM\ManyToMany(targetEntity="\AppBundle\Entity\Social\PostCategory", inversedBy="posts")
* #ORM\JoinTable(
* name="post_postcategory",
* joinColumns={#ORM\JoinColumn(name="postId", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="postCategoryId", referencedColumnName="id", onDelete="CASCADE")}
* )
*/
private $postCategories;
Now I want to let the user only select one category. For this I use the option 'multiple' => false in my form.
My form:
->add('postCategories', 'entity', array(
'label'=> 'Catégorie',
'required' => true,
'empty_data' => false,
'empty_value' => 'Sélectionnez une catégorie',
'class' => 'AppBundle\Entity\Social\PostCategory',
'multiple' => false,
'by_reference' => false,
'query_builder' => $queryBuilder,
'position' => array('before' => 'name'),
'attr' => array(
'data-toggle'=>"tooltip",
'data-placement'=>"top",
'title'=>"Choisissez la catégorie dans laquelle publier le feedback",
)))
This first gave me errors when saving and I had to change the setter as following :
/**
* #param \AppBundle\Entity\Social\PostCategory $postCategories
*
* #return Post
*/
public function setPostCategories($postCategories)
{
if (is_array($postCategories) || $postCategories instanceof Collection)
{
/** #var PostCategory $postCategory */
foreach ($postCategories as $postCategory)
{
$this->addPostCategory($postCategory);
}
}
else
{
$this->addPostCategory($postCategories);
}
return $this;
}
/**
* Add postCategory
*
* #param \AppBundle\Entity\Social\PostCategory $postCategory
*
* #return Post
*/
public function addPostCategory(\AppBundle\Entity\Social\PostCategory $postCategory)
{
$postCategory->addPost($this);
$this->postCategories[] = $postCategory;
return $this;
}
/**
* Remove postCategory
*
* #param \AppBundle\Entity\Social\PostCategory $postCategory
*/
public function removePostCategory(\AppBundle\Entity\Social\PostCategory $postCategory)
{
$this->postCategories->removeElement($postCategory);
}
/**
* Get postCategories
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getPostCategories()
{
return $this->postCategories;
}
/**
* Constructor
* #param null $user
*/
public function __construct($user = null)
{
$this->postCategories = new \Doctrine\Common\Collections\ArrayCollection();
}
Now, when editing a post, I also have an issue because it uses a getter which ouputs a collection, not a single entity, and my category field is not filled correctly.
/**
* Get postCategories
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getPostCategories()
{
return $this->postCategories;
}
It's working if I set 'multiple' => true but I don't want this, I want the user to only select one category and I don't want to only constraint this with asserts.
Of course there are cases when I want to let the user select many fields so I want to keep the manyToMany relationship.
What can I do ?
If you want to set the multiple option to false when adding to a ManyToMany collection, you can use a "fake" property on the entity by creating a couple of new getters and setters, and updating your form-building code.
(Interestingly, I saw this problem on my project only after upgrading to Symfony 2.7, which is what forced me to devise this solution.)
Here's an example using your entities. The example assumes you want validation (as that's slightly complicated, so makes this answer hopefully more useful to others!)
Add the following to your Post class:
public function setSingleCategory(PostCategory $category = null)
{
// When binding invalid data, this may be null
// But it'll be caught later by the constraint set up in the form builder
// So that's okay!
if (!$category) {
return;
}
$this->postCategories->add($category);
}
// Which one should it use for pre-filling the form's default data?
// That's defined by this getter. I think you probably just want the first?
public function getSingleCategory()
{
return $this->postCategories->first();
}
And now change this line in your form:
->add('postCategories', 'entity', array(
to be
->add('singleCategory', 'entity', array(
'constraints' => [
new NotNull(),
],
i.e. we've changed the field it references, and also added some inline validation - you can't set up validation via annotations as there is no property called singleCategory on your class, only some methods using that phrase.
You can setup you form type to not to use PostCategory by reference (set by_reference option to false)
This will force symfony forms to use addPostCategory and removePostCategory instead of setPostCategories.
UPD
1) You are mixing working with plain array and ArrayCollection. Choose one strategy. Getter will always output an ArrayCollection, because it should do so. If you want to force it to be plain array add ->toArray() method to getter
2) Also I understand that choice with multiple=false return an entity, while multiple=true return array independend of mapped relation (*toMany, or *toOne). So just try to remove setter from class and use only adder and remover if you want similar behavior on different cases.
/** #var ArrayCollection|PostCategory[] */
private $postCategories;
public function __construct()
{
$this->postCategories = new ArrayCollection();
}
public function addPostCategory(PostCategory $postCategory)
{
if (!$this->postCategories->contains($postCategory) {
$postCategory->addPost($this);
$this->postCategories->add($postCategory);
}
}
public function removePostCategory(PostCategory $postCategory)
{
if ($this->postCategories->contains($postCategory) {
$postCategory->removePost($this);
$this->postCategories->add($postCategory);
}
}
/**
* #return ArrayCollection|PostCategory[]
*/
public function getPostCategories()
{
return $this->postCategories;
}
In my case, the reason was that Doctrine does not have relation One-To-Many, Unidirectional with Join Table. In Documentations example is show haw we can do this caind of relation by ManyToMany (adding flag unique=true on second column).
This way is ok but Form component mixes himself.
Solution is to change geters and seters in entity class... even those generated automatically.
Here is my case (I hope someone will need it). Assumption: classic One-To-Many relation, Unidirectional with Join Table
Entity class:
/**
* #ORM\ManyToMany(targetEntity="B2B\AdminBundle\Entity\DictionaryValues")
* #ORM\JoinTable(
* name="users_responsibility",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="responsibility_id", referencedColumnName="id", unique=true, onDelete="CASCADE")}
* )
*/
private $responsibility;
/**
* Constructor
*/
public function __construct()
{
$this->responsibility = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add responsibility
*
* #param \B2B\AdminBundle\Entity\DictionaryValues $responsibility
*
* #return User
*/
public function setResponsibility(\B2B\AdminBundle\Entity\DictionaryValues $responsibility = null)
{
if(count($this->responsibility) > 0){
foreach($this->responsibility as $item){
$this->removeResponsibility($item);
}
}
$this->responsibility[] = $responsibility;
return $this;
}
/**
* Remove responsibility
*
* #param \B2B\AdminBundle\Entity\DictionaryValues $responsibility
*/
public function removeResponsibility(\B2B\AdminBundle\Entity\DictionaryValues $responsibility)
{
$this->responsibility->removeElement($responsibility);
}
/**
* Get responsibility
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getResponsibility()
{
return $this->responsibility->first();
}
Form:
->add('responsibility', EntityType::class,
array(
'required' => false,
'label' => 'Obszar odpowiedzialności:',
'class' => DictionaryValues::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('n')
->where('n.parent = 2')
->orderBy('n.id', 'ASC');
},
'choice_label' => 'value',
'placeholder' => 'Wybierz',
'multiple' => false,
'constraints' => array(
new NotBlank()
)
)
)
I know its a pretty old question, but the problem is still valid today.
Using a simple inline data transformer did the trick for me.
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('profileTypes', EntityType::class, [
'multiple' => false,
'expanded' => true,
'class' => ProfileType::class,
]);
// data transformer so profileTypes does work with multiple => false
$builder->get('profileTypes')
->addModelTransformer(new CallbackTransformer(
// return first item from collection
fn ($data) => $data instanceof Collection && $data->count() ? $data->first() : $data,
// convert single ProfileType into collection
fn ($data) => $data && $data instanceof ProfileType ? new ArrayCollection([$data]) : $data
));
}
PS: Array functions are available in PHP 7.4 and above.

Symfony 2 - actions to save data with one to one relationship

I am new to Symfony 2. So pardon me if this is a simple problem but I just can't figure it out how to deal with it.
I have 2 entities called Book and Page.
This is the snippet of Book entity code
/**
* Book
*
* #ORM\Table(name="`book`")
*/
class Book {
/**
* #var integer
*
* #ORM\Column(name="`id`", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="`name`", type="string", length=255, nullable=false)
*/
private $name;
...
}
And this is the snippet for my Page entity
/**
* Page
*
* #ORM\Table(name="`page`")
*/
class Page {
/**
* #var integer
*
* #ORM\Column(name="`id`", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\OneToOne(targetEntity="Book")
* #ORM\JoinColumn(name="`book_id`", referencedColumnName="`id`")
*/
private $book;
/**
* #var string
*
* #ORM\Column(name="`page_number`", type="bigint", nullable=false)
*/
private $pageNumber;
...
}
I am currently using PosgreSQL as my RDBMS.
Creating and saving data into Book table is fine. But when it comes to saving data for the Page, I need to make it reference to a Book by book id. The book is already stored in the database.
I know that the old school way is to pass the book id as hidden field into the Page form.
I am just wondering, is this the right way to do in Symfony?
What I have tried is that I pass a GET request to the page create action.
Something along the line of
http://sites/page/create?bookId=123
And the appropriate action on the Page controller is:
class PageController extends Controller {
public function createAction(Request $request) {
// Check if bookId has been passed
$bookId = $request->get('bookId');
// Find the book by its $bookId
$book = $this->getDoctrine()->getRepository("MyBundle:Book")->find($bookId);
// Create new page
$page = new Page();
// Assign the book to the page
$page->setBook($book);
// Create the page form
$pageForm = $this->createForm(
new PageType(),
$page
);
$pageForm->handleRequest($request);
if ($pageForm->isValid()) {
$pageData = $pageForm->getData();
// SAVE DATA HERE
// Redirect to index page
return new RedirectResponse($this->generateUrl('index_page'));
}
// Then render to the template
return $this->render(
'MyBundle:Page:index.html.twig',
array('form' => $pageForm->createView())
);
}
}
And my page type is:
class PageType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('page_number', 'text', array(
'label' => 'Page Number',
'required' => true,
)
);
$builder->add('content', 'textarea', array(
'label' => 'Page Content',
'required' => false,
)
);
$builder->add('save', 'submit', array(
'label' => 'Save',
'attr' => array('class' => 'btn')
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'data_class' => 'MyBundle\Entity\Page'
));
}
...
}
My issue is that:
Is this the right way to deal with creating entities that has one to one relationship with another entity?
When I did my way above, it seems like when I submit the form, I don't get the book entity anymore. What is wrong?
I would really appreciate it if someone could point me in the right direction regarding creating an entity that has one to one relationship with another entity.
And if there is anything wrong with the code, could someone please point it out?
your codes have same issue:
(1)Every form needs to know the name of the class that holds the underlying data (Symfony doc)
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\TaskBundle\Entity\Task',
));
}
(2)form field name must same as entity field name
What I did was:
To create entities that has one to one relationship with another entity, I use REST way to design the URL like below (where 123 is the bookId)
http:sites/page/create/123
I have also changed my createAction on PageController to receive $bookId instead of $request.
It look something like below.
class PageController extends Controller {
public function createAction($bookId) {
// Find the book by its $bookId
$book = $this->getDoctrine()->getRepository("MyBundle:Book")->find($bookId);
// Create new page
$page = new Page();
// Assign the book to the page
$page->setBook($book);
// Create the page form
$pageForm = $this->createForm(
new PageType(),
$page
);
$pageForm->handleRequest($request);
if ($pageForm->isValid()) {
$pageData = $pageForm->getData();
// SAVE DATA HERE
// Redirect to index page
return new RedirectResponse($this->generateUrl('index_page'));
}
// Then render to the template
return $this->render(
'MyBundle:Page:index.html.twig',
array('form' => $pageForm->createView())
);
}
}
To get it to pass $bookId into my createAction, I have define the routing to be:
page_create:
pattern: /page/create/{bookId}
defaults: { _controller: MyBundle:Page:create }
requirements:
bookId: \d+
Thanks to Wing for the suggestion.

symfony2 adds data to object but never persists to DB (doctrine ORM many-to-many)

I have an issue with one of my many-to-many relations. I have debugged my form and I see that the chosen objects are actually in the entity object after form validation, but it never gets saved to the database, so there must be an issue with my mapping code, but I copy-pasted from a working one just substituted the fields and paths...
Here's the company object's relevant code
/**
* #ORM\ManyToMany(targetEntity="BizTV\ContentManagementBundle\Entity\Template", inversedBy="companies")
* #ORM\JoinTable(name="templatePermissions")
*/
private $templatePermissions;
/**
* Add templatePermissions
*
* #param BizTV\ContentManagementBundle\Entity\Template $templatePermissions
*/
public function addTemplate(\BizTV\ContentManagementBundle\Entity\Template $templatePermissions)
{
$this->templatePermissions[] = $templatePermissions;
}
and the template object
/**
* #ORM\ManyToMany(targetEntity="BizTV\BackendBundle\Entity\company", mappedBy="templatePermissions")
*/
private $companies;
/**
* Add companies
*
* #param BizTV\BackendBundle\Entity\company $companies
*/
public function addCompany(\BizTV\BackendBundle\Entity\company $companies)
{
$this->companies[] = $companies;
}
/**
* Get companies
*
* #return Doctrine\Common\Collections\Collection
*/
public function getCompanies()
{
return $this->companies;
}
The code for updating (and creating is similar, and has the same problem) is just a standard ...
public function updateAction($id)
{
$em = $this->getDoctrine()->getEntityManager();
$entity = $em->getRepository('BizTVContentManagementBundle:Template')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Template entity.');
}
$editForm = $this->createForm(new TemplateType(), $entity);
$deleteForm = $this->createDeleteForm($id);
$request = $this->getRequest();
$editForm->bindRequest($request);
if ($editForm->isValid()) {
//DEBUG
// foreach($entity->getCompanies() as $c) {
// echo $c->getCompanyName();
// }
// die;
$em->persist($entity);
$em->flush();
$this->getRequest()->getSession()->setFlash('success', 'Template '.$entity->getId().' har uppdaterats.' );
return $this->redirect($this->generateUrl('listTemplates'));
}
Here's my form, like I said, it works perfectly for putting the stuff into my object (template entity) but it doesn't get persisted to DB...
$builder
->add('companies', 'entity', array(
'label' => 'Företag som har tillgång till mallen',
'multiple' => true, // Multiple selection allowed
'expanded' => true, // Render as checkboxes
'property' => 'company_name',
'class' => 'BizTV\BackendBundle\Entity\company',
))
;
What am I missing?
Pay attention for picking owning/inverse side. The owning side should be the one which is responsible for persist action. In your case owning side should be Template entity not Company.
For more info look here:
http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/association-mapping.html#picking-owning-and-inverse-side
Have you company entity class name starts with lower case? If no, you have a typo:
BizTV\BackendBundle\Entity\company
try add cascade on persist
At the end your relation definition in Company entity should look sth like this:
/**
* #ORM\ManyToMany(targetEntity="BizTV\BackendBundle\Entity\company", inversedBy="templatePermissions", cascade={"persist"})
*/
private $companies;

Categories