Many to many relation can get associated object - symfony

I have a user and a school entity. This both entities have a many to many relation.
class User
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\School")
* #ORM\JoinTable(name="user_school",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="school_id", referencedColumnName="id")}
* )
*/
private $school;
public function __construct()
{
$this->school = new ArrayCollection();
}
}
class School
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
public function getId()
{
return $this->id;
}
}
If i call the method for retriving the school id in my controller like:
public function indexAction(Request $request)
{
$user = $this->getUser();
$school = $user->getSchool();
echo $school->getId();
}
I get the error message
Attempted to call an undefined method named "getId" of class "Doctrine\ORM\PersistentCollection"
Can someone give me hint , what i'm doing wrong?

Even though $user->getSchool(); is singular in method name, it returns a PersistentCollection - a collection of schools (since the relationship is ManyToMany). If you want to get a specific school's id, you would have to iterate through the schools, like so:
$scools = $user->getSchool()->toArray();
foreach ($scools as $scool) {
// do something with $school
}

In the mapping you defined, User::$school is a ManyToMany, which means the result of getSchool will be a Collection of Schools, not a single School entity.
Two scenarios:
A User can have multiple Schools. Then you probably should rename $school to $schools and getSchool to getSchools. And you can't call $user->getSchools()->getId() since $user->getSchools() is a Collection and does not have a getId method. Check #NoyGabay's answer for a way to access Schools ids.
A User can only have one School. Then you did not define your mapping correctly; you wanted a ManyToOne instead of ManyToMany. Check Doctrine's documentation for association mapping.

Related

Doctrine OneToMany relation does not fetch related entities

I am stuck at this case, I reproduced it in an example from symfony documentation, here it how it looks:
FIXTURES
/**
* #ORM\Entity
*/
class Category
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="category", fetch="EAGER")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function products(): Collection
{
return $this->products;
}
public function id()
{
return $this->id;
}
}
and related Product class
/**
* #ORM\Entity
*/
class Product
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="products")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
public function __construct($category)
{
$this->category = $category;
}
public function id()
{
return $this->id;
}
public function category()
{
return $this->category;
}
}
TEST
Now I have this snippet of test code where I want to fetch Category and be able to get its Products:
$cat = new Category();
$prod = new Product($cat);
$this->entityManager->persist($prod);
$this->entityManager->persist($cat);
$this->entityManager->flush();
$crepo = $this->getEntityManager()->getRepository(Category::class);
$c = $crepo->findAll()[0];
var_dump(get_class($c->products()), $c->products()->count())
What I am getting is products of class PersistentCollection which is expected, but the count is 0 while there should be 1 product.
I can see that in the database I have proper category and product records with proper foreign key set.
WORKAROUND
I am debugging PersistentCollection for products and can see that its flag is set to initialized = true. With this I am able to force this to work by calling
$c->products()->setInitialized(false);
$c->products()->initialize();
But afaik this is not how it should work, should it ?
I managed to found an answer. It basically works but not when run in the same process. If I split the script in two - first one persists, second retrieves the data then the products collection will contain products related to category.
This is because when it is done in single process doctrine does not know that the category in question has products, and since it retrieves the same object it just saved and that was created few lines above, the entity manager won't populate the collection using database but will use the one from the category object. And the category object does not have products in products collection, since there is no call like $this->products()->add($category) neither in Product constructor or anywhere else. Only forcing to reinitialize the collection works since then it really retrieves products from database

List entities belonging to an association in Doctrine

