How to add extra WHERE clauses when retrieving relationships in Doctrine2 - php

I have two entities Post and Comment.
Structure:
Post:
id
title
body
Comment:
id
post_id
body
active
class Post
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="title", type="string", length=255)
*/
private $title;
/**
* #ORM\Column(name="body", type="text")
*/
private $body;
/**
* #ORM\OneToMany(
* targetEntity="Comment",
* mappedBy="post"
* )
*/
private $comments;
class Comment
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="body", type="text")
*/
private $body;
/**
* #ORM\ManyToOne(
* targetEntity="Post",
* inversedBy="comments"
* )
* #ORM\JoinColumn(
* name="post_id",
* referencedColumnName="id"
* )
*/
private $post;
As a result when I want to get all comments for a post I use $post->getComments() and it works.
How I can add extra Where clauses into this relationship if I want to get only posts with active = 1.
I know that I can do it by DQL or queryBuilder but I want to know how I can do it by mapping

I think the cleanest way to retrieve only active comments is to use Doctrine's Criteria object in the getComments method of your Post entity
use Doctrine\Common\Collections\Criteria;
and
public function getComments()
{
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('active', 1));
return $this->comments->matching($criteria);
}
Edit
If you want to prevent multiple queries each time you retrieve the active comments, you'll need to store them in a local variable. Instead of modifying getComments, you could add $active_comments and getActiveComments, which will populate $active_comments and only query the db if $active_comments is false.
class Post {
private $active_comments;
public function getActiveComments()
{
if(!$this->active_comments) {
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('active', 1));
$this->active_comments = $this->comments->matching($criteria);
}
return $this->active_comments;
}

Related

Doctrine - OneToMany relation, all result row doesn't fetch in object

I try to get all my objects DemandCab with their children object (DecisionCab).
My 2 entities
/**
* DemandCab.
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="DemandCabRepository")
*/
class DemandCab
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var DecisionCab
*
* #ORM\OneToMany(targetEntity="\My\CabBundle\Entity\DecisionCab", mappedBy="demandCab")
*/
private $decisionsCab;
/**
* #var \DateTime
*
* #ORM\Column(name="startDate", type="datetime")
*/
private $startDate;
/**
* #var \DateTime
*
* #ORM\Column(name="endDate", type="datetime", nullable=true)
*/
private $endDate;
/**
* #var int
*
* #ORM\Column(name="followup", type="integer", nullable=true)
*/
private $followup;
/**
* #var InfoCab
*
* #ORM\ManyToOne(targetEntity="\My\CabBundle\Entity\InfoCab", inversedBy="demandsCab")
*/
private $infoCab;
}
/**
* DecisionCab.
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="DecisionCabRepository")
*/
class DecisionCab
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var DemandCab
*
* #ORM\ManyToOne(targetEntity="\My\CabBundle\Entity\DemandCab", inversedBy="decisionsCab")
*/
private $demandCab;
/**
* #var bool
*
* #ORM\Column(name="decision", type="boolean", nullable=true)
*/
private $decision;
/**
* #var string
*
* #ORM\Column(name="motif", type="string", length=500, nullable=true)
*/
private $motif;
/**
* #var string
*
* #ORM\Column(name="role", type="string", length=255)
*/
private $role;
/**
* #var \DateTime
*
* #ORM\Column(name="date", type="datetime", nullable=true)
*/
private $date;
/**
* #var DemandCab
*
* #ORM\ManyToOne(targetEntity="\My\CabBundle\Entity\DemandCab", inversedBy="decisionsCab")
*/
private $demandCab;
/**
* #var User
*
* #ORM\ManyToOne(targetEntity="\My\CabBundle\Entity\User", inversedBy="decisionsCab")
*/
private $user;
}
In my DemandCabRepository
public function findAllCompleted(){
$qb = $this->createQueryBuilder("dem");
$qb->select('dem, dec');
$qb->leftJoin("dem.decisionsCab", "dec");
$qb->andWhere("dem.completed = 1");
$qb->orderBy("dem.startDate", "DESC");
return $qb->getQuery()->getResult();
}
My DemandCab data
My DecisionCab data
When i dump result, only 2 decisions appear ...
... whereas when i use getArrayResult, i have my 4 decisions ...
The query is good but i dont understand why hydration remove DecisionCab object with attribute decision at 0 or 1 (only null are display).
I would like to understand why and is there a solution to get DemandCab object with all DecisionCab children object.
Thanks
I am able to reproduce your issue, but I am not sure if this is your case.
Anyway, my assumption is that you query the demand Entity joined with decision relation at least once with the help of a query builder. Maybe this is done in your action, in an event listener or somewhere else in your code.
So you may have something like:
$qb = $this->getDoctrine()
->getRepository(DemandCab::class)->createQueryBuilder("dem");
$qb->select('dem, dec');
$qb->leftJoin("dem.decisionsCab", "dec");
$qb->andWhere("dec.decision IS NULL");
$qb->orderBy("dem.startDate", "DESC");
$results = $qb->getQuery()->getResult(); // <-- the decisionsCab collection is hydrated but filtered
$qb2 = $this->getDoctrine()
->getRepository(DemandCab::class)->createQueryBuilder("dem");
$qb2->select('dem, dec');
$qb2->leftJoin("dem.decisionsCab", "dec");
$qb2->andWhere("dem.completed = 1");
$qb2->orderBy("dem.startDate", "DESC");
$q = $qb2->getQuery();
//$q->setHint(Query::HINT_REFRESH, true);
$results = $q->getResult();
The issue is in Doctrine\ORM\Internal\Hydration\ObjectHydrator, it has the property "initializedCollections" where already initialized collections are kept and the collections are hashed by the parent entity type and the entity itself. Unfortunately in the above case, the heydrator does not understand that the collection is filtered in the 1st query and uses it in the 2nd query in order to avoid rehydration.(github link)
The solution is to cause the query builder to refresh. Try the code:
$qb->orderBy("dem.startDate", "DESC");
$q = $qb->getQuery();
$q->setHint(Query::HINT_REFRESH, true); // <-- Tell the hydrator to refresh
return $q->getResult();
First you have initialize your class with the relation ManyToOne with an ArrayCollection.
And you don't need any of this 'DemandCabRepository'. All the work is done by Doctrine

