Managing users/roles/groups in FOSUserBundle - php

I am developing a simple CRUD to manage users/roles/groups of the application in which I am working. To manage users I'm using FOSUserBundle. What I want to do can be accomplished in several ways:
Assigning roles to groups and then assign users to these groups
Assigning roles to users directly
But I have no idea how. I knew that FOSUser BaseUser class already has a column roles and in the documentation of FOSUser explains how to establish a ManyToMany relationship between users and groups but do not talk anything about roles. The only idea that comes to mind is to create an entity to manage the roles as well as a form for the same purpose, something like what you see below:
Role Entity
use Symfony\Component\Security\Core\Role\RoleInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Table(name="fos_role")
* #ORM\Entity(repositoryClass="UserBundle\Entity\Repository\RoleRepository")
*
* #see User
* #see \UserBundle\Role\RoleHierarchy
*
*/
class Role implements RoleInterface
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id()
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=80, unique=true)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="Role", inversedBy="children")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
* #var Role[]
*/
private $parent;
/**
* #ORM\OneToMany(targetEntity="Role", mappedBy="parent")
* #var ArrayCollection|Role[]
*/
private $children;
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="roles")
*/
private $users;
public function __construct($role = "")
{
if (0 !== strlen($role)) {
$this->name = strtoupper($role);
}
$this->users = new ArrayCollection();
$this->children = new ArrayCollection();
}
/**
* #see RoleInterface
*/
public function getRole()
{
return $this->name;
}
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getUsers()
{
return $this->users;
}
public function addUser($user, $addRoleToUser = true)
{
$this->users->add($user);
$addRoleToUser && $user->addRole($this, false);
}
public function removeUser($user)
{
$this->users->removeElement($user);
}
public function getChildren()
{
return $this->children;
}
public function addChildren(Role $child, $setParentToChild = true)
{
$this->children->add($child);
$setParentToChild && $child->setParent($this, false);
}
public function getDescendant(& $descendants = array())
{
foreach ($this->children as $role) {
$descendants[spl_object_hash($role)] = $role;
$role->getDescendant($descendants);
}
return $descendants;
}
public function removeChildren(Role $children)
{
$this->children->removeElement($children);
}
public function getParent()
{
return $this->parent;
}
public function setParent(Role $parent, $addChildToParent = true)
{
$addChildToParent && $parent->addChildren($this, false);
$this->parent = $parent;
}
public function __toString()
{
if ($this->children->count()) {
$childNameList = array();
foreach ($this->children as $child) {
$childNameList[] = $child->getName();
}
return sprintf('%s [%s]', $this->name, implode(', ', $childNameList));
}
return sprintf('%s', $this->name);
}
}
Role Form Type
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class RoleType extends AbstractType {
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('parent');
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\UserBundle\Entity\Role'
));
}
/**
* #return string
*/
public function getName()
{
return 'role';
}
}
If so what would add to my user form would look something like this
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username', 'text')
->add('email', 'email')
->add('enabled', null, array(
'label' => 'Habilitado',
'required' => false
))
->add('rolesCollection', 'entity', array(
'class' => 'UserBundle:Role',
'multiple' => true,
'expanded' => true,
'attr' => array('class' => 'single-line-checks')
))
->add('groups', 'entity', array(
'class' => 'UserBundle:Group',
'multiple' => true,
'expanded' => true,
));
}
But I do not know if it is the right way to handle the roles since in this case would be creating a new table in my DB called fos_roles where were the relationships between users/roles is handled but relationships between groups/roles stay out of it, then that's where I'm a little lost and need help from the more experienced in that tell me and alert if I'm on track and that would make them to achieve what I explain in the first two points. Any advice or help? How do you deal with this?