Let's say I have a "Person" entity. A person can belong to a "Group". They are associated through a ManyToMany, Join Table strategy.
The general code looks like this:
/**
* Vendor\AcmeBundle\Entity\Person
*
* #ORM\Entity(repositoryClass="Vendor\AcmeBundle\Entity\PersonRepository")
*/
class Person extends BaseUser
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="Vendor\AcmeBundle\Entity\Group")
*/
protected $groups;
}
and the group entity
/**
* #ORM\Entity
*/
class Group extends BaseGroup
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", nullable=true)
*/
protected $publicName;
}
What do I want to achieve?
Given a group, list users belonging to that group in a consistent way, including pagination options (aka limit and offset)
Something like this:
function getUserFromGroup(Group $group, $criteria, $limit, $offset){};
Considerations:
The entities are mutable, they can be adapted to achieve this requisite (e.g. the association could be changed from unidirectional to bidirectional)
The amount of person entities is in the thousands (2000~8000)
The amount of groups is less than 10
This is explained in the Symfony2 Book, chapter on Doctrine
For your case, I would suggest using the findBy() method.
From the official doctrine documentation:
function getUserFromGroup($group, $criteria, $limit, $offset){
// You should probably build the criteria into a paramaters array,
// but I'll just asume it's "fieldName" => "valueToFilterBy"
$criteria['groups'] = $group;
$users = $em->getRepository('AppBundle\Entity\User')
->findBy(
$criteria, // Filter by columns
array('name' => 'ASC'), // Sorting
$limit, // How many entries to select
$offset // Offset
);
return $users;
};
I would not use the associations to list group's members, but a custom repository call. This should be close enough:
class PersonRepository extends EntityRepository
{
public function findPeopleInGroup(Group $group, $criteria, $limit, $offset){
$qb = $this->createQueryBuilder('p');
$qb->join('p.groups', 'g')
->where(':group MEMBER OF p.groups')
->setParameter('group', $group)
->orderBy('p.'.$criteria);
$qb->setFirstResult($offset);
$qb->setMaxResults($limit);
return $qb->getQuery()->getResult();
}
}

Many-to-Many, Doctrine's Entity Generator and Pluralization

