I have a Doctrine user entity that has a many-to-many relationship with a roles entity. I also have a form that displays user details in it based on the user entity fields. I'm setting up the form in the normal way, in my controller using the form builder, and passing in an entity instance loaded from the database. This all works fine.
Now, what I want is a select menu(s) in this form with the role(s) the user is assigned to selected, and populated from the available roles in the database. I have created a field in my UserType form called roles that has a type of 'collection' and passed in a RoleType form instance. In my RoleType form I am adding a field with type entity and defining my role class etc. This is all as per the documentation. This all works fine, it loads the select menu populated with the Roles BUT it doesn't select the correct roles saved against the user entity.
When I trace through the form value for 'roles' (or set up a data transformer on my roles entity field) the value I am getting is a string containing the name of the role that the user is associated with. I am not getting a Role instance or a Collection/Array. Also, if I set the role entity field to be multiple = true, I get a Expected a Doctrine\Common\Collections\Collection object. error from a doctrine data transformer. Again, this is because it is expecting a collection and getting a string.
Could this be something do with the way my user entity is being hydrated? I am using $repository->findOneBy(array('id' => $id));
This is a simplified version of what I am doing:
User Class
class User implements AdvancedUserInterface, \Serializable
{
/**
* #ORM\ManyToMany(targetEntity="Role", inversedBy="users")
*/
public $roles;
public function __construct()
{
$this->roles = new ArrayCollection();
}
public function getRoles()
{
return $this->roles->toArray();
}
}
Role Class
class Role implements RoleInterface
{
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="roles")
*/
public $users;
public function __construct()
{
$this->users = new ArrayCollection();
}
public function getUsers()
{
return $this->users;
}
}
User Form Type
class UserType extends AbstractType
{
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'NameSpace\MyBundle\Entity\User',
));
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('id', 'hidden')
->add('roles', 'collection', array('type' => new RoleType()))
->add('save', 'submit');
}
public function getName()
{
return 'user';
}
}
Role Form Type
class RoleType extends AbstractType
{
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'NameSpace\MyBundle\Entity\Role',
));
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', 'entity', array(
'class' => 'NameSpace\MyBundle\Entity\Role',
'property' => 'name'
[multiple => true]
));
}
public function getName()
{
return 'role';
}
}
You are not providing a collection to your form from the user entity for roles:
public function getRoles()
{
//this returns an array
return $this->roles->toArray();
}
The form class uses the getters and setters from your entity so you are actually returning an array because that is what the Symfony2 security system needs. What you need to do is also implement a getRolesCollection field and use that in the form instead:
public function getRolesCollection()
{
//this returns a collection
return $this->roles;
}
//and (updated from comment below)
->add('roles_collection', 'entity', array('type' => new RoleType()))
The oro platform do something like this: https://github.com/orocrm/platform/blob/master/src/Oro/Bundle/UserBundle/Form/Type/UserType.php
This blog post may also be helpful: http://blog.jmoz.co.uk/symfony2-fosuserbundle-role-entities/
It is a guy adding roles in the database for the FOSUserBundle
Related
I've got a Company that has many Employees. In my form, I want the user to be able to dynamically add employees (easy enough). EmployeeType (an AbstractType) is compound, containing a first and last name. On form submission, Symfony doesn't seem to carry over the data from the form into the constructor for the "new" Employee. I get an erro
ArgumentCountError: Too few arguments to function Employee::__construct() ... 0 passed in ... and exactly 3 expected
Showing and editing existing Employees works, so I'm confident my relationships, etc., are all correct.
Abbreviated code:
Company
class Company
{
protected $employees;
public function __construct()
{
$this->employees = new ArrayCollection();
}
public function addEmployee(Employee $employee)
{
if ($this->employees->contains($employee)) {
return;
}
$this->employees->add($employee);
}
public function removeEmployee(Employee $employee)
{
if (!$this->employees->contains($employee)) {
return;
}
$this->employees->removeElement($employee);
}
}
Employee
class Employee
{
// ... firstName and lastName properties...
public function __construct(Company $company, $firstName, $lastName)
{
$this->company = $company;
$this->company->addEmployee($this);
}
// ...getter and setter for firstName / lastName...
}
CompanyType
class CompanyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('employees', CollectionType::class, [
'entry_type' => EmployeeType::class,
'allow_add' => true,
'allow_delete' => false,
'required' => false,
]);
// ...other fields, some are CollectionType of TextTypes that work correctly...
}
}
EmployeeType
class EmployeeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('firstName')
->add('lastName');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Employee::class,
]);
}
}
CompanyController
class CompanyController
{
// Never mind that this is a show and not edit, etc.
public function showAction()
{
// Assume $this->company is a new or existing Company
$form = $this->createForm(CompanyType::class, $this->company);
$form->handleRequest($this->request);
if ($form->isSubmitted() && $form->isValid()) {
$company = $form->getData();
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($company);
$entityManager->flush();
}
// set flash message, redirect, etc.
}
// ...render view...
}
The above will work when modifying existing Employees, just not when creating new ones. Debugging from within the Symfony code, I can see that no data exists for the new employees, so it's trying to find a closure or definition for empty_data in CompanyType. I've tried this every which way (via configureOptions, and empty_data option when building the CompanyType::buildForm form), e.g. https://symfony.com/doc/current/form/use_empty_data.html. My gut tells me I don't even need to do this, because the form data should not be empty (I explicitly filled out the fields).
I tried using a model transformer as well. In that case, the transformation from the form (second function argument passed to new CallbackTransformer) isn't even hit.
The view properly sets name attributes when adding new employee fields, e.g. form[employees][1][firstName], etc. That isn't the problem. It also sends the right data to the controller. I confirmed this by inspecting the form submission data via CompanyType::onPreSubmit (using an event listener).
I also have a CollectionType of TextTypes for other things in CompanyType, those work fine. So the issue seems to be related to the fact that EmployeeType is compound (containing multiple fields).
Hopefully the above is enough to illustrate the problem. Any ideas?
UPDATE:
It seems the issue is there isn't an instantiation of Employee for Symfony to work with. Internally, each field gets passed to Symfony\Component\Form\Form::submit(). For existing employees, there is also an Employee passed in. For the new one, it's null. That explains why it's looking for empty_data, but I don't know why I can't get empty_data to work.
The solution was to define empty_data in the compound form, and not the CollectionType form.
My situation is a little weird, because I also need the instance of Company in my EmployeeType, as it must be passed to the constructor for Employee. I accomplished this by passing in the Company as form option into configureOptions (supplied by the controller), and then into entry_options. I don't know if this is best practice, but it works:
CompanyController
Make sure we pass in the Company instance, so it can be used in EmployeeType when building a new Employee:
$form = $this->createForm(CompanyType::class, $this->company, [
// This is a form option, valid because it's in CompanyType::configureOptions()
'company' => $this->company,
]);
CompanyType
class CompanyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('employees', CollectionType::class, [
// ...
// Pass the Company instance to the EmployeeType.
'entry_options' => [ 'company' => $options['company'] ],
// This was also needed, apparently.
'by_reference' => false,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// Allows the controller to pass in a Company instance.
'company' => null,
]);
}
}
EmployeeType
Here we make sure empty_data properly builds an Employee from the form data.
class EmployeeType extends AbstractType
{
private $company;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('firstName')
->add('lastName');
// A little weird to set a class property here, but we need the Company
// instance in the 'empty_data' definition in configureOptions(),
// at which point we otherwise wouldn't have access to the Company.
$this->company = $options['company'];
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Employee::class,
'empty_data' => function (FormInterface $form) use ($resolver) {
return new Employee(
$this->company,
$form->get('firstName')->getData(),
$form->get('lastName')->getData(),
);
},
]);
}
}
Viola! I can now add new employees.
Hope this helps other people!
A bit familiar with symfony until I found myself in the middle of this.
I have two entities: user and accounts.
Each user has a series of accounts like google account, facebook account ,etc. Users are in one table and the accounts in a different table. Obviously, accounts are fetched based on the user's id.
How can I merge both entities into one form so I can fetch, create, edit and/or delete entries?
my user entity:
use statements here
...
private $id;
private $fullname;
private $accounts;
public function __construct(){
$this->accounts = new ArrayCollection();
}
//setters and getters...
my account entity:
use statements here
...
private $id;
private $user_id; //id in user entity
private $service; //i.e google account
private $username;
private $password;
//setters and getters...
My user form:
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
$builder
->add('id',HiddenType::class)
->add('FullName',TextType::class)
->add('accounts'.,CollectionType::class,[
'entry_type' => AccountType::class
])
...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\User'
));
}
Accounts form:
class AccountType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
$builder
->add('id',HiddenType::class)
->add('accountname',TextType::class) //i.e Google Account
->add('username',TextType::class)
->add('password',TextType::class)
...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Account'
));
}
My controller:
...
#Route("/users/{id}/edit")
...
public function EditAction(Request $request, User $user){
...
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
...
...
return $this->render("/pages/users/edit.html.twig",
["form"=>$form->createView()]
);
No errors at all, but form is not displaying any of the fields for the accounts entity.
i,e,. {{ form_row(accounts.username) }}
Any ideas or help or pointing me to the right direction would be highly appreciated.
you need to call a form inside another form like this calling its Type:
$builder
->add('id',HiddenType::class)
->add('FullName',TextType::class)
->add('accounts', 'collection', array(
'type' => new AccountType(),
'allow_add' => true,))
Answer was actually pretty easy. All I needed was to add the proper annotation to the $accounts property.
/**
* #ORM\ManyToMany(targetEntity="Account")
*
private $accounts;
Thanks for all of your inputs. Whole script is now working
I have a form responsible of creating and updating users. A user can (or not) have an address (OneToOne unidirectional relation from user).
When I create a user, no problem.
When I update a user, usually no problem.
Problems come up when i update a user which already has an address and try to unset all the address fields. There is then a validation error.
The wanted behavior would be to have the user->address relation set to null (and delete the previously set address on the DB).
There is a cascade_validation, the addess field in form (nested form) is set to not be required and the user entity allow the address to be null.
UPDATE
Relevant entities and forms :
User entity (Getters & Setters are classical, Symfony generated):
class User
{
[...]
/**
* #var \Address
*
* #ORM\OneToOne(targetEntity="Address", cascade="persist")
* #ORM\JoinColumn(
* name="address_id", referencedColumnName="id"
* )
*/
private $address;
[...]
}
The address entity is classical, no bidirectionnal relation to user.
User form
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
[...]
->add('address', new AddressType(), array('required' => false))
[...]
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Xentia\FuneoBundle\Entity\User',
'cascade_validation' => true
));
}
public function getName()
{
return 'user';
}
}
The address nested form is classical
As you can see, the is a quite classical and straightforward code. The only particular case is that address is optional. Leading to an validation error only in the case that the address was previously set (and, thus, exist in the DB and as a not null relation with the user) and the user want to unset it (all address fields are left empty).
It seems that if the related address has not an actual instance it can still be optional. But, if an instance of the address exist and is linked with the user, it can not be optional anymore.
UPDATE 2
namespace Xentia\FuneoBundle\Form\Type;
use Doctrine\Common\Util\Debug;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add("suggestLocality", null, array(
'mapped' => false,
'label' => 'Locality'
))
->add("suggestStreet", null, array(
'mapped' => false,
'label' => 'Street'
))
->add('street')
->add('locality')
->add('postalCode')
->add('country', null, array(
'label' => false,
))
->add('latitude', 'hidden')
->add('longitude', 'hidden');
$builder->addEventListener(FormEvents::PRE_SUBMIT,
function(FormEvent $event) {
$address = $event->getData();
if (!empty($address)) {
$addressLocality = $address['locality'];
if (empty($addressLocality)) {
$event->setData(null);
}
}
}
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Xentia\FuneoBundle\Entity\Address',
'validation_groups' => array('Default'),
));
}
public function getName()
{
return 'address';
}
}
Try setting orphanRemoval on your relation
/** #OneToOne(targetEntity="...", orphanRemoval=true) */
$address
EDIT
I see now, you have placed the wrong listener. First of all it should be POST_SUBMIT, PRE_SUBMIT is to process request data and modify form. On POST SUBMIT you can modify the object.
I am learning to use Symfony2 and in the documentation I have read, all entities being used with Symfony forms have empty constructors, or none at all. (examples)
http://symfony.com/doc/current/book/index.html Chapter 12
http://symfony.com/doc/current/cookbook/doctrine/registration_form.html
I have parametrized constructors in order to require certain information at time of creation. It seems that Symfony's approach is to leave that enforcement to the validation process, essentially relying on metadata assertions and database constraints to ensure that the object is properly initialized, forgoing constructor constraints to ensure state.
Consider:
Class Employee {
private $id;
private $first;
private $last;
public function __construct($first, $last)
{ .... }
}
...
class DefaultController extends Controller
{
public function newAction(Request $request)
{
$employee = new Employee(); // Obviously not going to work, KABOOM!
$form = $this->createFormBuilder($employee)
->add('last', 'text')
->add('first', 'text')
->add('save', 'submit')
->getForm();
return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
'form' => $form->createView(),
));
}
}
Should I not be using constructor arguments to do this?
Thanks
EDIT : Answered Below
Found a solution:
Looking into the API for the Controllers "createForm()" method I found something that is not obvious from the examples. It seems that the second argument is not necessarily an object:
**Parameters**
string|FormTypeInterface $type The built type of the form
mixed $data The initial data for the form
array $options Options for the form
So rather than pass in an instance of the Entity, you can simply pass in an Array with the appropriate field values:
$data = array(
'first' => 'John',
'last' => 'Doe',
);
$form = $this->createFormBuilder($data)
->add('first','text')
->add('last', 'text')
->getForm();
Another option (which may be better), is to create an empty data set as a default option in your Form Class.
Explanations here and here
class EmployeeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('first');
$builder->add('last');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'empty_data' => new Employee('John', 'Doe'),
));
}
//......
}
class EmployeeFormController extends Controller
{
public function newAction(Request $request)
{
$form = $this->createForm(new EmployeeType());
}
//.........
}
Hope this saves others the head scratching.
I have entity Message with ManyToOne relation with entity User:
class Message
{
...
/**
* #var User $sender
*
* #ORM\ManyToOne(targetEntity="Acme\UserBundle\Entity\User")
* #ORM\JoinColumn(name="sender_id", referencedColumnName="id")
*
**/
private $sender;
...
}
If $sender doesn't have email value i need to create new field for my form, so i create form for Message entity in Contoller:
$user = $this->getUser();
$message = new Message();
$message->setSender($user);
$formBuilder = $this->createFormBuilder($message, array(
'cascade_validation' => true
));
$formBuilder->add('body', 'textarea');
if (!$user->getEmail()) {
$formBuilder->add('email', 'email', array(
'property_path' => 'sender.email'
));
}
And i have some validation rules in validation.yml for entity User. Can i automatically validate this field by my validation rules for User entity in another entity's form? I don't know how to do it.
I found workaround right now: create new MissingEmailType:
class MissingEmailType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\UserBundle\Entity\User',
'validation_groups' => array(
'MissingEmail'
),
));
}
public function getName()
{
return 'missing_email';
}
}
But it looks complicated. Is there any better solutions?
You could redirect the page to the user profile page instead of loading the message form and state that the user needs to add an email prior to adding the message. If you redirect quickly or create a popup, the user might not be turned off as long as they can return to the original page after adding their email. Then validtion is simple since you only need to validate the user entity.