Tree nested behavior will implement the standard Nested-Set behavior on your Entity. Tree supports different strategies. Currently it supports nested-set, closure-table and materialized-path. Also this behavior can be nested with other extensions to translate or generated slugs of your tree nodes.
Features:
Thanks for contributions to:
Update 2012-06-28
Update 2012-02-23
Update 2011-05-07
Update 2011-04-11
Update 2011-02-08
Update 2011-02-02
Note:
Portability:
This article will cover the basic installation and functionality of Tree behavior
Content:
Read the documentation or check the example code on how to setup and use the extensions in most optimized way.
Note: that Node interface is not necessary, except in cases there you need to identify entity as being Tree Node. The metadata is loaded only once then cache is activated
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @Gedmo\Tree(type="nested")
* @ORM\Table(name="categories")
* use repository for handy tree functions
* @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
*/
class Category
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @ORM\Column(name="title", type="string", length=64)
*/
private $title;
/**
* @Gedmo\TreeLeft
* @ORM\Column(name="lft", type="integer")
*/
private $lft;
/**
* @Gedmo\TreeLevel
* @ORM\Column(name="lvl", type="integer")
*/
private $lvl;
/**
* @Gedmo\TreeRight
* @ORM\Column(name="rgt", type="integer")
*/
private $rgt;
/**
* @Gedmo\TreeRoot
* @ORM\Column(name="root", type="integer", nullable=true)
*/
private $root;
/**
* @Gedmo\TreeParent
* @ORM\ManyToOne(targetEntity="Category", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="SET NULL")
*/
private $parent;
/**
* @ORM\OneToMany(targetEntity="Category", mappedBy="parent")
* @ORM\OrderBy({"lft" = "ASC"})
*/
private $children;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setParent(Category $parent = null)
{
$this->parent = $parent;
}
public function getParent()
{
return $this->parent;
}
}
Yaml mapped Category: /mapping/yaml/Entity.Category.dcm.yml
---
Entity\Category:
type: entity
repositoryClass: Gedmo\Tree\Entity\Repository\NestedTreeRepository
table: categories
gedmo:
tree:
type: nested
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 64
lft:
type: integer
gedmo:
- treeLeft
rgt:
type: integer
gedmo:
- treeRight
root:
type: integer
nullable: true
gedmo:
- treeRoot
lvl:
type: integer
gedmo:
- treeLevel
manyToOne:
parent:
targetEntity: Entity\Category
inversedBy: children
joinColumn:
name: parent_id
referencedColumnName: id
onDelete: SET NULL
gedmo:
- treeParent
oneToMany:
children:
targetEntity: Entity\Category
mappedBy: parent
orderBy:
lft: ASC
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
<entity name="Mapping\Fixture\Xml\NestedTree" table="nested_trees">
<indexes>
<index name="name_idx" columns="name"/>
</indexes>
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="name" type="string" length="128"/>
<field name="left" column="lft" type="integer">
<gedmo:tree-left/>
</field>
<field name="right" column="rgt" type="integer">
<gedmo:tree-right/>
</field>
<field name="root" type="integer">
<gedmo:tree-root/>
</field>
<field name="level" column="lvl" type="integer">
<gedmo:tree-level/>
</field>
<many-to-one field="parent" target-entity="NestedTree">
<join-column name="parent_id" referenced-column-name="id" on-delete="SET_NULL"/>
<gedmo:tree-parent/>
</many-to-one>
<gedmo:tree type="nested"/>
</entity>
</doctrine-mapping>
<?php
$food = new Category();
$food->setTitle('Food');
$fruits = new Category();
$fruits->setTitle('Fruits');
$fruits->setParent($food);
$vegetables = new Category();
$vegetables->setTitle('Vegetables');
$vegetables->setParent($food);
$carrots = new Category();
$carrots->setTitle('Carrots');
$carrots->setParent($vegetables);
$this->em->persist($food);
$this->em->persist($fruits);
$this->em->persist($vegetables);
$this->em->persist($carrots);
$this->em->flush();
The result after flush will generate the food tree:
/food (1-8)
/fruits (2-3)
/vegetables (4-7)
/carrots (5-6)
<?php
$repo = $em->getRepository('Entity\Category');
$food = $repo->findOneByTitle('Food');
echo $repo->childCount($food);
// prints: 3
echo $repo->childCount($food, true/*direct*/);
// prints: 2
$children = $repo->children($food);
// $children contains:
// 3 nodes
$children = $repo->children($food, false, 'title');
// will sort the children by title
$carrots = $repo->findOneByTitle('Carrots');
$path = $repo->getPath($carrots);
/* $path contains:
0 => Food
1 => Vegetables
2 => Carrots
*/
// verification and recovery of tree
$repo->verify();
// can return TRUE if tree is valid, or array of errors found on tree
$repo->recover();
$em->clear(); // clear cached nodes
// if tree has errors it will try to fix all tree nodes
UNSAFE: be sure to backup before runing this method when necessary, if you can use $em->remove($node);
// which would cascade to children
// single node removal
$vegies = $repo->findOneByTitle('Vegitables');
$repo->removeFromTree($vegies);
$em->clear(); // clear cached nodes
// it will remove this node from tree and reparent all children
// reordering the tree
$food = $repo->findOneByTitle('Food');
$repo->reorder($food, 'title');
// it will reorder all "Food" tree node left-right values by the title
<?php
$food = new Category();
$food->setTitle('Food');
$fruits = new Category();
$fruits->setTitle('Fruits');
$vegetables = new Category();
$vegetables->setTitle('Vegetables');
$carrots = new Category();
$carrots->setTitle('Carrots');
$treeRepository
->persistAsFirstChild($food)
->persistAsFirstChildOf($fruits, $food)
->persistAsLastChildOf($vegitables, $food)
->persistAsNextSiblingOf($carrots, $fruits);
$em->flush();
For more details you can check the NestedTreeRepository __call function
Moving up and down the nodes in same level:
Tree example:
/Food
/Vegitables
/Onions
/Carrots
/Cabbages
/Potatoes
/Fruits
Now move carrots up by one position
<?php
$repo = $em->getRepository('Entity\Category');
$carrots = $repo->findOneByTitle('Carrots');
// move it up by one position
$repo->moveUp($carrots, 1);
Tree after moving the Carrots up:
/Food
/Vegitables
/Carrots <- moved up
/Onions
/Cabbages
/Potatoes
/Fruits
Moving carrots down to the last position
<?php
$repo = $em->getRepository('Entity\Category');
$carrots = $repo->findOneByTitle('Carrots');
// move it down to the end
$repo->moveDown($carrots, true);
Tree after moving the Carrots down as last child:
/Food
/Vegitables
/Onions
/Cabbages
/Potatoes
/Carrots <- moved down to the end
/Fruits
Note: tree repository functions: verify, recover, removeFromTree. Will require to clear the cache of Entity Manager because left-right values will differ. So after that use $em->clear(); if you will continue using the nodes after these operations.
<?php
namespace Entity\Repository;
use Gedmo\Tree\Entity\Repository\NestedTreeRepository;
class CategoryRepository extends NestedTreeRepository
{
// your code here
}
// and then on your entity link to this repository
/**
* @Gedmo\Tree(type="nested")
* @Entity(repositoryClass="Entity\Repository\CategoryRepository")
*/
class Category implements Node
{
//...
}
If you would like to load whole tree as node array hierarchy use:
<?php
$repo = $em->getRepository('Entity\Category');
$arrayTree = $repo->childrenHierarchy();
All node children will stored under __children key for each node.
To load a tree as ul - li html tree use:
<?php
$repo = $em->getRepository('Entity\Category');
$htmlTree = $repo->childrenHierarchy(
null, /* starting from root nodes */
false, /* load all children, not only direct */
array(
'decorate' => true,
'representationField' => 'slug',
'html' => true
)
);
<?php
$repo = $em->getRepository('Entity\Category');
$options = array(
'decorate' => true,
'rootOpen' => '<ul>',
'rootClose' => '</ul>',
'childOpen' => '<li>',
'childClose' => '</li>',
'nodeDecorator' => function($node) {
return '<a href="/page/'.$node['slug'].'">'.$node[$field].'</a>';
}
);
$htmlTree = $repo->childrenHierarchy(
null, /* starting from root nodes */
false, /* load all children, not only direct */
$options
);
<?php
$repo = $em->getRepository('Entity\Category');
$query = $entityManager
->createQueryBuilder()
->select('node')
->from('Entity\Category', 'node')
->orderBy('node.root, node.lft', 'ASC')
->where('node.root = 1')
->getQuery()
;
$options = array('decorate' => true);
$tree = $repo->buildTree($query->getArrayResult(), $options);
$controller = $this;
$tree = $root->childrenHierarchy(null,false,array('decorate' => true,
'rootOpen' => function($tree) {
if(count($tree) && ($tree[0]['lvl'] == 0)){
return '<div class="catalog-list">';
}
},
'rootClose' => function($child) {
if(count($child) && ($child[0]['lvl'] == 0)){
return '</div>';
}
},
'childOpen' => '',
'childClose' => '',
'nodeDecorator' => function($node) use (&$controller) {
if($node['lvl'] == 1) {
return '<h1>'.$node['title'].'</h1>';
}elseif($node["isVisibleOnHome"]) {
return '<a href="'.$controller->generateUrl("wareTree",array("id"=>$node['id'])).'">'.$node['title'].'</a> ';
}
}
));
If you want to attach TranslationListener also add it to EventManager after the SluggableListener and TreeListener. It is important because slug must be generated first before the creation of it`s translation.
<?php
$evm = new \Doctrine\Common\EventManager();
$treeListener = new \Gedmo\Tree\TreeListener();
$evm->addEventSubscriber($treeListener);
$sluggableListener = new \Gedmo\Sluggable\SluggableListener();
$evm->addEventSubscriber($sluggableListener);
$translatableListener = new \Gedmo\Translatable\TranslationListener();
$translatableListener->setTranslatableLocale('en_us');
$evm->addEventSubscriber($translatableListener);
// now this event manager should be passed to entity manager constructor
And the Entity should look like:
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @Gedmo\Tree(type="nested")
* @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
*/
class Category
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @Gedmo\Translatable
* @Gedmo\Sluggable
* @ORM\Column(name="title", type="string", length=64)
*/
private $title;
/**
* @Gedmo\TreeLeft
* @ORM\Column(name="lft", type="integer")
*/
private $lft;
/**
* @Gedmo\TreeRight
* @ORM\Column(name="rgt", type="integer")
*/
private $rgt;
/**
* @Gedmo\TreeLevel
* @ORM\Column(name="lvl", type="integer")
*/
private $lvl;
/**
* @Gedmo\TreeParent
* @ORM\ManyToOne(targetEntity="Category", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="SET NULL")
*/
private $parent;
/**
* @ORM\OneToMany(targetEntity="Category", mappedBy="parent")
*/
private $children;
/**
* @Gedmo\Translatable
* @Gedmo\Slug
* @ORM\Column(name="slug", type="string", length=128)
*/
private $slug;
public function getId()
{
return $this->id;
}
public function getSlug()
{
return $this->slug;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setParent(Category $parent)
{
$this->parent = $parent;
}
public function getParent()
{
return $this->parent;
}
}
Yaml mapped Category: /mapping/yaml/Entity.Category.dcm.yml
---
Entity\Category:
type: entity
repositoryClass: Gedmo\Tree\Entity\Repository\NestedTreeRepository
table: categories
gedmo:
tree:
type: nested
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 64
gedmo:
- translatable
- sluggable
lft:
type: integer
gedmo:
- treeLeft
rgt:
type: integer
gedmo:
- treeRight
lvl:
type: integer
gedmo:
- treeLevel
slug:
type: string
length: 128
gedmo:
- translatable
- slug
manyToOne:
parent:
targetEntity: Entity\Category
inversedBy: children
joinColumn:
name: parent_id
referencedColumnName: id
onDelete: SET NULL
gedmo:
- treeParent
oneToMany:
children:
targetEntity: Entity\Category
mappedBy: parent
Note: that using dql without object hydration, the nodes will not be translated. Because the postLoad event never will be triggered
Now the generated treenode slug will be translated by Translatable behavior
Easy like that, any suggestions on improvements are very welcome
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\MaterializedPathRepository")
* @Gedmo\Tree(type="materializedPath")
*/
class Category
{
/**
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @Gedmo\TreePath
* @ORM\Column(name="path", type="string", length=3000, nullable=true)
*/
private $path;
/**
* @Gedmo\TreePathSource
* @ORM\Column(name="title", type="string", length=64)
*/
private $title;
/**
* @Gedmo\TreeParent
* @ORM\ManyToOne(targetEntity="Category", inversedBy="children")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="SET NULL")
* })
*/
private $parent;
/**
* @Gedmo\TreeLevel
* @ORM\Column(name="lvl", type="integer", nullable=true)
*/
private $level;
/**
* @ORM\OneToMany(targetEntity="Category", mappedBy="parent")
*/
private $children;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setParent(Category $parent = null)
{
$this->parent = $parent;
}
public function getParent()
{
return $this->parent;
}
public function setPath($path)
{
$this->path = $path;
}
public function getPath()
{
return $this->path;
}
public function getLevel()
{
return $this->level;
}
}
<?php
namespace Document;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MONGO;
/**
* @MONGO\Document(repositoryClass="Gedmo\Tree\Document\MongoDB\Repository\MaterializedPathRepository")
* @Gedmo\Tree(type="materializedPath", activateLocking=true)
*/
class Category
{
/**
* @MONGO\Id
*/
private $id;
/**
* @MONGO\Field(type="string")
* @Gedmo\TreePathSource
*/
private $title;
/**
* @MONGO\Field(type="string")
* @Gedmo\TreePath(separator="|")
*/
private $path;
/**
* @Gedmo\TreeParent
* @MONGO\ReferenceOne(targetDocument="Category")
*/
private $parent;
/**
* @Gedmo\TreeLevel
* @MONGO\Field(type="int")
*/
private $level;
/**
* @Gedmo\TreeLockTime
* @MONGO\Field(type="date")
*/
private $lockTime;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setParent(Category $parent = null)
{
$this->parent = $parent;
}
public function getParent()
{
return $this->parent;
}
public function getLevel()
{
return $this->level;
}
public function getPath()
{
return $this->path;
}
public function getLockTime()
{
return $this->lockTime;
}
}
When an entity is inserted, a path is generated using the value of the field configured as the TreePathSource. If For example:
$food = new Category();
$food->setTitle('Food');
$em->persist($food);
$em->flush();
// This would print "Food-1" assuming the id is 1.
echo $food->getPath();
$fruits = new Category();
$fruits->setTitle('Fruits');
$fruits->setParent($food);
$em->persist($fruits);
$em->flush();
// This would print "Food-1,Fruits-2" assuming that $food id is 1,
// $fruits id is 2 and separator = "," (the default value)
echo $fruits->getPath();
Why you need a locking mechanism for MongoDB? Sadly, MongoDB lacks of full transactional support, so if two or more users try to modify the same tree concurrently, it could lead to an inconsistent tree. So we've implemented a simple locking mechanism to avoid this type of problems. It works like this: As soon as a user tries to modify a node of a tree, it first check if the root node is locked (or if the current lock has expired).
If it is locked, then it throws an exception of type "Gedmo\Exception\TreeLockingException". If it's not locked, it locks the tree and proceed with the modification. After all the modifications are done, the lock is freed.
If, for some reason, the lock couldn't get freed, there's a lock timeout configured with a default time of 3 seconds. You can change this value using the lockingTimeout parameter under the Tree annotation (or equivalent in XML and YML). You must pass a value in seconds to this parameter.
To be able to use this strategy, you'll need an additional entity which represents the closures. We already provide you an abstract entity, so you'd only need to extend it.
<?php
namespace YourNamespace\Entity;
use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class CategoryClosure extends AbstractClosure
{
}
Next step, define your entity.
<?php
namespace YourNamespace\Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @Gedmo\Tree(type="closure")
* @Gedmo\TreeClosure(class="YourNamespace\Entity\CategoryClosure")
* @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\ClosureTreeRepository")
*/
class Category
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @ORM\Column(name="title", type="string", length=64)
*/
private $title;
/**
* This parameter is optional for the closure strategy
*
* @ORM\Column(name="level", type="integer", nullable=true)
* @Gedmo\TreeLevel
*/
private $level;
/**
* @Gedmo\TreeParent
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE")
* @ORM\ManyToOne(targetEntity="Category", inversedBy="children")
*/
private $parent;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setParent(Category $parent = null)
{
$this->parent = $parent;
}
public function getParent()
{
return $this->parent;
}
public function addClosure(CategoryClosure $closure)
{
$this->closures[] = $closure;
}
public function setLevel($level)
{
$this->level = $level;
}
public function getLevel()
{
return $this->level;
}
}
And that's it!
There are repository methods that are available for you in all the strategies:
This list is not complete yet. We're working on including more methods in the common API offered by repositories of all the strategies. Soon we'll be adding more helpful methods here.