Doctrine - self-referencing entity - disable fetching of children - php

I have a very simple entity(WpmMenu) that holds menu items connected to one another in a self-referencing relationship (adjecent list it's called)?
so in my entity I have:
protected $id
protected $parent_id
protected $level
protected $name
with all the getters/setters the relationships are:
/**
* #ORM\OneToMany(targetEntity="WpmMenu", mappedBy="parent")
*/
protected $children;
/**
* #ORM\ManyToOne(targetEntity="WpmMenu", inversedBy="children", fetch="LAZY")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", onUpdate="CASCADE", onDelete="CASCADE")
*/
protected $parent;
public function __construct() {
$this->children = new ArrayCollection();
}
And everything works fine. When I render the menu tree, I get the root element from the repository, get its children, and then loop through each child, get its children and do this recursively until I have rendered each item.
What happens (and for what I am seeking a solution)is this:
At the moment I have 5 level=1 items and each of these items have 3 level=2 items attached (and in the future I will be using level=3 items as well). To get all elements of my menu tree Doctrine executes:
1 query for the root element +
1 query to get the 5 children(level=1) of the root element +
5 queries to get the 3 children(level=2) of each of the level 1 items +
15 queries (5x3) to get the children(level=3) of each level 2 items
TOTAL: 22 queries
So, I need to find a solution for this and ideally I would like to have 1 query only.
So this is what I am trying to do:
In my entities repository(WpmMenuRepository) I use queryBuilder and get a flat array of all menu items ordered by level. Get the root element(WpmMenu) and add "manually" its children from the loaded array of elements. Then do this recursively on children. Doing this way I could have the same tree but with a single query.
So this is what I have:
WpmMenuRepository:
public function setupTree() {
$qb = $this->createQueryBuilder("res");
/** #var Array */
$res = $qb->select("res")->orderBy('res.level', 'DESC')->addOrderBy('res.name','DESC')->getQuery()->getResult();
/** #var WpmMenu */
$treeRoot = array_pop($res);
$treeRoot->setupTreeFromFlatCollection($res);
return($treeRoot);
}
and in my WpmMenu entity I have:
function setupTreeFromFlatCollection(Array $flattenedDoctrineCollection){
//ADDING IMMEDIATE CHILDREN
for ($i=count($flattenedDoctrineCollection)-1 ; $i>=0; $i--) {
/** #var WpmMenu */
$docRec = $flattenedDoctrineCollection[$i];
if (($docRec->getLevel()-1) == $this->getLevel()) {
if ($docRec->getParentId() == $this->getId()) {
$docRec->setParent($this);
$this->addChild($docRec);
array_splice($flattenedDoctrineCollection, $i, 1);
}
}
}
//CALLING CHILDREN RECURSIVELY TO ADD REST
foreach ($this->children as &$child) {
if ($child->getLevel() > 0) {
if (count($flattenedDoctrineCollection) > 0) {
$flattenedDoctrineCollection = $child->setupTreeFromFlatCollection($flattenedDoctrineCollection);
} else {
break;
}
}
}
return($flattenedDoctrineCollection);
}
And this is what happens:
Everything works out fine, BUT I end up with each menu items present twice. ;) Instead of 22 queries now I have 23. So I actually worsened the case.
What really happens, I think, is that even if I add the children added "manually", the WpmMenu entity is NOT considered in-sync with the database and as soon as I do the foreach loop on its children the loading is triggered in ORM loading and adding the same children that were added already "manually".
Q: Is there a way to block/disable this behaviour and tell these entities they they ARE in sync with the db so no additional querying is needed?