Delete link between two table with a query builder

I have made a queryBuilder inside an entity repository to delete a link between two table.
I have this two entities
Domain :
/**
* #var int
*
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(type="string", length=64)
* #Assert\NotBlank
* #Assert\Length(max="64")
* #AppAssert\DomainName
*/
private $name;
// Some other fields
/**
* #var SshKey[]|ArrayCollection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\SshKey", inversedBy="domains")
* #ORM\JoinTable(name="domain_sshkey",
* joinColumns={#ORM\JoinColumn(referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="key_id", referencedColumnName="id")}
* )
*/
private $sshKeys;
And SshKeys :
/**
* #var int
*
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \DateTime
*
* #Gedmo\Timestampable(on="create")
* #ORM\Column(type="datetime")
*/
private $createdAt;
// Other fields
/**
* #var Domain[]|ArrayCollection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Domain", mappedBy="sshKeys")
*/
private $domains;
I am trying to delete links between this two tables when SshKeys id is in sshKeys field inside domain table.
So I made this query builder in my DomainRepository
public function deleteSshkeyDomainLink($invalidSshkey)
{
$qb = $this->createQueryBuilder('d');
$qb->delete()
->where($qb->expr()->in('ssh.id', ':ssh_keys_id'))
->setParameter('ssh_keys_id', $invalidSshkey)
->join('d.sshKeys', 'ssh')
;
return $qb->getQuery()->execute();
}
But this QB return this error
[Doctrine\ORM\Query\QueryException]
[Semantical Error] line 0, col 39 near 'ssh.id IN(:s': Error: 'ssh' is not defined.
[Doctrine\ORM\Query\QueryException]
DELETE AppBundle\Entity\Domain d WHERE ssh.id IN(:ssh_keys_id)
I don't understand why this is returning ssh is not defined because I have made a join with this alias.
This query builder should work ? I really don't know how too fix this.
Thanks for your help.
why do you want to delete Domain when you just need to delete sshKey from Domain (link between them)?
In Domain entity you can define method removeSshKey like this for example
public function removeSshKey(SshKey $key)
{
$this->sshKeys->removeElement($key);
return $this;
}
Then in controller where you want to delete the link between entities you should call it something like this
$domain = $this->getDoctrine()->getRepository('Domain')->find($domainId);
foreach ($domain->getSshKeys() as $sshKey)
{
if ($sshKey->getId() == $invalidSshKeyId)
{
$domain->removeSshKey($sshKey);
}
}
$em = $this->getDoctrine()->getManager();
$em->flush();
this should delete the link

How can I reference an existing entry to a new entity in Doctrine 2?

I working on the following Models:
User
Category
One User can only get one Category. Categories are "standalone". So I can update, create and delete (okay, maybe with cascading) Categories whenever I want.
When I create a new User, I want do reference one Category to the User.
How can I do so? I want to avoid bad practices.
User entity:
/**
* User
*
* #ORM\Table(name="user")
* #ORM\Entity(repositoryClass="XXX\Repository\UserRepository")
*/
class User
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="user")
* #ORM\JoinColumn(name="category", referencedColumnName="id")
*/
private $category;
//...
}
Category entity:
/**
* Category
*
* #ORM\Table(name="category")
* #ORM\Entity(repositoryClass="XXX\Repository\CategoryRepository")
*/
class Category
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="User", mappedBy="category")
*/
private $user;
//...
}
You write a User can have one Category, but it seems from your description that a Category can be used by several users. If this is true then your category definition should be changed a bit:
/**
* Category
*
* #ORM\Table(name="category")
* #ORM\Entity(repositoryClass="XXX\Repository\CategoryRepository")
*/
class Category
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var Collection
* #ORM\OneToMany(targetEntity="User", mappedBy="category")
*/
private $users;
public function __construct()
{
// It is important to initialize your collection
$this->users = new ArrayCollection();
}
// add methods for adding/removing and getting users:
// addUser, removeUser, addUsers, removeUsers and getUsers method
}
Read more on initializing collections in the doctrine documentation chapter 27.7. Initialize collections in the constructor.
Then in your User class you will need methods for setting and getting category.
public function setCategory(Category $category)
{
$category->addUser($user);
$this->category = $category;
}
public function getCategory()
{
return $this->category;
}
Now you can do:
$category = // get the category you want to set from entity manager
$user = new User();
$user->setCategory($category);
$entityManager->persist($user);
$entityManager->flush();
Now both your user and category are correctly set.
This is just a basic example that will help you get on your way.

