Doctrine - how to get scalars hydrated to the related children level - php

i'm trying to get from the database some Tasks with the count of contents (related to task) and the assignment objects (related to task) with the count of answers (related to assignment).
So I'm using scalars for the counts and it's pretty fine but I cannot manage to have one count_of_answers by assignments...
Here if my entities relationships :
class Task
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\Assignment", mappedBy="task", orphanRemoval=true)
*/
private $assignments;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Content", mappedBy="task", orphanRemoval=true)
*/
private $contents;
}
class Content
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Task", inversedBy="contents")
* #ORM\JoinColumn(nullable=false)
*/
private $task;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Answer", mappedBy="content", orphanRemoval=true)
*/
private $answers;
}
class Assignment
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Task", inversedBy="assignments")
* #ORM\JoinColumn(nullable=false)
*/
private $task;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Answer", mappedBy="assignment", orphanRemoval=true)
*/
private $answers;
}
class Answer
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Content", inversedBy="answers")
* #ORM\JoinColumn(nullable=false)
*/
private $content;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Assignment", inversedBy="answers")
* #ORM\JoinColumn(nullable=false)
*/
private $assignment;
}
I would like to get in one query (I think this is possible to avoid querying inside a loop) something like :
Array [[
Task1{
properties,
assignments [
Assignment1{
properties,
count_of_answers
},
Assignment2{
properties,
count_of_answers
},...
]
},
count_of_contents
],
[
Task2{},
count_of_contents
],...
]
So i tried this query in my task repository :
$r = $this->createQueryBuilder('t')
->innerJoin('t.assignments', 'a')
->addSelect('a')
->innerJoin('t.contents', 'c')
->addSelect('COUNT(DISTINCT c.id) AS count_of_contents')
->leftJoin('a.answers', 'an')
->addSelect('COUNT(DISTINCT an.id) AS count_of_answers')
->groupBy('a.id')
->getQuery()
->getResult();
But it's giving something like :
Array[[
Task1{}
count_of_contents,
count_of_answers
],
Task2{}
count_of_contents,
count_of_answers
]]
Could you please help ?
Maybe I should use DQL with a subquery but I'm affraid to lose performence in the sql side. I believe the data fetched are good (when I try the sql query, I do have one count_of_answers by assignment), but the hydratation is not correctly mapped and I only get the last count_of_answers associated to the task instead of assignment.

Related

Symfony find user by role (JSON array Doctrine property)

I am doing a small project where I have an entity with a roles property which consists of an array.
What I am trying to do is, in some controller, find an existing entity which has a specific role inside of the roles array.
I am trying to use the findOneBy() method, but I can't seem to make it work, it always returns null even though entities with the specific role I'm trying to find exist.
Here is my entity and its properties:
/**
* #ORM\Entity(repositoryClass=SalarieRepository::class)
*/
class Salarie
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $nom;
/**
* #ORM\Column(type="string", length=255)
*/
private $prenom;
/**
* #ORM\Column(type="string", length=255)
*/
private $email;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $telephone;
/**
* #ORM\Column(type="string", length=255)
*/
private $service;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
// Getters & setters
}
And here is an example of something I tried with findOneBy() inside a controller, that returns null:
$rolecheck = $this->salarieRepository->findOneBy(["roles" => ["ROLE_RESPONSABLE_RH"]]);
When I try with any other property of the entity which isn't an array it works well, if I do something like this:
$rolecheck = $this->salarieRepository->findOneBy(["nom" => "test"]);
dd($rolecheck);
It will show the right entity :
SalarieController.php on line 47:
App\Entity\Salarie {#1501 ▼
-id: 6
-nom: "test"
-prenom: "test"
-email: "test#test.test"
-telephone: null
-service: "Graphisme"
-roles: array:3 [▼
0 => "ROLE_RESPONSABLE_RH"
1 => "ROLE_RESPONSABLE_SERVICE"
2 => "ROLE_SALARIE"
]
}
Where we can also see it does have the roles array with the role I'm trying to find inside it.
Any clues on how I could try to find one entity which has the specific role "ROLE_RESPONSABLE_RH"?
Your $roles property is of type json, which means it is stored as this in your database:
["ROLE_RESPONSABLE_RH", "ROLE_RESPONSABLE_SERVICE", "ROLE_SALARIE"]
You need to ask Doctrine if the JSON array contains the role, but you can't do that with the findOneBy() method.
When you hit the ORM limitations you can use a Native Query with ResultSetMapping. It allows you to write a pure SQL query using specific features of your DBMS but still get entity objects.
Create this method in your SalarieRepository class:
public function findByRole(string $role): array
{
// The ResultSetMapping maps the SQL result to entities
$rsm = $this->createResultSetMappingBuilder('s');
$rawQuery = sprintf(
'SELECT %s
FROM salarie s
WHERE /* your WHERE clause depending on the DBMS */',
$rsm->generateSelectClause()
);
$query = $this->getEntityManager()->createNativeQuery($rawQuery, $rsm);
$query->setParameter('role', $role);
return $query->getResult();
}
Then you need to replace the comment I put in the WHERE clause depending on the DBMS:
MariaDB - JSON_SEARCH():
SELECT %s
FROM salarie s
WHERE JSON_SEARCH(s.roles, 'one', :role) IS NOT NULL
MySQL - JSON_CONTAINS():
SELECT %s
FROM salarie s
WHERE JSON_CONTAINS(s.roles, :role, '$')
Warning: you must enclose the role parameter with double quotes:
$query->setParameter('role', sprintf('"%s"', $role));
PostgreSQL - jsonb escaped "?" operator:
SELECT %s
FROM salarie s
WHERE s.roles::jsonb ?? :role
Warning: will require PHP 7.4+. See the RFC
CAST JSON to TEXT
class JSONText extends FunctionNode
{
private $expr1;
public function getSql(SqlWalker $sqlWalker)
{
return sprintf(
"CAST(%s AS TEXT)",
$this->expr1->dispatch($sqlWalker)
);
}
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->expr1 = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}
Add to your Doctrine DQL:
dql:
string_functions:
JSON_TEXT: YOUR_NAMESPACE\JSONText
Use your cast function
$qb->andWhere("JSON_TEXT(d.topics) LIKE '%$role%'")