With immense relief (and a lots of learning about Doctrine Hydration and UnitOfWork) I found the answer to this question. And as with lots of things once you find the answer you realize that you can achieve this with a few lines of code. I am still testing this for unknown side-effects but it seems to be working correctly.
I had quite a lot of difficulties to identify what the problem was - once I did it was much easier to search for an answer.
So the problem is this: Since this is a self-referencing entity where the entire tree is loaded as a flat array of elements and then they are "fed manually" to the $children array of each element by the setupTreeFromFlatCollection method - when the getChildren() method is called on any of the entities in the tree (including the root element), Doctrine (NOT knowing about this 'manual' approach) sees the element as "NOT INITIALIZED" and so executes an SQL to fetch all its related children from the database.
So I dissected the ObjectHydrator class (\Doctrine\ORM\Internal\Hydration\ObjectHydrator) and I followed (sort of) the dehydration process and I got to a $reflFieldValue->setInitialized(true); #line:369 which is a method on the \Doctrine\ORM\PersistentCollection class setting the $initialized property on the class true/false. So I tried and IT WORKS!!!
Doing a ->setInitialized(true) on each of the entities returned by the getResult() method of the queryBuilder (using the HYDRATE_OBJECT === ObjectHydrator) and then calling ->getChildren() on the entities now do NOT trigger any further SQLs!!!
Integrating it in the code of WpmMenuRepository, it becomes:
public function setupTree() {
$qb = $this->createQueryBuilder("res");
/** #var $res Array */
$res = $qb->select("res")->orderBy('res.level', 'DESC')->addOrderBy('res.name','DESC')->getQuery()->getResult();
/** #var $prop ReflectionProperty */
$prop = $this->getClassMetadata()->reflFields["children"];
foreach($res as &$entity) {
$prop->getValue($entity)->setInitialized(true);//getValue will return a \Doctrine\ORM\PersistentCollection
}
/** #var $treeRoot WpmMenu */
$treeRoot = array_pop($res);
$treeRoot->setupTreeFromFlatCollection($res);
return($treeRoot);
}
And that's all!

Add the annotation to your association to enable eager loading. This should allow you to load the entire tree with only 1 query, and avoid having to reconstruct it from a flat array.
Example:
/**
* #ManyToMany(targetEntity="User", mappedBy="groups", fetch="EAGER")
*/
The annotation is this one but with the value changed
https://doctrine-orm.readthedocs.org/en/latest/tutorials/extra-lazy-associations.html?highlight=fetch

You can't solve this problem if using adjacent list. Been there, done that. The only way is to use nested-set and then you would be able to fetch everything you need in one single query.
I did that when I was using Doctrine1. In nested-set you have root, level, left and right columns which you can use to limit/expand fetched objects. It does require somewhat complex subqueries but it is doable.
D1 documentation for nested-set is pretty good, I suggest to check it and you will understand the idea better.

This is more like a completion and more cleaner solution, but is based on the accepted answer...
The only thing needed is a custom repository that is going to query the flat tree structure, and then, by iterating this array it will, first mark the children collection as initialized and then will hydratate it with the addChild setter present in the parent entity..
<?php
namespace Domain\Repositories;
use Doctrine\ORM\EntityRepository;
class PageRepository extends EntityRepository
{
public function getPageHierachyBySiteId($siteId)
{
$roots = [];
$flatStructure = $this->_em->createQuery('SELECT p FROM Domain\Page p WHERE p.site = :id ORDER BY p.order')->setParameter('id', $siteId)->getResult();
$prop = $this->getClassMetadata()->reflFields['children'];
foreach($flatStructure as &$entity) {
$prop->getValue($entity)->setInitialized(true); //getValue will return a \Doctrine\ORM\PersistentCollection
if ($entity->getParent() != null) {
$entity->getParent()->addChild($entity);
} else {
$roots[] = $entity;
}
}
return $roots;
}
}
edit: the getParent() method will not trigger additional queries as long as the relationship is made to the primary key, in my case, the $parent attribute is a direct relationship to the PK, so the UnitOfWork will return the cached entity and not query the database.. If your property doesn't relates by the PK, it WILL generate additional queries.

Related

How to set doctrine associations?

