Get many-to-many entities in a form properly - symfony

I'm new with symfony, I looked around but I didn't find the right answer to my problem.
I have two entities linked with a many-to-many relation. Entity User -> Entity FollowedUser.
One User should be able to follow several FollowedUser and one FollowedUser should has several Users who follow him.
My problem is that when I try to list all FollowedUser for one User, say my CurrentUser, I get all FollowedUser not only those associated to my CurrentUser.
Here is my code.
User Entity (src/MyBundle/Entity/User.php) :
namespace MyBundle\Entity;
use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="My_user")
*/
class User extends BaseUser
// ...
/**
* #var FollowedUser[] $followedUsers
*
* #ORM\ManyToMany(targetEntity="MyBundle\Entity\FollowedUser")
*/
private $followedUsers;
// ...
public function getFollowedUsers()
{
return $this->followedUsers;
}
}
UserType:
namespace MyBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use MyBundle\Entity\FollowedUserRepository;
class UserType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('followedUsers'); // This shows me the whole table
//,'entity' , array('class' => 'MyBundle\Entity\FollowedUser',
// 'multiple' => true,
// 'query_builder' => function(FollowedUserRepository $followedUserRepo) use ($options) {
// $followedUsers = $options['data']->getFollowedUsers();
// $choices = array();
// foreach ( $followedUsers as $followedUser){
// $choices[] = $followedUser->getId();
// }
// $qb = $followedUserRepo->createQueryBuilder('u');
// $qb->select('u')
// ->where( $qb->expr()->in('u.id',$choices));
// return $qb;
// }
// ));
}
public function getName()
{
return 'followedUser';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'MyBundle\Entity\User',
);
}
}
NB: The lines I commented is the only way I found to do what I want. But it does not feel the right way to do it.
In my Controller:
$currentUser = $this->container->get('security.context')->getToken()->getUser();
$followedUsers = $currentUser->getFollowedUsers(); // That works properly
$form = $this->createForm(new UserType(),$currentUser);
EDIT :
Actually my problem was that I forgot some annotation in my ManyToMany declaration. Here is the default annotation which should be used for an unidirectionnal ManyToMany relation:
/**
* #ManyToMany(targetEntity="Group")
* #JoinTable(name="users_groups",
* joinColumns={#JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="group_id", referencedColumnName="id")}
* )
*/
Solution was found in the doctrine documentation here : doctrine2 unidirectionnal ManyToMany.

If specified, this is used to query the subset of options (and their
order) that should be used for the field. The value of this option can
either be a QueryBuilder object or a Closure. If using a Closure, it
should take a single argument, which is the EntityRepository of the
entity.
Without specifying query_builder Symfony 2 option you'll get all FollowedUser, as you said. The meaning of:
$builder->add('followedUsers');
Is something like:
Add a field whose property is followedUsers of the User class.
Guess it's type (entity type).
query_builder option not specified? Then fetch all users.
Select (depending of expanded and multiple options) those (options) users actually following the user from the model, leaving all other (options) users unselected.
So, question for you is: why you want to display only the users following the user in the form model? It's a no sense... the actual user of the application will never be able to add new following users.

Related

Easy Admin 3 (Symfony 4) AssociationField in OneToOne relationship shows already associated entities