Symfony Roles
The way FOSUserBundle deals with Roles is to store them in the roles column that you've seen, in a serialised format like this: a:1:{i:0;s:10:"ROLE_ADMIN";}. So there's no need for any other tables or entities^.
^ This is in contrast to Groups, which need to be explicitly configured, are represented by a separate Table/Entity, and do involve relating Users to Groups in the DB. Groups let you define arbitrary collections of Roles which can then be given to each User as a discrete bundle.
A User can be a member of any number of Roles. They're identified by strings starting with "ROLE_", you can just start using a new Role.
What the Roles mean for your application is completely up to you, but they're quite a high-level tool - a User is either in a particular Role or they aren't.
You put people in Roles either via the Symfony console:
php app/console fos:user:promote testuser ROLE_ADMIN
Or in PHP:
$user = $this->getUser();
$userManager = $container->get('fos_user.user_manager');
$user->addRole('ROLE_ADMIN');
$userManager->updateUser($user);
And you can test membership in PHP:
$user = $this->getUser();
if ($user->hasRole('ROLE_ADMIN'))
{
//do something
}
Or using Annotations:
/**
* #Security("has_role('ROLE_ADMIN')")
*/
public function adminAction()
{
//...
or
/**
* #Security("has_role('ROLE_ADMIN')")
*/
class AdminController
{
//...

I added the functionality to add default group to the user during registration by overriding the confirmAction in Registration Controller
What I did is I overrided the Registration Controller in my project Bundle by defining the parent to FosUserBUndle .
Then created a function confirmedAction and in the body of the function added this code
$repository = $em->getRepository('AdminAdminBundle:Group');
$group = $repository->findOneByName('staff');
$em = $this->getDoctrine()->getEntityManager();
$user = $this->getUser();
$user->addGroup($group);
$userManager = $this->get('fos_user.user_manager');
$userManager->updateUser($user);
if (!is_object($user) || !$user instanceof FOS\UserBundle\Model\UserInterface) {
throw new AccessDeniedException('This user does not have access to this section.');
}
return $this->render('FOSUserBundle:Registration:confirmed.html.twig',
['user' => $user]);
And it perfectly saved in db with group assignment.
Hope this will help some one in need as there is little information about the implementation in official fosuserbundle doc.

Related

Doctrine Array type field not being updated

My question is more or less the same as this one:
How to force Doctrine to update array type fields?
but for some reason the solution didn't work for me, so there must be a detail I am missing and I would be happy if someone could point it out to me.
The context is a Symfony 5.2 app with Doctrine ^2.7 being used.
Entity-Class-Excerpt:
class MyEntity {
// some properties
/**
* #var string[]
* #Groups("read")
* #ORM\Column(type="array")
* #Assert\Valid
*/
protected array $abbreviations = [];
public function getAbbreviations(): ?array
{
return $this->abbreviations;
}
//this is pretty much the same set-function as in the question I referenced
public function setAbbreviations(array $abbreviations)
{
if (!empty($abbreviations) && $abbreviations === $this->abbreviations) {
reset($abbreviations);
$abbreviations[key($abbreviations)] = clone current($abbreviations);
}
$this->abbreviations = $abbreviations;
}
public function addAbbreviation(LocalizableStringEmbeddable $abbreviation): self
{
foreach ($this->abbreviations as $existingAbbreviation) {
if ($abbreviation->equals($abbreviation)) {
return $this;
}
}
$this->abbreviations[] = $abbreviation;
return $this;
}
public function removeAbbreviation(LocalizableStringEmbeddable $abbreviation): self
{
foreach ($this->abbreviations as $i => $existingAbbreviation) {
if ($abbreviation->equals($existingAbbreviation)) {
array_splice($this->abbreviations, $i, 1);
return $this;
}
}
return $this;
}
}
But none of these methods are ever being called (I also tried removing add-/removeAbbreviation only leaving get/set in place).
LocalizableStringEmbeddable being an Embeddable like this:
* #ORM\Embeddable
*/
class LocalizableStringEmbeddable
{
/**
* #var string|null
* #Groups("read")
* #ORM\Column(type="string", nullable=false)
*/
private ?string $text;
/**
* #var string|null
* #Groups("read")
* #ORM\Column(type="string", nullable=true)
* #Assert\Language
*/
private ?string $language;
//getters/setters/equals/#Assert\Callback
}
By using dd(...) I can furthermore say that in my controller on submit
$myEntity = $form->getData();
yields a correctly filled MyEntity with an updated array but my subsequent call to
$entityManager->persist($myEntity);
$entityManager->flush();
doesn't change the database.
What am I missing?
EDIT: I was asked to give information about the Type I use. It is a custom one that is based on this class. So technically at the base of things I am using a collection type.
abstract class AbstractLocalizableUnicodeStringArrayType extends AbstractType implements DataMapperInterface
{
abstract public function getDataClassName(): string;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('items', CollectionType::class, [
'entry_type' => LocalizedStringEmbeddableType::class,
'entry_options' => [
'data_class' => $this->getDataClassName(),
'attr' => ['class' => 'usa-item-list--item-box'],
],
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
])
;
$builder->setDataMapper($this);
}
public function mapDataToForms($viewData, iterable $forms): void
{
$forms = iterator_to_array($forms);
if (null === $viewData) {
return;
}
if (!is_array($viewData)) {
throw new UnexpectedTypeException($viewData, "array");
}
$forms['items']->setData($viewData);
}
public function mapFormsToData(iterable $forms, &$viewData): void
{
$forms = iterator_to_array($forms);
$viewData = $forms['items']->getData();
}
}
As I found out this can be solved (in an unelegant way in my opinion but until something better comes around I want to state this) by combining two answers from another post on SO:
https://stackoverflow.com/a/13231876/6294605
https://stackoverflow.com/a/59898632/6294605
This means combining a preflush-hook in your entity class with a "fake-change" of the array in question.
Remark that doing the fake-change in a setter/adder/remover didn't work for me as those are not being called when editing an existing entity. In this case only setters of the changed objects inside the array will be called thus making Doctrine not recognize there was a change to the array itself as no deep-check seems to be made.
Another thing that was not stated in the other thread I wanna point out:
don't forget to annotate your entity class with
#ORM\HasLifecycleCallbacks
or else your preflush-hook will not be executed.

Symfony 3: allow_delete in Forms not working

I have a problem with my form in Symfony 3. I have one-to-many-to-one join (Doctrine 2). It handles Orders (Order), Products (Product) and joining entity (OrderProduct), which holds amount of product in order.
I have a form for add and update order entries, which uses Collection of OrderProducts. It's all based on documentation (link).
In the form I have a button for add a product (from documentation, adds a <li> to DOM) and every added has a button for remove it (from documentation, removes <li> from DOM). This part is working - adding to and removing from DOM.
Adding products works (as at the new order, than when editting).
But my problem is with removing. Products which was succesfully deleted from form, are still appearing in $editForm->getData().
OrderProduct Form
namespace AppBundle\Form;
use AppBundle\Entity\ProductType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class OrderProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('amount')
->add('product')
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
array(
'data_class' => 'AppBundle\Entity\OrderProduct',
)
);
}
public function getName()
{
return 'app_bundle_order_product_type';
}
}
Order Form
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class OrderType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('customer')
->add('date', null, array('widget' => 'single_text'))
->add('payment', null, array('widget' => 'single_text'))
->add('processed', null, array('widget' => 'single_text'))
->add(
'orderProducts',
CollectionType::class,
array(
'entry_type' => OrderProductType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'prototype' => true,
'delete_empty' => true,
'entry_options' => array('data_class' => 'AppBundle\Entity\OrderProduct'),
)
);
}
public function configureOptions(OptionsResolver $resolver)
{
}
public function getName()
{
return 'app_bundle_order_type';
}
}
Current action in OrderController
(Adding product works, removing not)
public function editAction (Request $request, $orderId) {
$em = $this->getDoctrine()->getManager();
$order = $em->getRepository('AppBundle:Order')->find($orderId);
if (!$order) {
throw $this->createNotFoundException('No order found for id '.$orderId);
}
$editForm = $this->createForm(OrderType::class, $order);
$editForm->add('submit', SubmitType::class);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$order = $editForm->getData();
//print '<pre>';
//var_dump($order->getOrderProducts());
//die();
$orderProducts = $order->getOrderProducts();
$em->persist($order);
foreach ($orderProducts as $oneOrderProduct) {
$oneOrderProduct->setOrder($order);
$em->persist($oneOrderProduct);
}
//print '<pre>';
//var_dump($order->getOrderProducts());
//die();
$em->flush();
return $this->redirectToRoute('one_order', array('orderId' => $order->getId()));
}
return $this->render(
'order/new.html.twig', array(
'form' => $editForm->createView(),
));
}
I know that I must remove removed OrderProducts from Order in editAction, but now I can't, because from the form is sent all OrderProducts.
Order Entity
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\OrderProduct;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
/**
* #ORM\Entity
* #ORM\Table(name="order_")
*/
class Order
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Customer", inversedBy="orders")
* #ORM\JoinColumn(name="customer_id", referencedColumnName="id")
*/
private $customer;
/**
* #ORM\Column(type="date")
*/
private $date;
/**
* #ORM\Column(type="date")
*/
private $payment;
/**
* #ORM\Column(type="date")
*/
private $processed;
public function __toString()
{
return strval($this->getId());
}
/**
* #ORM\OneToMany(targetEntity="OrderProduct", mappedBy="order")
*/
private $orderProducts;
public function __construct()
{
$this->orderProducts = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set date
*
* #param \DateTime $date
*
* #return Order
*/
public function setDate($date)
{
$this->date = $date;
return $this;
}
/**
* Get date
*
* #return \DateTime
*/
public function getDate()
{
return $this->date;
}
/**
* Set payment
*
* #param \DateTime $payment
*
* #return Order
*/
public function setPayment($payment)
{
$this->payment = $payment;
return $this;
}
/**
* Get payment
*
* #return \DateTime
*/
public function getPayment()
{
return $this->payment;
}
/**
* Set processed
*
* #param \DateTime $processed
*
* #return Order
*/
public function setProcessed($processed)
{
$this->processed = $processed;
return $this;
}
/**
* Get processed
*
* #return \DateTime
*/
public function getProcessed()
{
return $this->processed;
}
/**
* Set customer
*
* #param \AppBundle\Entity\Customer $customer
*
* #return Order
*/
public function setCustomer(\AppBundle\Entity\Customer $customer = null)
{
$this->customer = $customer;
return $this;
}
/**
* Get customer
*
* #return \AppBundle\Entity\Customer
*/
public function getCustomer()
{
return $this->customer;
}
/**
* Add orderProduct
*
* #param \AppBundle\Entity\OrderProduct $orderProduct
*
* #return Order
*/
public function addOrderProduct(\AppBundle\Entity\OrderProduct $orderProduct)
{
$this->orderProducts[] = $orderProduct;
return $this;
}
/**
* Remove orderProduct
*
* #param \AppBundle\Entity\OrderProduct $orderProduct
*/
public function removeOrderProduct(\AppBundle\Entity\OrderProduct $orderProduct)
{
$this->orderProducts->removeElement($orderProduct);
}
/**
* Get orderProducts
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getOrderProducts()
{
return $this->orderProducts;
}
}
But in POST it's OK as you can see here, here I removed 2 of 4 products. Problem appears in form handling.
If by_reference is set to false the underlying Order entity MUST have a method in your case called [removeOrderProduct]. What also might be the problem is that you are not specifying data_class option inside the configureOptions method. In your case if Order entity is in 'AppBundle\Entity\Order', then the configureOptions method should contain:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Order',
));
}
And I would suggest you do the same in OrderProductType class also.
My guess would be that since you haven't specified the 'data_class' option in your OrderType class, the by_reference option in the 'orderProducts' field might not be able to figure out where to look for the [removeOrderProduct] method. So set that option and make sure you have that method inside your Order entity class.
IF this is not the problem then you should provide more information about your order entity and where exactly are you calling the getData method.
Update:
Looking at your code I can't identify the problem, that might be causing the entities to not get removed. But I have spotted a few oddities in your code:
In the controller, when handling form submittions it is not necessary to call the getData method: after you call handleRequest the $order object gets updated and holds the new information (since objects are passed by reference, the form can't apply the changes without changing the original $order as well). So there is no need to $order = $form->getData(), since you have already defined the $order variable before and it holds the reference to the same object to which the form mapped the posted values.
If that does not help, I suggest you add die; statements all over the place, just to make sure that at each step the right methods are called. For instance add die; to the removeOrderProduct method, to check if its hit. If it was hit, the problem is not going to be evident from the data you have provided to us, so it will require further debugging.
Also it might not be a problem now, but if you want to remove the products not present in the list after submittion, you have to call $order->getOrderProducts and add each of the items to a new collection that holds the previous orderProducts (before submittion) and compare that to the values after submittion to figure out which ones need removing.
I solved it last night :)
This is working controller action:
/**
* #Route("/{orderId}/edit", name="edit_order", requirements={"orderId": "\d+"})
* #param Request $request
* #param $orderId
*
* #return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function editAction(Request $request, $orderId)
{
$em = $this->getDoctrine()->getManager();
$order = $em->getRepository('AppBundle:Order')->find($orderId);
if (!$order) {
return $this->redirectToRoute('new_order');
}
$originalOrderProducts = new ArrayCollection();
foreach ($order->getOrderProducts() as $orderProduct) {
$originalOrderProducts->add($orderProduct);
}
$editForm = $this->createForm(OrderType::class, $order);
$editForm->add('submit', SubmitType::class);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$orderProducts = $order->getOrderProducts();
foreach ($originalOrderProducts as $oneOriginalOrderProduct) {
if (false === $order->getOrderProducts()->contains($oneOriginalOrderProduct)) {
$order->removeOrderProduct($oneOriginalOrderProduct);
$em->remove($oneOriginalOrderProduct);
$em->flush();
}
}
foreach ($orderProducts as $oneOrderProduct) {
if ($oneOrderProduct->getAmount() == 0) {
$order->removeOrderProduct($oneOrderProduct);
$em->remove($oneOrderProduct);
} else {
if (!$originalOrderProducts->contains($oneOrderProduct)) {
$oneOrderProduct->setOrder($order);
}
$em->persist($oneOrderProduct);
}
$em->persist($order);
$em->flush();
}
return $this->redirectToRoute('order');
}
return $this->render('order/new.html.twig', array('form' => $editForm->createView()));
}

Symfony2 Registration form with only single record of collection field

So I'm trying to create a registration form in Symfony 2 which contains my "Person" entity. The person entity has a one-to-many join, and I want the registration form to allow the user to select a single instance of this "Many" side of the join.
The structure is Users and Institutions. A user can have many institutions. I want a user to select a single institution at registration time (but the model allows for more later).
The basic structure is:
RegistrationType -> PersonType -> PersonInstitutionType
…with corresponding models:
Registration (simple model) -> Person (doctrine entity) -> PersonInstitution (doctrine entity, oneToMany relation from Person)
I tried to pre-populate an empty Person & PersonInstitution record in the RegistrationController but it gives me the error:
Expected argument of type "string or Symfony\Component\Form\FormTypeInterface", "TB\CtoBundle\Entity\PersonInstitution" given
(ok above has been fixed).
I've moved the code from my website to here below, trying to remove all the irrelevant bits.
src/TB/CtoBundle/Form/Model/Registration.php
namespace TB\CtoBundle\Form\Model;
use TB\CtoBundle\Entity\Person;
class Registration
{
/**
* #var Person
*/
private $person
private $termsAccepted;
}
src/TB/CtoBundle/Form/RegistrationType.php
namespace TB\CtoBundle\Form;
use TB\CtoBundle\Form\PersonType;
class RegistrationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('person', new PersonType());
$builder->add('termsAccepted','checkbox');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'TB\CtoBundle\Form\Model\Registration',
'cascade_validation' => true,
));
}
public function getName()
{
return 'registration';
}
}
src/TB/CtoBundle/Entity/Person.php
namespace TB\CtoBundle\Entity;
use TB\CtoBundle\Entity\PersonInstitution
/**
* #ORM\Entity()
*/
class Person
{
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="PersonInstitution", mappedBy="person", cascade={"persist"})
*/
private $institutions;
}
src/TB/CtoBundle/Form/PersonType.php
namespace TB\CtoBundle\Form;
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('institutions', 'collection', array('type' => new PersonInstitutionType()))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'TB\CtoBundle\Entity\Person',
));
}
/**
* #return string
*/
public function getName()
{
return 'tb_ctobundle_person';
}
}
src/TB/CtoBundle/Entity/PersonInstitution.php
namespace TB\CtoBundle\Entity
/**
* PersonInstitution
*
* #ORM\Table()
* #ORM\Entity
*/
class PersonInstitution
{
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="institutions", cascade={"persist"})
*/
private $person;
/**
* #ORM\ManyToOne(targetEntity="Institution", inversedBy="members")
*/
private $institution;
/**
* #ORM\Column(type="boolean")
*/
private $approved;
}
src/TB/CtoBundle/Form/PersonInstititionType.php
namespace TB\CtoBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class PersonInstitutionType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('approved')
->add('person')
->add('institution')
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'TB\CtoBundle\Entity\PersonInstitution'
));
}
/**
* #return string
*/
public function getName()
{
return 'tb_ctobundle_personinstitution';
}
}
src/TB/CtoBundle/Controller/Registration.php
namespace TB\CtoBundle\Controller;
class RegisterController extends Controller
{
/**
*
* #param Request $request
* #return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function registerAction(Request $request)
{
$registration = new Registration;
$person = new Person();
$institution = new PersonInstitution();
$person->addInstitution($institution);
$registration->setPerson($person);
// this causes error:
// Entities passed to the choice field must be managed. Maybe persist them in the entity manager?
// $institution->setPerson($person);
$form = $this->createForm(new RegistrationType(), $registration);
$form->handleRequest($request);
if($form->isValid()) {
$registration = $form->getData();
$person = $registration->getPerson();
// new registration - account status is "pending"
$person->setAccountStatus("P");
// I'd like to get rid of this if possible
// for each "PersonInstitution" record, set the 'person' value
foreach($person->getInstitutions() as $rec) {
$rec->setPerson($person);
}
$em = $this->getDoctrine()->getManager();
$em->persist($person);
$em->flush();
}
return $this->render('TBCtoBundle:Register:register.html.twig', array('form' => $form->createView()));
}
}
Here is a detailed solution for adding an Collection field to Person entity and formType.
Your complex question with Registration entity can be solved with this.
I suggest you to use this 3 entity related connection if it is really needed. (only because of termsAccepted data!?)
If you won't change your opinion, then use this annotation:
Registration code:
use TB\CtoBundle\Entity\Person;
/**
* #ORM\OneToOne(targetEntity="Person")
* #var Person
*/
protected $person;
Person code:
use TB\CtoBundle\Entity\PersonInstitution;
/**
* #ORM\OneToMany(targetEntity="PersonInstitution", mappedBy = "person")
* #var ArrayCollection
*/
private $institutions;
/* I suggest you to define these functions:
setInstitutions(ArrayCollection $institutions),
getInstitutions()
addInstitution(PersonInstitution $institution)
removeInstitution(PersonInstitution $institution)
*/
PersonInstitution code:
use TB\CtoBundle\Entity\Person;
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="institutions", cascade={"persist"}))
* #var Person
*/
private $person;
PersonType code:
use TB\CtoBundle\Form\PersonInstitutionType;
->add('institutions', 'collection', array(
'type' => new PersonInstitutionType(), // here is your mistake!
// Other options can be selected here.
//'allow_add' => TRUE,
//'allow_delete' => TRUE,
//'prototype' => TRUE,
//'by_reference' => FALSE,
));
PersonController code:
use TB\CtoBundle\Entity\Person;
use TB\CtoBundle\Entity\PersonInstitution;
/**
* ...
*/
public funtcion newAction()
{
$person = new Person;
$institution = new PersonInstitution;
$institution->setPerson($person);
$person->addInstitution($institution);
$form = $this->createForm(new PersonType($), $person); // you can use formFactory too.
// If institution field is required, then you have to check,
// that is there any institution able to chose in the form by the user.
// Might you can redirect to institution newAction in that case.
return array( '...' => $others, 'form' => $form);
}
If you need more help in twig code, then ask for it.

