Skip to content
Browse files

Merge pull request #340 from comfortablynumb/tree-features

[WIP] Tree: Some missing features for Materialized path and Closure strategies + Common Repository Api
  • Loading branch information...
2 parents 2aed6d5 + cbb2810 commit d294fe4340cf72926e0649378aba5dbf1641a5a1 @l3pp4rd committed Jul 8, 2012
Showing with 2,188 additions and 468 deletions.
  1. +157 −1 doc/tree.md
  2. +20 −0 lib/Gedmo/Exception/FeatureNotImplementedException.php
  3. +132 −2 lib/Gedmo/Tree/Document/MongoDB/Repository/AbstractTreeRepository.php
  4. +97 −38 lib/Gedmo/Tree/Document/MongoDB/Repository/MaterializedPathRepository.php
  5. +162 −2 lib/Gedmo/Tree/Entity/Repository/AbstractTreeRepository.php
  6. +184 −79 lib/Gedmo/Tree/Entity/Repository/ClosureTreeRepository.php
  7. +64 −32 lib/Gedmo/Tree/Entity/Repository/MaterializedPathRepository.php
  8. +75 −235 lib/Gedmo/Tree/Entity/Repository/NestedTreeRepository.php
  9. +58 −0 lib/Gedmo/Tree/RepositoryInterface.php
  10. +155 −0 lib/Gedmo/Tree/RepositoryUtils.php
  11. +56 −0 lib/Gedmo/Tree/RepositoryUtilsInterface.php
  12. +115 −2 lib/Gedmo/Tree/Strategy/ORM/Closure.php
  13. +4 −0 tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.ClosureTree.dcm.xml
  14. +4 −0 tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.ClosureCategory.dcm.yml
  15. +2 −0 tests/Gedmo/Mapping/Fixture/Xml/ClosureTree.php
  16. +12 −0 tests/Gedmo/Mapping/Fixture/Yaml/ClosureCategory.php
  17. +347 −32 tests/Gedmo/Tree/ClosureTreeRepositoryTest.php
  18. +35 −2 tests/Gedmo/Tree/ClosureTreeTest.php
  19. +16 −0 tests/Gedmo/Tree/Fixture/Closure/Category.php
  20. +63 −0 tests/Gedmo/Tree/Fixture/Closure/CategoryWithoutLevel.php
  21. +14 −0 tests/Gedmo/Tree/Fixture/Closure/CategoryWithoutLevelClosure.php
  22. +26 −0 tests/Gedmo/Tree/Fixture/Closure/Person.php
  23. +178 −14 tests/Gedmo/Tree/MaterializedPathODMMongoDBRepositoryTest.php
  24. +187 −28 tests/Gedmo/Tree/MaterializedPathORMRepositoryTest.php
  25. +25 −1 tests/Gedmo/Tree/NestedTreeRootRepositoryTest.php