Using Symfony 4.4 with Easy Admin 3:
I've a OneToOne relationship
class Usuario
{
...
/**
* #ORM\OneToOne(targetEntity=Hora::class, inversedBy="usuario", cascade={"persist", "remove"})
*/
private $hora;
...
}
class Hora
{
...
/**
* #ORM\OneToOne(targetEntity=Usuario::class, mappedBy="hora", cascade={"persist", "remove"})
*/
private $usuario;
...
}
I've got a CRUD Controller for Usuario:
class UsuarioCrudController extends AbstractCrudController
{
public function configureFields(string $pageName): iterable
{
...
return [
...
AssociationField::new('hora', 'Hora'),
];
Everything seems ok, but in the admin form for "Usuario", the field "hora" shows all values in database, even the ones already assigned to other "Usuario" entities:
I would like the dropdown control to show only not assigned values, PLUS the value of the actual "Usuario" entity, so the control be easy to use.
Which is the proper way to do this with easyadmin?
I've managed to code the field to show only the not associated "Hora" values, using $this->getDoctrine() and ->setFormTypeOptions([ "choices" => in UsuarioCrudController class,
but I am not able to access the actual entity being managed, nor in UsuarioCrudController class (maybe there it is not accesible) neither in Usuario class (I've tried here __construct(EntityManagerInterface $entityManager) to no avail as the value doesn't seem to be injected, dunno why).
It is possible to customize a few things in easy admin by either overriding EasyAdmin methods or listening to EasyAdmin events.
Example of methods:
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
public function createEntity(string $entityFqcn)
public function createEditForm(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormInterface
//etc..
Example of events:
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterCrudActionEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityDeletedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityPersistedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterEntityUpdatedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeCrudActionEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityDeletedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityUpdatedEvent;
You could override easy admin createEditFormBuilder or createNewFormBuilder method, this way you could access the current form data and modify your hora field.
Something like :
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore;
public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface {
$formBuilder = parent::createEditFormBuilder($entityDto, $formOptions, $context);
$unassignedValues = $this->yourRepo->findUnassignedValues();
$data = $context->getEntity()->getInstance();
if(isset($data) && $data->getHora()){
//if your repo return an ArrayCollection
$unassignedValues = $unassignedValues->add($data->getHora());
}
// if 'class' => 'App\Entity\Hora' is not passed as option, an error is raised (see //github.com/EasyCorp/EasyAdminBundle/issues/3095):
// An error has occurred resolving the options of the form "Symfony\Bridge\Doctrine\Form\Type\EntityType": The required option "class" is missing.
$formBuilder->add('hora', EntityType::class, ['class' => 'App\Entity\Hora', 'choices' => $unassignedValues]);
return $formBuilder;
}
Currently, easyadmin3 still lack documentation so sometimes the best way to do something is to look at how easy admin is doing things.
fwiw, the actual entity being edited can be accessed in a Symfony easyadmin CrudController's configureFields() method using:
if ( $pageName === 'edit' ) {
...
$this->get(AdminContextProvider::class)->getContext()->getEntity()->getInstance()
...
This way in configureFields() I could add code to filter my entities:
$horas_libres = $this->getDoctrine()->getRepository(Hora::class)->findFree();
and then add the actual entity value also, which is what I was trying to do:
array_unshift( $horas_libres,
$this->get(AdminContextProvider::class)->getContext()->getEntity()->getInstance()->getHora() );
Now the field can be constructed in the returned array with "choices":
return [ ...
AssociationField::new('hora', 'Hora')->setFormTypeOptions([
"choices" => $horas_libres
]),
]

How to show on the select only specific records when it's generated by buildForm for ManyToOne association

I have Entity like this
class User implements UserInterface
{
// ...
/**
* Many Departments have One Central Department (User)
* #ORM\ManyToOne(targetEntity="User")
*/
private $centralDepartment;
// ...
}
with self-referencing association. In related buildForm I use
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('centralDepartment');
// ...
}
}
and it creates in my view select list with list of Users. It's correct.
But the goal is to show on the list only Users with specific Role. If there is possibility I want also to validate before saving in database if selected User has specific Role.
Should I use option choice_loader from https://symfony.com/doc/3.4/reference/forms/types/choice.html or is there some better option in Symfony? I tried to change at first the label using
->add('centralDepartment', ChoiceType::class, array('label' => 'Choose Central Department'));
But my select list is empty now.
First try using EntityType instead of ChoiceType which is a more specialized ChoiceType for entity relations.
With EntityType you have a query_builder option to select the wanted choices.
See: https://symfony.com/doc/current/reference/forms/types/entity.html#query-builder
This could also be achieved through the choice_loader option with a ChoiceType but requires more work.
Going by #Joe suggestion I used EntityType
->add('centralDepartment', EntityType::class, array(
'class' => 'AppBundle:User',
'query_builder' => function(UserRepository $er) {
return $er->findAllBC(true);
}
));
where findAllBC(true) is my method in UserRepository to return Query Builder object which included specific Users for my use:
public function findAllBC($returnQB = false)
{
$qb = $this->createQueryBuilder('u')
->andWhere('u.roles LIKE :role')
->setParameter('role', '%BC%');
if(!$returnQB) return $qb->getQuery()->execute();
else return $qb;
}

Symfony: How to use translation component in entity __toString?

Yes, I know this has been asked before and discouraged, but I have a good use case for that. I am interested in learning the view-oriented supplementary approach.
The use case:
I have an entity, say Venue (id, name, capacity) which I use as collection in EasyAdmin. To render choices, I require this entity to have string representation.
I want the display to say %name% (%capacity% places).
As you've correctly guessed, I require the word "places" translated.
I could want to do it
directly in the entity's __toString() method
in form view by properly rendering __toString() output
I have no idea how to implement either but I agree that the first approach violates the MVC pattern.
Please advise.
Displaying it as %name% (%capacity% places) is just a "possible" representation in your form view so I would shift this very specific representation to your Form Type.
What can belong in the __toString() method of your Venue entity:
class Venue
{
private $name;
... setter & getter method
public function __toString()
{
return $this->getName();
}
}
messages.en.yml:
my_translation: %name% (%capacity% places)
Next your Form Type using choice_label (also worth knowing: choice_translation_domain) :
use Symfony\Component\Translation\TranslatorInterface;
class YourFormType extends AbstractType
{
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'venue',
EntityType::class,
array(
'choice_label' => function (Venue $venue, $key, $index) {
// Translatable choice labels
return $this->translator->trans('my_translation', array(
'%name%' => $venue->getName(),
'%capacity%' => $venue->getCapacity(),
));
}
)
);
}
}
& also register your form type as a service in services.yml:
your_form_type:
class: Your\Bundle\Namespace\Form\YourFormType
arguments: ["#translator"]
tags:
- { name: form.type }
I implemented a more or less complex solution for that problem, see my answer on this related question: https://stackoverflow.com/a/54038948/2564552

