i think my question is not clear but i try to illustrate my point here. assuming i have a many to many, self referencing relationship where a user can be a teacher (say u post answers at SO) and a teacher can be a student (u may answer questions but may ask too) too.
namespace Entities;
/** #Entity #Table(name="users")) */
class User {
/**
* #Id #Column(type="integer")
* #GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #Column(type="string", length="30")
*/
private $name;
/**
* #ManyToMany(targetEntity="User", inversedBy="teachers")
* #JoinTable(name="Teachers_Students",
* joinColumns={#JoinColumn(name="teacher", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="student", referencedColumnName="id")}
* )
*/
private $students;
/**
* #ManyToMany(targetEntity="User", mappedBy="students")
*/
private $teachers;
function getName() {
return $this->name;
}
function setName($name) {
$this->name = $name;
}
function getStudents() {
return $this->students;
}
function getTeachers() {
return $this->teachers;
}
}
say i have a few users
$user1 = new User;
$user1->setName("user 1");
$user2 = new User;
$user2->setName("user 2");
$user3 = new User;
$user3->setName("user 3");
$user4 = new User;
$user3->setName("user 4");
and i like to setup teacher-student relationships between them, i was reading up doctrine reference, saw that u can use the Collections::add() to add elements to a collection
// user1 is a teacher to user2 & 3
$user1->getStudents()->add($user2);
$user1->getStudents()->add($user3);
// user2 is a teacher to user3
$user2->getStudents()->add($user3);
// user4 is a student to user2
// tests if adding something from the inverse side works
$user4->getTeachers()->add($user2);
but this fails with
Fatal error: Call to a member function
add() on a non-object in
D:\ResourceLibrary\Frameworks\Doctrine\tools\sandbox\index.php
on line 70
how can i add elements to a collection or a relationship?
Remember that your collection variables are just regular ol' class properties. Which means they'll be null until you initialize them. The typical thing to do is instantiate them using Doctrine's ArrayCollection class, which will allow you to use the methods you described.
Try this:
public function __construct()
{
$this->students = new \Doctrine\Common\Collections\ArrayCollection();
$this->teachers = new \Doctrine\Common\Collections\ArrayCollection();
}
Related
I am currently trying to filter a Doctrine-generated association property by using Criteria as described in the Doctrine manual: Filtering Collections
Here is some sample code: A person who owns several cars. For the sake of simplicity we save the date when the person bought and sold the car in the car entity instead of creating an association entity.
class Car
{
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="cars")
* #ORM\JoinColumn(name="owner_id", referencedColumnName="id", nullable=false)
*/
private $owner;
/**
* #ORM\Column(name="bought", type="date")
*/
private $bought;
/**
* #ORM\Column(name="sold", type="date")
*/
private $sold;
}
class Person
{
/**
* #ORM\OneToMany(targetEntity="Car", mappedBy="owner")
*/
private $cars;
public function __construct
{
$this->cars = new ArrayCollection();
}
/**
* Get cars
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getCars()
{
return $this->cars;
}
public function getCarsOwnedAtDate(\DateTime $date)
{
$allCars = $this->getCars();
$criteria = Criteria::create()->where(
Criteria::expr()->andX(
Criteria::expr()->lte("bought", $date),
Criteria::expr()->gte("sold", $date)
)
);
return $allCars->matching($criteria);
}
}
The documentation for getCars says that it returns a Collection. Note that this documentation is auto-generated by Doctrine.
So when I call matching on Collection in my getCarsOwnedAtDate method my IDE issues a warning that Collection does not have a matching method. Turns out, it is right.
The object returned by getCars is an ArrayCollection and has a matching method so the code runs fine.
My question is: Can I rely on the fact that methods auto-generated by Doctrine will always return an ArrayCollection? Or will the above code fail in a certain case? If it does always return ArrayCollection, then why does the PhpDoc comment not mention this class explicitly?
I have a doctrine setup where i cant use the many side of collections.
The objects used:
User.php:
class User extends \App\Entity
{
/**
* #Id #Column(type="integer")
* #GeneratedValue
*/
private $id;
/**
* #ManyToOne(targetEntity="Company", inversedBy="users")
*/
private $company;
public function getCompany()
{
return $this->company;
}
public function setCompany($company)
{
$this->company = $company;
}
}
Company.php:
class Company extends \App\Entity
{
/**
* #Id #Column(type="integer", nullable=false)
* #GeneratedValue
*/
private $id;
/**
* #OneToMany(targetEntity="User", mappedBy="company")
*/
private $users;
public function __construct()
{
$this->users = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getUsers()
{
return $this->users;
}
}
When i create the relation seems to be working. Get no errors. Then i tried the following:
$company = $this->em->getRepository('App\Entity\Company')->findOneByName("Walmart");
$user = $this->em->getRepository('App\Entity\User')->findOneByName("Niek");
$user->setCompany($company);
$company2 = $this->em->getRepository('App\Entity\Company')->findOneByName("Ford");
$user2 = $this->em->getRepository('App\Entity\User')->findOneByName("Henk");
$company2->getUsers()->add($user2);
$this->em->flush();
When i inspect the database for the first user the company is set. Relation is there. The seconds does not persists. and when i do this:
print_r(\Doctrine\Common\Util\Debug::dump($company->getUsers(),$doctrineDepth));
print_r(\Doctrine\Common\Util\Debug::dump($company->getUsers2(),$doctrineDepth));
i get 2 empty arrays.
So it seems that the array isnt connected. It only behaves like this on OneToMany ore ManyToOne relationships. Got one ManyToMany and that one works perfect in the same project
Any ideas?
You still need to set both sides of the relationship with a one-to-many or a many-to-one relationship:
$company2 = $this->em->getRepository('App\Entity\Company')->findOneByName("Ford");
$user2 = $this->em->getRepository('App\Entity\User')->findOneByName("Henk");
$company2->getUsers()->add($user2);
$user2->setCompany($company2);
$this->em->flush();
The reason your many-to-many works is because you don't need to set both sides of the relationship.
I'm trying to do the following :
/**
* #Entity
*/
class Player {
/**
*#Column
*#Id
*/
private $uuid; //gets assigned a Uuid in the constructor
/**
* #ManyToOne(targetEntity="Team", cascade={"persist"})
* #JoinColumn(referencedColumnName="uuid")
*/
private $team;
public function setTeam(Team $team) {
$this->team = $team;
}
//...
}
/**
* #Entity
*/
class Team {
/**
* #Column
* #Id
*/
private $uuid; //gets assigned a Uuid in the constructor
//...
}
$player = new Player;
$team = new Team;
$player->setTeam($team);
$entityManager->persist($player);
$entityManager->flush();
The team is not persisted in to the database.
I do not want to call $entityManager->persist($team) as in my case, the Team is created in a part of the code where I don't have knowledge of persistence.
My expectation is that the cascade={"persist"} option should make the EntityManager also persist the Team.
Why is my expectation wrong or what am I doing wrong?
I was not mistaken... This works as expected!
Taking a second look, there was never a flush after a added the Team to the Player.
Reading my own question again, I realised what that I missed it...
I should talk to my rubber duck more often, so it seems...
I do not understad why with some Entity objects I can set the Id and for others objects I get an error and says me that the Id can't be null and I have to pass an object instead.
e.g.:
$log = new Log();
$log->setTypeId(1);
$log->setUserId(1);
$entityManager->persist($log);
$entityManager->flush();
If I try the code above I get error that says: Integrity constraint violation: 1048 Column 'user_id' cannot be null. And I have to first create the Type Object and de User object and the pass them:
$log->setType($TypeObject)
$log->setUser($UserObject)
But for other entity objects I have no problem assigning the value directly, why is that?
This is my Entity Log:
<?php
/**
* #Entity
* #Table(name="log")
* #HasLifecycleCallbacks
*/
class Log
{
/**
* #var type
* #Id
* #Column(type="integer")
* #GeneratedValue
*/
protected $id;
/**
*
* #var type
* #Column(type="integer")
*/
protected $user_id;
/**
*
* #var type
* #Column(type="integer")
*/
protected $type_id;
/**
*
* #var type
* #Column(type="datetime")
*/
protected $created;
/**
*
* #var type
* #ManyToOne(targetEntity="User", inversedBy="logs")
*/
protected $user;
/**
*
* #ManyToOne(targetEntity="Type", inversedBy="logs")
*/
protected $type;
public function getId()
{
return $this->id;
}
public function getUserId()
{
return $this->user_id;
}
public function getTypeId()
{
return $this->type_id;
}
public function getCreated()
{
return $this->created;
}
public function setUserId($userId)
{
$this->user_id = $userId;
}
public function setTypeId($typeId)
{
$this->type_id = $typeId;
}
public function setCreated($created)
{
$this->created = $created;
}
public function setUser($user)
{
$this->user = $user;
}
public function setType($type)
{
$this->type = $type;
}
/**
* #PrePersist
*/
public function prePersist()
{
$this->setCreated(new DateTime());
}
}
?>
The existing answer never did sit well with me. There are many valid scenarios where loading an object just to define the relationship while already having the FK handy just does not make any sense at all.
A better solution is to use Doctrine's EntityManager's getRefrence method.
Reference Proxies...
The method EntityManager#getReference($entityName, $identifier) lets
you obtain a reference to an entity for which the identifier is known,
without loading that entity from the database. This is useful, for
example, as a performance enhancement, when you want to establish an
association to an entity for which you have the identifier. You could
simply do this:
<?php
// $em instanceof EntityManager, $cart instanceof MyProject\Model\Cart
// $itemId comes from somewhere, probably a request parameter
$item = $em->getReference(\MyProject\Model\Item::class, $itemId);
$cart->addItem($item);
Maybe this was not available when this question was first posted - I don't know.
EDIT
I found this statement on the website of Doctrine2. It's a best practice that you might want to follow when coding your models.
Doctrine2 Best Practices
25.9. Don’t map foreign keys to fields in an entity
Foreign keys have no meaning whatsoever in an object model. Foreign keys are how a relational database establishes relationships. Your object model establishes relationships through object references. Thus mapping foreign keys to object fields heavily leaks details of the relational model into the object model, something you really should not do
EDIT
Doctrine does the mapping from your objects to their respective Ids.
What you've done here is a bit redundant.
You've essentially told doctrine the same thing twice.
You've told it that it has a 'user_id' column AND that it also has a User object, which are the same thing. But doctrine can already guess that this relationship will have a user_id column based on the fact that the log class has a user object inside.
You should simply do the following instead
<?php
/**
* #Entity
* #Table(name="log")
* #HasLifecycleCallbacks
*/
class Log
{
/**
* #var type
* #Id
* #Column(type="integer")
* #GeneratedValue
*/
protected $id;
/**
*
* #var type
* #Column(type="datetime")
*/
protected $created;
/**
*
* #var type
* #ManyToOne(targetEntity="User", inversedBy="logs")
*/
protected $user;
/**
*
* #ManyToOne(targetEntity="Type", inversedBy="logs")
*/
protected $type;
public function getId()
{
return $this->id;
}
public function getCreated()
{
return $this->created;
}
public function setCreated($created)
{
$this->created = $created;
}
public function setUser($user)
{
$this->user = $user;
}
public function setType($type)
{
$this->type = $type;
}
/**
* #PrePersist
*/
public function prePersist()
{
$this->setCreated(new DateTime());
}
}
Doctrine will worry about the user_id and type_id on it's own. You don't have to worry about it. This way you get to work with full fledged objects, making it easier to program, instead of having to worry about id's. Doctrine will handle that.
If ALL you have is an id, because that's what you're using on the front end, then just fetch the object associated with that id using the Entitymanager.
$user = $em->getEntity( 'User', $idFromWeb );
$log = new Log();
$log->setUser( $user );
I have question about inserting entity into a database. I have two models:
class News {
/**
* #Column(type="string", length=100)
* #var string
*/
protected $title;
/**
* #ManyToOne(targetEntity="User", inversedBy="news")
* #JoinColumn(referencedColumnName="id")
*/
protected $author;
}
class User {
/**
* #Id #GeneratedValue #Column(type="integer")
* #var integer
*/
protected $id;
/**
* #OneToMany(targetEntity="News", mappedBy="author")
*/
protected $news;
public function __construct() {
$this->news = new \Doctrine\Common\Collections\ArrayCollection;
}
}
To add new news I must include both User and News classes (if they're in separate files, for ex. UserModel.php and NewsModel.php) and write a code:
$news = new News()
$news->setTitle('TEST title');
$news->setAuthor($database->find('User', 1));
$database->persist($news);
My question is: Is there any way to insert news without including User class?
You don't need to actually load the User.
Instead, you can use a reference proxy:
<?PHP
$news = new News()
$news->setTitle('TEST title');
$news->setAuthor($em->getReference('User',1));
$em->persist($news);
one other thing you could do (thinking in a more object-oriented kinda way) is add a method called addNews($news) on your user entity:
public function addNews($news) {
// you should check if the news doesn't already exist here first
$this->news->add($news);
$news->setAuthor($this);
}
and add cascade persist to your mapping:
/**
* #OneToMany(targetEntity="News", mappedBy="author", cascade={"persist"})
*/
protected $news;
then fetch your user, add the news, and merge the changes:
$news = new News()
$news->setTitle('TEST title');
$author = $database->find('User', 1);
$author->addNews($news);
//merge changes on author entity directly
$em->merge($author);
I preferr this approach because it gives you the opportunity to do extra checks or controls while adding the news, making for reusable and easy to read code