Doctrine's many-to-many logic is confusing me a bit. I have a pretty simple many-to-many relationship of recipes to categories. My base entity classes are equally simple.
The Recipe entity class...
class Recipe
{
/**
* #ORM\ManyToMany(targetEntity="Category", inversedBy="categories")
* #ORM\JoinTable(name="recipe_category")
**/
private $categories;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255)
*/
private $title;
public function __construct() {
$this->categories = new \Doctrine\Common\Collections\ArrayCollection();
}
}
And the Category entity class...
class Category
{
/**
* #ORM\ManyToMany(targetEntity="Recipe")
**/
private $recipes;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
public function __construct() {
$this->recipes = new \Doctrine\Common\Collections\ArrayCollection();
}
}
Seems pretty strait forward and matches Doctrine (and Symfony2's) documentation examples. The strange behavior comes when I try and generate the getters and setters for these classes via the Symfony console app.
The relationship setters/getters are incorrect. Take, for instance, the Category setter in the Recipe class that's generated...
/**
* Add categories
*
* #param \Namespace\CookbookBundle\Entity\Category $categories
* #return Recipe
*/
public function addCategorie(\Namespace\CookbookBundle\Entity\Category $categories)
{
$this->categories[] = $categories;
return $this;
}
It looks like the auto-generation of the method name is off. It should be "addCategory" and should be passed a "category."
While I can just correct this manually, if I re-run the entity generator, it will just add them again.
Am I doing this incorrectly or is this just a quirk of the entity generator? Can I specify an over-ride via annotation?
You're not doing anything wrong as that's how symfony generates them. I usually don't use the app/console to generate them as currently they're not doing a good job. One example is as you've mentioned the pluralization of words as you've mentioned. Another obvious one is the fact that it's using the [] notation which is pretty much treating an ArrayCollection object as a PHP array. You should never treat ArrayCollections as arrays.
This is how I have implemented it myself:
public function addCategory(Category $category)
{
if (!$this->categories->contains($category)
$this->categories->add($category);
return $this;
}
Which doesn't add duplicates to the Array collection if it's already added. Same thing goes with remove:
public function removeCategory(Category $category)
{
if ($this->categories->contains($category)
$this->categories->remove($category);
}
What I've run into many times is let's say you have 4 categories and you add and remove them
$r = new Recipe();
$c1 = new Category();
$c2 = new Category();
$r->addCategory($c1);
$r->addCategory($c2);
// at this point $r->getCategories()->toArray()[0] contains $c1
// and $r->getCategories()->toArray()[1] contains $c2
$r->removeCategory($c1);
// now $r->getCategories()->toArray()[0] is empty and
// $r->getCategories()->toArray()[1] contains $c2 still
// so in order to get the first category you need to:
$r->getCategories()->first();
You are not doing anything wrong. It is just that Doctrine automatically tries to singularize the names of method stubs whenever there is a plural name for a collection property. This is the function that Doctrine calls when you run the command doctrine:generate:entities:
$methodName = Inflector::singularize($methodName);
In your case, Doctrine tries to 'singularize' the word categories but fails to recognize the singular form correctly, so it just removes an 's' from the end returning categorie.
Also, as you see, Doctrine does not singularize the parameter passed to the method stubs, leaving $categories instead of being consistent and modifying it to $categorie.
If you want to avoid this, either you do not use plural words for collections, or use plural words and change the methods afterwards. As #keyboardSmasher comments to your post, doctrine won't overwrite methods you already have when using the command doctrine:generate:entities, and wrong methods won't hurt much if left there alone.
A final note: using ArrayCollections as arrays is perfectly fine, so this code is correct:
$this->categories[] = $category;
ArrayCollection object implements Collection, which in turn implements ArrayAccess. It is done this way precisely to be able to use ArrayCollections as Arrays.

ManyToMany relationship with extra fields in symfony2 orm doctrine

Hi i have that same question as here: Many-to-many self relation with extra fields? but i cant find an answer :/ I tried first ManyToOne and at the other site OneToMany ... but then i could not use something like
public function hasFriend(User $user)
{
return $this->myFriends->contains($user);
}
because there was some this problem:
This function is called, taking a User type $user variable and you then use the contains() function on $this->myFriends.
$this->myFriends is an ArrayCollection of Requests (so different type than User) and from the doctrine documentation about contains():
The comparison of two elements is strict, that means not only the value but also the type must match.
So what is the best way to solve this ManyToMany relationship with extra fields? Or if i would go back and set the onetomany and manytoone relationship how can i modify the hasFriend method? To example check if ID is in array collection of ID's.
EDIT: i have this table... and what i need is:
1. select my friends... and my followers ...check if i am friend with him or not. (because he can be friend with me and i dont have to be with him... like on twitter). I could make manytomany but i need extra fields like: "viewed" "time when he subscribe me" as you can see at my table.
And make query like this and then be able in twig check if (app.user.hasFriend(follower) or something like that)
$qb = $this->createQueryBuilder('r')
->select('u')
->innerJoin('UserBundle:User', 'u')
->Where('r.friend_id=:id')
->setParameter('id', $id)
->orderBy('r.time', 'DESC')
->setMaxResults(50);
return $qb->getQuery()
->getResult();
I was trying to have a many to many relationship with extra fields, and couldn't make it work either... The thing I read in a forum (can't remember where) was:
If you add data to a relationship, then it's not a relationship anymore. It's a new entity.
And it's the right thing to do. Create a new entity with the new fields, and if you need it, create a custom repository to add the methods you need.
A <--- Many to many with field ---> B
would become
A --One to many--> C (with new fields) <-- One to many--B
and of course, C has ManyToOne relationships with both A and B.
I searched everywhere on how to do this, but in the end, it's the right thing to do, if you add data, it's no longer a relationship.
You can also copy what contains usually do, or try to overwrite it in a custom repository, to do whatever you need it to do.
I hope this helps.
I'm adding another answer since it has nothing to do with my original answer. Using the new info you posted, I'm calling the table/entity you posted "Follower". The original entity, "User".
What happens if you create the following associations:
namespace Acme\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Acme\UserBundle\Entity\User
*
* #ORM\Table()
* #ORM\Entity
*/
class User
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Acme\FollowerBundle\Entity\Follower", mappedBy="followeduser")
*/
protected $followers;
/**
* #ORM\OneToMany(targetEntity="Acme\FollowerBundle\Entity\Follower", mappedBy="followeeuser")
*/
protected $followees;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
public function __construct()
{
$this->followers = new \Doctrine\Common\Collections\ArrayCollection();
$this->followees = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add followers
*
* #param Acme\FollowerBundle\Entity\Follower $follower
*/
public function addFollower(\Acme\FollowerBundle\Entity\Follower $follower)
{
$this->followers[] = $follower;
}
/**
* Add followees
*
* #param Acme\FollowerBundle\Entity\Follower $followee
*/
public function addFollowee(\Acme\FollowerBundle\Entity\Follower $followee)
{
$this->followees[] = $followee;
}
/**
* Get followers
*
* #return Doctrine\Common\Collections\Collection
*/
public function getFollowers()
{
return $this->followers;
}
/**
* Get followees
*
* #return Doctrine\Common\Collections\Collection
*/
public function getFollowees()
{
return $this->followees;
}
}
namespace Acme\FollowerBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Acme\FollowerBundle\Entity\Follower
*
* #ORM\Table()
* #ORM\Entity
*/
class Follower
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Acme\UserBundle\Entity\User", inversedBy="followers")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $followeduser;
/**
* #ORM\ManyToOne(targetEntity="Acme\UserBundle\Entity\User", inversedBy="followees")
* #ORM\JoinColumn(name="followee_id", referencedColumnName="id")
*/
protected $followeeuser;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set followeduser
*
* #param Acme\UserBundle\Entity\User $followeduser
*/
public function setFolloweduser(\Acme\UserBundle\Entity\User $followeduser)
{
$this->followeduser = $followeduser;
}
/**
* Get followeduser
*
* #return Acme\UserBundle\Entity\User
*/
public function getFolloweduser()
{
return $this->followeduser;
}
/**
* Set followeeuser
*
* #param Acme\UserBundle\Entity\User $followeeuser
*/
public function setFolloweeuser(\Acme\UserBundle\Entity\User $followeeuser)
{
$this->followeeuser = $followeeuser;
}
/**
* Get followeeuser
*
* #return Acme\UserBundle\Entity\User
*/
public function getFolloweeuser()
{
return $this->followeeuser;
}
}
I'm not sure if this would do the trick, I really don't have much time to test it, but if it doesn't, I thnk that it's on it's way. I'm using two relations, because you don't need a many to many. You need to reference that a user can have a lot of followers, and a follower can follow a lot of users, but since the "user" table is the same one, I did two relations, they have nothing to do with eachother, they just reference the same entity but for different things.
Try that and experiment what happens. You should be able to do things like:
$user->getFollowers();
$follower->getFollowedUser();
and you could then check if a user is being followed by a follower whose user_id equals $userThatIwantToCheck
and you could search in Followers for a Follower whose user = $user and followeduser=$possibleFriend

Implementing a friends list in Symfony2.1 with Doctrine

I want to implement friends list of particular user in Symfony2.1 and Doctrine.
Lets say friends table:
User1 User2 Status //0-pending request,1-accepted
A B 0
A C 1
D A 1
E A 1
Now I want to get A's friends name in the list. For this SQL query can be implemented using UNION as read in many other answers. But I want to implement this in doctrine query builder.
One option is like query separately for two columns and combine the result and sort. But this takes more time to execute and get result. I want to get quick response as soon as possible. Is there any way to query it?
You don't need any additional effort, e.g. by using Doctrine Query Builder!
Simply design the entity class User to have a many-to-many self-reference with User, e.g.:
* #ORM\Table()
* #ORM\Entity()
*/
class User
{
....
/**
* #var string $name
*
* #ORM\Column(name="name", type="string", unique=true, length=255)
*
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="myFriends")
**/
private $friendsWithMe;
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="friendsWithMe")
* #ORM\JoinTable(name="friends",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="friend_user_id", referencedColumnName="id")}
* )
**/
private $myFriends;
public function __construct() {
$this->friendsWithMe = new \Doctrine\Common\Collections\ArrayCollection();
$this->myFriends = new \Doctrine\Common\Collections\ArrayCollection();
}
}
Then you can simply get the User entity and obtains all the friends as follows:
$user = $this->getDoctrine()
->getRepository('AcmeUserBundle:User')
->findOneById($anUserId);
$friends = $user->getMyFriends();
$names = array();
foreach($friends as $friend) $names[] = $friend->getName();

Resources