Sf2/3 Displaing different forms for Different roles(users)

I have an App that requires a complex access control. And the Voters is what I need to make decisions on Controller-level.
However, I need to build form for different users by different way.
Example: There are Admin(ROLE_ADMIN) and User(ROLE_USER). There is a Post that contains fields:
published
moderated
author
body
timestamps
Admin must be able to edit all fields of any Post.
User - only particular fields: published, body. (bay the way, only if this is an author of this post, but this is decided by voters).
Possible solution i found is dynamic form modification. But if we need more complexity, for example posts belongs to Blog, Blog belongs to author. And Post can be edited by direct author and author of the blog.
And Author of the Blog can also edit postedAt field, but it can't be done by direct author of the post.
I need to write some login in PRE_BIND listener.
Maybe there is some kind of common practice for that situation, or someone can show their own examples of.
You can do this creating a form type extension
Imagine a form type where you want to display a field only if ROLE_ADMIN is granted. For that you can simply add a new property to the field ('author' in this example)
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('published', 'text')
->add('moderated', 'text')
->add('author', 'text', [
'is_granted' => 'ROLE_ADMIN',
])
;
}
For this parameter to be interpreted, you must create a form type extension by injecting the SecurityContext Symfony to ensure the rights of the logged on user.
<?php
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\SecurityContextInterface;
class SecurityTypeExtension extends AbstractTypeExtension
{
/**
* The security context
* #var SecurityContextInterface
*/
private $securityContext;
/**
* Object constructor
*/
public function __construct(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$grant = $options['is_granted'];
if (null === $grant || $this->securityContext->isGranted($grant)) {
return;
}
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
if ($form->isRoot()) {
return;
}
$form->getParent()->remove($form->getName());
});
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefined(array('is_granted' => null));
}
/**
* {#inheritdoc}
*/
public function getExtendedType()
{
return 'form';
}
}
Finally, you just have to save the extension as a service :
services:
yourbundle.security_type_extension:
class: YourProject\Bundle\ForumBundle\Form\Extension\SecurityTypeExtension
arguments:
- #security.context
tags:
- { name: form.type_extension, alias: form }
Dynamic form modification seems unnecessary. Once the user is logged in the roles should not change.
You could inject the security.authorization_checker service in your form type and use that in the buildForm method to conditionally add fields to your form. Depending on how much the forms differ, this might become messy with too many if-statements. In that case I would suggest writing different form types altogether (possibly extending a base form type for repeated things).

