2 entities 1 form Symfony 3 - symfony

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

Related

Use query_builder on CollectionType in symfony4 forms?

In a symfony 4 form, I need to use something like a query_builder option that is available on EntityType but from a CollectionType. There is a similar question here with no good answers.
In my project, each Site entity has many Goal. Each Goal has a numeric goal and a specific date. I'd like to edit the goals of a site for a specific date only. The problem is that a CollectionType form pulls all goals to show in the form, but I only want to pull the goals for a given date. How? There is no query_builder on a CollectionType like there is on an EntityType. I could change the getter in my Site entity, but I don't know how to pass the needed date to my getter.
For now my work-around is to render the entire form (with ALL associated goals for a given site), and then use some javascript to hide all goals except those with the date to edit. This works, but it's a terrible solution for sites with lots of goals spanning a range of dates.
My Site entity (only relevant code is shown):
class Site
{
public function __construct()
{
$this->goals = new ArrayCollection();
}
/** #ORM\OneToMany(targetEntity="App\Entity\Goal", mappedBy="site") */
private $goals;
public function getGoals()
{
return $this->goals;
}
}
and my related Goal entity:
class Goal
{
/** #ORM\Column(type="date") */
private $goalDate;
/** #ORM\Column(type="integer") */
private $goal;
/** #ORM\ManyToOne(targetEntity="App\Entity\Site", inversedBy="goals") */
private $site;
// ...
}
My forms:
class SiteGoalsAdminForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('goals', CollectionType::class, [
'entry_type' => GoalsEmbeddedForm::class,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Site::class
]);
}
}
and the individual goal form:
class GoalsEmbeddedForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('goal', IntegerType::class)
->add('goalDate', DateType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Goal::class,
]);
}
}
Using Form Events, while avoiding the allow_add and allow_delete options for the CollectionType form might land you in the right neighbourhood:
First - let's assume we're filtering by year, for ease of example, and that the year is being scooped up from a ?y=2018 style of querystring. We'll pass that info down to the form builder:
<?php
// Inside a *Action method of a controller
public function index(Request $request): Response
{
// ...
$filteredYear = $request->get('y');
$form = $this->createForm(SiteGoalsAdminForm::class, $site, ['year_filter' => $filteredYear]);
// ...
}
This implies we should be updating the default options for the SiteGoalsAdminForm class:
<?php
// SiteGoalsAdminForm.php
// ...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Site::class,
'year_filter' => 2018
]);
}
// ...
Then, in the buildForm method of that same class, we could access the Site object and remove Goals from it where the year of the goalDate did not fall inside the form's
<?php
// SiteGoalsAdminForm.php
namespace App\Form;
// ... other `use` statements, plus:
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class SiteGoalsAdminForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($options) {
$form = $event->getForm();
/** #var Site */
$site = $event->getData();
$goals = $site->getGoals();
foreach ($goals as $g) {
if ($g->getGoalDate()->format('Y') !== (string) $options['year_filter']) {
$site->removeGoal($g);
}
}
$form->add('goals', CollectionType::class, [
'entry_type' => GoalsEmbeddedForm::class,
]);
}
);
}
// ...
}
Not a query_builder exactly, but functionally similar.
Filter the results using the entity manager in the controller that you want to set on the collection type.
$goals = $entityManager->getRepository(Goals::class)->findBy(['year' => 2020]);
$form = $this->createForm(SiteGoalsType::class, $site, [
'goals' => $goals
]);
Then configure the SiteGoalsType::class to accept new option goals.
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Site::class,
]);
$resolver->setRequired(['goals']);
}
In the buildForm method of SiteGoalsType::class Set the data to the collection type field from the options.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('goals', Type\CollectionType::class, [
'entry_type' => GoalsEmbeddedType::class,
'data' => $options['goals'],
'mapped` => false
]);
}
Make sure the add the 'mapped' => false to your collection type field else it may lead to removing the records that didn't falls in the filter we have written in the controller.
$goals = $entityManager->getRepository(Goals::class)->findBy(['year' => 2020]);

