Add tree repository traits

To be removed if available in doctrine extensions lib
pull/3844/head
Julio Montoya 5 years ago
parent 606df48596
commit 307ff058e8
  1. 683
      src/CoreBundle/Traits/Repository/ORM/ClosureTreeRepositoryTrait.php
  2. 288
      src/CoreBundle/Traits/Repository/ORM/MaterializedPathRepositoryTrait.php
  3. 1115
      src/CoreBundle/Traits/Repository/ORM/NestedTreeRepositoryTrait.php
  4. 273
      src/CoreBundle/Traits/Repository/ORM/TreeRepositoryTrait.php

@ -0,0 +1,683 @@
<?php
namespace Chamilo\CoreBundle\Traits\Repository\ORM;
use Doctrine\ORM\QueryBuilder;
use Gedmo\Exception\InvalidArgumentException;
use Doctrine\ORM\Query;
use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure;
use Gedmo\Tree\Entity\Repository\ClosureTreeRepository;
use Gedmo\Tree\Strategy;
use Gedmo\Tool\Wrapper\EntityWrapper;
/**
* The ClosureTreeRepository has some useful functions
* to interact with Closure tree. Repository uses
* the strategy used by listener
*
* @author Gustavo Adrian <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
trait ClosureTreeRepositoryTrait
{
use TreeRepositoryTrait;
/**
* {@inheritDoc}
*/
public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc')
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
$qb = $this->getQueryBuilder();
$qb->select('node')
->from($config['useObjectClass'], 'node')
->where('node.'.$config['parent']." IS NULL");
if ($sortByField) {
$qb->orderBy('node.'.$sortByField, strtolower($direction) === 'asc' ? 'asc' : 'desc');
}
return $qb;
}
/**
* {@inheritDoc}
*/
public function getRootNodesQuery($sortByField = null, $direction = 'asc')
{
return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery();
}
/**
* {@inheritDoc}
*/
public function getRootNodes($sortByField = null, $direction = 'asc')
{
return $this->getRootNodesQuery($sortByField, $direction)->getResult();
}
/**
* Get the Tree path query by given $node
*
* @param object $node
*
* @throws InvalidArgumentException - if input is not valid
*
* @return Query
*/
public function getPathQuery($node)
{
$meta = $this->getClassMetadata();
$em = $this->getEntityManager();
if (!$node instanceof $meta->name) {
throw new InvalidArgumentException("Node is not related to this repository");
}
if (!$em->getUnitOfWork()->isInIdentityMap($node)) {
throw new InvalidArgumentException("Node is not managed by UnitOfWork");
}
$config = $this->listener->getConfiguration($em, $meta->name);
$closureMeta = $em->getClassMetadata($config['closure']);
$dql = "SELECT c, node FROM {$closureMeta->name} c";
$dql .= " INNER JOIN c.ancestor node";
$dql .= " WHERE c.descendant = :node";
$dql .= " ORDER BY c.depth DESC";
$q = $em->createQuery($dql);
$q->setParameters(compact('node'));
return $q;
}
/**
* Get the Tree path of Nodes by given $node
*
* @param object $node
*
* @return array - list of Nodes in path
*/
public function getPath($node)
{
return array_map(function (AbstractClosure $closure) {
return $closure->getAncestor();
}, $this->getPathQuery($node)->getResult());
}
/**
* Get list of nodes related to a given $node
* @param string $way - search direction: "down" (for children) or "up" (for ancestors)
* @param object $node - if null, all tree nodes will be taken
* @param boolean $direct - true to take only direct children or parents
* @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 parents, null on failure
*/
public function closureLocateQueryBuilder($way = 'down', $node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
switch($way) {
case 'down':
$first = 'ancestor';
$second = 'descendant';
break;
case 'up':
$first = 'descendant';
$second = 'ancestor';
break;
default:
throw new InvalidArgumentException("Direction must be 'up' or 'down' but '$way' found");
}
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
$qb = $this->getQueryBuilder();
if ($node !== null) {
if ($node instanceof $meta->name) {
if (!$this->getEntityManager()->getUnitOfWork()->isInIdentityMap($node)) {
throw new InvalidArgumentException('Node is not managed by UnitOfWork');
}
$where = "c.$first = :node AND ";
$qb->select('c, node')
->from($config['closure'], 'c')
->innerJoin("c.$second", 'node');
if ($direct) {
$where .= 'c.depth = 1';
} else {
$where .= "c.$second <> :node";
}
$qb->where($where);
if ($includeNode) {
$qb->orWhere("c.$first = :node AND c.$second = :node");
}
} else {
throw new \InvalidArgumentException("Node is not related to this repository");
}
} else {
$qb->select('node')
->from($config['useObjectClass'], 'node');
if ($direct) {
$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}");
}
}
if ($node) {
$qb->setParameter('node', $node);
}
return $qb;
}
/**
* @see getChildrenQueryBuilder
*/
public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
return $this->closureLocateQueryBuilder('down', $node, $direct, $sortByField, $direction, $includeNode);
}
/**
* @see getChildrenQuery
*/
public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
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 (AbstractClosure $closure) {
return $closure->getDescendant();
}, $result);
}
return $result;
}
/**
* {@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);
}
/**
* @see getAncestorsQueryBuilder
*/
public function ancestorsQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
return $this->closureLocateQueryBuilder('up', $node, $direct, $sortByField, $direction, $includeNode);
}
/**
* @see getAncestorsQuery
*/
public function ancestorsQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
return $this->ancestorsQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery();
}
/**
* @see getAncestors
*/
public function ancestors($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
$result = $this->ancestorsQuery($node, $direct, $sortByField, $direction, $includeNode)->getResult();
if ($node) {
$result = array_map(function (AbstractClosure $closure) {
return $closure->getAncestor();
}, $result);
}
return $result;
}
/**
* Get the list of ancestors that lead to the 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 QueryBuilder - QueryBuilder object
*/
public function getAncestorsQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
return $this->ancestorsQueryBuilder($node, $direct, $sortByField, $direction, $includeNode);
}
/**
* Get the list of ancestors that lead to the given $node. This returns a Query 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 Query - Query object
*/
public function getAncestorsQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
return $this->ancestorsQuery($node, $direct, $sortByField, $direction, $includeNode);
}
/**
* Get the list of ancestors that lead to the 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 parents, null on failure
*/
public function getAncestors($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
return $this->ancestors($node, $direct, $sortByField, $direction, $includeNode);
}
/**
* @see childrenCount
*/
public function ancestorsCount($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->getEntityManager());
if (!$wrapped->hasValidIdentifier()) {
throw new InvalidArgumentException("Node is not managed by UnitOfWork");
}
}
$qb = $this->getAncestorsQueryBuilder($node, $direct);
// We need to remove the ORDER BY DQL part since some vendors could throw an error
// in count queries
$dqlParts = $qb->getDQLParts();
// We need to check first if there's an ORDER BY DQL part, because resetDQLPart doesn't
// check if its internal array has an "orderby" index
if (isset($dqlParts['orderBy'])) {
$qb->resetDQLPart('orderBy');
}
$aliases = $qb->getRootAliases();
$alias = $aliases[0];
$qb->select('COUNT('.$alias.')');
return (int) $qb->getQuery()->getSingleScalarResult();
}
/**
* Removes given $node from the tree and reparents its descendants
*
* @todo may be improved, to issue single query on reparenting
*
* @param object $node
*
* @throws \Gedmo\Exception\InvalidArgumentException
* @throws \Gedmo\Exception\RuntimeException - if something fails in transaction
*/
public function removeFromTree($node)
{
$meta = $this->getClassMetadata();
$em = $this->getEntityManager();
if (!$node instanceof $meta->name) {
throw new InvalidArgumentException("Node is not related to this repository");
}
$wrapped = new EntityWrapper($node, $em);
if (!$wrapped->hasValidIdentifier()) {
throw new InvalidArgumentException("Node is not managed by UnitOfWork");
}
$config = $this->listener->getConfiguration($em, $meta->name);
$pk = $meta->getSingleIdentifierFieldName();
$nodeId = $wrapped->getIdentifier();
$parent = $wrapped->getPropertyValue($config['parent']);
$dql = "SELECT node FROM {$config['useObjectClass']} node";
$dql .= " WHERE node.{$config['parent']} = :node";
$q = $em->createQuery($dql);
$q->setParameters(compact('node'));
$nodesToReparent = $q->getResult();
// process updates in transaction
$em->getConnection()->beginTransaction();
try {
foreach ($nodesToReparent as $nodeToReparent) {
$id = $meta->getReflectionProperty($pk)->getValue($nodeToReparent);
$meta->getReflectionProperty($config['parent'])->setValue($nodeToReparent, $parent);
$dql = "UPDATE {$config['useObjectClass']} node";
$dql .= " SET node.{$config['parent']} = :parent";
$dql .= " WHERE node.{$pk} = :id";
$q = $em->createQuery($dql);
$q->setParameters(compact('parent', 'id'));
$q->getSingleScalarResult();
$this->listener
->getStrategy($em, $meta->name)
->updateNode($em, $nodeToReparent, $node);
$oid = spl_object_hash($nodeToReparent);
$em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['parent'], $parent);
}
$dql = "DELETE {$config['useObjectClass']} node";
$dql .= " WHERE node.{$pk} = :nodeId";
$q = $em->createQuery($dql);
$q->setParameters(compact('nodeId'));
$q->getSingleScalarResult();
$em->getConnection()->commit();
} catch (\Exception $e) {
$em->close();
$em->getConnection()->rollback();
throw new \Gedmo\Exception\RuntimeException('Transaction failed: '.$e->getMessage(), null, $e);
}
// remove from identity map
$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->getEntityManager(), $meta->name);
$nestedTree = array();
$idField = $meta->getSingleIdentifierFieldName();
$hasLevelProp = !empty($config['level']);
$levelProp = $hasLevelProp ? $config['level'] : ClosureTreeRepository::SUBQUERY_LEVEL;
$childrenIndex = $this->repoUtils->getChildrenIndex();
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[$childrenIndex] = array();
$level = $hasLevelProp ? $node[$levelProp] : $n[$levelProp];
if ($l < $level) {
$l = $level;
}
if ($l == $firstLevel) {
$tmp = &$nestedTree;
} else {
$tmp = &$refs[$n['parent_id']][$childrenIndex];
}
$key = count($tmp);
$tmp[$key] = $node;
$refs[$node[$idField]] = &$tmp[$key];
}
unset($refs);
}
return $nestedTree;
}
/**
* {@inheritdoc}
*/
public function getNodesHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false)
{
return $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult();
}
/**
* {@inheritdoc}
*/
public function getNodesHierarchyQuery($node = null, $direct = false, array $options = array(), $includeNode = false)
{
return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery();
}
/**
* {@inheritdoc}
*/
public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = array(), $includeNode = false)
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
$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 '.ClosureTreeRepository::SUBQUERY_LEVEL;
}
$q = $this->getEntityManager()->createQueryBuilder()
->select('c, node, p.'.$idField.' AS parent_id'.$subQuery)
->from($config['closure'], 'c')
->innerJoin('c.descendant', 'node')
->leftJoin('node.'.$config['parent'], 'p')
->addOrderBy(($hasLevelProp ? 'node.'.$config['level'] : ClosureTreeRepository::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;
}
public function verify()
{
$nodeMeta = $this->getClassMetadata();
$nodeIdField = $nodeMeta->getSingleIdentifierFieldName();
$config = $this->listener->getConfiguration($this->_em, $nodeMeta->name);
$closureMeta = $this->_em->getClassMetadata($config['closure']);
$errors = [];
$q = $this->_em->createQuery("
SELECT COUNT(node)
FROM {$nodeMeta->name} AS node
LEFT JOIN {$closureMeta->name} AS c WITH c.ancestor = node AND c.depth = 0
WHERE c.id IS NULL
");
if ($missingSelfRefsCount = intval($q->getSingleScalarResult())) {
$errors[] = "Missing $missingSelfRefsCount self referencing closures";
}
$q = $this->_em->createQuery("
SELECT COUNT(node)
FROM {$nodeMeta->name} AS node
INNER JOIN {$closureMeta->name} AS c1 WITH c1.descendant = node.{$config['parent']}
LEFT JOIN {$closureMeta->name} AS c2 WITH c2.descendant = node.$nodeIdField AND c2.ancestor = c1.ancestor
WHERE c2.id IS NULL AND node.$nodeIdField <> c1.ancestor
");
if ($missingClosuresCount = intval($q->getSingleScalarResult())) {
$errors[] = "Missing $missingClosuresCount closures";
}
return $errors ?: true;
}
public function recover()
{
if ($this->verify() === true) {
return;
}
$this->cleanUpClosure();
$this->rebuildClosure();
}
public function rebuildClosure()
{
$nodeMeta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->_em, $nodeMeta->name);
$closureMeta = $this->_em->getClassMetadata($config['closure']);
$insertClosures = function ($entries) use ($closureMeta) {
$closureTable = $closureMeta->getTableName();
$ancestorColumnName = $this->getJoinColumnFieldName($closureMeta->getAssociationMapping('ancestor'));
$descendantColumnName = $this->getJoinColumnFieldName($closureMeta->getAssociationMapping('descendant'));
$depthColumnName = $closureMeta->getColumnName('depth');
$conn = $this->_em->getConnection();
$conn->beginTransaction();
foreach ($entries as $entry) {
$conn->insert($closureTable, array_combine(
[$ancestorColumnName, $descendantColumnName, $depthColumnName],
$entry
));
}
$conn->commit();
};
$buildClosures = function ($dql) use ($insertClosures) {
$newClosuresCount = 0;
$batchSize = 1000;
$q = $this->_em->createQuery($dql)->setMaxResults($batchSize)->setCacheable(false);
do {
$entries = $q->getScalarResult();
$insertClosures($entries);
$newClosuresCount += count($entries);
} while (count($entries) > 0);
return $newClosuresCount;
};
$nodeIdField = $nodeMeta->getSingleIdentifierFieldName();
$newClosuresCount = $buildClosures("
SELECT node.id AS ancestor, node.$nodeIdField AS descendant, 0 AS depth
FROM {$nodeMeta->name} AS node
LEFT JOIN {$closureMeta->name} AS c WITH c.ancestor = node AND c.depth = 0
WHERE c.id IS NULL
");
$newClosuresCount += $buildClosures("
SELECT IDENTITY(c1.ancestor) AS ancestor, node.$nodeIdField AS descendant, c1.depth + 1 AS depth
FROM {$nodeMeta->name} AS node
INNER JOIN {$closureMeta->name} AS c1 WITH c1.descendant = node.{$config['parent']}
LEFT JOIN {$closureMeta->name} AS c2 WITH c2.descendant = node.$nodeIdField AND c2.ancestor = c1.ancestor
WHERE c2.id IS NULL AND node.$nodeIdField <> c1.ancestor
");
return $newClosuresCount;
}
public function cleanUpClosure()
{
$conn = $this->_em->getConnection();
$nodeMeta = $this->getClassMetadata();
$nodeIdField = $nodeMeta->getSingleIdentifierFieldName();
$config = $this->listener->getConfiguration($this->_em, $nodeMeta->name);
$closureMeta = $this->_em->getClassMetadata($config['closure']);
$closureTableName = $closureMeta->getTableName();
$dql = "
SELECT c1.id AS id
FROM {$closureMeta->name} AS c1
LEFT JOIN {$nodeMeta->name} AS node WITH c1.descendant = node.$nodeIdField
LEFT JOIN {$closureMeta->name} AS c2 WITH c2.descendant = node.{$config['parent']} AND c2.ancestor = c1.ancestor
WHERE c2.id IS NULL AND c1.descendant <> c1.ancestor
";
$deletedClosuresCount = 0;
$batchSize = 1000;
$q = $this->_em->createQuery($dql)->setMaxResults($batchSize)->setCacheable(false);
while (($ids = $q->getScalarResult()) && !empty($ids)) {
$ids = array_map(function ($el) {
return $el['id'];
}, $ids);
$query = "DELETE FROM {$closureTableName} WHERE id IN (".implode(', ', $ids).")";
if (!$conn->executeQuery($query)) {
throw new \RuntimeException('Failed to remove incorrect closures');
}
$deletedClosuresCount += count($ids);
}
return $deletedClosuresCount;
}
protected function getJoinColumnFieldName($association)
{
if (count($association['joinColumnFieldNames']) > 1) {
throw new \RuntimeException('More association on field ' . $association['fieldName']);
}
return array_shift($association['joinColumnFieldNames']);
}
/**
* {@inheritdoc}
*/
protected function validate()
{
return $this->listener->getStrategy($this->getEntityManager(), $this->getClassMetadata()->name)->getName() === Strategy::CLOSURE;
}
}

@ -0,0 +1,288 @@
<?php
namespace Chamilo\CoreBundle\Traits\Repository\ORM;
use Gedmo\Tree\Strategy;
use Gedmo\Tool\Wrapper\EntityWrapper;
/**
* The MaterializedPathRepository has some useful functions
* to interact with MaterializedPath tree. Repository uses
* the strategy used by listener
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
trait MaterializedPathRepositoryTrait
{
use TreeRepositoryTrait;
/**
* Get tree query builder
*
* @param object $rootNode
*
* @return \Doctrine\ORM\QueryBuilder
*/
public function getTreeQueryBuilder($rootNode = null)
{
return $this->getChildrenQueryBuilder($rootNode, false, null, 'asc', true);
}
/**
* Get tree query
*
* @param object $rootNode
*
* @return \Doctrine\ORM\Query
*/
public function getTreeQuery($rootNode = null)
{
return $this->getTreeQueryBuilder($rootNode)->getQuery();
}
/**
* Get tree
*
* @param object $rootNode
*
* @return array
*/
public function getTree($rootNode = null)
{
return $this->getTreeQuery($rootNode)->execute();
}
/**
* {@inheritDoc}
*/
public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc')
{
return $this->getChildrenQueryBuilder(null, true, $sortByField, $direction);
}
/**
* {@inheritDoc}
*/
public function getRootNodesQuery($sortByField = null, $direction = 'asc')
{
return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery();
}
/**
* {@inheritDoc}
*/
public function getRootNodes($sortByField = null, $direction = 'asc')
{
return $this->getRootNodesQuery($sortByField, $direction)->execute();
}
/**
* Get the Tree path query builder by given $node
*
* @param object $node
*
* @return \Doctrine\ORM\QueryBuilder
*/
public function getPathQueryBuilder($node)
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->_em, $meta->name);
$alias = 'materialized_path_entity';
$qb = $this->getQueryBuilder()
->select($alias)
->from($config['useObjectClass'], $alias);
$node = new EntityWrapper($node, $this->_em);
$nodePath = $node->getPropertyValue($config['path']);
$paths = [];
$nodePathLength = strlen($nodePath);
$separatorMatchOffset = 0;
while ($separatorMatchOffset < $nodePathLength) {
$separatorPos = strpos($nodePath, $config['path_separator'], $separatorMatchOffset);
if ($separatorPos === false || $separatorPos === $nodePathLength - 1) {
// last node, done
$paths[] = $nodePath;
$separatorMatchOffset = $nodePathLength;
} elseif ($separatorPos === 0) {
// path starts with separator, continue
$separatorMatchOffset = 1;
} else {
// add node
$paths[] = substr($nodePath, 0, $config['path_ends_with_separator'] ? $separatorPos + 1 : $separatorPos);
$separatorMatchOffset = $separatorPos + 1;
}
}
$qb->where($qb->expr()->in(
$alias.'.'.$config['path'],
$paths
));
$qb->orderBy($alias.'.'.$config['level'], 'ASC');
return $qb;
}
/**
* Get the Tree path query by given $node
*
* @param object $node
*
* @return \Doctrine\ORM\Query
*/
public function getPathQuery($node)
{
return $this->getPathQueryBuilder($node)->getQuery();
}
/**
* Get the Tree path of Nodes by given $node
*
* @param object $node
*
* @return array - list of Nodes in path
*/
public function getPath($node)
{
return $this->getPathQuery($node)->getResult();
}
/**
* {@inheritDoc}
*/
public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false)
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name);
$separator = addcslashes($config['path_separator'], '%');
$alias = 'materialized_path_entity';
$path = $config['path'];
$qb = $this->getQueryBuilder()
->select($alias)
->from($config['useObjectClass'], $alias);
$expr = '';
$includeNodeExpr = '';
if (is_object($node) && $node instanceof $meta->name) {
$node = new EntityWrapper($node, $this->getEntityManager());
$nodePath = $node->getPropertyValue($path);
$expr = $qb->expr()->andx()->add(
$qb->expr()->like(
$alias.'.'.$path,
$qb->expr()->literal(
$nodePath
.($config['path_ends_with_separator'] ? '' : $separator).'%'
)
)
);
if ($includeNode) {
$includeNodeExpr = $qb->expr()->eq($alias.'.'.$path, $qb->expr()->literal($nodePath));
} else {
$expr->add($qb->expr()->neq($alias.'.'.$path, $qb->expr()->literal($nodePath)));
}
if ($direct) {
$expr->add(
$qb->expr()->orx(
$qb->expr()->eq($alias.'.'.$config['level'], $qb->expr()->literal($node->getPropertyValue($config['level']))),
$qb->expr()->eq($alias.'.'.$config['level'], $qb->expr()->literal($node->getPropertyValue($config['level']) + 1))
)
);
}
} elseif ($direct) {
$expr = $qb->expr()->not(
$qb->expr()->like($alias.'.'.$path,
$qb->expr()->literal(
($config['path_starts_with_separator'] ? $separator : '')
.'%'.$separator.'%'
.($config['path_ends_with_separator'] ? $separator : '')
)
)
);
}
if ($expr) {
$qb->where('('.$expr.')');
}
if ($includeNodeExpr) {
$qb->orWhere('('.$includeNodeExpr.')');
}
$orderByField = null === $sortByField ? $alias.'.'.$config['path'] : $alias.'.'.$sortByField;
$orderByDir = $direction === 'asc' ? 'asc' : 'desc';
$qb->orderBy($orderByField, $orderByDir);
return $qb;
}
/**
* {@inheritDoc}
*/
public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false)
{
return $this->getChildrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery();
}
/**
* {@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 = false, 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 = false, array $options = array(), $includeNode = false)
{
return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery();
}
/**
* {@inheritdoc}
*/
public function getNodesHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false)
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->_em, $meta->name);
$path = $config['path'];
$nodes = $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult();
usort(
$nodes,
function ($a, $b) use ($path) {
return strcmp($a[$path], $b[$path]);
}
);
return $nodes;
}
/**
* {@inheritdoc}
*/
protected function validate()
{
return $this->listener->getStrategy($this->getEntityManager(), $this->getClassMetadata()->name)->getName() === Strategy::MATERIALIZED_PATH;
}
}