Why is my PersistentCollection empty?

I'm using the Symfony 3 Framework with Doctrine and MongoDB.
I've two documents that are in an OneToMany relationship.
/**
* Class Waypoint
* #package AppBundle\Document
* #MongoDB\Document(collection="waypoints", repositoryClass="AppBundle\Repository\WaypointRepository")
*/
class Waypoint
{
/**
* #var int
*
* #MongoDB\Id(strategy="auto")
*/
private $id;
/**
* #var ArrayCollection
* #MongoDB\ReferenceMany(targetDocument="Comment", cascade={"delete"})
*/
private $comments;
}
**
* Class Comment
* #package AppBundle\Document
* #MongoDB\Document(collection="comments", repositoryClass="AppBundle\Repository\CommentRepository")
*/
class Comment
{
/**
* #var int
*
* #MongoDB\Id(strategy="auto")
*/
private $id;
/**
* #var Waypoint
*
* #MongoDB\ReferenceOne(targetDocument="Waypoint", inversedBy="comments")
* #Assert\NotBlank()
*/
private $waypoint;
}
Now I'm getting a part of my Waypoint entries from an repository query and want to display them with twig.
/**
* WaypointRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class WaypointRepository extends DocumentRepository
{
public function getWaypointsForCruiseByPage(Cruise $cruise, $page)
{
$displayLimit = 10;
$amountToSkip = 0;
if ($page > 1)
{
$amountToSkip = ($page -1) * $displayLimit;
}
$qb = $this->createQueryBuilder()
->select()
->field('cruise')->equals($cruise)
->field('isAutoWaypoint')->equals(false)
->sort('date', -1)
->skip($amountToSkip)
->limit($displayLimit)
;
$qb
->addOr($qb->expr()->field('hasImage')->equals(true))
->addOr($qb->expr()->field('hasAudio')->equals(true))
->addOr($qb->expr()->field('description')->notEqual(''))
;
return $qb->getQuery()->toArray();
}
}
Now, I'm trying to do {{ waypoint.comments.count }} or {{ waypoint.comments|length }} will always be 0, even if I'm having datasets in my MongoDB collection.
If I'm getting the comments over the CommentRepository by the ID of the Waypoint I'm getting the expected results.
// returns the expected results
public function getAllCommentsForWaypoint(Waypoint $waypoint)
{
return $this->createQueryBuilder()
->select()
->field('waypoint')->equals($waypoint)
->getQuery()->toArray()
;
}
The mapping is fine as far as I can tell, no flaws or errors to find.
Why is the PersistentCollection empty, event though informations are there in the collection?
I'm not sure how are you creating documents, but this is my best shot:
Waypoint::$comments is not mapped as an inverse side thus ODM expects list of references to be available in the waypoint.comments field in the database. Most probably it's not there (i.e. you're not explicitly adding new Comment to the collection in the Waypoint) and that's why you're seeing empty collection when querying for waypoints, but have results when querying for comments. Given you have inversedBy="comments" in the Comment mapping I think you forgot to set the Waypoint::$comments as the inverse side:
/**
* #var ArrayCollection
* #MongoDB\ReferenceMany(targetDocument="Comment", mappedBy="waypoint")
*/
private $comments;

Filtering on many-to-many association with Doctrine2

