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
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).
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
]),
]
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).
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'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.