Filter sub entity in doctrine - php

I have two entities that have a one to many relation.
There is a project:
class Project
{
// ...
/**
* #var \Doctrine\ORM\PersistentCollection|Template[]
*
* #ORM\OneToMany(targetEntity="Template", mappedBy="project")
*/
private $templates;
// ...
}
And I have templates which might be limited to certain users:
class Template
{
/**
* #var Project
*
* #ORM\ManyToOne(targetEntity="Project", inversedBy="templates")
* #ORM\JoinColumn(referencedColumnName="project_id", nullable=false)
*/
private $projectId;
/**
* #var array|null
*
* #ORM\Column(type="simple_array", nullable=true)
*/
private $userIds;
// ...
}
I now want to get all the projects. But the templates variable of each project should only have the templates that have either userIds NULL or the userId of the current user.
In my repository I already tried the following:
public function findForUser(int $userId): array
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('p')
->from(Project::class, 'p')
->leftJoin(Template::class, 't', Join::WITH, 't.project = p.projectId')
->where(
$qb->expr()
->isNull('t.userIds')
)
->orWhere('FIND_IN_SET(:userId, t.userIds) > 0');
$qb->setParameter(':userId', $userId);
return $qb->getQuery()
->execute();
}
But when I call getTemplates() I still have all templates in it even the ones the user is not allowed to see.
I also tried instead of ->select('p') to use ->select('p, t') as I hoped doctrine would then already fill the templates field with the selected templates but instead it then returns a mixed array of Templates and Projects.

I think that you entity schema is not quite optimised for this kind of operation.
You must not save the IDs of the Users in a simple array, but instead, use a ManyToMany relation :
class Template
{
// ...
/**
* #var User[]|Collection
*
* #ORM\ManyToMany(targetEntity="User", inversedBy="templates")
*/
private $users;
}
Then, the dql query should look something like that :
public function findForUser(User $user): array
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('p')
->from(Project::class, 'p')
->leftJoin(Template::class, 't', Join::WITH, 't.project = p.projectId')
->leftJoin(User::class, 'u', Join::WITH, 't.users = u.id')
->where('COUNT(t.users) = 0')
->orWhere('u = :user');
$qb->setParameter(':user', $user);
return $qb->getQuery()
->execute();
}

Related

"NOT EXISTS" Query with Many to Many Relation Doctrine Symfony3

I would like to build a query that brings me all the games for a logged in user that he has not yet joined. For this I have built these 2 Entities. They are connected by many to many.
class Game
{
public function __construct()
{
$this->users = new ArrayCollection();
}
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
*
* #var Users[]
*
* #ORM\ManyToMany(
* targetEntity="Domain\Entity\User",
* inversedBy="user",
* fetch="EAGER"
* )
*/
private $users;
/**
* #return array
*/
public function getUsers() : array
{
return $this->users->getValues();
}
/**
* #param User $users
*/
public function setUser($users)
{
if(is_array($users)){
/** #var User $user */
foreach ($users as $user){
$this->users->add($user);
}
} else {
$this->users->add($users);
}
}
}
And the User Entity
class User implements AdvancedUserInterface
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
}
The Entities has more attributes but i think they are not important.
Also I tried these Query, but it doesn't work.
/**
* #param User $user
* #return array
*/
public function fetchAllNonActivatedWhereYouNotJoin(User $user): array
{
$qb = $this->createQueryBuilder('g');
$qb->select('g')
->innerJoin('g.users', 'u')
->where('u.id != :user')
->andWhere('g.activate = 0')
->setParameter('user', $user->getId())
->getQuery()->getResult();
return $qb->getQuery()->getResult();
}
Does anyone know a solution? Its Symfony 3 and Doctrine in PHP 7.1
One way to do it is left join the 2 entities starting from the game repository as you do, with a join condition to the logged in user and have a condition that users is empty:
$qb->leftJoin('g.users', 'u', Join::WITH, 'u.id = :user')
->andWhere('g.activate = 0')
->having('COUNT(u) = 0')
->groupby('g')
->setParameter('user', $user->getId())
->getQuery()->getResult();
This works because of doctrine hydration, which hydrates the users property on the limited joined query(in this case each game will either have the logged in user or not in the users collection).
There are also other ways to achieve this
Be careful with this if you are doing consecutive queries with the query builder, as the entity manager keeps references to the already hydrated relations. Reference of the issue

How to query the inverse side of a many to many relationship with Doctrine