Array Collection, symfony: add a relation [closed]

Closed. This question is not reproducible or was caused by typos. It is not currently accepting answers.
This question was caused by a typo or a problem that can no longer be reproduced. While similar questions may be on-topic here, this one was resolved in a way less likely to help future readers.
Closed 8 years ago.
Improve this question
I'm new of Symfony and php, and I'm trying to understand, without outcome, the array collection.
Now I have two entity, Mission and User, in relation ManytoMany. I have a form to create new Missions and a form to create new User.
Now I have to create a "modifyMissionAction" that allows me to set the Users for that missions, but I didn't understand how to do it.
I read the documentation here but it doesn't help. How could I do?
Thank you
This is my User Entity is:
abstract class User extends BaseUser
{
/**
* #var \Doctrine\Common\Collections\ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Acme\ManagementBundle\Entity\Mission", inversedBy="users", orphanRemoval=true)
* #ORM\JoinTable(name="user_mission")
*/
private $missions;
/**
* Add missions
*
* #param \Acme\ManagementBundle\Entity\Mission $missions
* #return User
*/
public function addMission(\Acme\ManagementBundle\Entity\Mission $missions)
{
$this->missions[] = $missions;
return $this;
}
//...
And my Mission Entity:
<?php
namespace Acme\ManagementBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
/**
* #ORM\Entity
*/
class Mission {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #var integer
*/
protected $id;
/**
* #ORM\Column(type="string", length=60)
* #var String
*/
protected $name;
/**
* #ORM\Column(type="string", length=600)
* #var String
*/
protected $description;
/**
* #var \Doctrine\Common\Collections\ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Acme\ManagementBundle\Entity\User", mappedBy="missions", cascade={"all"}, orphanRemoval=true)
*/
private $users;
public function __construct(){
$this -> users = new ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Mission
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set description
*
* #param string $description
* #return Mission
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Add users
*
* #param \Acme\ManagementBundle\Entity\User $users
* #return Mission
*/
public function addUser(\Acme\ManagementBundle\Entity\User $users)
{
$this->users[] = $users;
return $this;
}
/**
* Remove users
*
* #param \Acme\ManagementBundle\Entity\User $users
*/
public function removeUser(\Acme\ManagementBundle\Entity\User $users)
{
$this->users->removeElement($users);
}
/**
* Get users
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getUsers()
{
return $this->users;
}
public function __toString()
{
return $this->name;
}
}
first of all don't forget to add __constructor for both of classes and init ArrayCollection:
//src/WebHQ/NewBundle/Entity/Mission.php
//...
public function __construct()
{
$this->users = new ArrayCollection();
}
//...
I assume that you want to add controller action witch allows you to relate user object or objects to mission object. Please read embbed forms part of Symfony book
Then create your Action. Most important is to add form element, with is embedded form of users entity:
// src/WebHQ/NewBundle/Controller/MissionController.php
//...
public function newAction(Request $request)
{
$object = new \WebHQ\NewBundle\Entity\Mission();
$form = $this->createFormBuilder($object)
->add('name', 'text')
//...
// Users objects embed form
->add('users', 'user')
//...
->add('save', 'submit')
->getForm();
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($object);
$em->flush();
return $this->redirect($this->generateUrl('web_hq_new_mission_index'));
}
}
return $this->render('WebHQNewBundle:Mission:new.html.twig', array(
'form' => $form->createView(),
//...
));
}
public function editAction($id, Request $request)
{
$object = $this->getDoctrine()
->getRepository('WebHQNewBundle:Mission')
->find($id);
$form = $this->createFormBuilder($object)
->add('name', 'text')
//...
->add('users', 'user')
//...
->add('save', 'submit')
->add('delete', 'submit')
->getForm();
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$form->get('save')->isClicked() ? $em->persist($object) : $em->remove($object);
$em->flush();
return $this->redirect($this->generateUrl('web_hq_new_mission_index'));
}
}
return $this->render('WebHQNewBundle:Mission:edit.html.twig', array(
'form' => $form->createView(),
//...
));
}
//...
Check your routing. You should have route for edit action with {id} param. If name of param not fits you change it in route and in function definition:
// src/WebHQ/NewBundle/Resources/config/route.yml
//...
web_hq_new_mission_new:
pattern: /mission/new
defaults: { _controller: WebHQNewBundle:Mission:new }
web_hq_new_mission_edit:
pattern: /mission/{id}/edit
defaults: { _controller: WebHQNewBundle:Mission:edit }
//...
Then define Form Type for User objects:
// src/WebHQ/NewBundle/Form/Type/UserType.php
namespace WebHQ\NewBundle\Form\Type;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class UserType extends AbstractType
{
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'class' => 'WebHQNewBundle:User',
'property' => 'name',
'empty_value' => 'Choose',
'required' => true,
'multiple' => true,
'query_builder' => function (Options $options) {
return function(EntityRepository $er) use ($options) {
return $er->createQueryBuilder('c')
->orderBy('c.name', 'ASC');
};
},
));
}
public function getParent()
{
return 'entity';
}
public function getName()
{
return 'user';
}
}
And register a type in service.yml:
# src/WebHQ/NewBundle/Resources/config/services.yml
#...
services:
web_hq_new.form.type.user:
class: WebHQ\NewBundle\Form\Type\UserType
tags:
- { name: form.type, alias: user }
#...
Good Luck!

Symfony 2 - show only one entity from OneToMany relationship

I need to display the min date from a group of entities in a relationship. Eg. I have User and Contracts -> the User has many Contracts, but I need to display the minimum date from contracts.
Here is the code:
class User implements UserInterface
{
/**
* #ORM\OneToMany(targetEntity="Comp\ContractBundle\Entity\Contract", mappedBy="user_id")
* #ORM\OrderBy({"id" = "DESC"})
*
*/
private $contracts;
}
class Contract
{
/**
* #var string $datastart
*
* #ORM\Column(name="datastart", type="datetime")
*/
private $datastart;
/**
* #var string $dataend
*
* #ORM\Column(name="dataend", type="datetime")
*/
private $dataend;
/**
* #var integer $user_id
*
* #ORM\ManyToOne(targetEntity="Comp\AuthBundle\Entity\User")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user_id;
}
class UserType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options){
#... other data
$builder->add('contracts','collection', array(
'type' => new ContractType()
) );
}
public function getName()
{
return 'User';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Comp\AuthBundle\Entity\User',
);
}
}
And the ContractType:
class ContractType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options){
$builder->add('name','text');
$builder->add('datastart','datetime');
}
public function getName()
{
return 'Contract';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Comp\ContractBundle\Entity\Contract',
);
}
}
The problem is, I get each Entity related to the User Entity - I just need to fetch one. If you know any good example - it would be great.
Change contracts field type from collection to entity, and google for query_builder attribute of entity field type.
In that attribute you can pass a closure (returning QueryBuilder) or QueryBuilder itself, in which you can retrieve specific records from given relationship.

Categories