View
158 doc/tree.md
@@ -21,10 +21,14 @@ Features:
Thanks for contributions to:
-- **[comfortablynumb](http://github.com/comfortablynumb) Gustavo Falco** for Closure strategy
+- **[comfortablynumb](http://github.com/comfortablynumb) Gustavo Falco** for Closure and Materialized Path strategy
- **[everzet](http://github.com/everzet) Kudryashov Konstantin** for TreeLevel implementation
- **[stof](http://github.com/stof) Christophe Coevoet** for getTreeLeafs function
+Update **2012-06-28**
+
+- Added "buildTree" functionality support for Closure and Materialized Path strategies
+
Update **2012-02-23**
- Added a new strategy to support the "Materialized Path" tree model. It works with ODM (MongoDB) and ORM.
@@ -79,6 +83,8 @@ Content:
- Build [html tree](#html-tree)
- Advanced usage [examples](#advanced-examples)
- [Materialized Path](#materialized-path)
+- [Closure Table](#closure-table)
+- [Repository methods (all strategies)](#repository-methods)
<a name="including-extension"></a>
@@ -1045,3 +1051,153 @@ it locks the tree and proceed with the modification. After all the modifications
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.
+
+
+<a name="closure-table"></a>
+
+## Closure Table
+
+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.
+
+### Closure Entity
+
+``` php
+<?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.
+
+### ORM Entity example (Annotations)
+
+``` php
+<?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!
+
+
+<a name="repository-methods"></a>
+
+## Repository Methods (All strategies)
+
+There are repository methods that are available for you in all the strategies:
+
+* **getRootNodes** / **getRootNodesQuery** / **getRootNodesQueryBuilder**: Returns an array with the available root nodes. Arguments:
+ - *sortByField*: An optional field to order the root nodes. Defaults to "null".
+ - *direction*: In case the first argument is used, you can pass the direction here: "asc" or "desc". Defaults to "asc".
+* **getChildren** / **getChildrenQuery** / **getChildrenQueryBuilder**: Returns an array of children nodes. Arguments:
+ - *node*: If you pass a node, the method will return its children. Defaults to "null" (this means it will return ALL nodes).
+ - *direct*: If you pass true as a value for this argument, you'll get only the direct children of the node
+ (or only the root nodes if you pass "null" to the "node" argument).
+ - *sortByField*: An optional field to sort the children. Defaults to "null".
+ - *direction*: If you use the "sortByField" argument, this allows you to set the direction: "asc" or "desc". Defaults to "asc".
+ - *includeNode*: Using "true", this argument allows you to include in the result the node you passed as the first argument. Defaults to "false".
+* **childrenHierarchy**: This useful method allows you to build an array of nodes representing the hierarchy of a tree. Arguments:
+ - *node*: If you pass a node, the method will return its children. Defaults to "null" (this means it will return ALL nodes).
+ - *direct*: If you pass true as a value for this argument, you'll get only the direct children of the node
+ - *options*: An array of options that allows you to decorate the results with HTML. Available options:
+ * decorate: boolean (false) - retrieves tree as UL->LI tree
+ * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
+ * rootOpen: string || Closure ('\<ul\>') - branch start, closure will be given $children as a parameter
+ * rootClose: string ('\</ul\>') - branch close
+ * childStart: string || Closure ('\<li\>') - start of node, closure will be given $node as a parameter
+ * childClose: string ('\</li\>') - close of node
+ * childSort: array || keys allowed: field: field to sort on, dir: direction. 'asc' or 'desc'
+ - *includeNode*: Using "true", this argument allows you to include in the result the node you passed as the first argument. Defaults to "false".
+
+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.
View
20 lib/Gedmo/Exception/FeatureNotImplementedException.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Gedmo\Exception;
+
+use Gedmo\Exception;
+
+/**
+ * FeatureNotImplementedException
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.Exception
+ * @subpackage FeatureNotImplementedException
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class FeatureNotImplementedException
+ extends \RuntimeException
+ implements Exception
+{}
View
134 lib/Gedmo/Tree/Document/MongoDB/Repository/AbstractTreeRepository.php
@@ -5,9 +5,12 @@
use Doctrine\ODM\MongoDB\DocumentRepository,
Doctrine\ODM\MongoDB\DocumentManager,
Doctrine\ODM\MongoDB\Mapping\ClassMetadata,
- Doctrine\ODM\MongoDB\UnitOfWork;
+ Doctrine\ODM\MongoDB\UnitOfWork,
+ Gedmo\Tree\RepositoryUtils,
+ Gedmo\Tree\RepositoryUtilsInterface,
+ Gedmo\Tree\RepositoryInterface;
-abstract class AbstractTreeRepository extends DocumentRepository
+abstract class AbstractTreeRepository extends DocumentRepository implements RepositoryInterface
{
/**
* Tree listener on event manager
@@ -17,6 +20,11 @@
protected $listener = null;
/**
+ * Repository utils
+ */
+ protected $repoUtils = null;
+
+ /**
* {@inheritdoc}
*/
public function __construct(DocumentManager $em, UnitOfWork $uow, ClassMetadata $class)
@@ -43,6 +51,56 @@ public function __construct(DocumentManager $em, UnitOfWork $uow, ClassMetadata
if (!$this->validate()) {
throw new \Gedmo\Exception\InvalidMappingException('This repository cannot be used for tree type: ' . $treeListener->getStrategy($em, $class->name)->getName());
}
+
+ $this->repoUtils = new RepositoryUtils($this->dm, $this->getClassMetadata(), $this->listener, $this);
+ }
+
+ /**
+ * Sets the RepositoryUtilsInterface instance
+ *
+ * @param \Gedmo\Tree\RepositoryUtilsInterface $repoUtils
+ *
+ * @return $this
+ */
+ public function setRepoUtils(RepositoryUtilsInterface $repoUtils)
+ {
+ $this->repoUtils = $repoUtils;
+
+ return $this;
+ }
+
+ /**
+ * Returns the RepositoryUtilsInterface instance
+ *
+ * @return \Gedmo\Tree\RepositoryUtilsInterface|null
+ */
+ public function getRepoUtils()
+ {
+ return $this->repoUtils;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function childrenHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false)
+ {
+ return $this->repoUtils->childrenHierarchy($node, $direct, $options, $includeNode);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function buildTree(array $nodes, array $options = array())
+ {
+ return $this->repoUtils->buildTree($nodes, $options);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function buildTreeArray(array $nodes)
+ {
+ return $this->repoUtils->buildTreeArray($nodes);
}
/**
@@ -52,4 +110,76 @@ public function __construct(DocumentManager $em, UnitOfWork $uow, ClassMetadata
* @return bool
*/
abstract protected function validate();
+
+ /**
+ * Get all root nodes query builder
+ *
+ * @param string - Sort by field
+ * @param string - Sort direction ("asc" or "desc")
+ *
+ * @return \Doctrine\MongoDB\Query\Builder - QueryBuilder object
+ */
+ abstract public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc');
+
+ /**
+ * Get all root nodes query
+ *
+ * @param string - Sort by field
+ * @param string - Sort direction ("asc" or "desc")
+ *
+ * @return \Doctrine\MongoDB\Query\Query - Query object
+ */
+ abstract public function getRootNodesQuery($sortByField = null, $direction = 'asc');
+
+ /**
+ * Returns a QueryBuilder configured to return an array of nodes suitable for buildTree method
+ *
+ * @param object $node - Root node
+ * @param bool $direct - Obtain direct children?
+ * @param array $config - Metadata configuration
+ * @param array $options - Options
+ * @param boolean $includeNode - Include node in results?
+ *
+ * @return \Doctrine\MongoDB\Query\Builder - QueryBuilder object
+ */
+ abstract public function getNodesHierarchyQueryBuilder($node = null, $direct, array $config, array $options = array(), $includeNode = false);
+
+ /**
+ * Returns a Query configured to return an array of nodes suitable for buildTree method
+ *
+ * @param object $node - Root node
+ * @param bool $direct - Obtain direct children?
+ * @param array $config - Metadata configuration
+ * @param array $options - Options
+ * @param boolean $includeNode - Include node in results?
+ *
+ * @return \Doctrine\MongoDB\Query\Query - Query object
+ */
+ abstract public function getNodesHierarchyQuery($node = null, $direct, array $config, array $options = array(), $includeNode = false);
+
+ /**
+ * Get list of children followed by given $node. This returns a QueryBuilder object
+ *
+ * @param object $node - if null, all tree nodes will be taken
+ * @param boolean $direct - true to take only direct children
+ * @param string $sortByField - field name to sort by
+ * @param string $direction - sort direction : "ASC" or "DESC"
+ * @param bool $includeNode - Include the root node in results?
+ *
+ * @return \Doctrine\MongoDB\Query\Builder - QueryBuilder object
+ */
+ abstract public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false);
+
+ /**
+ * Get list of children followed by given $node. This returns a Query
+ *
+ * @param object $node - if null, all tree nodes will be taken
+ * @param boolean $direct - true to take only direct children
+ * @param string $sortByField - field name to sort by
+ * @param string $direction - sort direction : "ASC" or "DESC"
+ * @param bool $includeNode - Include the root node in results?
+ *
+ * @return \Doctrine\MongoDB\Query\Query - Query object
+ */
+ abstract public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false);
}
View
135 lib/Gedmo/Tree/Document/MongoDB/Repository/MaterializedPathRepository.php
@@ -24,96 +24,123 @@ class MaterializedPathRepository extends AbstractTreeRepository
/**
* Get tree query builder
*
- * @return Doctrine\ODM\MongoDB\QueryBuilder
+ * @param object Root node
+ *
+ * @return \Doctrine\ODM\MongoDB\Query\Builder
*/
- public function getTreeQueryBuilder()
+ public function getTreeQueryBuilder($rootNode = null)
{
- return $this->getChildrenQueryBuilder();
+ return $this->getChildrenQueryBuilder($rootNode, false, null, 'asc', true);
}
/**
* Get tree query
*
- * @return Doctrine\ODM\MongoDB\Query\Query
+ * @param object Root node
+ *
+ * @return \Doctrine\ODM\MongoDB\Query\Query
*/
- public function getTreeQuery()
+ public function getTreeQuery($rootNode = null)
{
- return $this->getTreeQueryBuilder()->getQuery();
+ return $this->getTreeQueryBuilder($rootNode)->getQuery();
}
/**
* Get tree
*
- * @return Doctrine\ODM\MongoDB\Cursor
+ * @param object Root node
+ *
+ * @return \Doctrine\ODM\MongoDB\Cursor
*/
- public function getTree()
+ public function getTree($rootNode = null)
{
- return $this->getTreeQuery()->execute();
+ return $this->getTreeQuery($rootNode)->execute();
}
/**
- * Get all root nodes query builder
- *
- * @return Doctrine\ODM\MongoDB\QueryBuilder
+ * {@inheritDoc}
*/
public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc')
{
return $this->getChildrenQueryBuilder(null, true, $sortByField, $direction);
}
/**
- * Get all root nodes query
- *
- * @return Doctrine\ODM\MongoDB\Query\Query
+ * {@inheritDoc}
*/
public function getRootNodesQuery($sortByField = null, $direction = 'asc')
{
return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery();
}
/**
- * Get all root nodes
- *
- * @return Doctrine\ODM\MongoDB\Cursor
+ * {@inheritDoc}
*/
public function getRootNodes($sortByField = null, $direction = 'asc')
{
return $this->getRootNodesQuery($sortByField, $direction)->execute();
}
/**
- * Get children from node
- *
- * @return Doctrine\ODM\MongoDB\QueryBuilder
+ * {@inheritDoc}
+ */
+ public function childCount($node = null, $direct = false)
+ {
+ $meta = $this->getClassMetadata();
+
+ if (is_object($node)) {
+ if (!($node instanceof $meta->name)) {
+ throw new InvalidArgumentException("Node is not related to this repository");
+ }
+
+ $wrapped = new MongoDocumentWrapper($node, $this->dm);
+
+ if (!$wrapped->hasValidIdentifier()) {
+ throw new InvalidArgumentException("Node is not managed by UnitOfWork");
+ }
+ }
+
+ $qb = $this->getChildrenQueryBuilder($node, $direct);
+
+ $qb->count();
+
+ return (int) $qb->getQuery()->execute();
+ }
+
+ /**
+ * {@inheritDoc}
*/
- public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'asc')
+ public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false)
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->dm, $meta->name);
$separator = preg_quote($config['path_separator']);
$qb = $this->dm->createQueryBuilder()
->find($meta->name);
+ $regex = false;
if (is_object($node) && $node instanceof $meta->name) {
$node = new MongoDocumentWrapper($node, $this->dm);
$nodePath = preg_quote($node->getPropertyValue($config['path']));
if ($direct) {
- $regex = sprintf('/^%s[^%s]+%s$/',
+ $regex = sprintf('/^%s([^%s]+%s)'.($includeNode ? '?' : '').'$/',
$nodePath,
$separator,
$separator);
} else {
- $regex = sprintf('/^%s.+/',
+ $regex = sprintf('/^%s(.+)'.($includeNode ? '?' : '').'/',
$nodePath);
}
-
- $qb->field($config['path'])->equals(new \MongoRegex($regex));
} else if ($direct) {
- $qb->field($config['path'])->equals(new \MongoRegex(sprintf('/^[^%s]+%s$/',
+ $regex = sprintf('/^([^%s]+)'.($includeNode ? '?' : '').'%s$/',
$separator,
- $separator)));
+ $separator);
+ }
+
+ if ($regex) {
+ $qb->field($config['path'])->equals(new \MongoRegex($regex));
}
$qb->sort(is_null($sortByField) ? $config['path'] : $sortByField, $direction === 'asc' ? 'asc' : 'desc');
@@ -122,23 +149,55 @@ public function getChildrenQueryBuilder($node = null, $direct = false, $sortByFi
}
/**
- * Get children query
- *
- * @return Doctrine\ODM\MongoDB\Query\Query
+ * G{@inheritDoc}
*/
- public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'asc')
+ public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false)
{
- return $this->getChildrenQueryBuilder($node, $direct, $sortByField, $direction)->getQuery();
+ return $this->getChildrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery();
}
/**
- * Get children
- *
- * @return Doctrine\ODM\MongoDB\Cursor
+ * {@inheritDoc}
+ */
+ public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false)
+ {
+ return $this->getChildrenQuery($node, $direct, $sortByField, $direction, $includeNode)->execute();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getNodesHierarchyQueryBuilder($node = null, $direct, array $config, array $options = array(), $includeNode = false)
+ {
+ $sortBy = array(
+ 'field' => null,
+ 'dir' => 'asc'
+ );
+
+ if (isset($options['childSort'])) {
+ $sortBy = array_merge($sortBy, $options['childSort']);
+ }
+
+ return $this->getChildrenQueryBuilder($node, $direct, $sortBy['field'], $sortBy['dir'], $includeNode);
+ }
+
+ /**
+ * {@inheritDoc}
*/
- public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'asc')
+ public function getNodesHierarchyQuery($node = null, $direct, array $config, array $options = array(), $includeNode = false)
{
- return $this->getChildrenQuery($node, $direct, $sortByField, $direction)->execute();
+ return $this->getNodesHierarchyQueryBuilder($node, $direct, $config, $options, $includeNode)->getQuery();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getNodesHierarchy($node = null, $direct, array $config, array $options = array(), $includeNode = false)
+ {
+ $query = $this->getNodesHierarchyQuery($node, $direct, $config, $options, $includeNode);
+ $query->setHydrate(false);
+
+ return $query->toArray();
}
/**
View
164 lib/Gedmo/Tree/Entity/Repository/AbstractTreeRepository.php
@@ -4,9 +4,14 @@
use Doctrine\ORM\EntityRepository,
Doctrine\ORM\EntityManager,
- Doctrine\ORM\Mapping\ClassMetadata;
+ Doctrine\ORM\Mapping\ClassMetadata,
+ Gedmo\Tool\Wrapper\EntityWrapper,
+ Gedmo\Tree\RepositoryUtils,
+ Gedmo\Tree\RepositoryUtilsInterface,
+ Gedmo\Tree\RepositoryInterface,
+ Gedmo\Exception\InvalidArgumentException;
-abstract class AbstractTreeRepository extends EntityRepository
+abstract class AbstractTreeRepository extends EntityRepository implements RepositoryInterface
{
/**
* Tree listener on event manager
@@ -16,6 +21,11 @@
protected $listener = null;
/**
+ * Repository utils
+ */
+ protected $repoUtils = null;
+
+ /**
* {@inheritdoc}
*/
public function __construct(EntityManager $em, ClassMetadata $class)
@@ -42,6 +52,84 @@ public function __construct(EntityManager $em, ClassMetadata $class)
if (!$this->validate()) {
throw new \Gedmo\Exception\InvalidMappingException('This repository cannot be used for tree type: ' . $treeListener->getStrategy($em, $class->name)->getName());
}
+
+ $this->repoUtils = new RepositoryUtils($this->_em, $this->getClassMetadata(), $this->listener, $this);
+ }
+
+ /**
+ * Sets the RepositoryUtilsInterface instance
+ *
+ * @param \Gedmo\Tree\RepositoryUtilsInterface $repoUtils
+ *
+ * @return $this
+ */
+ public function setRepoUtils(RepositoryUtilsInterface $repoUtils)
+ {
+ $this->repoUtils = $repoUtils;
+
+ return $this;
+ }
+
+ /**
+ * Returns the RepositoryUtilsInterface instance
+ *
+ * @return \Gedmo\Tree\RepositoryUtilsInterface|null
+ */
+ public function getRepoUtils()
+ {
+ return $this->repoUtils;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function childCount($node = null, $direct = false)
+ {
+ $meta = $this->getClassMetadata();
+
+ if (is_object($node)) {
+ if (!($node instanceof $meta->name)) {
+ throw new InvalidArgumentException("Node is not related to this repository");
+ }
+
+ $wrapped = new EntityWrapper($node, $this->_em);
+
+ if (!$wrapped->hasValidIdentifier()) {
+ throw new InvalidArgumentException("Node is not managed by UnitOfWork");
+ }
+ }
+
+ $qb = $this->getChildrenQueryBuilder($node, $direct);
+ $aliases = $qb->getRootAliases();
+ $alias = $aliases[0];
+
+ $qb->select('COUNT('.$alias.')');
+
+ return (int) $qb->getQuery()->getSingleScalarResult();
+ }
+
+ /**
+ * @see \Gedmo\Tree\RepositoryUtilsInterface::childrenHierarchy
+ */
+ public function childrenHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false)
+ {
+ return $this->repoUtils->childrenHierarchy($node, $direct, $options, $includeNode);
+ }
+
+ /**
+ * @see \Gedmo\Tree\RepositoryUtilsInterface::buildTree
+ */
+ public function buildTree(array $nodes, array $options = array())
+ {
+ return $this->repoUtils->buildTree($nodes, $options);
+ }
+
+ /**
+ * @see \Gedmo\Tree\RepositoryUtilsInterface::buildTreeArray
+ */
+ public function buildTreeArray(array $nodes)
+ {
+ return $this->repoUtils->buildTreeArray($nodes);
}
/**
@@ -51,4 +139,76 @@ public function __construct(EntityManager $em, ClassMetadata $class)
* @return bool
*/
abstract protected function validate();
+
+ /**
+ * Get all root nodes query builder
+ *
+ * @param string - Sort by field
+ * @param string - Sort direction ("asc" or "desc")
+ *
+ * @return \Doctrine\ORM\QueryBuilder - QueryBuilder object
+ */
+ abstract public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc');
+
+ /**
+ * Get all root nodes query
+ *
+ * @param string - Sort by field
+ * @param string - Sort direction ("asc" or "desc")
+ *
+ * @return \Doctrine\ORM\Query - Query object
+ */
+ abstract public function getRootNodesQuery($sortByField = null, $direction = 'asc');
+
+ /**
+ * Returns a QueryBuilder configured to return an array of nodes suitable for buildTree method
+ *
+ * @param object $node - Root node
+ * @param bool $direct - Obtain direct children?
+ * @param array $config - Metadata configuration
+ * @param array $options - Options
+ * @param boolean $includeNode - Include node in results?
+ *
+ * @return \Doctrine\ORM\QueryBuilder - QueryBuilder object
+ */
+ abstract public function getNodesHierarchyQueryBuilder($node = null, $direct, array $config, array $options = array(), $includeNode = false);
+
+ /**
+ * Returns a Query configured to return an array of nodes suitable for buildTree method
+ *
+ * @param object $node - Root node
+ * @param bool $direct - Obtain direct children?
+ * @param array $config - Metadata configuration
+ * @param array $options - Options
+ * @param boolean $includeNode - Include node in results?
+ *
+ * @return \Doctrine\ORM\Query - Query object
+ */
+ abstract public function getNodesHierarchyQuery($node = null, $direct, array $config, array $options = array(), $includeNode = false);
+
+ /**
+ * Get list of children followed by given $node. This returns a QueryBuilder object
+ *
+ * @param object $node - if null, all tree nodes will be taken
+ * @param boolean $direct - true to take only direct children
+ * @param string $sortByField - field name to sort by
+ * @param string $direction - sort direction : "ASC" or "DESC"
+ * @param bool $includeNode - Include the root node in results?
+ *
+ * @return \Doctrine\ORM\QueryBuilder - QueryBuilder object
+ */
+ abstract public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false);
+
+ /**
+ * Get list of children followed by given $node. This returns a Query
+ *
+ * @param object $node - if null, all tree nodes will be taken
+ * @param boolean $direct - true to take only direct children
+ * @param string $sortByField - field name to sort by
+ * @param string $direction - sort direction : "ASC" or "DESC"
+ * @param bool $includeNode - Include the root node in results?
+ *
+ * @return \Doctrine\ORM\Query - Query object
+ */
+ abstract public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false);
}
View
263 lib/Gedmo/Tree/Entity/Repository/ClosureTreeRepository.php
@@ -23,78 +23,42 @@
*/
class ClosureTreeRepository extends AbstractTreeRepository
{
+ /** Alias for the level value used in the subquery of the getNodesHierarchy method */
+ const SUBQUERY_LEVEL = 'level';
+
/**
- * Get all root nodes query
- *
- * @return Query
+ * {@inheritDoc}
*/
- public function getRootNodesQuery()
+ public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc')
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->_em, $meta->name);
$qb = $this->_em->createQueryBuilder();
$qb->select('node')
->from($config['useObjectClass'], 'node')
->where('node.' . $config['parent'] . " IS NULL");
- return $qb->getQuery();
+
+ if ($sortByField) {
+ $qb->orderBy($sortByField, strtolower($direction) === 'asc' ? 'asc' : 'desc');
+ }
+
+ return $qb;
}
/**
- * Get all root nodes
- *
- * @return array
+ * {@inheritDoc}
*/
- public function getRootNodes()
+ public function getRootNodesQuery($sortByField = null, $direction = 'asc')
{
- return $this->getRootNodesQuery()->getResult();
+ return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery();
}
/**
- * Counts the children of given TreeNode
- *
- * @param object $node - if null counts all records in tree
- * @param boolean $direct - true to count only direct children
- * @throws InvalidArgumentException - if input is not valid
- * @return integer
+ * {@inheritDoc}
*/
- public function childCount($node = null, $direct = false)
+ public function getRootNodes($sortByField = null, $direction = 'asc')
{
- $count = 0;
- $meta = $this->getClassMetadata();
- $config = $this->listener->getConfiguration($this->_em, $meta->name);
- if (null !== $node) {
- if ($node instanceof $meta->name) {
- if (!$this->_em->getUnitOfWork()->isInIdentityMap($node)) {
- throw new InvalidArgumentException("Node is not managed by UnitOfWork");
- }
- if ($direct) {
- $qb = $this->_em->createQueryBuilder();
- $qb->select('COUNT(node)')
- ->from($config['useObjectClass'], 'node')
- ->where('node.' . $config['parent'] . ' = :node');
-
- $q = $qb->getQuery();
- } else {
- $closureMeta = $this->_em->getClassMetadata($config['closure']);
- $dql = "SELECT COUNT(c) FROM {$closureMeta->name} c";
- $dql .= " WHERE c.ancestor = :node";
- $dql .= " AND c.descendant <> :node";
- $q = $this->_em->createQuery($dql);
- }
- $q->setParameters(compact('node'));
- $count = intval($q->getSingleScalarResult());
- } else {
- throw new InvalidArgumentException("Node is not related to this repository");
- }
- } else {
- $dql = "SELECT COUNT(node) FROM " . $config['useObjectClass'] . " node";
- if ($direct) {
- $dql .= ' WHERE node.' . $config['parent'] . ' IS NULL';
- }
- $q = $this->_em->createQuery($dql);
- $count = intval($q->getSingleScalarResult());
- }
- return $count;
+ return $this->getRootNodesQuery($sortByField, $direction)->getResult();
}
/**
@@ -139,16 +103,9 @@ public function getPath($node)
}
/**
- * Get tree children query followed by given $node
- *
- * @param object $node - if null, all tree nodes will be taken
- * @param boolean $direct - true to take only direct children
- * @param string $sortByField - field name to sort by
- * @param string $direction - sort direction : "ASC" or "DESC"
- * @throws InvalidArgumentException - if input is not valid
- * @return Query
+ * @see getChildrenQueryBuilder
*/
- public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC')
+ public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->_em, $meta->name);
@@ -159,14 +116,23 @@ public function childrenQuery($node = null, $direct = false, $sortByField = null
if (!$this->_em->getUnitOfWork()->isInIdentityMap($node)) {
throw new InvalidArgumentException("Node is not managed by UnitOfWork");
}
+
+ $where = 'c.ancestor = :node AND ';
+
$qb->select('c, node')
->from($config['closure'], 'c')
- ->innerJoin('c.descendant', 'node')
- ->where('c.ancestor = :node');
+ ->innerJoin('c.descendant', 'node');
+
if ($direct) {
- $qb->andWhere('c.depth = 1');
+ $where .= 'c.depth = 1';
} else {
- $qb->andWhere('c.descendant <> :node');
+ $where .= 'c.descendant <> :node';
+ }
+
+ $qb->where($where);
+
+ if ($includeNode) {
+ $qb->orWhere('c.ancestor = :node AND c.descendant = :node');
}
} else {
throw new \InvalidArgumentException("Node is not related to this repository");
@@ -178,32 +144,36 @@ public function childrenQuery($node = null, $direct = false, $sortByField = null
$qb->where('node.' . $config['parent'] . ' IS NULL');
}
}
+
if ($sortByField) {
if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) {
$qb->orderBy('node.' . $sortByField, $direction);
} else {
throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}");
}
}
- $q = $qb->getQuery();
+
if ($node) {
- $q->setParameters(compact('node'));
+ $qb->setParameter('node', $node);
}
- return $q;
+
+ return $qb;
}
/**
- * Get list of children followed by given $node
- *
- * @param object $node - if null, all tree nodes will be taken
- * @param boolean $direct - true to take only direct children
- * @param string $sortByField - field name to sort by
- * @param string $direction - sort direction : "ASC" or "DESC"
- * @return array - list of given $node children, null on failure
+ * @see getChildrenQuery
*/
- public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC')
+ public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
- $result = $this->childrenQuery($node, $direct, $sortByField, $direction)->getResult();
+ return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery();
+ }
+
+ /**
+ * @see getChildren
+ */
+ public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
+ {
+ $result = $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode)->getResult();
if ($node) {
$result = array_map(function($closure) {
return $closure->getDescendant();
@@ -213,6 +183,30 @@ public function children($node = null, $direct = false, $sortByField = null, $di
}
/**
+ * {@inheritDoc}
+ */
+ public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
+ {
+ return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
+ {
+ return $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
+ {
+ return $this->children($node, $direct, $sortByField, $direction, $includeNode);
+ }
+
+ /**
* Removes given $node from the tree and reparents its descendants
*
* @todo: may be improved, to issue single query on reparenting
@@ -273,14 +267,125 @@ public function removeFromTree($node)
} catch (\Exception $e) {
$this->_em->close();
$this->_em->getConnection()->rollback();
- throw new \Gedmo\Exception\RuntimeException('Transaction failed', null, $e);
+ throw new \Gedmo\Exception\RuntimeException('Transaction failed: '.$e->getMessage(), null, $e);
}
// remove from identity map
$this->_em->getUnitOfWork()->removeFromIdentityMap($node);
$node = null;
}
/**
+ * Process nodes and produce an array with the
+ * structure of the tree
+ *
+ * @param array - Array of nodes
+ *
+ * @return array - Array with tree structure
+ */
+ public function buildTreeArray(array $nodes)
+ {
+ $meta = $this->getClassMetadata();
+ $config = $this->listener->getConfiguration($this->_em, $meta->name);
+ $nestedTree = array();
+ $idField = $meta->getSingleIdentifierFieldName();
+ $hasLevelProp = !empty($config['level']);
+ $levelProp = $hasLevelProp ? $config['level'] : self::SUBQUERY_LEVEL;
+
+ if (count($nodes) > 0) {
+ $firstLevel = $hasLevelProp ? $nodes[0][0]['descendant'][$levelProp] : $nodes[0][$levelProp];
+ $l = 1; // 1 is only an initial value. We could have a tree which has a root node with any level (subtrees)
+ $refs = array();
+
+ foreach ($nodes as $n) {
+ $node = $n[0]['descendant'];
+ $node['__children'] = array();
+ $level = $hasLevelProp ? $node[$levelProp] : $n[$levelProp];
+
+ if ($l < $level) {
+ $l = $level;
+ }
+
+ if ($l == $firstLevel) {
+ $tmp = &$nestedTree;
+ } else {
+ $tmp = &$refs[$n['parent_id']]['__children'];
+ }
+
+ $key = count($tmp);
+ $tmp[$key] = $node;
+ $refs[$node[$idField]] = &$tmp[$key];
+ }
+
+ unset($refs);
+ }
+
+ return $nestedTree;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNodesHierarchy($node = null, $direct, array $config, array $options = array(), $includeNode = false)
+ {
+ return $this->getNodesHierarchyQuery($node, $direct, $config, $options, $includeNode)->getArrayResult();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNodesHierarchyQuery($node = null, $direct, array $config, array $options = array(), $includeNode = false)
+ {
+ return $this->getNodesHierarchyQueryBuilder($node, $direct, $config, $options, $includeNode)->getQuery();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNodesHierarchyQueryBuilder($node = null, $direct, array $config, array $options = array(), $includeNode = false)
+ {
+ $meta = $this->getClassMetadata();
+ $idField = $meta->getSingleIdentifierFieldName();
+ $subQuery = '';
+ $hasLevelProp = isset($config['level']) && $config['level'];
+
+ if (!$hasLevelProp) {
+ $subQuery = ', (SELECT MAX(c2.depth) + 1 FROM '.$config['closure'];
+ $subQuery .= ' c2 WHERE c2.descendant = c.descendant GROUP BY c2.descendant) AS '.self::SUBQUERY_LEVEL;
+ }
+
+ $q = $this->_em->createQueryBuilder()
+ ->select('c, node, p.'.$idField.' AS parent_id'.$subQuery)
+ ->from($config['closure'], 'c')
+ ->innerJoin('c.descendant', 'node')
+ ->leftJoin('node.parent', 'p')
+ ->addOrderBy(($hasLevelProp ? 'node.'.$config['level'] : self::SUBQUERY_LEVEL), 'asc');
+
+ if ($node !== null) {
+ $q->where('c.ancestor = :node');
+ $q->setParameters(compact('node'));
+ } else {
+ $q->groupBy('c.descendant');
+ }
+
+ if (!$includeNode) {
+ $q->andWhere('c.ancestor != c.descendant');
+ }
+
+ $defaultOptions = array();
+ $options = array_merge($defaultOptions, $options);
+
+ if (isset($options['childSort']) && is_array($options['childSort']) &&
+ isset($options['childSort']['field']) && isset($options['childSort']['dir'])) {
+ $q->addOrderBy(
+ 'node.'.$options['childSort']['field'],
+ strtolower($options['childSort']['dir']) == 'asc' ? 'asc' : 'desc'
+ );
+ }
+
+ return $q;
+ }
+
+ /**
* {@inheritdoc}
*/
protected function validate()
View
96 lib/Gedmo/Tree/Entity/Repository/MaterializedPathRepository.php
@@ -24,69 +24,67 @@ class MaterializedPathRepository extends AbstractTreeRepository
/**
* Get tree query builder
*
+ * @param object Root node
+ *
* @return Doctrine\ORM\QueryBuilder
*/
- public function getTreeQueryBuilder()
+ public function getTreeQueryBuilder($rootNode = null)
{
- return $this->getChildrenQueryBuilder();
+ return $this->getChildrenQueryBuilder($rootNode, false, null, 'asc', true);
}
/**
* Get tree query
*
+ * @param object Root node
+ *
* @return Doctrine\ORM\Query
*/
- public function getTreeQuery()
+ public function getTreeQuery($rootNode = null)
{
- return $this->getTreeQueryBuilder()->getQuery();
+ return $this->getTreeQueryBuilder($rootNode)->getQuery();
}
/**
* Get tree
*
+ * @param object Root node
+ *
* @return array
*/
- public function getTree()
+ public function getTree($rootNode = null)
{
- return $this->getTreeQuery()->execute();
+ return $this->getTreeQuery($rootNode)->execute();
}
/**
- * Get all root nodes query builder
- *
- * @return Doctrine\ORM\QueryBuilder
+ * {@inheritDoc}
*/
public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc')
{
return $this->getChildrenQueryBuilder(null, true, $sortByField, $direction);
}
/**
- * Get all root nodes query
- *
- * @return Doctrine\ORM\Query
+ * {@inheritDoc}
*/
public function getRootNodesQuery($sortByField = null, $direction = 'asc')
{
return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery();
}
/**
- * Get all root nodes
- *
- * @return array
+ * {@inheritDoc}
*/
public function getRootNodes($sortByField = null, $direction = 'asc')
{
return $this->getRootNodesQuery($sortByField, $direction)->execute();
}
/**
- * Get children from node
- *
- * @return Doctrine\ORM\QueryBuilder
+ * {@inheritDoc}
*/
- public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'asc')
+ public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false)
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->_em, $meta->name);
@@ -96,27 +94,32 @@ public function getChildrenQueryBuilder($node = null, $direct = false, $sortByFi
$qb = $this->_em->createQueryBuilder($meta->name)
->select($alias)
->from($meta->name, $alias);
+ $expr = '';
if (is_object($node) && $node instanceof $meta->name) {
$node = new EntityWrapper($node, $this->_em);
$nodePath = $node->getPropertyValue($path);
$expr = $qb->expr()->andx()->add(
$qb->expr()->like($alias.'.'.$path, $qb->expr()->literal($nodePath.'%'))
);
- $expr->add($qb->expr()->neq($alias.'.'.$path, $qb->expr()->literal($nodePath)));
+
+ if (!$includeNode) {
+ $expr->add($qb->expr()->neq($alias.'.'.$path, $qb->expr()->literal($nodePath)));
+ }
if ($direct) {
$expr->add(
$qb->expr()->not(
$qb->expr()->like($alias.'.'.$path, $qb->expr()->literal($nodePath.'%'.$separator.'%'.$separator))
));
}
-
- $qb->where('('.$expr.')');
} else if ($direct) {
$expr = $qb->expr()->not(
$qb->expr()->like($alias.'.'.$path, $qb->expr()->literal('%'.$separator.'%'.$separator.'%'))
);
+ }
+
+ if ($expr) {
$qb->where('('.$expr.')');
}
@@ -128,23 +131,52 @@ public function getChildrenQueryBuilder($node = null, $direct = false, $sortByFi
}
/**
- * Get children query
- *
- * @return Doctrine\ORM\Query
+ * {@inheritDoc}
*/
- public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'asc')
+ public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false)
{
- return $this->getChildrenQueryBuilder($node, $direct, $sortByField, $direction)->getQuery();
+ return $this->getChildrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery();
}
/**
- * Get children
- *
- * @return array
+ * {@inheritDoc}
+ */
+ public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false)
+ {
+ return $this->getChildrenQuery($node, $direct, $sortByField, $direction, $includeNode)->execute();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNodesHierarchyQueryBuilder($node = null, $direct, array $config, array $options = array(), $includeNode = false)
+ {
+ $sortBy = array(
+ 'field' => null,
+ 'dir' => 'asc'
+ );
+
+ if (isset($options['childSort'])) {
+ $sortBy = array_merge($sortBy, $options['childSort']);
+ }
+
+ return $this->getChildrenQueryBuilder($node, $direct, $sortBy['field'], $sortBy['dir'], $includeNode);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNodesHierarchyQuery($node = null, $direct, array $config, array $options = array(), $includeNode = false)
+ {
+ return $this->getNodesHierarchyQueryBuilder($node, $direct, $config, $options, $includeNode)->getQuery();
+ }
+
+ /**
+ * {@inheritdoc}
*/
- public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'asc')
+ public function getNodesHierarchy($node = null, $direct, array $config, array $options = array(), $includeNode = false)
{
- return $this->getChildrenQuery($node, $direct, $sortByField, $direction)->execute();
+ return $this->getNodesHierarchyQuery($node, $direct, $config, $options, $includeNode)->getArrayResult();
}
/**
View
310 lib/Gedmo/Tree/Entity/Repository/NestedTreeRepository.php
@@ -23,41 +23,42 @@
class NestedTreeRepository extends AbstractTreeRepository
{
/**
- * Get all root nodes query builder
- *
- * @return Doctrine\ORM\QueryBuilder
+ * {@inheritDoc}
*/
- public function getRootNodesQueryBuilder()
+ public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc')
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->_em, $meta->name);
$qb = $this->_em->createQueryBuilder();
- return $qb
+ $qb
->select('node')
->from($config['useObjectClass'], 'node')
->where($qb->expr()->isNull('node.'.$config['parent']))
- ->orderBy('node.' . $config['left'], 'ASC')
;
+
+ if ($sortByField !== null) {
+ $qb->orderBy('node.' . $sortByField, strtolower($direction) === 'asc' ? 'asc' : 'desc');
+ } else {
+ $qb->orderBy('node.' . $config['left'], 'ASC');
+ }
+
+ return $qb;
}
/**
- * Get all root nodes query
- *
- * @return Doctrine\ORM\Query
+ * {@inheritDoc}
*/
- public function getRootNodesQuery()
+ public function getRootNodesQuery($sortByField = null, $direction = 'asc')
{
- return $this->getRootNodesQueryBuilder()->getQuery();
+ return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery();
}
/**
- * Get all root nodes
- *
- * @return array
+ * {@inheritDoc}
*/
- public function getRootNodes()
+ public function getRootNodes($sortByField = null, $direction = 'asc')
{
- return $this->getRootNodesQuery()->getResult();
+ return $this->getRootNodesQuery($sortByField, $direction)->getResult();
}
/**
@@ -171,79 +172,9 @@ public function getPath($node)
}
/**
- * Counts the children of given TreeNode
- *
- * @param object $node - if null counts all records in tree
- * @param boolean $direct - true to count only direct children
- * @throws InvalidArgumentException - if input is not valid
- * @return integer
- */
- public function childCount($node = null, $direct = false)
- {
- $count = 0;
- $meta = $this->getClassMetadata();
- $nodeId = $meta->getSingleIdentifierFieldName();
- $config = $this->listener->getConfiguration($this->_em, $meta->name);
- if (null !== $node) {
- if ($node instanceof $meta->name) {
- $wrapped = new EntityWrapper($node, $this->_em);
- if (!$wrapped->hasValidIdentifier()) {
- throw new InvalidArgumentException("Node is not managed by UnitOfWork");
- }
- if ($direct) {
- $id = $wrapped->getIdentifier();
- $qb = $this->_em->createQueryBuilder();
- $qb->select($qb->expr()->count('node.' . $nodeId))
- ->from($config['useObjectClass'], 'node')
- ->where($id === null ?
- $qb->expr()->isNull('node.'.$config['parent']) :
- $qb->expr()->eq('node.'.$config['parent'], is_string($id) ? $qb->expr()->literal($id) : $id)
- )
- ;
-
- if (isset($config['root'])) {
- $rootId = $wrapped->getPropertyValue($config['root']);
- $qb->andWhere($rootId === null ?
- $qb->expr()->isNull('node.'.$config['root']) :
- $qb->expr()->eq('node.'.$config['root'], is_string($rootId) ? $qb->expr()->literal($rootId) : $rootId)
- );
- }
- $q = $qb->getQuery();
- $count = intval($q->getSingleScalarResult());
- } else {
- $left = $wrapped->getPropertyValue($config['left']);
- $right = $wrapped->getPropertyValue($config['right']);
- if ($left && $right) {
- $count = ($right - $left - 1) / 2;
- }
- }
- } else {
- throw new InvalidArgumentException("Node is not related to this repository");
- }
- } else {
- $qb = $this->_em->createQueryBuilder();
- $qb->select($qb->expr()->count('node.' . $nodeId))
- ->from($config['useObjectClass'], 'node')
- ;
- if ($direct) {
- $qb->where($qb->expr()->isNull('node.'.$config['parent']));
- }
- $count = intval($qb->getQuery()->getSingleScalarResult());
- }
- return $count;
- }
-
- /**
- * Get tree children query builder followed by given $node
- *
- * @param object $node - if null, all tree nodes will be taken
- * @param boolean $direct - true to take only direct children
- * @param string $sortByField - field name to sort by
- * @param string $direction - sort direction : "ASC" or "DESC"
- * @throws InvalidArgumentException - if input is not valid
- * @return Doctrine\ORM\QueryBuilder
+ * @see getChildrenQueryBuilder
*/
- public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC')
+ public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->_em, $meta->name);
@@ -281,6 +212,11 @@ public function childrenQueryBuilder($node = null, $direct = false, $sortByField
$qb->expr()->eq('node.'.$config['root'], is_string($rootId) ? $qb->expr()->literal($rootId) : $rootId)
);
}
+ if ($includeNode) {
+ $idField = $meta->getSingleIdentifierFieldName();
+ $qb->where('('.$qb->getDqlPart('where').') OR node.'.$idField.' = :rootNode');
+ $qb->setParameter('rootNode', $node);
+ }
} else {
throw new \InvalidArgumentException("Node is not related to this repository");
}
@@ -309,35 +245,47 @@ public function childrenQueryBuilder($node = null, $direct = false, $sortByField
}
/**
- * Get tree children query followed by given $node
- *
- * @param object $node - if null, all tree nodes will be taken
- * @param boolean $direct - true to take only direct children
- * @param string|array $sortByField - field names to sort by
- * @param string $direction - sort direction : "ASC" or "DESC"
- * @return Doctrine\ORM\Query
+ * @see getChildrenQuery
*/
- public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC')
+ public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
- return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction)->getQuery();
+ return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery();
}
/**
- * Get list of children followed by given $node
- *
- * @param object $node - if null, all tree nodes will be taken
- * @param boolean $direct - true to take only direct children
- * @param string $sortByField - field name to sort by
- * @param string $direction - sort direction : "ASC" or "DESC"
- * @return array - list of given $node children, null on failure
+ * @see getChildren
*/
- public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC')
+ public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
- $q = $this->childrenQuery($node, $direct, $sortByField, $direction);
+ $q = $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode);
return $q->getResult();
}
/**
+ * {@inheritDoc}
+ */
+ public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
+ {
+ return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
+ {
+ return $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
+ {
+ return $this->children($node, $direct, $sortByField, $direction, $includeNode);
+ }
+
+ /**
* Get tree leafs query builder
*
* @param object $root - root node in case of root tree is required
@@ -836,46 +784,33 @@ public function recover()
}
/**
- * Retrieves the nested array or the decorated output.
- * Uses @options to handle decorations
- *
- * @throws \Gedmo\Exception\InvalidArgumentException
- * @param object $node - from which node to start reordering the tree
- * @param boolean $direct - true to take only direct children
- * @param array $options :
- * decorate: boolean (false) - retrieves tree as UL->LI tree
- * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
- * rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
- * rootClose: string ('</ul>') - branch close
- * childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
- * childClose: string ('</li>') - close of node
- *
- * @return array|string
+ * {@inheritDoc}
*/
- public function childrenHierarchy($node = null, $direct = false, array $options = array())
+ public function getNodesHierarchyQueryBuilder($node = null, $direct, array $config, array $options = array(), $includeNode = false)
{
- $meta = $this->getClassMetadata();
- $config = $this->listener->getConfiguration($this->_em, $meta->name);
-
- if ($node !== null) {
- if ($node instanceof $meta->name) {
- $wrapped = new EntityWrapper($node, $this->_em);
- if (!$wrapped->hasValidIdentifier()) {
- throw new InvalidArgumentException("Node is not managed by UnitOfWork");
- }
- }
- }
-
- // Gets the array of $node results.
- // It must be order by 'root' and 'left' field
- $nodes = $this->childrenQuery(
+ return $this->childrenQueryBuilder(
$node,
$direct,
isset($config['root']) ? array($config['root'], $config['left']) : $config['left'],
- 'ASC'
- )->getArrayResult();
+ 'ASC',
+ $includeNode
+ );
+ }
- return $this->buildTree($nodes, $options);
+ /**
+ * {@inheritDoc}
+ */
+ public function getNodesHierarchyQuery($node = null, $direct, array $config, array $options = array(), $includeNode = false)
+ {
+ return $this->getNodesHierarchyQueryBuilder($node, $direct, $config, $options)->getQuery();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNodesHierarchy($node = null, $direct, array $config, array $options = array(), $includeNode = false)
+ {
+ return $this->getNodesHierarchyQuery($node, $direct, $config, $options)->getArrayResult();
}
/**
@@ -1073,99 +1008,4 @@ private function removeSingle(EntityWrapper $wrapped)
// remove from identity map
$this->_em->getUnitOfWork()->removeFromIdentityMap($wrapped->getObject());
}
-
- /**
- * Retrieves the nested array or the decorated output.
- * Uses @options to handle decorations
- * NOTE: @nodes should be fetched and hydrated as array
- *
- * @throws \Gedmo\Exception\InvalidArgumentException
- * @param array $nodes - list o nodes to build tree
- * @param array $options :
- * decorate: boolean (false) - retrieves tree as UL->LI tree
- * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
- * rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
- * rootClose: string ('</ul>') - branch close
- * childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
- * childClose: string ('</li>') - close of node
- *
- * @return array|string
- */
- public function buildTree(array $nodes, array $options = array())
- {
- //process the nested tree into a nested array
- $meta = $this->getClassMetadata();
- $config = $this->listener->getConfiguration($this->_em, $meta->name);
- $nestedTree = array();
- $l = 0;
-
- if (count($nodes) > 0) {
- // Node Stack. Used to help building the hierarchy
- $stack = array();
- foreach ($nodes as $child) {
- $item = $child;
- $item['__children'] = array();
- // Number of stack items
- $l = count($stack);
- // Check if we're dealing with different levels
- while($l > 0 && $stack[$l - 1][$config['level']] >= $item[$config['level']]) {
- array_pop($stack);
- $l--;
- }
- // Stack is empty (we are inspecting the root)
- if ($l == 0) {
- // Assigning the root child
- $i = count($nestedTree);
- $nestedTree[$i] = $item;
- $stack[] = &$nestedTree[$i];
- } else {
- // Add child to parent
- $i = count($stack[$l - 1]['__children']);
- $stack[$l - 1]['__children'][$i] = $item;
- $stack[] = &$stack[$l - 1]['__children'][$i];
- }
- }
- }
-
- $default = array(
- 'decorate' => false,
- 'rootOpen' => '<ul>',
- 'rootClose' => '</ul>',
- 'childOpen' => '<li>',
- 'childClose' => '</li>',
- 'nodeDecorator' => function ($node) use ($meta) {
- // override and change it, guessing which field to use
- if ($meta->hasField('title')) {
- $field = 'title';
- } else if ($meta->hasField('name')) {
- $field = 'name';
- } else {
- throw new InvalidArgumentException("Cannot find any representation field");
- }
- return $node[$field];
- }
- );
- $options = array_merge($default, $options);
- // If you don't want any html output it will return the nested array
- if (!$options['decorate']) {
- return $nestedTree;
- } elseif (!count($nestedTree)) {
- return '';
- }
-
- $build = function($tree) use (&$build, &$options) {
- $output = is_string($options['rootOpen']) ? $options['rootOpen'] : $options['rootOpen']($tree);
- foreach ($tree as $node) {
- $output .= is_string($options['childOpen']) ? $options['childOpen'] : $options['childOpen']($node);
- $output .= $options['nodeDecorator']($node);
- if (count($node['__children']) > 0) {
- $output .= $build($node['__children']);
- }
- $output .= is_string($options['childClose']) ? $options['childClose'] : $options['childClose']($node);
- }
- return $output . (is_string($options['rootClose']) ? $options['rootClose'] : $options['rootClose']($tree));
- };
-
- return $build($nestedTree);
- }
}
View
58 lib/Gedmo/Tree/RepositoryInterface.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Gedmo\Tree;
+
+/**
+ * This interface ensures a consisten api between repositories for the ORM and the ODM.
+ *
+ * @author Gustavo Falco <comfortablynumb84@gmail.com>
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package Gedmo.Tree
+ * @subpackage RepositoryInterface
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+interface RepositoryInterface extends RepositoryUtilsInterface
+{
+ /**
+ * Get all root nodes
+ *
+ * @return array
+ */
+ public function getRootNodes($sortByField = null, $direction = 'asc');
+
+ /**
+ * Returns an array of nodes suitable for method buildTree
+ *
+ * @param object $node - Root node
+ * @param bool $direct - Obtain direct children?
+ * @param array $config - Metadata configuration
+ * @param array $options - Options
+ * @param boolean $includeNode - Include node in results?
+ *
+ * @return array - Array of nodes
+ */
+ public function getNodesHierarchy($node = null, $direct, array $config, array $options = array(), $includeNode = false);
+
+ /**
+ * Get list of children followed by given $node
+ *
+ * @param object $node - if null, all tree nodes will be taken
+ * @param boolean $direct - true to take only direct children
+ * @param string $sortByField - field name to sort by
+ * @param string $direction - sort direction : "ASC" or "DESC"
+ * @param bool $includeNode - Include the root node in results?
+ * @return array - list of given $node children, null on failure
+ */
+ public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false);
+
+ /**
+ * Counts the children of given TreeNode
+ *
+ * @param object $node - if null counts all records in tree
+ * @param boolean $direct - true to count only direct children
+ * @throws \Gedmo\Exception\InvalidArgumentException - if input is not valid
+ * @return integer
+ */
+ public function childCount($node = null, $direct = false);
+}
View
155 lib/Gedmo/Tree/RepositoryUtils.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Gedmo\Tree;
+
+use Doctrine\Common\Persistence\Mapping\ClassMetadata;
+use Doctrine\Common\Persistence\ObjectManager;
+
+class RepositoryUtils implements RepositoryUtilsInterface
+{
+ /** @var \Doctrine\Common\Persistence\Mapping\ClassMetadata */
+ protected $meta;
+
+ /** @var \Gedmo\Tree\TreeListener */
+ protected $listener;
+
+ /** @var \Doctrine\Common\Persistence\ObjectManager */
+ protected $om;
+
+ /** @var \Gedmo\Tree\RepositoryInterface */
+ protected $repo;
+
+ public function __construct(ObjectManager $om, ClassMetadata $meta, $listener, $repo)
+ {
+ $this->om = $om;
+ $this->meta = $meta;
+ $this->listener = $listener;
+ $this->repo = $repo;
+ }
+
+ public function getClassMetadata()
+ {
+ return $this->meta;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function childrenHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false)
+ {
+ $meta = $this->getClassMetadata();
+ $config = $this->listener->getConfiguration($this->om, $meta->name);
+
+ if ($node !== null) {
+ if ($node instanceof $meta->name) {
+ $wrapperClass = $this->om instanceof \Doctrine\ORM\EntityManager ?
+ '\Gedmo\Tool\Wrapper\EntityWrapper' :
+ '\Gedmo\Tool\Wrapper\MongoDocumentWrapper';
+ $wrapped = new $wrapperClass($node, $this->om);
+ if (!$wrapped->hasValidIdentifier()) {
+ throw new InvalidArgumentException("Node is not managed by UnitOfWork");
+ }
+ }
+ } else {
+ $includeNode = true;
+ }
+
+ // Gets the array of $node results. It must be ordered by depth
+ $nodes = $this->repo->getNodesHierarchy($node, $direct, $config, $options, $includeNode);
+
+ return $this->buildTree($nodes, $options);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function buildTree(array $nodes, array $options = array())
+ {
+ $meta = $this->getClassMetadata();
+ $nestedTree = $this->repo->buildTreeArray($nodes);
+
+ $default = array(
+ 'decorate' => false,
+ 'rootOpen' => '<ul>',
+ 'rootClose' => '</ul>',
+ 'childOpen' => '<li>',
+ 'childClose' => '</li>',
+ 'nodeDecorator' => function ($node) use ($meta) {
+ // override and change it, guessing which field to use
+ if ($meta->hasField('title')) {
+ $field = 'title';
+ } elseif ($meta->hasField('name')) {
+ $field = 'name';
+ } else {
+ throw new InvalidArgumentException("Cannot find any representation field");
+ }
+ return $node[$field];
+ }
+ );
+ $options = array_merge($default, $options);
+ // If you don't want any html output it will return the nested array
+ if (!$options['decorate']) {
+ return $nestedTree;
+ }
+
+ if (!count($nestedTree)) {
+ return '';
+ }
+
+ $build = function($tree) use (&$build, &$options) {
+ $output = is_string($options['rootOpen']) ? $options['rootOpen'] : $options['rootOpen']($tree);
+ foreach ($tree as $node) {
+ $output .= is_string($options['childOpen']) ? $options['childOpen'] : $options['childOpen']($node);
+ $output .= $options['nodeDecorator']($node);
+ if (count($node['__children']) > 0) {
+ $output .= $build($node['__children']);
+ }
+ $output .= $options['childClose'];
+ }
+ return $output . $options['rootClose'];
+ };
+
+ return $build($nestedTree);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function buildTreeArray(array $nodes)
+ {
+ $meta = $this->getClassMetadata();
+ $config = $this->listener->getConfiguration($this->om, $meta->name);
+ $nestedTree = array();
+ $l = 0;
+
+ if (count($nodes) > 0) {
+ // Node Stack. Used to help building the hierarchy
+ $stack = array();
+ foreach ($nodes as $child) {
+ $item = $child;
+ $item['__children'] = array();
+ // Number of stack items
+ $l = count($stack);
+ // Check if we're dealing with different levels
+ while($l > 0 && $stack[$l - 1][$config['level']] >= $item[$config['level']]) {
+ array_pop($stack);
+ $l--;
+ }
+ // Stack is empty (we are inspecting the root)
+ if ($l == 0) {
+ // Assigning the root child
+ $i = count($nestedTree);
+ $nestedTree[$i] = $item;
+ $stack[] = &$nestedTree[$i];
+ } else {
+ // Add child to parent
+ $i = count($stack[$l - 1]['__children']);
+ $stack[$l - 1]['__children'][$i] = $item;
+ $stack[] = &$stack[$l - 1]['__children'][$i];
+ }
+ }
+ }
+
+ return $nestedTree;
+ }
+}
View
56 lib/Gedmo/Tree/RepositoryUtilsInterface.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Gedmo\Tree;
+
+interface RepositoryUtilsInterface
+{
+ /**
+ * Retrieves the nested array or the decorated output.
+ * Uses @options to handle decorations
+ *
+ * @throws \Gedmo\Exception\InvalidArgumentException
+ * @param object $node - from which node to start reordering the tree
+ * @param boolean $direct - true to take only direct children
+ * @param array $options :
+ * decorate: boolean (false) - retrieves tree as UL->LI tree
+ * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
+ * rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
+ * rootClose: string ('</ul>') - branch close
+ * childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
+ * childClose: string ('</li>') - close of node
+ * childSort: array || keys allowed: field: field to sort on, dir: direction. 'asc' or 'desc'
+ * @param boolean $includeNode - Include node on results?
+ *
+ * @return array|string
+ */
+ public function childrenHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false);
+
+ /**
+ * Retrieves the nested array or the decorated output.
+ * Uses @options to handle decorations
+ * NOTE: @nodes should be fetched and hydrated as array
+ *
+ * @throws \Gedmo\Exception\InvalidArgumentException
+ * @param array $nodes - list o nodes to build tree
+ * @param array $options :
+ * decorate: boolean (false) - retrieves tree as UL->LI tree
+ * nodeDecorator: Closure (null) - uses $node as argument and returns decorated item as string
+ * rootOpen: string || Closure ('<ul>') - branch start, closure will be given $children as a parameter
+ * rootClose: string ('</ul>') - branch close
+ * childStart: string || Closure ('<li>') - start of node, closure will be given $node as a parameter
+ * childClose: string ('</li>') - close of node