Too many db queries with DoctrineExtensions Translatable Symfony2

I just installed the Translatable DoctrineExtensions from the StofDoctrineExtensionsBundle.
I have two entities "Space" and "Equipment" with many to one relation :
/**
* Space
* #ORM\Table(name="space")
* #ORM\Entity(repositoryClass="Project\ProjectBundle\Repository\PlaceRepository")
*/
class Space implements Translatable
{
/**
* #var integer
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Project\ProjectBundle\Entity\Equipment", cascade={"persist"})
*/
private $equipments;
/**
* #var string
* #Gedmo\Translatable
* #ORM\Column(name="name", type="string", length=255, nullable=true)
*/
private $name;
/**
* #Gedmo\Locale
*/
private $locale;
public function setTranslatableLocale($locale)
{
$this->locale = $locale;
}
}
And :
/**
* Equipment
* #ORM\Table("equipment")
* #ORM\Entity(repositoryClass="Project\ProjectBundle\Repository\EquipmentRepository")
*/
class Equipment implements Translatable
{
/**
* #var integer
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
* #Gedmo\Translatable
* #ORM\Column(name="name", type="string", length=255, nullable=true)
*/
private $name;
/**
* #Gedmo\Locale
*/
private $locale;
public function setTranslatableLocale($locale)
{
$this->locale = $locale;
}
}
In the spaceRepository, I have, in my get query function :
public function getSpace($id) {
$query->setHint(\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker');
$query->setHint(\Gedmo\Translatable\TranslatableListener::HINT_TRANSLATABLE_LOCALE,$locale);
$db = $this
->createQueryBuilder('s')
->andWhere('s.id = :id')
->setParameter('id', $id)
->leftJoin('s.equipments', 'e')
->addSelect('e');
$query = $db->getQuery();
$query->setHint(\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, 'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker');
$query->setHint(\Gedmo\Translatable\TranslatableListener::HINT_TRANSLATABLE_LOCALE, 'en');
return $query->getSingleResult();
}
The TranslationWalker should get all the results in one single query but it doesn't work, and, finally, I have one query for each equipment (more than 150 queries).
How could I get all translated elements in one single query ?
Try to set the hydration mode manually as your issue is related to this problem.
Take a look at this answer on how to set the hydration mode. It worked for me.
In my case I simply added the following hints to a query
use Doctrine\ORM\Query;
use Gedmo\Translatable\Query\TreeWalker\TranslationWalker;
...
$query->setHydrationMode(TranslationWalker::HYDRATE_OBJECT_TRANSLATION);
$query->setHint(Query::HINT_REFRESH, true);
Hope that helps!

Doctrine2 getters for associations

I have 2 classes:
Company:
class ComCompany
{
/**
* #var integer
*
* #ORM\Column(name="cmp_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $cmpId;
/**
* #var string
*
* #ORM\Column(name="cmp_name", type="string", length=100, nullable=true)
*/
private $cmpName;
/**
* #var integer
*
* #ORM\Column(name="cmp_code", type="integer", nullable=true)
*/
private $cmpCode;
/**
* #var \Catalog\WebBundle\Entity\ComCity
*
* #ORM\ManyToOne(targetEntity="Catalog\WebBundle\Entity\ComCity")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="cmp_city", referencedColumnName="cit_id")
* })
*/
private $cmpCity;
public function getCmpName()
{
return $this->cmpName;
}
public function getCmpCode()
{
return $this->cmpCode;
}
public function getCmpCity()
{
return $this->cmpCity;
}
}
And city
class ComCity
{
/**
* #var integer
*
* #ORM\Column(name="cit_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $citId;
/**
* #var string
*
* #ORM\Column(name="cit_name", type="string", length=255, nullable=true)
*/
private $citName;
public function getCitId()
{
return $this->citId;
}
public function getCitName()
{
return $this->citName;
}
}
This 2 tables have associations Company.comCity = City.citId
How to add getter method to ComCompany class to get City.citName ?
I have foreign keys and Entity is generated properly, but there not method for get citName from Company class
Just add the following code to your ComCompany class
public function getCityName()
{
return $this->cmpCity->getCitName();
}
You don't need no this getter method while you already have it in ComCity class. Because adding it (like answer suggested) is making duplicate code. You should use
$company->getCmpCity()->getCitName()
instead
And also: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
Both answers provided are absolutely correct. However, take into account that if you're using lazy loading an extra query will be triggered each time you call getCityName unless you use a JOIN in your DQL/Query Builder.
This can have terrible performance issues if you call getCityName in a loop so I thought it was worth mentioning it.

Categories