I have an Account entity which has a collection of Section entities. Each Section entity has a collection of Element entities (OneToMany association). My problem is that instead of fetching all elements belonging to a section, I want to fetch all elements that belong to a section and are associated with a specific account. Below is my database model.
Thus, when I fetch an account, I want to be able to loop through its associated sections (this part is no problem), and for each section, I want to loop through its elements that are associated with the fetched account. Right now I have the following code.
$repository = $this->objectManager->getRepository('MyModule\Entity\Account');
$account = $repository->find(1);
foreach ($account->getSections() as $section) {
foreach ($section->getElements() as $element) {
echo $element->getName() . PHP_EOL;
}
}
The problem is that it fetches all elements belonging to a given section, regardless of which account they are associated with. The generated SQL for fetching a section's elements is as follows.
SELECT t0.id AS id1, t0.name AS name2, t0.section_id AS section_id3
FROM mydb.element t0
WHERE t0.section_id = ?
What I need it to do is something like the below (could be any other approach). It is important that the filtering is done with SQL.
SELECT e.id, e.name, e.section_id
FROM element AS e
INNER JOIN account_element AS ae ON (ae.element_id = e.id)
WHERE ae.account_id = ?
AND e.section_id = ?
I do know that I can write a method getElementsBySection($accountId) or similar in a custom repository and use DQL. If I can do that and somehow override the getElements() method on the Section entity, then that would be perfect. I would just very much prefer if there would be a way to do this through association mappings or at least by using existing getter methods. Ideally, when using an account object, I would like to be able to loop like in the code snippet above so that the "account constraint" is abstracted when using the object. That is, the user of the object does not need to call getElementsByAccount() or similar on a Section object, because it seems less intuitive.
I looked into the Criteria object, but as far as I remember, it cannot be used for filtering on associations.
So, what is the best way to accomplish this? Is it possible without "manually" assembling the Section entity with elements through the use of DQL queries? My current (and shortened) source code can be seen below. Thanks a lot in advance!
/**
* #ORM\Entity
*/
class Account
{
/**
* #var int
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=50, nullable=false)
*/
protected $name;
/**
* #var ArrayCollection
* #ORM\ManyToMany(targetEntity="MyModule\Entity\Section")
* #ORM\JoinTable(name="account_section",
* joinColumns={#ORM\JoinColumn(name="account_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="section_id", referencedColumnName="id")}
* )
*/
protected $sections;
public function __construct()
{
$this->sections = new ArrayCollection();
}
// Getters and setters
}
/**
* #ORM\Entity
*/
class Section
{
/**
* #var int
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=50, nullable=false)
*/
protected $name;
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="MyModule\Entity\Element", mappedBy="section")
*/
protected $elements;
public function __construct()
{
$this->elements = new ArrayCollection();
}
// Getters and setters
}
/**
* #ORM\Entity
*/
class Element
{
/**
* #var int
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=50, nullable=false)
*/
protected $name;
/**
* #var Section
* #ORM\ManyToOne(targetEntity="MyModule\Entity\Section", inversedBy="elements")
* #ORM\JoinColumn(name="section_id", referencedColumnName="id")
*/
protected $section;
/**
* #var \MyModule\Entity\Account
* #ORM\ManyToMany(targetEntity="MyModule\Entity\Account")
* #ORM\JoinTable(name="account_element",
* joinColumns={#ORM\JoinColumn(name="element_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="account_id", referencedColumnName="id")}
* )
*/
protected $account;
// Getters and setters
}
If I understand correctly, you want to be able to retrieve all Elements of all Sections of an Account, but only if those Elements are associated with that Account, and this from a getter in Account.
First off: An entity should never know of repositories. This breaks a design principle that helps you swap out the persistence layer. That's why you cannot simple access a repository from within an entity.
Getters only
If you only want to use getters in the entities, you can solve this by adding to following 2 methods:
class Section
{
/**
* #param Account $accout
* #return Element[]
*/
public function getElementsByAccount(Account $accout)
{
$elements = array();
foreach ($this->getElements() as $element) {
if ($element->getAccount() === $account) {
$elements[] = $element->getAccount();
}
}
return $elements;
}
}
class Account
{
/**
* #return Element[]
*/
public function getMyElements()
{
$elements = array()
foreach ($this->getSections() as $section) {
foreach ($section->getElementsByAccount($this) as $element) {
$elements[] = $element;
}
}
return $elements;
}
}
Repository
The solution above is likely to perform several queries, the exact amount depending on how many Sections and Elements are associated to the Account.
You're likely to get a performance boost when you do use a Repository method, so you can optimize the query/queries used to retrieve what you want.
An example:
class ElementRepository extends EntityRepository
{
/**
* #param Account $account [description]
* #return Element[]
*/
public function findElementsByAccount(Account $account)
{
$dql = <<< 'EOQ'
SELECT e FROM Element e
JOIN e.section s
JOIN s.accounts a
WHERE e.account = ?1 AND a.id = ?2
EOQ;
$q = $this->getEntityManager()->createQuery($dql);
$q->setParameters(array(
1 => $account->getId(),
2 => $account->getId()
));
return $q->getResult();
}
}
PS: For this query to work, you'll need to define the ManyToMany association between Section and Account as a bidirectional one.
Proxy method
A hybrid solution would be to add a proxy method to Account, that forwards the call to the repository you pass to it.
class Account
{
/**
* #param ElementRepository $repository
* #return Element[]
*/
public function getMyElements(ElementRepository $repository)
{
return $repository->findElementsByAccount($this);
}
}
This way the entity still doesn't know of repositories, but you allow one to be passed to it.
When implementing this, don't have ElementRepository extend EntityRepository, but inject the EntityRepository upon creation. This way you can still swap out the persistence layer without altering your entities.