I know that association property in entity is implements \Doctrine\Common\Collections\Collection. I know that in constructor such properties should be initialized:
$this->collection = new \Doctrine\Common\Collections\ArrayCollection()
I know that I can modify collections using ArrayCollection#add() and ArrayCollection#remove(). However I have a different case.
Suppose I have a new simple array of associative entities. Using existing methods I need to check every element in array: if entity collection has it. If no - add array element to entity collection. In addition to this, I need to check every element in entity collection. If any collection element is absent in new array, then I need to remove it from collection. So much work to do trivial thing.
What I want? To have the setProducts method implemented:
class Entity {
private $products;
// ... constructor
public function setProducts(array $products)
{
// synchronize $products with $this->products
}
}
I tried: $this->products = new ArrayCollection($products). However this makes doctrine remove all products and add those ones from $products parameter. I want similar result but without database queries.
Is there any built in solution in Doctrine for such case?
Edit:
I would like to have a method in ArrayCollection like fromArray which would merge elements in collections removing unneeded. This would just duplicate using add/remove calls for each element in collection argumen manually.
Doctrine collections do not have a "merge"-feature that will add/remove entities from an array or Collection in another Collection.
If you want to "simplify" the manual merge process you describe using add/remove, you could use array_merge assuming both arrays are not numeric, but instead have some kind of unique key, e.g. the entity's spl_object_hash:
public function setProducts(array $products)
{
$this->products = new ArrayCollection(
array_merge(
array_combine(
array_map('spl_object_hash', $this->products->toArray()),
$this->products->toArray()
),
array_combine(
array_map('spl_object_hash', $products),
$products->toArray()
)
)
);
}
You might want to use the product id instead of spl_object_hash as 2 products with the same id, but created as separate entities - e.g. one through findBy() in Doctrine and one manually created with new Product() - will be recognized as 2 distinct products and might cause another insert-attempt.
Since you replace the original PersistentCollection holding your previously fetched products with a new ArrayCollection this might still result in unneeded queries or yield unexpected results when flushing the EntityManager, though. Not to mention, that this approach might be harder to read than explicitly calling addElement/removeElement on the original Collection instead.
I would approach it by creating my own collection class that extends Doctrine array collection class:
use Doctrine\Common\Collections\ArrayCollection;
class ProductCollection extends ArrayCollection
{
}
In the entity itself you would initialise it in the __constructor:
public function __construct()
{
$this->products = new ProductCollection();
}
Here, Doctrine will you use your collection class for product results. After this you could add your own function to deal with your special merge, perhaps something:
public function mergeProducts(ProductCollection $products): ProductCollection
{
$result = new ProductCollection();
foreach($products as $product) {
$add = true;
foreach($this->getIterator() as $p) {
if($product->getId() === $p->getId()) {
$result->add($product);
$add = false;
}
}
if($add) {
$result->add($product);
}
}
return $result;
}
It will return a brand new product collection, that you can replace your other collection in the entity. However, if the entity is attached and under doctrine control, this will render SQL at the other end, if you want to play with the entity without risking database updates you need to detach the entity:
$entityManager->detach($productEntity);
Hopes this helps

Saving Related Entities Doctrine/Symfony2

