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).
Related
Use case
TLDR;
I have two entities with a ManytoMany relation. I want to persist two new objects at the same time with one single form. To do so, I created two FromTypes with one embedding the other.
A bit more...
The goal is to provide users with a form to make an inquiry for an event. The Event entity consists of properties like starttime, endtime e.g. that are simple properties of Event aswell as a location (Location entity with a OneToMany relation, one Event has one Location, one Location can have many Events) and a contactperson (Contact entity with a ManyToMany relation, one Event can have multiple Contacts, one Contact can have multiple Events). For the particular form in question it is enough (and a deliberate choice) for the user to provide only one Contact as that is the bare minimum needed and enough for a start.
To build reusable forms, there are two simple forms with LocationFormType and ContactFormType and a more complex EventFormType. More complex as it embedds both LocationFormType and ContactFormType to create an Event entity "in one go" so to speak.
When I build the EventFormType with option A (see code below), the form renders correct and the way it is intended. Everything looks fine until the form is submitted. Then the problem starts...
Problem
On $form->handleRequest() the FormSystem throws an error because the embedded form is not providing a Traversable for the related object.
The property "contact" in class "App\Entity\Event" can be defined with the methods "addContact()", "removeContact()" but the new value must be an array or an instance of \Traversable.
Obviously the embedded FormType is providing a single object, while the property for the relation needs a Collection. When I use CollectionType for embedding (option B, see code below), the form is not rendering anymore as CollectionType seemingly expects entities to be present already. But I want to create a new one. So there is no object I could pass.
My Code
#[ORM\Entity(repositoryClass: EventRepository::class)]
class Event
{
...
#[ORM\ManyToMany(targetEntity: Contact::class, inversedBy: 'events')]
...
private Collection $contact;
...
public function __construct()
{
$this->contact = new ArrayCollection();
}
...
/**
* #return Collection<int, Contact>
*/
public function getContact(): Collection
{
return $this->contact;
}
public function addContact(Contact $contact): self
{
if (!$this->contact->contains($contact)) {
$this->contact->add($contact);
}
return $this;
}
public function removeContact(Contact $contact): self
{
$this->contact->removeElement($contact);
return $this;
}
...
}
#[ORM\Entity(repositoryClass: ContactRepository::class)]
class Contact
{
...
#[ORM\ManyToMany(targetEntity: Event::class, mappedBy: 'contact')]
private Collection $events;
public function __construct()
{
$this->events = new ArrayCollection();
}
...
/**
* #return Collection<int, Event>
*/
public function getEvents(): Collection
{
return $this->events;
}
public function addEvent(Event $event): self
{
if (!$this->events->contains($event)) {
$this->events->add($event);
$event->addContact($this);
}
return $this;
}
public function removeEvent(Event $event): self
{
if ($this->events->removeElement($event)) {
$event->removeContact($this);
}
return $this;
}
}
class EventFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
...
// option A: embedding related FormType directly
->add('contact', ContactFormType::class, [
...
])
// option B: embedding with CollectionType
->add('contact', CollectionType::class, [
'entry_type' => ContactFormType::class
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Event::class,
]);
}
}
class ContactFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add(
... // here I only add the fields for Contact entity, no special config
)
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Contact::class,
]);
}
}
Failed solutions
'allow_add' => true with prototype
I found solutions suggesting to set 'allow_add' => true on the CollectionType and render the form in Twig with ..vars.prototype
Thats a hacky solution (so I think) in my use case. I don't want to add multiple forms. And without 'allow_add' there is no prototype in CollectionType, so the data to render the form is missing.
provide empty object to CollectionType
To omit 'allow_add' => true but have an object to render the form correctly, I tried passing an empty instance of Contact in my controller
$eventForm = $this->createForm(EventFormType::class);
if(!$eventForm->get('contact')) $eventForm->get('contact')->setData(array(new Contact()));
That works on initial load, but creates issues when the form is submitted. Maybe I could make it work, but my gut gives me 'hacky vibes' once again.
Conclusion
Actually I think I'm missing some basic point here as I think my use case is nothing edgy or in any way unusual. Can anyone give me a hint as where I'm going wrong with my approach?
P.S.: I'm unsure wether my issue was discussed (without a solution) over on Github.
Okay, so I solved the problem. For this scenario one has to make use of Data Mappers.
It is possible to map single form fields by using the 'getter' and 'setter' option keys (Docs). In this particular case the setter-option is enough:
->add('contact', ContactFormType::class, [
...
'setter' => function (Event &$event, Contact $contact, FormInterface $form) {
$event->addContact($contact);
}
])
The addContact()-method is provided by Symfonys CLI when creating ManyToMany relations, but can be added manually aswell (Docs, SymfonyCast).
I need to remove email field from th registration form.
My solution was to override the registration FormType:
<?php
// src/AppBundle/Form/RegistrationType.php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class RegistrationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->remove('email');
}
The filed is removed successfully but, the validation processus is fired "Please enter an email." Any idea about how to disable the validation for email field or even how to make the trick with the right way.
Hack the entity setter:
public function setUsername($username) {
$username = is_null($username) ? '' : $username;
parent::setUsername($username);
$this->setEmail($username);
return $this;
}
Remove the field from the FormType:
public function buildForm(FormBuilder $builder, array $options) {
parent::buildForm($builder, $options);
$builder->remove('email');
}
BUT before hack it, you should have a look to this presentation from jolicode.
If you are currently doing this kind of modifications, it is because FosUserBundle is not adapted to your project. I think you shouldn't use it. (Personnaly, I think this is not a good bundle, read the complete presentation above to make your own opinion)
If you want to replace it, I advice you to use this excellent tutorial to create your own security system. (Time to code/paste/understand it : 2 or 3 hours)
you can give it the value of username for example
User.php
* #ORM\HasLifecycleCallbacks
/**
*
* #ORM\PrePersist()
*/
public function setEmailUser(){
$this->email = $this->username;
}
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
Is there a way to make a form type container aware?
As an example I have 3 entities Account, User and Event. Users have ManyToMany relationship in order to associate many users with many other users (called approvers), reason for this is so that an Event created by a User can have a list of Users who area able approve it. In the User edit form where I have an approvers multiple select field the list needs to be filtered by Account so I need my form type to be container aware in order to filter the list of available Users by Account ID.
Am I right in thinking that making the form type container aware is the right way to go? I'd like to use the entity manager to filter a list of Users by Account.
1 Inject the entity manager through the constructor
<?php
namespace Acme\YourBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\ORM\EntityManager;
class YourType extends AbstractType
{
/**
* The entity manager
*
* #var EntityManager
*/
private $entityManager;
/**
* #param EntityManager
*/
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
//build your form here
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\YourBundle\Entity\YourEntity',
));
}
public function getName()
{
return 'name_of_your_form';
}
}
2 Declare it as a service
services:
# Form type
acme_your_bundle.name_of_your_form.form.type:
class: Acme\YourBundle\Form\Type\YourType
arguments:
entityManager: "#doctrine.orm.entity_manager"
Note:
If you're starting with Symfony, take this advice:
look very closely at the code of the FOSMessageBundle, it will give you exactly what you need to do anything in symfony, from form models, to form factories, to the creation of special services (like composer, authorizer, etc..). The more you study this bundle, the quicker you will learn symfony, I guarantee you that 100%. Finally, in your specific case, look at the FormFactory in this bundle
This solution allows you to inject the container into many forms without each of your form types being a service:
Create a new form type:
class ContainerAwareType extends AbstractType implements ContainerAwareInterface
{
protected $container;
public function setContainer(ContainerInterface $container = null) {
$this->container = $container;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'container' => $this->container
));
}
public function getName() {
return 'container_aware';
}
public function getParent() {
return 'form';
}
}
Declare as a service:
services:
app.container_aware_type:
class: Burgerfuel\CmsBundle\Form\Type\ContainerAwareType
calls:
- [setContainer, ['#service_container']]
tags:
- { name: form.type, alias: 'container_aware' }
This type is now available to be a 'parent' to any other form type - whether its a service or not. In this case the important part is that setDefaultOptions from this class will be used to help build the $options argument that will be passed into any 'child' form types.
In any of your form types you can do this:
class MyType extends AbstractType
{
public function getParent() {
return 'container_aware';
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$container = $options['container'];
$builder->add( ...
This solution will be beneficial if you can't make your form type a service for some reason.
Or it can save you time if you are creating many types that require access to the container.
A simple way to do this, without doing any dependency injection / declaring a service.
In your FormType file, force the form to require an EntityManager
//..
use Doctrine\ORM\EntityManager;
class YourFormType extends AbstractType
{
//...
//...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\YourBundle\Entity\YourEntity',
));
$resolver->setRequired('entity_manager');
$resolver->setAllowedTypes('entity_manager', EntityManager::class);
}
}
Then you'll be able to (and forced - important for testing), pass the entity manager from the controller.
public function yourControllerAction(Request $request)
{
//..
$em = $this->getDoctrine()->getManager();
$form = $this->createForm('Acme\YourBundle\Form\YourEntityType', $yourEntityObject, array(
'entity_manager'=>$em,
));
$form->handleRequest($request);
//..
}
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.