I want to know which professional diseases are included in all the medical records of a company production unit. The entity MedicalRecord has a many to many relationship with DiseaseTypology as follows:
/**
* AppBundle\Entity\HealthData\MedicalRecord
*
* #ORM\Table(name="medical_record")
* #ORM\Entity(repositoryClass="MedicalRecordRepository")
* #ORM\HasLifecycleCallbacks
*/
class MedicalRecord
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string $companyProductionUnitId
*
* #ORM\Column(name="company_production_unit_id", type="integer",nullable=true)
*/
protected $companyProductionUnitId;
/**
* #var ArrayCollection $professionalDiseases
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\HealthData\Core\DiseaseTypology")
* #ORM\JoinTable(name="medical_record_professional_disease",
* joinColumns={#ORM\JoinColumn(name="medical_record_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="professional_disease_id", referencedColumnName="id")}
* )
*
*/
protected $professionalDiseases;
In the MedicalRecordReposity class I created the following method:
public function getProfessionalDiseasesByProductionUnit($productionUnitId)
{
$em = $this->getEntityManager();
$repository = $em->getRepository('AppBundle:MedicalRecord');
return $repository->createQueryBuilder('m')
->select('m.professionalDiseases')
->where('m.companyProductionUnitId = :productionUnitId')
->setParameter('productionUnitId', $productionUnitId)
->getQuery()
->getArrayResult();
}
But I get the error:
[Semantical Error] line 0, col 9 near 'professionalDiseases': Error: Invalid PathExpression. Must be a StateFieldPathExpression.
How to query the inverse side of a many to many relationship? Thank you!
I don't know if I can understand what you want, but here's my try:
class MedicalRecordRepository extends \Doctrine\ORM\EntityRepository
{
public function getProfessionalDiseasesByProductionUnit($productionUnitId)
{
$qb = $this->createQueryBuilder('m');
$qb
->select('m, pd')
->innerJoin('m.professionalDiseases', 'pd')
->where('m.companyProductionUnitId = :productionUnitId')
->setParameter('productionUnitId', $productionUnitId)
;
return $qb->getQuery()->getArrayResult();
}
}
Explanation: i think you need a join between MedicalRecord and DiseaseTypology, and for that, if you have this setup (in both your entities):
#Entity/MedicalRecord.php
private $companyProductionIUnitId;
/**
* #var \AppBundle\Entity\DiseaseTypology
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\DiseaseTypology", mappedBy="medicalRecords")
*/
private $professionalDiseases;
First, you have to have that mappedBy option, to tell doctrine the inverse side of the relationship.
And
# Entity/DiseaseTypology.php
/**
* #var \AppBundle\Entity\MedicalRecord
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\MedicalRecord", inversedBy="professionalDiseases")
*/
private $medicalRecords;
You have to have that inversedBy option to tell doctrine the owning side of the relationship.
Once we've clarified that, to let doctrine do its things related to joins, you just need to tell it on which field to make the join. And as in my example, the relation between MedicalRecord and DiseaseTypology is made through $professionalDiseases fields. So this one will be the field to make the join with:
->innerJoin('m.professionalDiseases', 'pd') // this professionalDiseases is the $professionalDiseses from MedicalRecord entity
Ok, I've did all those explanations, because I saw how you've did your query, and I feel is not the right approach.
And my results after running the getProfessionalDiseasesByProductionUnit() method was like this:
Note: Use getResult() instead of getArrayResult(), because you fetch entities (DiseaseTypology), not set of fields
There are 2 options here:
Make relation MedicalRecord <=> DiseaseTypology bidirectional See documentation. Then your method would look very simple:
public function getProfessionalDiseasesByProductionUnit($productionUnitId)
{
$em = $this->getEntityManager();
$repository = $em->getRepository(DiseaseTypology::class);
return $repository->createQueryBuilder('dt')
->select('dt')
->join('dt.medicalRecords', 'm')
->where('m.companyProductionUnitId = :productionUnitId')
->setParameter('productionUnitId', $productionUnitId)
->getQuery()
->getResult();
}
Keep existing DB structure and add some logic after query
public function getProfessionalDiseasesByProductionUnit($productionUnitId)
{
$em = $this->getEntityManager();
$repository = $em->getRepository(MedicalRecord::class);
$mediaRecords = $repository->createQueryBuilder('m')
->select('m, dt')
//note: with this join all professionalDiseases will be loaded within one query for all MedicalRecords
->join('m.professionalDiseases', 'dt')
->where('m.companyProductionUnitId = :productionUnitId')
->setParameter('productionUnitId', $productionUnitId)
->getQuery()
->getResult();
$professionalDiseases = [];
foreach($mediaRecords as $mediaRecord) {
foreach($mediaRecord->professionalDiseases as $professionalDisease) {
$professionalDiseases[professionalDisease->id] = $professionalDisease;
}
}
return $professionalDiseases;
}