Allow a OneToOne relationship to be optional in Symfony2

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.

Cannot generate one web form by attaching two entities to it

I have a 1 to n relationship so one Brand has many Cars. What I want to do is to create only one web form where all the fields from both of the entities get displayed. To do that I created a form type but I think I'm doing something wrong because I' getting error below when trying to print the form fields in twig. Could anyone tell me where am I doing wrong?
Error:
Method "brand" for object "Symfony\Component\Form\FormView" does not exist in CarBrandBundle:Default:both.html.twig at line 1
Entities:
class BrandEntity
{
protected $name;
protected $origin;
//Followed by getters and setters
/**
* #ORM\ManyToOne(targetEntity="BrandEntity", inversedBy="car")
* #ORM\JoinColumn(name="brand_id", referencedColumnName="id", nullable=false)
* #var object $brand
*/
protected $brand;
}
class CarEntity
{
protected $model;
protected $price;
//Followed by getters and setters
/**
* #ORM\OneToMany(targetEntity = "CarEntity", mappedBy = "brand")
* #var object $car
*/
protected $car;
public function __construct()
{
$this->car = new ArrayCollection();
}
public function addCar(\Car\BrandBundle\Entity\CarEntity $car)
{
$this->car[] = $car;
return $this;
}
public function removeCar(\Car\BrandBundle\Entity\CarEntity $car)
{
$this->car->removeElement($car);
}
}
Form Type:
namespace Car\BrandBundle\Form\Type;
use Car\BrandBundle\Entity\BrandEntity;
use Car\BrandBundle\Entity\CarEntity;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Test\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class BothType extends AbstractType
{
public function builder(FormBuilderInterface $builder, array $options)
{
$builder
->setAction($options['action'])
->setMethod('POST')
->add('brand', new BrandEntity())
->add('car', new CarEntity())
->add('button', 'submit', array('label' => 'Add'))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
//'data_class' => 'Car\BrandBundle\Entity\CarEntity',
'cascade_validation' => true
));
}
public function getName()
{
return 'both';
}
}
Controller:
namespace Car\BrandBundle\Controller;
use Car\BrandBundle\Form\Type\BothType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class BothController extends Controller
{
public function indexAction()
{
$form = $this->getFrom();
return $this->render('CarBrandBundle:Default:both.html.twig',
array('page' => 'Both', 'form' => $form->createView()));
}
private function getFrom()
{
return $this->createForm(new BothType(), null,
array('action' => $this->generateUrl('bothCreate')));
}
}
Twig:
{{ form_row(form.brand.name) }}
{{ form_row(form.brand.origin) }}
{{ form_row(form.car.model) }}
{{ form_row(form.car.price) }}
If you want a "car form" in which you need to choose a brand, then the other answer will be ok.
If what you want is a "brand form" in which you can add /edit/delete several cars, then you need to embed a Collection of Forms.
The cookbook answer is here: http://symfony.com/doc/current/cookbook/form/form_collections.html
To render a brand form containing a collection of car forms (1-n relationship):
The form types will look like this:
The brand type
// src/Acme/TaskBundle/Form/Type/BrandType.php
//...
class BrandType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('description');
$builder->add('cars', 'collection', array('type' => new CarType()));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\BrandBundle\Entity\Brand',
));
}
public function getName()
{
return 'brand';
}
}
The car type:
namespace Acme\CarBundle\Form\Car;
//...
class TagType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\CarBundle\Entity\Car',
));
}
public function getName()
{
return 'car';
}
}
Then the controller and the views just like in the cookbook. It's very powerful and easy in the end.
To add entities to a form you must use the entity field type: from the docs
$builder->add('users', 'entity', array(
'class' => 'AcmeHelloBundle:User',
'property' => 'username',
));
In this case, all User objects will be loaded from the database and rendered as either a select tag, a set or radio buttons or a series of checkboxes (this depends on the multiple and expanded values). If the entity object does not have a __toString() method the property option is needed.
Also please post the relationships.

Symfony2 form entity field with many-to-many relationship

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

Symfony2 Forms: validate related entity property

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.

Resources