Welcome to the part 2 of the “Hierarchical Data in Relational Databases”. Part 2 heavily depends on the Part 1, and it will show you how to do the most common tasks with Symfony 4 and Doctrine using stofDoctrineExtensionsBundle.
If you didn’t, please read the Part 1 before continuing with this article.
Introduction
Working with hierarchical data in a relational database is not easy, convenient or efficient. The adjacency list model is the straightforward way to represent hierarchical data. In this mode each node has a parent id. This model is simple to understand but it is not efficient to work with. For example lets say we have category tree and associated products that can be in any category. It is impossible to fetch, with one efficient query, subtree, all products in a given category which are also found in the given category subcategories , total number of products in every category, categories at certain depth, etc…. In this model usually all categories are loaded from db, tree is created in the client code (PHP for example) and then products are fetched once we know subtree category ids.
In the nested set model categories are represented as nested containers. Beside parent id (which is not necessary, but it is used for convenience), two new fields are introduced: left and right (shown on the containers border).
Using self join and left and right values it is easy and efficient to fetch all the children (subtree), path (parents), leafs, etc … all with one efficient query. Also products table can be joined. All this enables as to work with hierarchical data and associated data in elegant and efficient way. With stofDoctrineExtensionsBundle we can enjoy the benefits of the nested set model without getting our hands dirty with the details of the implementation.
The negative side of nested set model is overhead when inserting and deleting nodes, since left and right values of other nodes needs to be recalculated. So it is best used when you need to read the tree more often then you need to change it.
For more details on nested set model and how to work with it without additional libraries read part 1 of the tutorial.
Installing
Accompanying code for this tutorial is available from https://github.com/dribtech/hierarchical-data-tutorial-part-2 .
Clone the code:
1 |
$ git clone git@github.com:dribtech/hierarchical-data-tutorial-part-2.git |
or
1 |
$ git clone https://github.com/dribtech/hierarchical-data-tutorial-part-2.git |
Run composer:
$ composer install
Edit .env, enter DBAL connection parameters (username, password, and host, port and db name if needed):
1 |
DATABASE_URL=mysql://user_name:password@127.0.0.1:3306/drib_sf4_hier_data |
stofDoctrineExtensionsBundle configuration
At the time of writing this article the documentation at symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle is/was a little bit outdated (more targeted to the sf3 instead of sf4) so take a peek but don’t literally use everything from that page.
Open config/packages/doctrine.yaml
The tree extension needs its own entity to do the work . Its mapping needs to be registered in Doctrine.
From app/config/config.yml in StofDoctrineExtensionsBundle documentation page I copied the part regarding the tree mappings:
1 2 3 4 5 6 |
gedmo_tree: type: annotation prefix: Gedmo\Tree\Entity dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Tree/Entity" alias: GedmoTree # (optional) it will default to the name set for the mapping is_bundle: false |
Open config/packages/stof_doctrine_extensions.yaml
By default the bundle doesn’t attach any listeners. So the extensions needs to be enabled explicitly.
1 2 3 4 5 |
stof_doctrine_extensions: default_locale: en_US orm: default: tree: true |
Category entity
Category entity is very similar to the Category entity from github.com/Atlantic18/DoctrineExtensions/blob/v2.4.x/doc/tree.md . The only difference is that I am using column name instead of title and that I created the indexes for the lft, rgt and lvl columns.
Checkout src/Entity/Category.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
<?php namespace App\Entity; use Gedmo\Mapping\Annotation as Gedmo; use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping\Index as Index; /** * Class Category * @package App\Entity * * @Gedmo\Tree(type="nested") * @ORM\Table( * name="category", * indexes={ * @Index(name="lft_ix", columns={"lft"}), * @Index(name="rgt_ix", columns={"rgt"}), * @Index(name="lvl_ix", columns={"lvl"}) * }) * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") */ class Category { /** * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string") */ private $name; /** * @Gedmo\TreeLeft() * @ORM\Column(type="integer") */ private $lft; /** * @Gedmo\TreeLevel() * @ORM\Column(type="integer") */ private $lvl; /** * @Gedmo\TreeRight() * @ORM\Column(type="integer") */ private $rgt; /** * @Gedmo\TreeRoot() * @ORM\ManyToOne(targetEntity="App\Entity\Category") * @ORM\JoinColumn(name="tree_root", referencedColumnName="id", onDelete="cascade") */ private $root; /** * @Gedmo\TreeParent() * @ORM\ManyToOne(targetEntity="App\Entity\Category") * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="cascade") */ private $parent; /** * @ORM\OneToMany(targetEntity="App\Entity\Category", mappedBy="parent") * @ORM\OrderBy({"lft" = "ASC"}) */ private $children; /** * @return integer */ public function getId() { return $this->id; } /** * @return string */ public function getName() { return $this->name; } /** * @param string $name */ public function setName($name): void { $this->name = $name; } /** * @return Category */ public function getParent() { return $this->parent; } /** * @param Category $parent */ public function setParent($parent): void { $this->parent = $parent; } /** * @return Category */ public function getRoot() { return $this->root; } } |
Please note the special annotations @Gedmo.
- @Gedmo\Mapping\Annotation\Tree(type="strategy") class annotation, sets the tree strategy. We are using nested – the same one which was described in part 1.
- @Gedmo\Mapping\Annotation\TreeLeft field used to store left tree value
- @Gedmo\Mapping\Annotation\TreeRight field used to store right tree value
- @Gedmo\Mapping\Annotation\TreeParentparent node
- @Gedmo\Mapping\Annotation\TreeLevel field used to store tree level/depth (so there is no need to calculate it with query as in part 1)
- @Gedmo\Mapping\Annotation\TreeRootroot node (in the same table you can store more independent trees – and each will have its root node – this is something we didn’t bother in part 1)
For full list of tree annotations visit the documentation.
Create database
Run:
$ bin/console doctrine:database:create
$ bin/console doctrine:schema:update --force
Tree demonstration will be done with symfony command in order to keep the tutorial do the point and not get into overhead of creating controllers, templates, etc…
In order to create this category tree:
Open src/Command/AppCreateCategoryCommand.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
// level 0: Home $home = new Category(); $home->setName('Home'); // level 1: Bikes, Components, Wheels & Tyres $bikes = new Category(); $bikes->setName('Bikes'); $bikes->setParent($home); $components = new Category(); $components->setName('Components'); $components->setParent($home); $wheelsAndTyres = new Category(); $wheelsAndTyres->setName('Wheels & Tyres'); $wheelsAndTyres->setParent($home); $this->entityManager->persist($home); $this->entityManager->persist($bikes); $this->entityManager->persist($components); $this->entityManager->persist($wheelsAndTyres); $this->entityManager->flush(); // demonstrate using repository functions $mountain = new Category(); $mountain->setName('Mountain'); $repository->persistAsLastChildOf($mountain, $bikes); $roadAndTimeTrail = new Category(); $roadAndTimeTrail->setName('Road & Time Trail'); $repository->persistAsNextSiblingOf($roadAndTimeTrail, $mountain); // children of Wheels & Tyres $rims = new Category(); $rims->setName('Rims'); $repository->persistAsLastChildOf($rims, $wheelsAndTyres); $hubs = new Category(); $hubs->setName('Hubs'); $repository->persistAsNextSiblingOf($hubs, $rims); $tyres = new Category(); $tyres->setName('Tyres'); $repository->persistAsNextSiblingOf($tyres, $hubs); // children of Mountain $enduro = new Category(); $enduro->setName('Enduro'); $repository->persistAsLastChildOf($enduro, $mountain); $xc = new Category(); $xc->setName('XC'); $repository->persistAsLastChildOf($xc, $mountain); $fatBike = new Category(); $fatBike->setName('Fat Bike'); $repository->persistAsLastChildOf($fatBike, $mountain); |
The tree can be created without and with repository methods – whatever is more convenient for you.
Without repository function, just create the Category entity and set its parent – the extension will take care of calculation of the left, right and level values – for the new node as well as of existing nodes. Just don’t forget to persist the entities. In the example above Home, Bikes, Components and Wheels & Tyres are created in that way.
Sometimes it might be more convenient to use repository methods. Their names are self explanatory:
- persistAsFirstChild($node)
- persistAsFirstChildOf($node, $parent)
- persistAsLastChild($node)
- persistAsLastChildOf($node, $parent)
- persistAsNextSibling($node)
- persistAsNextSiblingOf($node, $sibling)
- persistAsPrevSibling($node)
- persistAsPrevSiblingOf($node, $sibling)
Also checkout vendor/gedmo/doctrine-extensions/lib/Gedmo/Tree/Entity/Repository/NestedTreeRepository.php.
Execute command:
$ bin/console app:category:create
Then run:
$ bin/console doctrine:query:sql "select * from category"
And compare lft, rgt and lvl values with nested set diagram.
Retrieve the data
Retrieve the full tree (or subtree)
Retrieve “Bikes” category and all its subcategories using repository method childrenHierarchy
. childrenHierarchy
can return categories (and subcategories) as a array tree or formatted HTML string. For example nested
ul and
li.
You can use this method to get ready made HTML fragment to be used directly on the web page as a menu, or navigation tree structure, or whatever, and the HTML output is highly customizable. If you need even more flexibility just retrieve the tree in a array and process it as you wish.
Checkout src/Command/AppCategoryTreeCommand.php.
In order to see ouput of various way you can fetch the tree run bin/console app:category:tree :
- Array tree
- HTML (as nested UL)
- Customized HTML
- Array of objects (entities) from the root and from the arbitrary node
First get the node which subcategories we want to retrieve:
1 2 3 4 5 6 7 8 9 |
$repository = $this->entityManager->getRepository(Category::class); $bikesNode = $repository->findOneBy(['name' => 'Bikes']); if (is_null($bikesNode)) { $io->note('Category "Bikes" not found.'); return 1; } |
To retrieve the data we will use repository method:
1 |
public function childrenHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false); |
- $node – From which node to get the children. If null it will return the whole tree. In the example above we want “Bikes” and its subcategories.
- $direct – If true retrieve only direct children and not all subcategories. In the example above – we wanted all subcategories so $direct was set to false.
- $options – enable and customize HTML output instead of array tree. As shown in the example above it is enough to specify ‘decorate’ => true
- $includeNode – weather to include the $node in the result or not. In the following examples we did included it.
Retrieve the array tree
Children are stored under __children key:
1 2 3 |
$io->writeln('Children array tree:'); $arrayTree = $repository->childrenHierarchy($bikesNode, false, [], true); $io->writeln(print_r($arrayTree, true)); |
Output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
Array ( [0] => Array ( [id] => 2 [name] => Bikes [lft] => 2 [lvl] => 1 [rgt] => 13 [__children] => Array ( [0] => Array ( [id] => 5 [name] => Mountain [lft] => 3 [lvl] => 2 [rgt] => 10 [__children] => Array ( [0] => Array ( [id] => 10 [name] => Enduro [lft] => 4 [lvl] => 3 [rgt] => 5 [__children] => Array ( ) ) [1] => Array ( [id] => 11 [name] => XC [lft] => 6 [lvl] => 3 [rgt] => 7 [__children] => Array ( ) ) [2] => Array ( [id] => 12 [name] => Fat Bike [lft] => 8 [lvl] => 3 [rgt] => 9 [__children] => Array ( ) ) ) ) [1] => Array ( [id] => 6 [name] => Road & Time Trail [lft] => 11 [lvl] => 2 [rgt] => 12 [__children] => Array ( ) ) ) ) ) |
Retrieve the tree as nested html unordered list
In the $options set decorate to true:
1 2 3 4 5 6 7 8 9 |
$io->title('Children html tree:'); $htmlTree = $repository->childrenHierarchy( $bikesNode, false, [ 'decorate' => true, ], true ); |
Output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<ul> <li>Bikes <ul> <li>Mountain <ul> <li>Enduro</li> <li>XC</li> <li>Fat Bike</li> </ul> </li> <li>Road & Time Trail</li> </ul> </li> </ul> |
Customize childrenHierarchy HTML output
childrenHierarchy $options parameter accepts following options:
- decorate – if true retrieve tree as HTML
- nodeDecorator: Closure (or null) use $node as argument and returns decorated item as a string
- rootOpen – string (<ul>) or Closure – start of the branch, closure will get $children as parameter
- rootClose – string (</ul>) – branch close
- childOpen- string (<li>) or Closure – start of the node, closure will get $node as parameter
- childClose – string (‘</li>’) – node close
- childSort – array containing keys: “field” – on which field to sort and “dir” direction of the sort, “asc” or “desc”
Lets customize the output by adding attributes to the <li> and link for the nodes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
$io->title('Children html (customized) tree:'); $htmlTree = $repository->childrenHierarchy( $bikesNode, false, [ 'decorate' => true, 'nodeDecorator' => function ($node) { return "<a href=\"https://www.google.com/search?q=$node[name]\">$node[name]</a>"; }, //'rootOpen' => '<ul>', leave it as is //'rootClose' => '</ul>', 'childOpen' => function ($node) { return "<li data-node-id=\"$node[id]\">"; }, 'childClose' => '</li>', ], true ); $io->writeln($htmlTree); |
Output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<ul> <li data-node-id="2"><a href="https://www.google.com/search?q=Bikes">Bikes</a> <ul> <li data-node-id="5"><a href="https://www.google.com/search?q=Mountain">Mountain</a> <ul> <li data-node-id="10"><a href="https://www.google.com/search?q=Enduro">Enduro</a></li> <li data-node-id="11"><a href="https://www.google.com/search?q=XC">XC</a></li> <li data-node-id="12"><a href="https://www.google.com/search?q=Fat Bike">Fat Bike</a></li> </ul> </li> <li data-node-id="6"><a href="https://www.google.com/search?q=Road & Time Trail">Road & Time Trail</a></li> </ul> </li> </ul> |
Output the tree as objects (entities) instead of arrays
- With TreeObjectHydrator we can build the tree with objects, so it needs to be registrated as hydrator with Doctrine
- The hydrator requires HINT_INCLUDE_META_COLUMNS as query hint
- For output dump component is used instead of print_r or var_dump in order to avoid circular references.
Tree from root node:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$io->title('Array of objects (from the root node):'); $this->entityManager->getConfiguration()->addCustomHydrationMode( 'tree', 'Gedmo\Tree\Hydrator\ORM\TreeObjectHydrator' ); $tree = $repository->createQueryBuilder('node')->getQuery() ->setHint(Query::HINT_INCLUDE_META_COLUMNS, true) ->getResult('tree') ; dump($tree); |
Output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 |
array:1 [ 0 => Proxies\__CG__\App\Entity\Category {#394 +__isInitialized__: true -id: 1 -name: "Home" -lft: 1 -lvl: 0 -rgt: 24 -root: Proxies\__CG__\App\Entity\Category {#394 …2} -parent: null -children: Doctrine\ORM\PersistentCollection {#338 -snapshot: [] -owner: Proxies\__CG__\App\Entity\Category {#394 …2} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: true #collection: Doctrine\Common\Collections\ArrayCollection {#319 -elements: array:3 [ 0 => App\Entity\Category {#378 -id: 2 -name: "Bikes" -lft: 2 -lvl: 1 -rgt: 13 -root: Proxies\__CG__\App\Entity\Category {#394 …2} -parent: Proxies\__CG__\App\Entity\Category {#394 …2} -children: Doctrine\ORM\PersistentCollection {#382 -snapshot: [] -owner: App\Entity\Category {#378} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: true #collection: Doctrine\Common\Collections\ArrayCollection {#430 -elements: array:2 [ 0 => App\Entity\Category {#342 -id: 5 -name: "Mountain" -lft: 3 -lvl: 2 -rgt: 10 -root: Proxies\__CG__\App\Entity\Category {#394 …2} -parent: App\Entity\Category {#378} -children: Doctrine\ORM\PersistentCollection {#321 -snapshot: [] -owner: App\Entity\Category {#342} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: true #collection: Doctrine\Common\Collections\ArrayCollection {#462 -elements: array:3 [ 0 => App\Entity\Category {#306 -id: 10 -name: "Enduro" -lft: 4 -lvl: 3 -rgt: 5 -root: Proxies\__CG__\App\Entity\Category {#394 …2} -parent: App\Entity\Category {#342} -children: Doctrine\ORM\PersistentCollection {#308 -snapshot: [] -owner: App\Entity\Category {#306} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#309 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#310 -snapshot: [] -owner: App\Entity\Category {#306} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#431 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#311 -elements: [] } #initialized: false } } 1 => App\Entity\Category {#312 -id: 11 -name: "XC" -lft: 6 -lvl: 3 -rgt: 7 -root: Proxies\__CG__\App\Entity\Category {#394 …2} -parent: App\Entity\Category {#342} -children: Doctrine\ORM\PersistentCollection {#313 -snapshot: [] -owner: App\Entity\Category {#312} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#314 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#315 -snapshot: [] -owner: App\Entity\Category {#312} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#431 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#316 -elements: [] } #initialized: false } } 2 => App\Entity\Category {#366 -id: 12 -name: "Fat Bike" -lft: 8 -lvl: 3 -rgt: 9 -root: Proxies\__CG__\App\Entity\Category {#394 …2} -parent: App\Entity\Category {#342} -children: Doctrine\ORM\PersistentCollection {#411 -snapshot: [] -owner: App\Entity\Category {#366} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#373 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#367 -snapshot: [] -owner: App\Entity\Category {#366} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#431 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#466 -elements: [] } #initialized: false } } ] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#467 -snapshot: [] -owner: App\Entity\Category {#342} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#431 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#361 -elements: [] } #initialized: false } } 1 => App\Entity\Category {#317 -id: 6 -name: "Road & Time Trail" -lft: 11 -lvl: 2 -rgt: 12 -root: Proxies\__CG__\App\Entity\Category {#394 …2} -parent: App\Entity\Category {#378} -children: Doctrine\ORM\PersistentCollection {#350 -snapshot: [] -owner: App\Entity\Category {#317} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#352 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#346 -snapshot: [] -owner: App\Entity\Category {#317} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#431 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#351 -elements: [] } #initialized: false } } ] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#404 -snapshot: [] -owner: App\Entity\Category {#378} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#431 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#463 -elements: [] } #initialized: false } } 1 => App\Entity\Category {#341 -id: 3 -name: "Components" -lft: 14 -lvl: 1 -rgt: 15 -root: Proxies\__CG__\App\Entity\Category {#394 …2} -parent: Proxies\__CG__\App\Entity\Category {#394 …2} -children: Doctrine\ORM\PersistentCollection {#344 -snapshot: [] -owner: App\Entity\Category {#341} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#343 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#348 -snapshot: [] -owner: App\Entity\Category {#341} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#431 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#340 -elements: [] } #initialized: false } } 2 => App\Entity\Category {#347 -id: 4 -name: "Wheels & Tyres" -lft: 16 -lvl: 1 -rgt: 23 -root: Proxies\__CG__\App\Entity\Category {#394 …2} -parent: Proxies\__CG__\App\Entity\Category {#394 …2} -children: Doctrine\ORM\PersistentCollection {#324 -snapshot: [] -owner: App\Entity\Category {#347} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: true #collection: Doctrine\Common\Collections\ArrayCollection {#359 -elements: array:3 [ 0 => App\Entity\Category {#349 -id: 7 -name: "Rims" -lft: 17 -lvl: 2 -rgt: 18 -root: Proxies\__CG__\App\Entity\Category {#394 …2} -parent: App\Entity\Category {#347} -children: Doctrine\ORM\PersistentCollection {#330 -snapshot: [] -owner: App\Entity\Category {#349} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#322 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#318 -snapshot: [] -owner: App\Entity\Category {#349} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#431 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#332 -elements: [] } #initialized: false } } 1 => App\Entity\Category {#331 -id: 8 -name: "Hubs" -lft: 19 -lvl: 2 -rgt: 20 -root: Proxies\__CG__\App\Entity\Category {#394 …2} -parent: App\Entity\Category {#347} -children: Doctrine\ORM\PersistentCollection {#323 -snapshot: [] -owner: App\Entity\Category {#331} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#326 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#327 -snapshot: [] -owner: App\Entity\Category {#331} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#431 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#335 -elements: [] } #initialized: false } } 2 => App\Entity\Category {#334 -id: 9 -name: "Tyres" -lft: 21 -lvl: 2 -rgt: 22 -root: Proxies\__CG__\App\Entity\Category {#394 …2} -parent: App\Entity\Category {#347} -children: Doctrine\ORM\PersistentCollection {#307 -snapshot: [] -owner: App\Entity\Category {#334} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#303 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#304 -snapshot: [] -owner: App\Entity\Category {#334} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#431 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#305 -elements: [] } #initialized: false } } ] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#345 -snapshot: [] -owner: App\Entity\Category {#347} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#431 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#362 -elements: [] } #initialized: false } } ] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#339 -snapshot: [] -owner: Proxies\__CG__\App\Entity\Category {#394 …2} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#431 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#337 -elements: [] } #initialized: false } …2 } ] |
Tree from arbitrary node:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$io->title('Array of objects (from the Bikes node):'); $tree = $this->entityManager->createQueryBuilder() ->select('node') ->from(Category::class, 'node') ->orderBy('node.root, node.lft', 'ASC') ->where('node.name = \'Bikes\'') ->getQuery() ->setHint(Query::HINT_INCLUDE_META_COLUMNS, true) ->getResult('tree') ; dump($tree); |
Output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 |
array:1 [ 0 => App\Entity\Category {#376 -id: 2 -name: "Bikes" -lft: 2 -lvl: 1 -rgt: 13 -root: Proxies\__CG__\App\Entity\Category {#392 +__isInitialized__: true -id: 1 -name: "Home" -lft: 1 -lvl: 0 -rgt: 24 -root: Proxies\__CG__\App\Entity\Category {#392 …2} -parent: null -children: Doctrine\ORM\PersistentCollection {#336 -snapshot: [] -owner: Proxies\__CG__\App\Entity\Category {#392 …2} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: true #collection: Doctrine\Common\Collections\ArrayCollection {#317 -elements: array:3 [ 0 => App\Entity\Category {#376} 1 => App\Entity\Category {#339 -id: 3 -name: "Components" -lft: 14 -lvl: 1 -rgt: 15 -root: Proxies\__CG__\App\Entity\Category {#392 …2} -parent: Proxies\__CG__\App\Entity\Category {#392 …2} -children: Doctrine\ORM\PersistentCollection {#342 -snapshot: [] -owner: App\Entity\Category {#339} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#341 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#346 -snapshot: [] -owner: App\Entity\Category {#339} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#429 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#338 -elements: [] } #initialized: false } } 2 => App\Entity\Category {#345 -id: 4 -name: "Wheels & Tyres" -lft: 16 -lvl: 1 -rgt: 23 -root: Proxies\__CG__\App\Entity\Category {#392 …2} -parent: Proxies\__CG__\App\Entity\Category {#392 …2} -children: Doctrine\ORM\PersistentCollection {#322 -snapshot: [] -owner: App\Entity\Category {#345} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: true #collection: Doctrine\Common\Collections\ArrayCollection {#357 -elements: array:3 [ 0 => App\Entity\Category {#347 -id: 7 -name: "Rims" -lft: 17 -lvl: 2 -rgt: 18 -root: Proxies\__CG__\App\Entity\Category {#392 …2} -parent: App\Entity\Category {#345} -children: Doctrine\ORM\PersistentCollection {#328 -snapshot: [] -owner: App\Entity\Category {#347} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#320 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#316 -snapshot: [] -owner: App\Entity\Category {#347} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#429 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#330 -elements: [] } #initialized: false } } 1 => App\Entity\Category {#329 -id: 8 -name: "Hubs" -lft: 19 -lvl: 2 -rgt: 20 -root: Proxies\__CG__\App\Entity\Category {#392 …2} -parent: App\Entity\Category {#345} -children: Doctrine\ORM\PersistentCollection {#321 -snapshot: [] -owner: App\Entity\Category {#329} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#324 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#325 -snapshot: [] -owner: App\Entity\Category {#329} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#429 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#333 -elements: [] } #initialized: false } } 2 => App\Entity\Category {#332 -id: 9 -name: "Tyres" -lft: 21 -lvl: 2 -rgt: 22 -root: Proxies\__CG__\App\Entity\Category {#392 …2} -parent: App\Entity\Category {#345} -children: Doctrine\ORM\PersistentCollection {#305 -snapshot: [] -owner: App\Entity\Category {#332} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#301 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#302 -snapshot: [] -owner: App\Entity\Category {#332} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#429 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#303 -elements: [] } #initialized: false } } ] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#343 -snapshot: [] -owner: App\Entity\Category {#345} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#429 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#360 -elements: [] } #initialized: false } } ] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#337 -snapshot: [] -owner: Proxies\__CG__\App\Entity\Category {#392 …2} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#429 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#335 -elements: [] } #initialized: false } …2 } -parent: Proxies\__CG__\App\Entity\Category {#392 …2} -children: Doctrine\ORM\PersistentCollection {#380 -snapshot: [] -owner: App\Entity\Category {#376} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: true #collection: Doctrine\Common\Collections\ArrayCollection {#428 -elements: array:2 [ 0 => App\Entity\Category {#340 -id: 5 -name: "Mountain" -lft: 3 -lvl: 2 -rgt: 10 -root: Proxies\__CG__\App\Entity\Category {#392 …2} -parent: App\Entity\Category {#376} -children: Doctrine\ORM\PersistentCollection {#319 -snapshot: [] -owner: App\Entity\Category {#340} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: true #collection: Doctrine\Common\Collections\ArrayCollection {#460 -elements: array:3 [ 0 => App\Entity\Category {#304 -id: 10 -name: "Enduro" -lft: 4 -lvl: 3 -rgt: 5 -root: Proxies\__CG__\App\Entity\Category {#392 …2} -parent: App\Entity\Category {#340} -children: Doctrine\ORM\PersistentCollection {#306 -snapshot: [] -owner: App\Entity\Category {#304} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#307 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#308 -snapshot: [] -owner: App\Entity\Category {#304} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#429 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#309 -elements: [] } #initialized: false } } 1 => App\Entity\Category {#310 -id: 11 -name: "XC" -lft: 6 -lvl: 3 -rgt: 7 -root: Proxies\__CG__\App\Entity\Category {#392 …2} -parent: App\Entity\Category {#340} -children: Doctrine\ORM\PersistentCollection {#311 -snapshot: [] -owner: App\Entity\Category {#310} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#312 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#313 -snapshot: [] -owner: App\Entity\Category {#310} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#429 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#314 -elements: [] } #initialized: false } } 2 => App\Entity\Category {#364 -id: 12 -name: "Fat Bike" -lft: 8 -lvl: 3 -rgt: 9 -root: Proxies\__CG__\App\Entity\Category {#392 …2} -parent: App\Entity\Category {#340} -children: Doctrine\ORM\PersistentCollection {#409 -snapshot: [] -owner: App\Entity\Category {#364} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#371 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#365 -snapshot: [] -owner: App\Entity\Category {#364} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#429 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#464 -elements: [] } #initialized: false } } ] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#465 -snapshot: [] -owner: App\Entity\Category {#340} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#429 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#359 -elements: [] } #initialized: false } } 1 => App\Entity\Category {#315 -id: 6 -name: "Road & Time Trail" -lft: 11 -lvl: 2 -rgt: 12 -root: Proxies\__CG__\App\Entity\Category {#392 …2} -parent: App\Entity\Category {#376} -children: Doctrine\ORM\PersistentCollection {#348 -snapshot: [] -owner: App\Entity\Category {#315} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#252 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#350 -elements: [] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#344 -snapshot: [] -owner: App\Entity\Category {#315} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#429 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#349 -elements: [] } #initialized: false } } ] } #initialized: true } -products: Doctrine\ORM\PersistentCollection {#404 -snapshot: [] -owner: App\Entity\Category {#376} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#429 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#437 -elements: [] } #initialized: false } } ] |
Retrieve all parents of the node (and the node itself)
Use repository method getPath().
src/Command/AppCategoryPathCommand.php
1 2 3 4 5 6 |
$category = $repository->findOneBy(['name' => $categoryName]); /** @var ArrayCollection|Category[] $path */ $path = $repository->getPath($category); dump($path); |
Output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
$ bin/console app:category:path xc array:4 [ 0 => Proxies\__CG__\App\Entity\Category {#393 +__isInitialized__: true -id: 1 -name: "Home" -lft: 1 -lvl: 0 -rgt: 24 -root: Proxies\__CG__\App\Entity\Category {#393 …2} -parent: null -children: Doctrine\ORM\PersistentCollection {#342 -snapshot: [] -owner: Proxies\__CG__\App\Entity\Category {#393 …2} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#251 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#347 -elements: [] } #initialized: false } -products: Doctrine\ORM\PersistentCollection {#339 -snapshot: [] -owner: Proxies\__CG__\App\Entity\Category {#393 …2} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#447 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#346 -elements: [] } #initialized: false } …2 } 1 => App\Entity\Category {#325 -id: 2 -name: "Bikes" -lft: 2 -lvl: 1 -rgt: 13 -root: Proxies\__CG__\App\Entity\Category {#393 …2} -parent: Proxies\__CG__\App\Entity\Category {#393 …2} -children: Doctrine\ORM\PersistentCollection {#348 -snapshot: [] -owner: App\Entity\Category {#325} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#251 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#353 -elements: [] } #initialized: false } -products: Doctrine\ORM\PersistentCollection {#329 -snapshot: [] -owner: App\Entity\Category {#325} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#447 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#345 -elements: [] } #initialized: false } } 2 => Proxies\__CG__\App\Entity\Category {#381 +__isInitialized__: true -id: 5 -name: "Mountain" -lft: 3 -lvl: 2 -rgt: 10 -root: Proxies\__CG__\App\Entity\Category {#393 …2} -parent: App\Entity\Category {#325} -children: Doctrine\ORM\PersistentCollection {#321 -snapshot: [] -owner: Proxies\__CG__\App\Entity\Category {#381 …2} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#251 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#330 -elements: [] } #initialized: false } -products: Doctrine\ORM\PersistentCollection {#341 -snapshot: [] -owner: Proxies\__CG__\App\Entity\Category {#381 …2} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#447 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#349 -elements: [] } #initialized: false } …2 } 3 => App\Entity\Category {#377 -id: 11 -name: "XC" -lft: 6 -lvl: 3 -rgt: 7 -root: Proxies\__CG__\App\Entity\Category {#393 …2} -parent: Proxies\__CG__\App\Entity\Category {#381 …2} -children: Doctrine\ORM\PersistentCollection {#387 -snapshot: [] -owner: App\Entity\Category {#377} -association: array:16 [ …16] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "parent" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#251 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#428 -elements: [] } #initialized: false } -products: Doctrine\ORM\PersistentCollection {#441 -snapshot: [] -owner: App\Entity\Category {#377} -association: array:15 [ …15] -em: Doctrine\ORM\EntityManager {#186 …11} -backRefFieldName: "category" -typeClass: Doctrine\ORM\Mapping\ClassMetadata {#447 …} -isDirty: false #collection: Doctrine\Common\Collections\ArrayCollection {#437 -elements: [] } #initialized: false } } ] |
Retrieve leaf nodes (nodes without children)
Use repository method: getLeafs().
src/Command/AppCategoryLeafCommand.php
1 2 3 4 5 6 |
/** @var ArrayCollection|Category[] $leafs */ $leafs = $repository->getLeafs($rootNode); foreach ($leafs as $node) { $io->writeln($node->getName()); } |
Run:
1 |
$ bin/console app:category:leafs |
Output:
1 2 3 4 5 6 7 8 9 |
$ bin/console app:category:leafs Enduro XC Fat Bike Road & Time Trail Components Rims Hubs Tyres |
Retrieve the depth of the nodes
stofDoctrineExtensionsBundle already calculates depth of each node and saves it in the db – so no extra work is required. Just retrieve the nodes you want and get the Category::$lvl property.
Joins between the nested set model hierarchical data and its items
Or, in other words, how to work with categories and items that belong to that categories. Sample products table and data:
src/Entity/Product.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
<?php declare(strict_types=1); namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="product") */ class Product { /** * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string") */ private $name; /** * @ORM\ManyToOne(targetEntity="Category", inversedBy="products") * @ORM\JoinColumn(nullable=false) */ private $category; /** * @return null|string */ public function getName(): ?string { return $this->name; } /** * @param string $name */ public function setName(string $name): void { $this->name = $name; } /** * @return null|string */ public function getId(): ?int { return $this->id; } /** * @return null|Category */ public function getCategory(): ?Category { return $this->category; } /** * @param Category $category */ public function setCategory(Category $category): void { $this->category = $category; } } |
1 |
$ bin/console doctrine:schema:update --force |
Number of products in each category
Goal: retrieve the number of products from each category, parent categories should display the sum of products from all its subcategories.
Using query builder
Command/AppProductCountCommandQB.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$repository = $this->entityManager->getRepository(Category::class); $qb = $repository->childrenQueryBuilder(null, false, null, 'ASC', false) ->join('App:Category', 'parent', Join::WITH, 'parent.lft <= node.lft AND parent.rgt >= node.lft') ->leftJoin('App:Product', 'item', Join::WITH, 'node = item.category') ->select('parent.id, parent.name, parent.lft, parent.rgt, parent.lvl, count(item.id) as nm') ->groupBy('parent.id') ->orderBy('parent.lft') ; $rows = $qb->getQuery()->getArrayResult(); $tree = $repository->buildTree($rows, [ 'decorate' => true, 'nodeDecorator' => function ($node) { return "$node[name] ($node[nm])"; }, ]); $io->writeln($tree); |
Run:
$ bin/console app:product:qb-count
Output (after prettifying):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<ul> <li>Home (21) <ul> <li>Bikes (9) <ul> <li>Mountain (6) <ul> <li>Enduro (2)</li> <li>XC (2)</li> <li>Fat Bike (2)</li> </ul> </li> <li>Road & Time Trail (3)</li> </ul> </li> <li>Components (4)</li> <li>Wheels & Tyres (8) <ul> <li>Rims (3)</li> <li>Hubs (2)</li> <li>Tyres (3)</li> </ul> </li> </ul> </li> </ul> |
Each category contains the sum of its products and all its children products.
Using DQL
Command/AppProductCountCommandDQL.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$dql = " SELECT parent.id, parent.name, parent.lft, parent.rgt, parent.lvl, count(item.id) as nm FROM App:Category node JOIN App:Category parent WITH (parent.lft <= node.lft AND parent.rgt >= node.lft) LEFT JOIN App:Product item WITH (node = item.category) GROUP BY parent.id ORDER BY parent.lft "; |
Run:
$ bin/console app:product:dql-count
The output is the same as above.
The query created by Doctrine and executed by MySQL is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
SELECT c0_.id AS id_0, c0_.name AS name_1, c0_.lft AS lft_2, c0_.rgt AS rgt_3, c0_.lvl AS lvl_4, count(p1_.id) AS sclr_5 FROM category c2_ INNER JOIN category c0_ ON (c0_.lft <= c2_.lft AND c0_.rgt >= c2_.lft) LEFT JOIN product p1_ ON (c2_.id = p1_.category_id) GROUP BY c0_.id ORDER BY c0_.lft ASC |
Which is the same query we wrote in Part 1:
1 2 3 4 5 6 7 8 9 10 11 12 |
SELECT parent.name as parent_name, count(product.id) FROM nested_set_category as node INNER JOIN nested_set_category as parent ON ( parent.lft <= node.lft AND parent.rgt >= node.lft ) LEFT JOIN product ON (product.category_id = node.id) GROUP BY parent.id ORDER BY parent.lft; |
Delete a node
Just remove it like any other entity and let the bundle all the work:
Command/AppCategoryDeleteCommand.php
1 2 3 4 5 6 7 8 9 10 11 12 |
$repository = $this->entityManager->getRepository(Category::class); $mountain = $repository->findOneBy(['name' => "Mountain"]); if (is_null($mountain)) { $io->note("Category \"Mountain\" not found!"); return 0; } $this->entityManager->remove($mountain); $this->entityManager->flush(); |
If you want to remind your self how deleting the node works under the hood, please visit Part 1: Delete a node.
Conclusion
This article finishes “Hierarchical Data in Relational Databases” series. We now know how to deal with hierarchical data in the efficient, elegant and smart way. And thanks to the Part 1 – we also have the understanding what is going under the hood when using StofDoctrineExtensionsBundle extension.
I love this tutorial. Don’t know why nobody commented it ;).
I even know how to use that kind of stuff, but it was awesome to read.
Especially the part with the sum of all product of children was awesome.
That was new to me. Thank you.
Hi Michael, thank you for the feedback and kind words!