Symfony2/Doctrine UniqueEntity on ManyToOne entity ignored

I have "roles" associated to "projects".
I don't care if a role name is duplicated, but what I want to make sure is that for each project the role name cannot be duplicated.
Here is what I thought should work:
<?php
// src/AppBundle/Entity/Role.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* #ORM\Entity(repositoryClass="AppBundle\Entity\RoleRepository")
* #ORM\Table(name="roles")
* #UniqueEntity(fields={"name","project"}, message="Duplicated role for this project")
*/
class Role
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=100)
*/
protected $name;
/**
* #ORM\Column(type="text")
*/
protected $description;
...
other fields
...
/**
* #ORM\ManyToOne(targetEntity="Project")
*/
protected $project;
}
According to the documentation here that's exactly what I need:
This required option is the field (or list of fields) on which this
entity should be unique. For example, if you specified both the email
and name field in a single UniqueEntity constraint, then it would
enforce that the combination value where unique (e.g. two users could
have the same email, as long as they don't have the same name also).
The constraint is simply ignored (I mean that if I try to have the same role name for the same project, it stores the duplicated role name and projectID).
What am I missing?
EDIT: after I updated the DB with "php app/console doctrine:schema:update --force" I tried to generate the error directly with SQL, but no exceptions were thrown. Now, I don't know if this "UniqueEntity" validation is done at DB level or it's Doctrine's validator.
EDIT2: I tried to have only one field ("name") and the validation works properly (only on that field of course). I also tried to have validation on the fields "name" and "description" and it works!!! So basically it does not validate if the field to be validated is the ID pointing to another table.
Anyways, here is the controller:
/**
* #Route("/role/create/{projectID}", name="role_create")
*/
public function createRoleAction(Request $request, $projectID)
{
$prj = $this->getDoctrine()->getRepository('AppBundle:Project')->findOneById($projectID);
$role = new Role();
$form = $this->createForm(new RoleFormType(), $role);
$form->handleRequest($request);
if ($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$role->setProject($prj);
$em->persist($role);
$em->flush();
return $this->redirect($this->generateUrl('hr_manage', array('projectID' => $projectID)));
}
return $this->render('Role/createForm.html.twig', array('projectID' => $projectID, 'form' => $form->createView(),));
}
The validation is not performed, and the entity persisted on DB, with the "project" column pointing at the right project. Here is a snapshot of the 2 relevant fields:
Here is the RoleFormType (an extract of relevant fields):
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class RoleFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// add your custom field
$builder->add('name', 'text')
->add('description', 'text')
...lots of other fields, but "project" is not present as it's passed automatically from the controller
->add('save', 'submit', array('label' => 'Create'));
}
public function getName()
{
return 'role';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'AppBundle\Entity\Role',));
}
}
The problem is you are not actually validating the entity to check for the unique constraint violation. When you call $form->isValid(), it does call the validator for the Role entity since you passed that as the form's data class. However, since project is not set until after that, no validation occurs on the project field.
When you call $em->persist($role); and $em->flush(); this simply tells Doctrine to insert the entity into the database. Those 2 calls do not perform validation on their own, so the duplicates will be inserted.
Try setting the project before creating the form instead:
$role = new Role();
$role->setProject($prj);
$form = $this->createForm(new RoleFormType(), $role);
Now project will be set on the entity, so when $form->isValid() is called the Symfony validator will check for uniqueness.
If that doesn't work, you'll want to add a project type to the form as a hidden field as well so it's passed back, but I don't think that will be necessary.
The other thing I would state is that you definitely want to add the unique constraint on your database itself - this way even if you try to insert a duplicate, the database will thrown an exception back to you and not allow it, regardless of your code.

Resources