Query reference documents ODM Doctrine2

I have two documents Car and Driver
/**
* #ODM\Document(collection="cars")
*/
class Car {
/**
* #ODM\Id
*/
protected $id;
/**
* #ODM\ReferenceOne(targetDocument="Driver")
*/
protected $driver;
//...
}
/**
* #ODM\Document(collection="drivers")
*/
class Driver {
/**
* #ODM\Id
*/
protected $id;
/**
* #ODM\String
* #Assert\NotBlank()
*/
protected $name;
//...
}
I want one car driven by "Peter"
$car = $dm
->getRepository('Car')
->createQueryBuilder()
->field('driver.name')->equals("Peter")
->getQuery()->getSingleResult();
but the previous code return NULL even if the Car and the Driver exist in the database
I found a similar question i want to know if this drawback can be solved by other way
Try this
$car = $dm
->getRepository('Car')
->createQueryBuilder()
->where('driver.name =?1')
->setParameter(1, 'Peter')
->getQuery()->getSingleResult();
EDIT :
If the driver Peter has more then one Car, you should use
->getOneOrNullResult() instead of getSingleResult()

Doctrine 2: Building a nested array tree from a self-refrencing Entity

I have an Entity that looks like this:
class Privilege
{
/**
* #Id #Column(type="bigint")
* #GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #Column(type="string",length=255)
*/
private $name;
/**
* #Column(type="string",length=255)
*/
private $slug;
/**
* #OneToMany(targetEntity="Privilege", mappedBy="parent")
*/
private $children;
/**
* #ManyToOne(targetEntity="Privilege", inversedBy="children")
* #JoinColumn(name="p_id", referencedColumnName="id")
*/
private $parent;
If a Privilege Entity does not have a parent, the field is NULL. I have a basic query like this:
$qb = $this->em->createQueryBuilder()
->select('p')
->from('\Dashboard\Entity\Privilege', 'p')
->andWhere('p.parent IS NULL');
$q = $qb->getQuery();
$privileges = $q->getResult();
I would like the array result I return from this method to look similar to this:
root1:
child1:
subchild1a
subchild2a
child2:
subchild1b
subchild2b
subchild3b
subsubchild1b
child3:
subchild1c
root2:
....
....
Is there a way to HYDRATE the results from Doctrine 2 so it builds the array results this way? If not, how would you build this array? I am still playing around with Doctrine 2, and I noticed each element in my $privileges array has a $privilege->getChildren() which returns a PersistentCollection, obviously not the actual record.
If I have to build this nested tree myself (ie: no built in way in Doctrine to do it), how do I turn this PersistentCollection returned into the actual data so I can build some sort of recursive method to build it for me? I am looking through the docs, but obviously in the wrong place.
The results are already in a nested tree. The PersistentCollection can be iterated as if it was an array:
foreach($parent->getChildren() as $child) {
// $child is an instance of Privilige
}
Still you should try $privileges = $q->getArrayResult(); and see if that gives a result you would prefer.
I think what you're looking for is called "One-To-Many self-referenced association" in documentation: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html#one-to-one-self-referencing
Here's code for hierarchy of categories from the docs:
<?php
/** #Entity **/
class Category
{
// ...
/**
* #OneToMany(targetEntity="Category", mappedBy="parent")
**/
private $children;
/**
* #ManyToOne(targetEntity="Category", inversedBy="children")
* #JoinColumn(name="parent_id", referencedColumnName="id")
**/
private $parent;
// ...
public function __construct() {
$this->children = new \Doctrine\Common\Collections\ArrayCollection();
}
}
"This effectively models a hierarchy of categories and from the database perspective is known as an adjacency list approach."
So I think this should do all the job for you, and create the hierarchy of arrays you need.
Since you already have your annotations like in docs, your $parent->getChildren() should already contain all the hierarchy, as #rojoca said.

Categories