I have one specific issue. I have two entities:
class MyPlaylist {
...
/**
* #var Array
* #ORM\OneToMany(targetEntity="MyPlaylistContent", mappedBy="myPlaylist", orphanRemoval=true)
* #ORM\OrderBy({"position" = "DESC"})
*/
private $myPlaylistItems;
and
class MyPlaylistContent {
....
/**
* #ORM\ManyToOne(targetEntity="MyPlaylist", inversedBy="myPlaylistItems")
*/
private $myPlaylist;
Now I have this in my service
....
$myPlaylist = new MyPlaylist();
$myPlaylist->setUser($user);
$myPlaylist->setActive(true);
// add tracks
foreach ($playlist->getMyPlaylistItems() as $item) {
$entity = new MyPlaylistContent();
$entity->setTrack($item->getTrack());
$entity->setMyPlaylist($myPlaylist);
$this->em->persist($entity);
}
$this->em->persist($myPlaylist);
$this->em->flush();
\Doctrine\Common\Util\Debug::dump($myPlaylist);
return $myPlaylist;
so, I return a new playlist. If I look at the database, all works fine. I have both entities and in MyPlaylistContent - 3 tracks. But
\Doctrine\Common\Util\Debug::dump($myPlaylist); shows next
["active"]=> bool(true) ["myPlaylistItems"]=> array(0) { }
On the page, the app shows the empty playlist (no tracks). If I refresh the page, I can see all tracks.
The point is, if you open the page, the controller will call the service, build the content and return the list as a response.
It looks as the same example, but it does not work for me
http://symfony.com/doc/current/book/doctrine.html#saving-related-entities
What is wrong here? Why don't I get tracks for the current entity?
You forget to add MyPlaylistContent to MyPlaylist.
Use this snippet into foreach
$myPlaylist->addMyPlaylistContent($myPlaylistContent);
Of course change name or implement method accordingly
First note: this is because objects are "normal" php objects, they have nothing to do with doctrine so, relationships are only a doctrine concept. EntityManager in doctrine will handle this kind of processes, not php itself. If you take a look to your classes methods you will probably notice that no "connection" (assignments) are made between those objects. If you would like, you can modify MyPlaylistContent to add itself to MyPlaylist once assigned.
Something like
class MyPlaylistContent
{
[...]
public function setMyPlaylist(MyPlaylist $mp)
{
$this->myPlaylist = $myPlaylist;
$mp->addMyPlaylistContent($this);
return $this;
}
Second note: hope your names are more consistents of these ones :)

Hierarchal data with Doctrine2 using closure table model

I have some existing data stored using the closure table model. I'm new to Doctrine, and trying to implement an Entity for this the "Doctrine way", and not really sure how to proceed. The philosophy I'm trying to follow is that the Entity should just be a plain-old-PHP-object, and that some kind of annotation should be used to configure the parent-child associations.
In this post I'll use Category as an example entity. Here's what I imagine the entity looking like:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Table(name="categories)
* #ORM\Entity
*/
class Category
{
/**
* #ORM\Column(name="categoryID", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $categoryID;
/**
* #ORM\Column(name="title", type="string", length=255)
*/
protected $title;
/**
* #MyORM\TreeParent(targetEntity="Category", closureTable="categories_paths", ancestorColumn="ancestorID", descendantColumn="descendantID")
*/
protected $parent;
/**
* #MyORM\TreeChildren(targetEntity="Category", closureTable="categories_paths", ancestorColumn="ancestorID", descendantColumn="descendantID")
*/
protected $children;
public function __construct()
{
$this->children = new ArrayCollection();
}
public function getChildren()
{
return $this->children;
}
public function addChild(Category $child)
{
$this->children[] = $children;
}
public function getParent()
{
return $this->parent;
}
public function setParent(Category $parent)
{
$this->parent = $parent;
}
}
The closure table looks as follows:
categories_paths(ancestorID, descendantID, pathLength)
This table is essentially a join table -- it only stores the parent-child relations, so I don't think it makes sense for there to be an entity here, similar to how there's no entity when creating a many-to-many relationship with #JoinTable.
I'd like to be able to use my Category entity like any other Entity, with $parent / $children populated when I fetch it from the repository and when $em->flush() is called, have SQL executed to reflect newly added children.
Some examples of SQL used here:
Add a new child:
INSERT INTO categories_paths (ancestorID, descendantID, pathLength)
SELECT a.ancestorID, d.descendantID, a.pathLength+d.pathLength+1
FROM categories_paths a, categories_paths d
WHERE a.descendantID = $parentCategoryID AND d.ancestorID = $childCategoryID
Move a subtree to a new parent:
// Delete all paths that end at $child
DELETE a FROM categories_paths a
JOIN categories_paths d ON a.descendantID=d.descendantID
LEFT JOIN categories_paths x
ON x.ancestorID=d.ancestorID AND x.descendantID=a.ancestorID
WHERE d.ancestorID = $subtreeCategoryID and x.ancestorID IS NULL
// Add new paths
INSERT INTO categories_paths (ancestorID, descendantID, pathLength)
SELECT parent.ancestorID, subtree.descendantID,
parent.pathLength+subtree.pathLength+1
FROM categories_paths parent
JOIN categories_paths subtree
WHERE subtree.ancestorID = $subtreeCategoryID
AND parent.descendantID = $parentCategoryID;
Get all children of a Category:
SELECT * FROM categories
JOIN categories_paths cp ON cp.descendantID=categories.categoryID
WHERE cp.ancestorID = $catogeryID
AND cp.depth=1
I have a few questions here. First of all, does this seem like a reasonable approach / something that is possible to implement with Doctrine? If not, is there a better way to approach this?
If this does seem like a reasonable approach, I'm wondering how to go about attacking this? I'm more looking for where I need to put these files / how I need to set up classes vs. someone giving me an actual implementation. Any documentation or examples that would help me get started would be much appreciated. I have pretty much zero experience with Doctrine--hopefully I'm not missing anything obvious here.
I think if you want to build a hierarchical database you should look for the doctrine ODM project. All the things you want are built in into that and you can customize your node.
There's a mongoDB adapter and also you can take a look at DoctrinePHPCR project that has adapters for several databases.
Even if you want to implement your own approach using doctrine ORM you can look at their implementations to get an idea how they work. They have node based relationship so you always have reference to adjacent nodes in the tree in your object.
Hope that helps.

Knp DoctrineBehaviors Tree getChildNodes

I'm learning symfony and currently I'm looking into Knp DoctrineBehaviors Tree, and I'm confused a little bit, because in the trait: https://github.com/KnpLabs/DoctrineBehaviors/blob/master/src/Knp/DoctrineBehaviors/Model/Tree/Node.php there is a $childNodes and getter getChildNodes() which make sense but getChildNodes always returns empty array for me and I agree with this because I don't set $childNodes any where so it will always return empty ArrayCollection. So my question is should my application look after this? Or I undestand that wrongly?
/**
* #var ArrayCollection $childNodes the children in the tree
*/
private $childNodes;
...
...
...
/**
* {#inheritdoc}
**/
public function getChildNodes()
{
return $this->childNodes = $this->childNodes ?: new ArrayCollection;
}
Thanks for any help.
Yes you need to set the child nodes. But the api for doing this are as bellow:
//To set $child as child node for a $parent entity
$child->setParentNode($parent);
//OR
$child->setChildOf($parent);
$parent->isParentOf($child); //return true
$child->isParentOf($parent); //return false
Whenever you need to access getChildNodes() you need Build the tree first:
$root = $em->getRepository('Category')->getTree();
$root->getParent(); // null
$root->getChildNodes(); // ArrayCollection
$root[0][1]; // node or null
$root->isLeaf(); // boolean
$root->isRoot(); // boolean
The idea behind this tree implementation is that there is no relationship between parent and children (foreign key). All the relations are defined via a materialized path, a string stored in each node. So before build tree you can't access this getParentNode() and getChildNodes() functions.
Just for your thought: To get all child by parent_id you can define the following function in your repository
public function getChildrenByParentID($parentId)
{
$parentPath = $this->find($parentId)->getRealMaterializedPath();
return $this->getTree($parentPath)->getChildNodes();
}
Updated: You can use following function(define in your repository class) to read all root level nodes:
public function getRootLevelNodes()
{
$qb = $this->createQueryBuilder('t');
return $qb
->where($qb->expr()->eq('t.materializedPath', '?1'))
->setParameter(1, '');
}
You can read more about Doctrine ORM behaviors, or how to use traits efficiently

Symfony2 - How to implement nested records and recursive functions into Entity Field or Twig Layout?

I have a serious doubt about doing a combo box with nested records from an entity in Symfony2. I have read about nested tree extension for Doctrine 2 in http://gediminasm.org/article/tree-nestedset-behavior-extension-for-doctrine-2, it appears to be interesting but it does not refer how to implementing this nested tree into an entity field in a form.
Also, I have read more about recursive functions in PHP, and I have found an interesting blog where it is analyzed, here is the link http://www.sitepoint.com/hierarchical-data-database/, it explains specifically about this recursive function:
function display_children($parent, $level) {
// Retrieve all children of $parent
$result = mysql_query('SELECT title FROM tree WHERE parent="'.$parent.'"');
// Display each child
while ($row = mysql_fetch_array($result)) {
// Indent and display the title of this child
echo str_repeat(' ',$level).$row['title']."\n";
// Call this function again to display this child's children
display_children($row['title'], $level+1);
}
}
Somebody knows how to translate this code into Symfony2 and where it would be stored (Controller, Entity, etc.). If someone has other ideas about working nested records with Twig Extensions, It would be appreciate too.
Thanks a lot your help.
This is how we implemented Nested tree for Categories (indented dropdown) for use in Product edit form:
Define your Category entity class as shown in the documentation
Add a method to the Category entity class that shows the name indented by nesting level
/**
* #ORM\Table()
* #ORM\Entity(repositoryClass="CP\YourBundle\Entity\CategoryRepository")
* #Gedmo\Tree(type="nested")
*/
class Category
{
public function getOptionLabel()
{
return str_repeat(
html_entity_decode(' ', ENT_QUOTES, 'UTF-8'),
($this->getLevel() + 1) * 3
) . $this->getName();
}
Define Product entity relations with the Category entities using Doctrine2 annotations (in our case we have multiple categories support for one product)
class Product
{
/**
* #var ArrayCollection
* #ORM\ManyToMany(targetEntity="Category", cascade={"persist", "remove"})
*/
private $categories;
...
Now all you have to do is add the following to the ProductType form class
class ProductType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('categories', null, array('property' => 'optionLabel'));
}
Now the form should show the dropdown with a correctly indented Category list
You can have a look to this tree implementation that is not based on nested sets but on materialzed paths: https://github.com/KnpLabs/materialized-path .
You could imagine use its API to get a flat resultset of the tree, like in your code snippet:
$root = $repo->find($id);
$repo->buildTree($root);
$flatArray = $root->toFlatArray(function(NodeInterface $node) {
$pre = $node->getLevel() > 1 ? implode('', array_fill(0, $node->getLevel(), '--')) : '';
return $pre.(string)$node;
});
return $this->get('templating')->render('::tree.html.twig', $flatArray);

Categories