Symfony doctrine repository return instance of entity

I have an entity which store the 3D objects what I printed.
private $id;
/**
* #ORM\Column(type="array", nullable=true)
*/
private $images;
/**
* #ORM\Column(type="datetime")
*/
private $date_created;
/**
* #ORM\Column(type="datetime")
*/
private $date_modified;
/**
* #ORM\ManyToOne(targetEntity="App\UserBundle\Entity\User")
*/
private $user;
/**
* #ORM\ManyToOne(targetEntity="App\ThreedBundle\Entity\Threedobject", cascade={"all"})
*/
private $threedobject;
/**
* #ORM\Column(type="text", nullable=true)
*/
private $description;
There is a SQL query which looks like this:
select threedobject_id from threed_print where user_id = {RANDOM_NUMBER} group by threedobject_id;
I have to get the $threedobject all instance which (I mean the App\ThreedBundle\Entity\Threedobject instances) which represent the following sql query through Doctrine.
I tried the following querybuilder, but it have returned the array representation of the values, but most of the cases I have to use the methods of the elements, so I want to get the instances.
$query = $this->em->createQueryBuilder();
$result = $query
->select('tp')
->addSelect('to')
->from('ThreedBundle:ThreedPrint', 'tp')
->join('tp.threedobject', 'to')
->join('tp.user', 'u')
->where('u.id = :userID')
->groupby('to.id')
->setParameter('userID', $userID)
->getQuery();
return $result->getResult();
I read about the repository find method, but in this is not what I want, or I'm not totally understand how it is working.
UPDATE:
Basically what I need:
$query = $this->em->createQueryBuilder();
$result = $query
->select('to')
->from('ThreedBundle:ThreedPrint', 'tp')
->join('tp.threedobject', 'to')
->join('tp.user', 'u')
->where('u.id = :userID')
->groupby('to.id')
->setParameter('userID', $userID)
->getQuery();
return $result->getResult();
But I got the following error for that:
'SELECT to FROM': Error: Cannot select entity through identification variables without choosing at least one root entity alias.
You should implement the inverse relationship oneToMany from Threedobject to Threedprint, adding the $threedprints field in Threedobject.
Then you could write this
$threedobjects=$this->em->createQueryBuilder()
->select('to')
->from('ThreedBundle:Threedobject')
->join('to.threedprints', 'tp')
->join('tp.user', 'u')
->where('u.id = :userID')
->setParameter('userID', $userID)
->getQuery()->getResult();
If you want to use the repository, you should create a folder in your project under src (which normally you call it Repository) and create a new class with a name (for example: ThreedobjectRepository).
then you put the following in that class:
namespace NameProject\NameBundle\Repository;
use Doctrine\ORM\EntityRepository;
class ThreedobjectRepository extends EntityRepository
{
function findById3D($a)
{
$query = $this->getEntityManager()
->createQuery("Select th.threedobject_id AS id
FROM NameprojectNameBundle:threed_print th
Where th.user_id=:user_id
GROUP BY threedobject_id")
set parameter (user_id, $a);
return $query->getResult();
}
}
don't forget to put on your threed_print.php this line:
/**
* #ORM\Entity(repositoryClass="Nameproject\NameBundle\Repository\ThreedobjectRepository")
* #ORM\Table
*/
then you can go to the controller you are working on and instead of using findall() or findoneby(), you just use the function that you already create findById3D($a) and use easily the instance you wanted to use.
I hope I helped you.

Symfony2 doctrine passing a parameter on foreign key in findBy function

i'm trying to load an entity passing a parameter to a foreign key which is the inverse side
i have this two entities
Ad
/**
* Ad
*
* #ORM\Table(name="Ad")
* #ORM\Entity(repositoryClass="Symarket\MarketBundle\Repository\AdRepository")
*/
class Ad
{
/**
* #var AdImage
*
* #ORM\OneToMany(targetEntity="AdImage", mappedBy="ad", cascade={"persist", "merge"})
*/
private $images;
//...
}
AdImage
/**
* AdImage
*
* #ORM\Table(name="AdImage")
* #ORM\Entity(repositoryClass="Symarket\MarketBundle\Repository\AdImageRepository")
*/
class AdImage {
/**
* #var boolean
*
* #ORM\Column(name="adi_is_visible", type="boolean")
*/
protected $isVisible;
/**
* #var Ad
*
* #ORM\ManyToOne(targetEntity="Ad", inversedBy="images", cascade={"persist"})
* #ORM\JoinColumn(name="adi_ad_id", referencedColumnName="ad_id")
*/
protected $ad;
}
now, by getting a Ad from the database i want to get ONLY the images which are "isVisibile" => true
how is this possible with queryBuilder?
what i tried so far with the findBy function is this
public function findById($adId) {
$res = $this->findBy(array("id" => $adId, "isVisible" => true, "images" => array("isVisible" => true)));
$ad = reset($res);
return $ad;
}
and i got this error
You cannot search for the association field 'Symarket\MarketBundle\Entity\Ad#images', because it is the inverse side of an association. Find methods only work on owning side associations.
then i tried this way with the querybuilder
public function findById($adId) {
$res = $query = $this->createQueryBuilder('ad')
->leftJoin('ad.images', 'img')
->where('img.isVisible = :adVisible')
->andWhere('ad.id = :id')
->setParameter('adId', $adId)
->setParameter('imgVisible', true)
->getQuery();
$ad = reset($res);
return $ad;
}
and i get NULL
Thanks in advance.
for those who may encounter this problem here goes my solution:
public function findById($adId) {
$ad = $this->getEntityManager()
->createQuery("SELECT a, i from MarketBundle:Ad a LEFT JOIN a.images i with i.isVisible = :visible where a.id = :adId")
->setParameter("adId", $adId)
->setParameter("visible", true)->getSingleResult();
return $ad;
}
You can do this with the default repository for your AdImage entity. Consider this example:
$entityManager = $this->getEntityManager();
// You said you already have the Ad entity instance you care about so just grabbing by id for demonstration purposes
$ad = $entityManager->getRepository('Ad')->findOneById(5);
$adImagesVisible = $entityManager->getRepository('AdImage')->findBy(array('isVisible' => true, 'ad' => $ad->getId()));
If you approach this from the entity on the "Many" side of the relationship you should be able to get what you want without having to write a custom DQL query.

Doctrine 2: Find by entity with composite primary key

I have two entity classes, Product and OrderEntry, defined like this (some annotations left out for compactness):
class Product {
/**
* #Id
*/
protected $id;
/**
* #Column()
* #Id
*/
protected $prodNumber;
/**
* #Column()
* #Id
*/
protected $group;
// more cols here
}
class OrderEntry {
// more cols here
/**
* #ManyToOne(targetEntity="Product")
* #JoinColumns({
* #JoinColumn(name="prodNumber", referencedColumnName="prodNumber"),
* #JoinColumn(name="group", referencedColumnName="group")
* })
*/
protected $Product;
}
Now I want to find an OrderEntry by its associated Product with the query builder. The most logical thing for me would be this:
class OrderEntryRepository extends EntityRepository {
public function findByProduct($product) {
$qb = $this->getQueryBuilder('o');
$qb->where($qb->expr()->eq('o.Product', '?1')
->setParameter(1, $product)
->setMaxResults(1);
return $qb->getQuery()->execute();
}
}
However, this throws an exception that says
A single-valued association path expression to an entity with a
composite primary key is not supported. Explicitly name the components
of the composite primary key in the query.
How do I name the components explicitly? I know I could do it with a JOIN, but I have no use for the Product in this case and it would just make the query more expensive.
You can't avoid joining the products table with the current mapping:
public function findByProduct($product) {
$qb = $this->getQueryBuilder('o');
$qb
->join('o.Product', 'p')
->where('p.prodNumber = ?1')
->setParameter(1, $product->getProdNumber())
->andWhere('p.group = ?2')
->setParameter(2, $product->getGroup())
;
return $qb->getQuery()->getOneOrNullResult();
}
You can add separate properties to OrderEntry, which would use the same columns as the JoinColumns, e.g.:
/**
* #Column(name="prodNumber")
*/
protected $prodNumber;
And then you can use them in conditions:
...
->where('o.prodNumber = ?1')
->setParameter(1, $product->getProdNumber()
...

Categories