Symfony ManyToMany Bidirectionnal Relationship Only persist on one direction - symfony

After a long time searching on the web, i decide myself to write my first post.
I hope I do it the right way.
Here is my problem.
I use symfony 2.1 on my project.
And I have to deal with a bi-directionnal Many-to-many relationship Between 2 objects.
I've created the 2 entities, done the mapping, done the controllers, the templates and the formBuilder.
I manage to persist both entities.
For one entity, i could persist the entity and the relation with the other.
But For the other entity, i could only persist the entity.
The relation with the other entity do not persist.
Here are extracts of the 2 entities :
class EntrainementCategorie{
{...}
/** #ORM\ManyToMany(targetEntity="EntrainementType", mappedBy="categories", cascade="persist") */
protected $types;
}
Here is the second entity :
class EntrainementType{
{...}
/**
* #ORM\ManyToMany(targetEntity="EntrainementCategorie",inversedBy="types", cascade="persist")
* #ORM\JoinTable(name="mottet_entrainement_types_categories",
* joinColumns={#ORM\JoinColumn(name="idType", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="idCategorie", referencedColumnName="id")})
*/
protected $categories;
}
So you can see, there is a bidirectionnal Many-to-Many relationship between category and type.
Here are the controllers :
class EntrainementCategorieController extends GenericController{
{...}
public function creerAction(Request $request){
return $this->creerActionGeneric($request,new Categorie(),new CategorieType());
}
}
The second one :
class EntrainementTypeController extends GenericController{
{...}
public function creerAction(Request $request){
return $this->creerActionGeneric($request,new Type(),new TypeType());
}
}
And here is the GenericController :
class GenericController extends Controller{
{...}
protected function creerActionGeneric(Request $request,$object,$objectType){
$form = $this->createForm($objectType,$object);
$isThereProblem = false;
if ($request->isMethod('POST')) {
$isThereProblem = true;
$form->bind($request);
if ($form->isValid()) {
$this->getEntityManager()->persist($object);
$this->getEntityManager()->flush();
$this->get('session')->getFlashBag()->add('information', $this->FORM_SUCCESS_MESSAGE);
$isThereProblem = false;
}
}
if ($isThereProblem){
$this->get('session')->getFlashBag()->add('error', $this->FORM_ERROR_MESSAGE);
}
return $this->render($this->BUNDLE.':'.$this->ENTITY.':'.$this->CREATE_TEMPLATE, array('form' => $form->createView()));
}
}
Here are the formBuilder :
class EntrainementCategorieType extends AbstractType{
{...}
public function buildForm(FormBuilderInterface $builder, array $options){
$builder->add('label','text')
->add('types','entity',array(
'class' => 'KarateEntrainementBundle:EntrainementType',
'property' => 'label',
'multiple' => true,
'expanded' => true));
}
}
And the second one :
class EntrainementTypeType extends AbstractType{
{...}
public function buildForm(FormBuilderInterface $builder, array $options){
$builder->add('label','text')
->add('categories','entity',array(
'class' => 'KarateEntrainementBundle:EntrainementCategorie',
'property' => 'label',
'multiple' => true,
'expanded' => true));
}
}
So when i fill the EntrainementType form, both the type and its relations with category are persisted.
But when i fille the EntrainementCategory form, only the category is persisted, not its relations with type.
Does anyone knows what am i doing the wrong way ?
Hope i've been clear enought.
Thank you for you help !

I finally manage to do it.
I can't use the creerActionGeneric on that one.
I have to set explicitly the association between category and each type :
$form->bind($request);
if ($form->isValid()) {
$this->getEntityManager()->persist($categorie);
foreach($categorie->getTypes() as $type){
$type->addCategorie($categorie);
$this->getEntityManager()->persist($type);
}
$this->getEntityManager()->flush();
}
And that is working just fine.
But I don't know why on the other direction when i persist from the Type, I do not have to do like that ??? oO

There is a better way to go about doing via doctrine configuration of relationship. Doctrine will allow you to specify the join table and colums which reference the relationships.
I actually stumbled upon this answer before deciding to read more into it as this didn't seem appropriate... This is the right way to go about making a relationship like this (and in yml because that's what I prefer), and with my entities obviously (feeds can have/belong-to many lists and lists can have/belong-to many feeds)
Configuration for Feed:
manyToMany:
providerLists:
targetEntity: ProviderList
joinTable:
name: providerlist_feed
joinColumns:
feed_id:
referencedColumnName: id
inverseJoinColumns:
providerlist_id:
referencedColumnName: id
Configuration for list
manyToMany:
feeds:
targetEntity: Feed
joinTable:
name: providerlist_feed
joinColumns:
providerlist_id:
referencedColumnName: id
inverseJoinColumns:
feed_id:
referencedColumnName: id
so with that both entities now own each other, and both will persist the same way :)

Related

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

Get child entities returns null instead of arrayCollection object