@ -0,0 +1,273 @@
<?php
namespace Chamilo\CoreBundle\Traits\Repository\ORM;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Gedmo\Tool\Wrapper\EntityWrapper;
use Gedmo\Tree\RepositoryUtils;
use Gedmo\Tree\RepositoryUtilsInterface;
use Gedmo\Exception\InvalidArgumentException;
use Gedmo\Tree\TreeListener;
trait TreeRepositoryTrait
{
/**
* Tree listener on event manager
*
* @var TreeListener
*/
protected $listener = null;
/**
* Repository utils
*
* @var RepositoryUtils
*/
protected $repoUtils = null;
/**
* @return EntityManager
*/
abstract protected function getEntityManager();
/**
* @return ClassMetadata
*/
abstract protected function getClassMetadata();
/**
* This method should be called in your repository __construct().
* Example:
*
* class MyTreeRepository extends EntityRepository
* {
* use NestedTreeRepository; // or ClosureTreeRepository, or MaterializedPathRepository.
*
* public function __construct(EntityManager $em, ClassMetadata $class)
* {
* parent::__construct($em, $class);
*
* $this->initializeTreeRepository($em, $class);
* }
*
* // ...
* }
*
*/
public function initializeTreeRepository(EntityManager $em, ClassMetadata $class)
{
$treeListener = null;
foreach ($em->getEventManager()->getListeners() as $listeners) {
foreach ($listeners as $listener) {
if ($listener instanceof TreeListener) {
$treeListener = $listener;
break;
}
}
if ($treeListener) {
break;
}
}
if (null === $treeListener) {
throw new \Gedmo\Exception\InvalidMappingException('Tree listener was not found on your entity manager, it must be hooked into the event manager');
}
$this->listener = $treeListener;
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->getEntityManager(), $this->getClassMetadata(), $this->listener, $this);
}
/**
* @return \Doctrine\ORM\QueryBuilder
*/
protected function getQueryBuilder()
{
return $this->getEntityManager()->createQueryBuilder();
}
/**
* Sets the RepositoryUtilsInterface instance
*
* @param \Gedmo\Tree\RepositoryUtilsInterface $repoUtils
*
* @return static
*/
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->getEntityManager());
if (!$wrapped->hasValidIdentifier()) {
throw new InvalidArgumentException("Node is not managed by UnitOfWork");
}
}
$qb = $this->getChildrenQueryBuilder($node, $direct);
// We need to remove the ORDER BY DQL part since some vendors could throw an error
// in count queries
$dqlParts = $qb->getDQLParts();
// We need to check first if there's an ORDER BY DQL part, because resetDQLPart doesn't
// check if its internal array has an "orderby" index
if (isset($dqlParts['orderBy'])) {
$qb->resetDQLPart('orderBy');
}
$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);
}
/**
* @see \Gedmo\Tree\RepositoryUtilsInterface::setChildrenIndex
*/
public function setChildrenIndex($childrenIndex)
{
$this->repoUtils->setChildrenIndex($childrenIndex);
}
/**
* @see \Gedmo\Tree\RepositoryUtilsInterface::getChildrenIndex
*/
public function getChildrenIndex()
{
return $this->repoUtils->getChildrenIndex();
}
/**
* Checks if current repository is right
* for currently used tree strategy
*
* @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 $options - Options
* @param boolean $includeNode - Include node in results?
*
* @return \Doctrine\ORM\QueryBuilder - QueryBuilder object
*/
abstract public function getNodesHierarchyQueryBuilder($node = null, $direct = false, 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 $options - Options
* @param boolean $includeNode - Include node in results?
*
* @return \Doctrine\ORM\Query - Query object
*/
abstract public function getNodesHierarchyQuery($node = null, $direct = false, 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);
}
Loading…
Cancel
Save