I have a User class which have a One-to-One association with another entity Result
Everything is working fine but anytime I load a set of users, i can see in the profiler that for each user symfony makes a query to load his result.
I don't need the result everywhere, and i manually retrieve it when i need.
I came to believe that symfony loads one-to-one relations automatically with the entity but didn't find out how to avoid it.
My classes :
class User extends BaseUser {
/**
* #ORM\OneToOne(targetEntity="Result", mappedBy="user", cascade={"all"}, orphanRemoval=TRUE)
*/
protected $result;
}
class Result {
/**
* #ORM\OneToOne(targetEntity="User", inversedBy="result")
* #ORM\JoinColumn(name="id_user", referencedColumnName="id")
*/
protected $user;
}
----- EDIT -----
I found out that it only happen when the User entity is loaded in a formbuilder :
$builder
->add('user', 'entity', array(
'class' => 'ThemBaseBundle:User',
'query_builder' => function($repository) {
return $repository->createQueryBuilder('a')
->orderBy('a.lastName', 'ASC');
},
'property' => 'fullName'
))
;
I'm not sure, but probably one-to-one relations are fetched eagerly by default. Try switching the fetching strategry to lazy. See this section for more information.
Related
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
]),
]
An API has been created to allow Posts to be created with a description and any number of attached Photos.
The problem was that when an API request to edit a Post was received with a null description the typehint would fail.
class Post {
/**
* #Assert\NotBlank
* #ORM\Column(type="text")
*/
private $description;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Photo")
*/
private $photos;
public function setDescription(string $descripton)
Which means instead of the Symfony validation failing Assert\NotBlank it was returning a 500.
This could have been fixed by allowing nulls in the method ?string, this would allow the validation to be called, but result in a dirty entity.
The DTO (Data Transfer Object) approach, a new class to represent the data was created and the validation rules applied to this, this was then added to the form.
class PostData {
/**
* #Assert\NotBlank
*/
public $description;
/**
* #Assert\Valid
* #var Photo[]
*/
public $photos;
The form has was modified:
class PostType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
$builder
->add('description')
->add('photos', EntityType::class, [
'class' => Photo::class,
'multiple' => true,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => PostData::class,
));
}
This worked for description it could be validated without changing the Post entity. If a null was received PostData would trigger the Assert\NotBlank and Post::setDescription would not be called with null.
The problem came when trying to validate Photos existed, if the photo existed it would work, if it didn't there would be a 500 error.
Potentially meaningless 500 error which doesn't indicate the reason
Checking only for cacheable HTTP methods with Symfony\Component\HttpFoundation\Request::isMethodSafe() is not supported. (500 Internal Server Error)
How can I use DTO PostData to validate a Photo entity exists?
Update composer.json and run composer update
"symfony/http-foundation": "4.4.*",
The issue is related to https://github.com/symfony/symfony/issues/27339
This will give a more meaningful Symfony Form error
Unable to reverse value for property path \"photos\": Could not find all matching choices for the given values
It will also return a lot of extra information if you serilize form errors including DATABASE_URL and APP_SECRET.
I do not recommend running this in production.
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.
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.
I'm using Symfony 2 and I have two entities in different bundles like:
//this class overrides fos_user class
//User\UserBundle\Entity\User
class User extends BaseUser
{
//..
/**
* #ORM\OneToMany(targetEntity="News\AdminBundle\Entity\News", mappedBy="author_id")
*/
protected $news_author;
//...
}
//News\AdminBundle\Entity\News
class News
{
//...
/**
* #ORM\ManyToOne(targetEntity="\User\UserBundle\Entity\User", inversedBy="news_author")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
protected $news_author;
//...
}
Both classes (entities) works fine. I have successfully setup fos_user bundle with registration and other stuff. The same if for News class. Then I build relation between those two classes OneTo Many (User -> News) as it is shown in code. This also works fine without errors and I can add news that belongs to user. The problem is when I build a form with entity class like:
->add('year', 'entity', array(
'class' => 'NewsAdminBundle:News',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('u')
->groupBy('u.year')
->orderBy('u.year', 'DESC');
},))
This form shows me a years when news are posted (like archive). Years are showing fine, but when I submit (post) a form then I've got error:
Class User\UserBundle\Entity\News does not exist
I figure out that this error is connected with sentence
$form->bindRequest($request);
The problem is because I have two entities in different bundles. How can I solve this error?
Edit:
I solved the problem. When I run
php app/console doctrine:generate:entities User
php app/console doctrine:generate:entities News
then Doctrine generate getters and setters in User and News. In entity News it generates method
/**
* Add news_author
*
* #param User\UserBundle\Entity\News $newsAuthor
*/
public function addNews(User\UserBundle\Entity\News $newsAuthor)
{
$this->news_author[] = $newsAuthor;
}
I was not paying attention to this method and I change it to this
/**
* Add news_author
*
* #param News\AdminBundle\Entity\News $newsAuthor
*/
public function addNews(News\AdminBundle\Entity\News $newsAuthor)
{
$this->news_author[] = $newsAuthor;
}
Now everything works fine. Thanks for all answers.
/**
* #ORM\ManyToOne(targetEntity="User\UserBundle\Entity\User", inversedBy="news_author")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
protected $news_author;
You have to remove prefix backslash – see note in Doctrine documentation