I have two entities with a oneToMany relationship:
Post entity:
...
oneToMany:
images:
mappedBy: post
targetEntity: Shop\Bundle\ManagementBundle\Entity\Image
Image entity:
...
manyToOne:
post:
targetEntity: Shop\Bundle\ManagementBundle\Entity\Post
inversedBy: images
joinColumn:
onDelete: cascade
With $entity instance of Post, when I was doing $entity->getImages(), I was receiving something like:
object(Doctrine\ORM\PersistentCollection)[65]
private 'snapshot' =>
array (size=0)
empty
private 'owner' =>
object(Acme\Bundle\ImageUpBundle\Entity\Post)[54]
private 'id' => int 41
private 'title' => string 'kbd' (length=3)
private 'images' =>
&object(Doctrine\ORM\PersistentCollection)[65]
private 'association' =>
array (size=15)
'fieldName' => string 'images' (length=6)
'targetEntity' => string 'Shop\Bundle\ManagementBundle\Entity\Image' (length=38)
'mappedBy' => string 'post' (length=4)
'type' => int 4
'inversedBy' => null
'isOwningSide' => boolean false
'sourceEntity' => string 'Shop\Bundle\ManagementBundle\Entity\Post' (length=37)
'fetch' => int 2
'cascade' =>
array (size=0)
empty
'isCascadeRemove' => boolean false
'isCascadePersist' => boolean false
'isCascadeRefresh' => boolean false
'isCascadeMerge' => boolean false
'isCascadeDetach' => boolean false
'orphanRemoval' => boolean false
private 'em' =>
....
But now i unfortunately get null.
I really did all my best to figure out what might cause such issue. Your help is much appreciated.
Edit:
given an in integer $id, I fetch a Post entity using:
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('ShopManagementBundle:Post')->find($id);
I successfully get all the attributes of Post entity except from images.
Well here are the things that solved my issue:
1- I gathered all One To Many associations under one oneToMany parameter in config file. In other words, instead of having:
oneToMany:
images:
targetEntity....
....
oneToMany:
messages:
targerEntity...
....
I would have:
oneToMany:
images:
targerEntity...
....
messages:
targerEntity...
....
I generated entities again using app/console doc:gen:entities making the only one constructor constructs the two ArrayCollections.
/**
* Constructor
*/
public function __construct()
{
$this->messages = new \Doctrine\Common\Collections\ArrayCollection();
$this->images = new \Doctrine\Common\Collections\ArrayCollection();
}
Now when I call $em->getRepository('ShopManagementBundle:Post)->find($id) I have the child entities (Images) attached to my parent entity (Post) when concrete records exist in database, and not Null. When I insantiate a new entity using new Post(), I have empty ArrayCollection and not Null.
I know this answer is lacking of programming logic and seems arbitrary effort, but I write it in the sake of sharing in the case someone encounters suddenly this problem (As I did). I hope it helps.
$entity = $em->getRepository('ShopManagementBundle:Post')->find($id);
By Default, you will get only the proxy object of the child entities, this lazy way of fetching the associated entities is called Lazy loading, which will fetch values from images entity only a call to its getter method is invoked such as
$entity->getImage();
To load all the associated entities at one shot, you should instruct doctrine to do eager loading. This can be done using DQL, unfortunately its not possible to instruct using find method
$query = $em->createQuery("SELECT p FROM Shop\Bundle\ManagementBundle\Entity\Post p");
$query->setFetchMode("Shop\Bundle\ManagementBundle\Entity\Post", "images", \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER);
$query->execute();
Make sure your column name, member name and method naming all syntactically align. For example 'searchCampaignType':
ORM Column: searchCampaignTypeId
Member: $searchCampaignType
Getter: getSearchCampaignTypes
Setter: setSearchCampaignTypes
Other steps to take:
Regenerate your Doctrine Proxies
If you are using memcached then ensure you restart it or
rebuild the data
The question was oneToMany. The example below is ManyToMany but the principle is the same.
/**
* #ORM\ManyToMany(targetEntity="CampaignType")
* #ORM\JoinTable(name="Roles__SearchCampaignTypes",
* joinColumns={#ORM\JoinColumn(name="roleId", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="searchCampaignTypeId", referencedColumnName="id")}
* )
**/
private $searchCampaignTypes;
/**
* #return ArrayCollection
*/
public function getSearchCampaignTypes()
{
return $this->searchCampaignTypes;
}
/**
* #param $searchCampaignTypes
*/
public function setSearchCampaignTypes($searchCampaignTypes)
{
$this->searchCampaignTypes = $searchCampaignTypes;
}
Thanks to the hint of #satdev86 due to proxies, I could solve the problem in my case by only regenerating the proxies with orm:generate-proxies.

One to many relation throw exception on save

I have two entites. "Institution" and "User". The Insitution have a onetomany relation to user. I create a form "InstitutionType" and if i want to save a new insitution the "handleReqeust($request)" throw this exception.
Neither the property "users" nor one of the methods "addUser()"/"removeUser()", "setUsers()", "users()", "__set()" or "__call()" exist and have public access in class "App\UserBundle\Entity\Institution".
Entity User
/**
* #ORM\ManyToOne(targetEntity="Institution", inversedBy="users")
* #ORM\JoinColumn(name="institution_id", referencedColumnName="id")
*/
protected $institution;
public function setInstitution(\App\UserBundle\Entity\Institution $institution = null)
{
$this->institution = $institution;
return $this;
}
public function getInstitution()
{
return $this->institution;
}
Entity Institution
/**
* #ORM\OneToMany(targetEntity="App\UserBundle\Entity\User", mappedBy="institution")
*/
protected $users;
public function addUser(\App\UserBundle\Entity\User $users)
{
$this->users[] = $users;
return $this;
}
public function removeUser(\Ebm\UserBundle\Entity\User $users)
{
$this->users->removeElement($users);
}
public function getUsers()
{
return $this->users;
}
InstitutionType
$users = $this->entityManager->getRepository('AppUserBundle:User')->findByIsActive(true);
->add('users', 'entity', array(
'label' => 'responsibleperson',
'attr' => array(),
'class' => 'AppUserBundle:User',
'choices' => $users,
'multiple' => false,
'expanded' => false,
'empty_value' => '----------------')
)
Can someone help my to solve this issue?
Rename addUser to addUsers and removeUser to removeUsers.
Symfony2/Doctrine obviously has no knowledge of singular/plural words and can't guess that to add a single entry to the users collection it would be semantically more correct to call addUser() instead of addUsers.
Please note that you can always use the console command doctrine:generate:entities AcmeDemoBundle:Institution to generate missing fields in the entity classes.
If this doesn't help you need to make sure you use the same notation in your form type like in your entity configuration (annotation, yml or xml).

Access currently logged in user in EntityRepository

I want to create a simple blog example where users have a favourite category attached to there account. This means the can only write articles for this category. (Some of them - mostly admins -will get the opportunity to switch categories, but that's not the problem... for now ^^)
So I first created a relation between the User- and the Category entity. Everything works fine. Each user now has a main category selected.
Only thing which bothers me, is that I cant get hold of the current logged in user in the EntityType (formbuilder) and EntityRepository classes.
In my "New Post" form there are relations to other entities (e.g. Tags). I use the 'entity' formtype in the EntityType class to generate these form elements. Now i wan't to filter the tags, to only allow tags which have the same category relation as the currently logged in users category to be selectable.
I tried to use the query_builder option from the entity formtype. But as i can't get the current user object, I don't know which category he has selected. Same problem with the EntityRepository.
Now I could filter the tags already in the PostController but the problem is, that I will need this over and over again. And therefore I don't wan't to code this everytime I add something new.
I thought it would be the best to place this filter in the EntityRepository. So I can always access the findAllByCategory. But I need the user-object in there.
What is the best way to accomplish this? Have searched a lot, but either I searched for the wrong terms or no one has this problem :)
You can get user object from Security Context
$user = $serviceContainer->get('security.context')->getToken()->getUser();
Small tip: In case if user is not logged in - you'll have string in $user, otherwise - User object.
Cheers ;)
You can inject security context in your form type defined as a service. Then in your tags field use the query builder with $user (current logged user) to filter tags which have the same category relation as the currently logged:
/** #DI\Service("form.type.post") */
class PostType extends \Symfony\Component\Form\AbstractType
{
/**
* #var \Symfony\Component\Security\Core\SecurityContext
*/
protected $securityContext;
/**
* #DI\InjectParams({"securityContext" = #DI\Inject("security.context")})
*
* #param \Symfony\Component\Security\Core\SecurityContext $context
*/
public function __construct(SecurityContext $securityContext)
{
$this->securityContext = $securityContext;
}
public function buildForm(FormBuilder $builder, array $options)
{
$user = $this->securityContext()->getToken()->getUser();
$builder->add('tags', 'entity', array(
'label' => 'Tags',
'class' => 'Acme\HelloBundle\Entity\Tag',
'property' => 'name',
'query_builder' => function(EntityRepository $er) use($user) {
return $er->getAllSuitableForUserChoiceQueryBuilder($user);
},
'multiple' => true,
'expanded' => true,
}
}
Filter tags into your repository:
class TagRepository extends EntityRepository
{
public function getAllSuitableForUserChoiceQueryBuilder(User $user)
{
// Filter tags that can be selected by a given user
}
}
tried to use the query_builder option from the entity formtype. But as i can't get the current user object, i don't know which category he has selected. Same problem with the EntityRepository.
As long the Category has a relation to the user, you should be able to get the User there.
For Example:
Controller
$someThing = new SomeThing();
$someThing->setUser($user);
$form = $this->createForm(new someThingType(), $someThing);
Form
$someThing = $options['data'];
$user = $someThing->getUser();
$builder->add('category', null, array(
'class' => 'MyBundle:Cateogry',
'query_builder' =>
function(EntityRepository $er) use ($user) {
return $er->getCategoriesForUser($user);
}
));
Repository
public function getCategoriesForUser($user)
{
$qb = $this->createQueryBuilder('c');
$qb->leftJoin('c.user', 'u', 'with', 'u.user = :user');
$qb->setParameter('user', $user)
;
return $qb;
}
this is not exactly your use-case but pretty similar to it. maybe it helps you.

Resources