Actualización

This commit is contained in:
Xes
2025-04-10 12:24:57 +02:00
parent 8969cc929d
commit 45420b6f0d
39760 changed files with 4303286 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
<?php
namespace Gedmo\Tree\Document\MongoDB\Repository;
use Doctrine\ODM\MongoDB\DocumentRepository;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\UnitOfWork;
use Gedmo\Tree\RepositoryUtils;
use Gedmo\Tree\RepositoryUtilsInterface;
use Gedmo\Tree\RepositoryInterface;
abstract class AbstractTreeRepository extends DocumentRepository implements RepositoryInterface
{
/**
* Tree listener on event manager
*
* @var AbstractTreeListener
*/
protected $listener = null;
/**
* Repository utils
*/
protected $repoUtils = null;
/**
* {@inheritdoc}
*/
public function __construct(DocumentManager $em, UnitOfWork $uow, ClassMetadata $class)
{
parent::__construct($em, $uow, $class);
$treeListener = null;
foreach ($em->getEventManager()->getListeners() as $listeners) {
foreach ($listeners as $listener) {
if ($listener instanceof \Gedmo\Tree\TreeListener) {
$treeListener = $listener;
break;
}
}
if ($treeListener) {
break;
}
}
if (is_null($treeListener)) {
throw new \Gedmo\Exception\InvalidMappingException('This repository can be attached only to ODM MongoDB tree listener');
}
$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->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);
}
/**
* @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();
}
/**
* {@inheritDoc}
*/
public function buildTreeArray(array $nodes)
{
return $this->repoUtils->buildTreeArray($nodes);
}
/**
* 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\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 $options - Options
* @param boolean $includeNode - Include node in results?
*
* @return \Doctrine\MongoDB\Query\Builder - 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\MongoDB\Query\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\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 File

@@ -0,0 +1,205 @@
<?php
namespace Gedmo\Tree\Document\MongoDB\Repository;
use Gedmo\Exception\InvalidArgumentException;
use Gedmo\Tree\Strategy;
use Gedmo\Tool\Wrapper\MongoDocumentWrapper;
/**
* 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)
*/
class MaterializedPathRepository extends AbstractTreeRepository
{
/**
* Get tree query builder
*
* @param object $rootNode
*
* @return \Doctrine\ODM\MongoDB\Query\Builder
*/
public function getTreeQueryBuilder($rootNode = null)
{
return $this->getChildrenQueryBuilder($rootNode, false, null, 'asc', true);
}
/**
* Get tree query
*
* @param object $rootNode
*
* @return \Doctrine\ODM\MongoDB\Query\Query
*/
public function getTreeQuery($rootNode = null)
{
return $this->getTreeQueryBuilder($rootNode)->getQuery();
}
/**
* Get tree
*
* @param object $rootNode
*
* @return \Doctrine\ODM\MongoDB\Cursor
*/
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();
}
/**
* {@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', $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)'.($includeNode ? '?' : '').'$/',
$nodePath,
$separator,
$separator);
} else {
$regex = sprintf('/^%s(.+)'.($includeNode ? '?' : '').'/',
$nodePath);
}
} elseif ($direct) {
$regex = sprintf('/^([^%s]+)'.($includeNode ? '?' : '').'%s$/',
$separator,
$separator);
}
if ($regex) {
$qb->field($config['path'])->equals(new \MongoRegex($regex));
}
$qb->sort(is_null($sortByField) ? $config['path'] : $sortByField, $direction === 'asc' ? 'asc' : 'desc');
return $qb;
}
/**
* G{@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)
{
$query = $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode);
$query->setHydrate(false);
return $query->toArray();
}
/**
* {@inheritdoc}
*/
protected function validate()
{
return $this->listener->getStrategy($this->dm, $this->getClassMetadata()->name)->getName() === Strategy::MATERIALIZED_PATH;
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Gedmo\Tree\Entity\MappedSuperclass;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\MappedSuperclass
*/
abstract class AbstractClosure
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
* @ORM\Column(type="integer")
*/
protected $id;
/**
* Mapped by listener
* Visibility must be protected
*/
protected $ancestor;
/**
* Mapped by listener
* Visibility must be protected
*/
protected $descendant;
/**
* @ORM\Column(type="integer")
*/
protected $depth;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set ancestor
*
* @param object $ancestor
*
* @return static
*/
public function setAncestor($ancestor)
{
$this->ancestor = $ancestor;
return $this;
}
/**
* Get ancestor
*
* @return object
*/
public function getAncestor()
{
return $this->ancestor;
}
/**
* Set descendant
*
* @param object $descendant
*
* @return static
*/
public function setDescendant($descendant)
{
$this->descendant = $descendant;
return $this;
}
/**
* Get descendant
*
* @return object
*/
public function getDescendant()
{
return $this->descendant;
}
/**
* Set depth
*
* @param integer $depth
*
* @return static
*/
public function setDepth($depth)
{
$this->depth = $depth;
return $this;
}
/**
* Get depth
*
* @return integer
*/
public function getDepth()
{
return $this->depth;
}
}

View File

@@ -0,0 +1,248 @@
<?php
namespace Gedmo\Tree\Entity\Repository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Gedmo\Tool\Wrapper\EntityWrapper;
use Gedmo\Tree\RepositoryUtils;
use Gedmo\Tree\RepositoryUtilsInterface;
use Gedmo\Tree\RepositoryInterface;
use Gedmo\Exception\InvalidArgumentException;
use Gedmo\Tree\TreeListener;
abstract class AbstractTreeRepository extends EntityRepository implements RepositoryInterface
{
/**
* Tree listener on event manager
*
* @var TreeListener
*/
protected $listener = null;
/**
* Repository utils
*/
protected $repoUtils = null;
/**
* {@inheritdoc}
*/
public function __construct(EntityManagerInterface $em, ClassMetadata $class)
{
parent::__construct($em, $class);
$treeListener = null;
foreach ($em->getEventManager()->getListeners() as $listeners) {
foreach ($listeners as $listener) {
if ($listener instanceof TreeListener) {
$treeListener = $listener;
break;
}
}
if ($treeListener) {
break;
}
}
if (is_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->_em, $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->_em);
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);
}

View File

@@ -0,0 +1,605 @@
<?php
namespace Gedmo\Tree\Entity\Repository;
use Gedmo\Exception\InvalidArgumentException;
use Doctrine\ORM\Query;
use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure;
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)
*/
class ClosureTreeRepository extends AbstractTreeRepository
{
/** Alias for the level value used in the subquery of the getNodesHierarchy method */
const SUBQUERY_LEVEL = 'level';
/**
* {@inheritDoc}
*/
public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc')
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->_em, $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();
if (!$node instanceof $meta->name) {
throw new InvalidArgumentException("Node is not related to this repository");
}
if (!$this->_em->getUnitOfWork()->isInIdentityMap($node)) {
throw new InvalidArgumentException("Node is not managed by UnitOfWork");
}
$config = $this->listener->getConfiguration($this->_em, $meta->name);
$closureMeta = $this->_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 = $this->_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());
}
/**
* @see getChildrenQueryBuilder
*/
public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false)
{
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->_em, $meta->name);
$qb = $this->getQueryBuilder();
if ($node !== null) {
if ($node instanceof $meta->name) {
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');
if ($direct) {
$where .= 'c.depth = 1';
} else {
$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");
}
} 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 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);
}
/**
* 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();
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");
}
$config = $this->listener->getConfiguration($this->_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 = $this->_em->createQuery($dql);
$q->setParameters(compact('node'));
$nodesToReparent = $q->getResult();
// process updates in transaction
$this->_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 = $this->_em->createQuery($dql);
$q->setParameters(compact('parent', 'id'));
$q->getSingleScalarResult();
$this->listener
->getStrategy($this->_em, $meta->name)
->updateNode($this->_em, $nodeToReparent, $node);
$oid = spl_object_hash($nodeToReparent);
$this->_em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['parent'], $parent);
}
$dql = "DELETE {$config['useObjectClass']} node";
$dql .= " WHERE node.{$pk} = :nodeId";
$q = $this->_em->createQuery($dql);
$q->setParameters(compact('nodeId'));
$q->getSingleScalarResult();
$this->_em->getConnection()->commit();
} catch (\Exception $e) {
$this->_em->close();
$this->_em->getConnection()->rollback();
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;
$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->_em, $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 '.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()
{
return $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName() === Strategy::CLOSURE;
}
public function verify()
{
$nodeMeta = $this->getClassMetadata();
$nodeIdField = $nodeMeta->getSingleIdentifierFieldName();
$config = $this->listener->getConfiguration($this->_em, $nodeMeta->name);
$closureMeta = $this->_em->getClassMetadata($config['closure']);
$errors = array();
$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";
}
$q = $this->_em->createQuery("
SELECT COUNT(c1.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
");
if ($invalidClosuresCount = intval($q->getSingleScalarResult())) {
$errors[] = "Found $invalidClosuresCount invalid closures";
}
if (!empty($config['level'])) {
$levelField = $config['level'];
$maxResults = 1000;
$q = $this->_em->createQuery("
SELECT node.$nodeIdField AS id, node.$levelField AS node_level, MAX(c.depth) AS closure_level
FROM {$nodeMeta->name} AS node
INNER JOIN {$closureMeta->name} AS c WITH c.descendant = node.$nodeIdField
GROUP BY node.$nodeIdField, node.$levelField
HAVING node.$levelField IS NULL OR node.$levelField <> MAX(c.depth) + 1
")->setMaxResults($maxResults);
if ($invalidLevelsCount = count($q->getScalarResult())) {
$errors[] = "Found $invalidLevelsCount invalid level values";
}
}
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(
array($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;
}
public function updateLevelValues()
{
$nodeMeta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->_em, $nodeMeta->name);
$levelUpdatesCount = 0;
if (!empty($config['level'])) {
$levelField = $config['level'];
$nodeIdField = $nodeMeta->getSingleIdentifierFieldName();
$closureMeta = $this->_em->getClassMetadata($config['closure']);
$batchSize = 1000;
$q = $this->_em->createQuery("
SELECT node.$nodeIdField AS id, node.$levelField AS node_level, MAX(c.depth) AS closure_level
FROM {$nodeMeta->name} AS node
INNER JOIN {$closureMeta->name} AS c WITH c.descendant = node.$nodeIdField
GROUP BY node.$nodeIdField, node.$levelField
HAVING node.$levelField IS NULL OR node.$levelField <> MAX(c.depth) + 1
")->setMaxResults($batchSize)->setCacheable(false);
do {
$entries = $q->getScalarResult();
$this->_em->getConnection()->beginTransaction();
foreach ($entries as $entry) {
unset($entry['node_level']);
$this->_em->createQuery("
UPDATE {$nodeMeta->name} AS node SET node.$levelField = (:closure_level + 1) WHERE node.$nodeIdField = :id
")->execute($entry);
}
$this->_em->getConnection()->commit();
$levelUpdatesCount += count($entries);
} while (count($entries) > 0);
}
return $levelUpdatesCount;
}
protected function getJoinColumnFieldName($association)
{
if (count($association['joinColumnFieldNames']) > 1) {
throw new \RuntimeException('More association on field ' . $association['fieldName']);
}
return array_shift($association['joinColumnFieldNames']);
}
}

View File

@@ -0,0 +1,286 @@
<?php
namespace Gedmo\Tree\Entity\Repository;
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)
*/
class MaterializedPathRepository extends AbstractTreeRepository
{
/**
* 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 = array();
$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->_em, $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->_em);
$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 = is_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->_em, $this->getClassMetadata()->name)->getName() === Strategy::MATERIALIZED_PATH;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,265 @@
<?php
namespace Gedmo\Tree\Hydrator\ORM;
use Doctrine\Common\Collections\AbstractLazyCollection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
use Doctrine\ORM\Proxy\Proxy;
use Gedmo\Tool\Wrapper\EntityWrapper;
use Gedmo\Tree\TreeListener;
/**
* Automatically maps the parent and children properties of Tree nodes
*
* @author Ilija Tovilo <ilija.tovilo@me.com>
* @link http://www.gediminasm.org
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class TreeObjectHydrator extends ObjectHydrator
{
/**
* @var array
*/
private $config;
/**
* @var string
*/
private $idField;
/**
* @var string
*/
private $parentField;
/**
* @var string
*/
private $childrenField;
/**
* We hook into the `hydrateAllData` to map the children collection of the entity
*
* {@inheritdoc}
*/
protected function hydrateAllData()
{
$data = parent::hydrateAllData();
if (count($data) === 0) {
return $data;
}
$listener = $this->getTreeListener($this->_em);
$entityClass = $this->getEntityClassFromHydratedData($data);
$this->config = $listener->getConfiguration($this->_em, $entityClass);
$this->idField = $this->getIdField($entityClass);
$this->parentField = $this->getParentField();
$this->childrenField = $this->getChildrenField($entityClass);
$childrenHashmap = $this->buildChildrenHashmap($data);
$this->populateChildrenArray($data, $childrenHashmap);
// Only return root elements or elements who's parents haven't been fetched
// The sub-nodes will be accessible via the `children` property
return $this->getRootNodes($data);
}
/**
* Creates a hashmap to quickly find the children of a node
*
* ```
* [parentId => [child1, child2, ...], ...]
* ```
*
* @param array $nodes
* @return array
*/
protected function buildChildrenHashmap($nodes)
{
$r = array();
foreach ($nodes as $node) {
$parentProxy = $this->getPropertyValue($node, $this->config['parent']);
$parentId = null;
if ($parentProxy !== null) {
$parentId = $this->getPropertyValue($parentProxy, $this->idField);
}
$r[$parentId][] = $node;
}
return $r;
}
/**
* @param array $nodes
* @param array $childrenHashmap
*/
protected function populateChildrenArray($nodes, $childrenHashmap)
{
foreach ($nodes as $node) {
$nodeId = $this->getPropertyValue($node, $this->idField);
$childrenCollection = $this->getPropertyValue($node, $this->childrenField);
if ($childrenCollection === null) {
$childrenCollection = new ArrayCollection();
$this->setPropertyValue($node, $this->childrenField, $childrenCollection);
}
// Mark all children collections as initialized to avoid select queries
if ($childrenCollection instanceof AbstractLazyCollection) {
$childrenCollection->setInitialized(true);
}
if (!isset($childrenHashmap[$nodeId])) {
continue;
}
$childrenCollection->clear();
foreach ($childrenHashmap[$nodeId] as $child) {
$childrenCollection->add($child);
}
}
}
/**
* @param array $nodes
* @return array
*/
protected function getRootNodes($nodes)
{
$idHashmap = $this->buildIdHashmap($nodes);
$rootNodes = array();
foreach ($nodes as $node) {
$parentProxy = $this->getPropertyValue($node, $this->config['parent']);
$parentId = null;
if ($parentProxy !== null) {
$parentId = $this->getPropertyValue($parentProxy, $this->idField);
}
if ($parentId === null || !key_exists($parentId, $idHashmap)) {
$rootNodes[] = $node;
}
}
return $rootNodes;
}
/**
* Creates a hashmap of all nodes returned in the query
*
* ```
* [node1.id => true, node2.id => true, ...]
* ```
*
* @param array $nodes
* @return array
*/
protected function buildIdHashmap(array $nodes)
{
$ids = array();
foreach ($nodes as $node) {
$id = $this->getPropertyValue($node, $this->idField);
$ids[$id] = true;
}
return $ids;
}
/**
* @return string
*/
protected function getIdField($entityClass)
{
$meta = $this->getClassMetadata($entityClass);
return $meta->getSingleIdentifierFieldName();
}
/**
* @return string
*/
protected function getParentField()
{
if (!isset($this->config['parent'])) {
throw new \Gedmo\Exception\InvalidMappingException('The `parent` property is required for the TreeHydrator to work');
}
return $this->config['parent'];
}
/**
* @return string
*/
protected function getChildrenField($entityClass)
{
$meta = $this->getClassMetadata($entityClass);
foreach ($meta->getReflectionProperties() as $property) {
// Skip properties that have no association
if (!$meta->hasAssociation($property->getName())) {
continue;
}
$associationMapping = $meta->getAssociationMapping($property->getName());
// Make sure the association is mapped by the parent property
if ($associationMapping['mappedBy'] !== $this->parentField) {
continue;
}
return $associationMapping['fieldName'];
}
throw new \Gedmo\Exception\InvalidMappingException('The children property could not found. It is identified through the `mappedBy` annotation to your parent property.');
}
/**
* @param EntityManagerInterface $em
* @return TreeListener
*/
protected function getTreeListener(EntityManagerInterface $em)
{
foreach ($em->getEventManager()->getListeners() as $listeners) {
foreach ($listeners as $listener) {
if ($listener instanceof TreeListener) {
return $listener;
}
}
}
throw new \Gedmo\Exception\InvalidMappingException('Tree listener was not found on your entity manager, it must be hooked into the event manager');
}
/**
* @param array $data
* @return string
*/
protected function getEntityClassFromHydratedData($data)
{
$firstMappedEntity = array_values($data);
$firstMappedEntity = $firstMappedEntity[0];
return $this->_em->getClassMetadata(get_class($firstMappedEntity))->rootEntityName;
}
protected function getPropertyValue($object, $property)
{
$meta = $this->_em->getClassMetadata(get_class($object));
return $meta->getReflectionProperty($property)->getValue($object);
}
public function setPropertyValue($object, $property, $value)
{
$meta = $this->_em->getClassMetadata(get_class($object));
$meta->getReflectionProperty($property)->setValue($object, $value);
}
}

View File

@@ -0,0 +1,252 @@
<?php
namespace Gedmo\Tree\Mapping\Driver;
use Gedmo\Mapping\Driver\AbstractAnnotationDriver;
use Gedmo\Exception\InvalidMappingException;
use Gedmo\Tree\Mapping\Validator;
/**
* This is an annotation mapping driver for Tree
* behavioral extension. Used for extraction of extended
* metadata from Annotations specifically for Tree
* extension.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @author <rocco@roccosportal.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Annotation extends AbstractAnnotationDriver
{
/**
* Annotation to define the tree type
*/
const TREE = 'Gedmo\\Mapping\\Annotation\\Tree';
/**
* Annotation to mark field as one which will store left value
*/
const LEFT = 'Gedmo\\Mapping\\Annotation\\TreeLeft';
/**
* Annotation to mark field as one which will store right value
*/
const RIGHT = 'Gedmo\\Mapping\\Annotation\\TreeRight';
/**
* Annotation to mark relative parent field
*/
const PARENT = 'Gedmo\\Mapping\\Annotation\\TreeParent';
/**
* Annotation to mark node level
*/
const LEVEL = 'Gedmo\\Mapping\\Annotation\\TreeLevel';
/**
* Annotation to mark field as tree root
*/
const ROOT = 'Gedmo\\Mapping\\Annotation\\TreeRoot';
/**
* Annotation to specify closure tree class
*/
const CLOSURE = 'Gedmo\\Mapping\\Annotation\\TreeClosure';
/**
* Annotation to specify path class
*/
const PATH = 'Gedmo\\Mapping\\Annotation\\TreePath';
/**
* Annotation to specify path source class
*/
const PATH_SOURCE = 'Gedmo\\Mapping\\Annotation\\TreePathSource';
/**
* Annotation to specify path hash class
*/
const PATH_HASH = 'Gedmo\\Mapping\\Annotation\\TreePathHash';
/**
* Annotation to mark the field to be used to hold the lock time
*/
const LOCK_TIME = 'Gedmo\\Mapping\\Annotation\\TreeLockTime';
/**
* List of tree strategies available
*
* @var array
*/
protected $strategies = array(
'nested',
'closure',
'materializedPath',
);
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
$validator = new Validator();
$class = $this->getMetaReflectionClass($meta);
// class annotations
if ($annot = $this->reader->getClassAnnotation($class, self::TREE)) {
if (!in_array($annot->type, $this->strategies)) {
throw new InvalidMappingException("Tree type: {$annot->type} is not available.");
}
$config['strategy'] = $annot->type;
$config['activate_locking'] = $annot->activateLocking;
$config['locking_timeout'] = (int) $annot->lockingTimeout;
if ($config['locking_timeout'] < 1) {
throw new InvalidMappingException("Tree Locking Timeout must be at least of 1 second.");
}
}
if ($annot = $this->reader->getClassAnnotation($class, self::CLOSURE)) {
if (!$cl = $this->getRelatedClassName($meta, $annot->class)) {
throw new InvalidMappingException("Tree closure class: {$annot->class} does not exist.");
}
$config['closure'] = $cl;
}
// property annotations
foreach ($class->getProperties() as $property) {
if ($meta->isMappedSuperclass && !$property->isPrivate() ||
$meta->isInheritedField($property->name) ||
isset($meta->associationMappings[$property->name]['inherited'])
) {
continue;
}
// left
if ($this->reader->getPropertyAnnotation($property, self::LEFT)) {
$field = $property->getName();
if (!$meta->hasField($field)) {
throw new InvalidMappingException("Unable to find 'left' - [{$field}] as mapped property in entity - {$meta->name}");
}
if (!$validator->isValidField($meta, $field)) {
throw new InvalidMappingException("Tree left field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}");
}
$config['left'] = $field;
}
// right
if ($this->reader->getPropertyAnnotation($property, self::RIGHT)) {
$field = $property->getName();
if (!$meta->hasField($field)) {
throw new InvalidMappingException("Unable to find 'right' - [{$field}] as mapped property in entity - {$meta->name}");
}
if (!$validator->isValidField($meta, $field)) {
throw new InvalidMappingException("Tree right field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}");
}
$config['right'] = $field;
}
// ancestor/parent
if ($this->reader->getPropertyAnnotation($property, self::PARENT)) {
$field = $property->getName();
if (!$meta->isSingleValuedAssociation($field)) {
throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->name}");
}
$config['parent'] = $field;
}
// root
if ($this->reader->getPropertyAnnotation($property, self::ROOT)) {
$field = $property->getName();
if (!$meta->isSingleValuedAssociation($field)) {
if (!$meta->hasField($field)) {
throw new InvalidMappingException("Unable to find 'root' - [{$field}] as mapped property in entity - {$meta->name}");
}
if (!$validator->isValidFieldForRoot($meta, $field)) {
throw new InvalidMappingException(
"Tree root field should be either a literal property ('integer' types or 'string') or a many-to-one association through root field - [{$field}] in class - {$meta->name}"
);
}
}
$annotation = $this->reader->getPropertyAnnotation($property, self::ROOT);
$config['rootIdentifierMethod'] = $annotation->identifierMethod;
$config['root'] = $field;
}
// level
if ($this->reader->getPropertyAnnotation($property, self::LEVEL)) {
$field = $property->getName();
if (!$meta->hasField($field)) {
throw new InvalidMappingException("Unable to find 'level' - [{$field}] as mapped property in entity - {$meta->name}");
}
if (!$validator->isValidField($meta, $field)) {
throw new InvalidMappingException("Tree level field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}");
}
$config['level'] = $field;
}
// path
if ($pathAnnotation = $this->reader->getPropertyAnnotation($property, self::PATH)) {
$field = $property->getName();
if (!$meta->hasField($field)) {
throw new InvalidMappingException("Unable to find 'path' - [{$field}] as mapped property in entity - {$meta->name}");
}
if (!$validator->isValidFieldForPath($meta, $field)) {
throw new InvalidMappingException("Tree Path field - [{$field}] type is not valid. It must be string or text in class - {$meta->name}");
}
if (strlen($pathAnnotation->separator) > 1) {
throw new InvalidMappingException("Tree Path field - [{$field}] Separator {$pathAnnotation->separator} is invalid. It must be only one character long.");
}
$config['path'] = $field;
$config['path_separator'] = $pathAnnotation->separator;
$config['path_append_id'] = $pathAnnotation->appendId;
$config['path_starts_with_separator'] = $pathAnnotation->startsWithSeparator;
$config['path_ends_with_separator'] = $pathAnnotation->endsWithSeparator;
}
// path source
if ($this->reader->getPropertyAnnotation($property, self::PATH_SOURCE)) {
$field = $property->getName();
if (!$meta->hasField($field)) {
throw new InvalidMappingException("Unable to find 'path_source' - [{$field}] as mapped property in entity - {$meta->name}");
}
if (!$validator->isValidFieldForPathSource($meta, $field)) {
throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->name}");
}
$config['path_source'] = $field;
}
// path hash
if ($this->reader->getPropertyAnnotation($property, self::PATH_HASH)) {
$field = $property->getName();
if (!$meta->hasField($field)) {
throw new InvalidMappingException("Unable to find 'path_hash' - [{$field}] as mapped property in entity - {$meta->name}");
}
if (!$validator->isValidFieldForPathHash($meta, $field)) {
throw new InvalidMappingException("Tree PathHash field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->name}");
}
$config['path_hash'] = $field;
}
// lock time
if ($this->reader->getPropertyAnnotation($property, self::LOCK_TIME)) {
$field = $property->getName();
if (!$meta->hasField($field)) {
throw new InvalidMappingException("Unable to find 'lock_time' - [{$field}] as mapped property in entity - {$meta->name}");
}
if (!$validator->isValidFieldForLockTime($meta, $field)) {
throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It must be \"date\" in class - {$meta->name}");
}
$config['lock_time'] = $field;
}
}
if (isset($config['activate_locking']) && $config['activate_locking'] && !isset($config['lock_time'])) {
throw new InvalidMappingException("You need to map a date field as the tree lock time field to activate locking support.");
}
if (!$meta->isMappedSuperclass && $config) {
if (isset($config['strategy'])) {
if (is_array($meta->identifier) && count($meta->identifier) > 1) {
throw new InvalidMappingException("Tree does not support composite identifiers in class - {$meta->name}");
}
$method = 'validate'.ucfirst($config['strategy']).'TreeMetadata';
$validator->$method($meta, $config);
} else {
throw new InvalidMappingException("Cannot find Tree type for class: {$meta->name}");
}
}
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace Gedmo\Tree\Mapping\Driver;
use Gedmo\Mapping\Driver\Xml as BaseXml;
use Gedmo\Exception\InvalidMappingException;
use Gedmo\Tree\Mapping\Validator;
/**
* This is a xml mapping driver for Tree
* behavioral extension. Used for extraction of extended
* metadata from xml specifically for Tree
* extension.
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @author Miha Vrhovnik <miha.vrhovnik@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Xml extends BaseXml
{
/**
* List of tree strategies available
*
* @var array
*/
private $strategies = array(
'nested',
'closure',
'materializedPath',
);
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
/**
* @var \SimpleXmlElement $xml
*/
$xml = $this->_getMapping($meta->name);
$xmlDoctrine = $xml;
$xml = $xml->children(self::GEDMO_NAMESPACE_URI);
$validator = new Validator();
if (isset($xml->tree) && $this->_isAttributeSet($xml->tree, 'type')) {
$strategy = $this->_getAttribute($xml->tree, 'type');
if (!in_array($strategy, $this->strategies)) {
throw new InvalidMappingException("Tree type: $strategy is not available.");
}
$config['strategy'] = $strategy;
$config['activate_locking'] = $this->_getAttribute($xml->tree, 'activate-locking') === 'true' ? true : false;
if ($lockingTimeout = $this->_getAttribute($xml->tree, 'locking-timeout')) {
$config['locking_timeout'] = (int) $lockingTimeout;
if ($config['locking_timeout'] < 1) {
throw new InvalidMappingException("Tree Locking Timeout must be at least of 1 second.");
}
} else {
$config['locking_timeout'] = 3;
}
}
if (isset($xml->{'tree-closure'}) && $this->_isAttributeSet($xml->{'tree-closure'}, 'class')) {
$class = $this->_getAttribute($xml->{'tree-closure'}, 'class');
if (!$cl = $this->getRelatedClassName($meta, $class)) {
throw new InvalidMappingException("Tree closure class: {$class} does not exist.");
}
$config['closure'] = $cl;
}
if (isset($xmlDoctrine->field)) {
foreach ($xmlDoctrine->field as $mapping) {
$mappingDoctrine = $mapping;
$mapping = $mapping->children(self::GEDMO_NAMESPACE_URI);
$field = $this->_getAttribute($mappingDoctrine, 'name');
if (isset($mapping->{'tree-left'})) {
if (!$validator->isValidField($meta, $field)) {
throw new InvalidMappingException("Tree left field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}");
}
$config['left'] = $field;
} elseif (isset($mapping->{'tree-right'})) {
if (!$validator->isValidField($meta, $field)) {
throw new InvalidMappingException("Tree right field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}");
}
$config['right'] = $field;
} elseif (isset($mapping->{'tree-root'})) {
if (!$validator->isValidFieldForRoot($meta, $field)) {
throw new InvalidMappingException("Tree root field - [{$field}] type is not valid and must be any of the 'integer' types or 'string' in class - {$meta->name}");
}
$config['root'] = $field;
} elseif (isset($mapping->{'tree-level'})) {
if (!$validator->isValidField($meta, $field)) {
throw new InvalidMappingException("Tree level field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}");
}
$config['level'] = $field;
} elseif (isset($mapping->{'tree-path'})) {
if (!$validator->isValidFieldForPath($meta, $field)) {
throw new InvalidMappingException("Tree Path field - [{$field}] type is not valid. It must be string or text in class - {$meta->name}");
}
$separator = $this->_getAttribute($mapping->{'tree-path'}, 'separator');
if (strlen($separator) > 1) {
throw new InvalidMappingException("Tree Path field - [{$field}] Separator {$separator} is invalid. It must be only one character long.");
}
$appendId = $this->_getAttribute($mapping->{'tree-path'}, 'append_id');
if (!$appendId) {
$appendId = true;
} else {
$appendId = strtolower($appendId) == 'false' ? false : true;
}
$startsWithSeparator = $this->_getAttribute($mapping->{'tree-path'}, 'starts_with_separator');
if (!$startsWithSeparator) {
$startsWithSeparator = false;
} else {
$startsWithSeparator = strtolower($startsWithSeparator) == 'false' ? false : true;
}
$endsWithSeparator = $this->_getAttribute($mapping->{'tree-path'}, 'ends_with_separator');
if (!$endsWithSeparator) {
$endsWithSeparator = true;
} else {
$endsWithSeparator = strtolower($endsWithSeparator) == 'false' ? false : true;
}
$config['path'] = $field;
$config['path_separator'] = $separator;
$config['path_append_id'] = $appendId;
$config['path_starts_with_separator'] = $startsWithSeparator;
$config['path_ends_with_separator'] = $endsWithSeparator;
} elseif (isset($mapping->{'tree-path-source'})) {
if (!$validator->isValidFieldForPathSource($meta, $field)) {
throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->name}");
}
$config['path_source'] = $field;
} elseif (isset($mapping->{'tree-lock-time'})) {
if (!$validator->isValidFieldForLockTime($meta, $field)) {
throw new InvalidMappingException("Tree LockTime field - [{$field}] type is not valid. It must be \"date\" in class - {$meta->name}");
}
$config['lock_time'] = $field;
}
}
}
if (isset($config['activate_locking']) && $config['activate_locking'] && !isset($config['lock_time'])) {
throw new InvalidMappingException("You need to map a date field as the tree lock time field to activate locking support.");
}
if ($xmlDoctrine->getName() == 'mapped-superclass') {
if (isset($xmlDoctrine->{'many-to-one'})) {
foreach ($xmlDoctrine->{'many-to-one'} as $manyToOneMapping) {
/**
* @var \SimpleXMLElement $manyToOneMapping
*/
$manyToOneMappingDoctrine = $manyToOneMapping;
$manyToOneMapping = $manyToOneMapping->children(self::GEDMO_NAMESPACE_URI);
if (isset($manyToOneMapping->{'tree-parent'})) {
$field = $this->_getAttribute($manyToOneMappingDoctrine, 'field');
$targetEntity = $meta->associationMappings[$field]['targetEntity'];
if (!$cl = $this->getRelatedClassName($meta, $targetEntity)) {
throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->name}");
}
$config['parent'] = $field;
}
if (isset($manyToOneMapping->{'tree-root'})) {
$field = $this->_getAttribute($manyToOneMappingDoctrine, 'field');
$targetEntity = $meta->associationMappings[$field]['targetEntity'];
if (!$cl = $this->getRelatedClassName($meta, $targetEntity)) {
throw new InvalidMappingException("Unable to find root descendant relation through root field - [{$field}] in class - {$meta->name}");
}
$config['root'] = $field;
}
}
} elseif (isset($xmlDoctrine->{'reference-one'})) {
foreach ($xmlDoctrine->{'reference-one'} as $referenceOneMapping) {
/**
* @var \SimpleXMLElement $referenceOneMapping
*/
$referenceOneMappingDoctrine = $referenceOneMapping;
$referenceOneMapping = $referenceOneMapping->children(self::GEDMO_NAMESPACE_URI);
if (isset($referenceOneMapping->{'tree-parent'})) {
$field = $this->_getAttribute($referenceOneMappingDoctrine, 'field');
if (!$cl = $this->getRelatedClassName($meta, $this->_getAttribute($referenceOneMappingDoctrine, 'target-document'))) {
throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->name}");
}
$config['parent'] = $field;
}
if (isset($referenceOneMapping->{'tree-root'})) {
$field = $this->_getAttribute($referenceOneMappingDoctrine, 'field');
if (!$cl = $this->getRelatedClassName($meta, $this->_getAttribute($referenceOneMappingDoctrine, 'target-document'))) {
throw new InvalidMappingException("Unable to find root descendant relation through root field - [{$field}] in class - {$meta->name}");
}
$config['root'] = $field;
}
}
}
} elseif ($xmlDoctrine->getName() == 'entity') {
if (isset($xmlDoctrine->{'many-to-one'})) {
foreach ($xmlDoctrine->{'many-to-one'} as $manyToOneMapping) {
/**
* @var \SimpleXMLElement $manyToOneMapping
*/
$manyToOneMappingDoctrine = $manyToOneMapping;
$manyToOneMapping = $manyToOneMapping->children(self::GEDMO_NAMESPACE_URI);
if (isset($manyToOneMapping->{'tree-parent'})) {
$field = $this->_getAttribute($manyToOneMappingDoctrine, 'field');
$targetEntity = $meta->associationMappings[$field]['targetEntity'];
if (!$cl = $this->getRelatedClassName($meta, $targetEntity)) {
throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->name}");
}
$config['parent'] = $field;
}
if (isset($manyToOneMapping->{'tree-root'})) {
$field = $this->_getAttribute($manyToOneMappingDoctrine, 'field');
$targetEntity = $meta->associationMappings[$field]['targetEntity'];
if (!$cl = $this->getRelatedClassName($meta, $targetEntity)) {
throw new InvalidMappingException("Unable to find root descendant relation through root field - [{$field}] in class - {$meta->name}");
}
$config['root'] = $field;
}
}
}
} elseif ($xmlDoctrine->getName() == 'document') {
if (isset($xmlDoctrine->{'reference-one'})) {
foreach ($xmlDoctrine->{'reference-one'} as $referenceOneMapping) {
/**
* @var \SimpleXMLElement $referenceOneMapping
*/
$referenceOneMappingDoctrine = $referenceOneMapping;
$referenceOneMapping = $referenceOneMapping->children(self::GEDMO_NAMESPACE_URI);
if (isset($referenceOneMapping->{'tree-parent'})) {
$field = $this->_getAttribute($referenceOneMappingDoctrine, 'field');
if (!$cl = $this->getRelatedClassName($meta, $this->_getAttribute($referenceOneMappingDoctrine, 'target-document'))) {
throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->name}");
}
$config['parent'] = $field;
}
if (isset($referenceOneMapping->{'tree-root'})) {
$field = $this->_getAttribute($referenceOneMappingDoctrine, 'field');
if (!$cl = $this->getRelatedClassName($meta, $this->_getAttribute($referenceOneMappingDoctrine, 'target-document'))) {
throw new InvalidMappingException("Unable to find root descendant relation through root field - [{$field}] in class - {$meta->name}");
}
$config['root'] = $field;
}
}
}
}
if (!$meta->isMappedSuperclass && $config) {
if (isset($config['strategy'])) {
if (is_array($meta->identifier) && count($meta->identifier) > 1) {
throw new InvalidMappingException("Tree does not support composite identifiers in class - {$meta->name}");
}
$method = 'validate'.ucfirst($config['strategy']).'TreeMetadata';
$validator->$method($meta, $config);
} else {
throw new InvalidMappingException("Cannot find Tree type for class: {$meta->name}");
}
}
}
}

View File

@@ -0,0 +1,215 @@
<?php
namespace Gedmo\Tree\Mapping\Driver;
use Gedmo\Mapping\Driver\File;
use Gedmo\Mapping\Driver;
use Gedmo\Exception\InvalidMappingException;
use Gedmo\Tree\Mapping\Validator;
/**
* This is a yaml mapping driver for Tree
* behavioral extension. Used for extraction of extended
* metadata from yaml specifically for Tree
* extension.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Yaml extends File implements Driver
{
/**
* File extension
* @var string
*/
protected $_extension = '.dcm.yml';
/**
* List of tree strategies available
*
* @var array
*/
private $strategies = array(
'nested',
'closure',
'materializedPath',
);
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
$mapping = $this->_getMapping($meta->name);
$validator = new Validator();
if (isset($mapping['gedmo'])) {
$classMapping = $mapping['gedmo'];
if (isset($classMapping['tree']['type'])) {
$strategy = $classMapping['tree']['type'];
if (!in_array($strategy, $this->strategies)) {
throw new InvalidMappingException("Tree type: $strategy is not available.");
}
$config['strategy'] = $strategy;
$config['activate_locking'] = isset($classMapping['tree']['activateLocking']) ?
$classMapping['tree']['activateLocking'] : false;
$config['locking_timeout'] = isset($classMapping['tree']['lockingTimeout']) ?
(int) $classMapping['tree']['lockingTimeout'] : 3;
if ($config['locking_timeout'] < 1) {
throw new InvalidMappingException("Tree Locking Timeout must be at least of 1 second.");
}
}
if (isset($classMapping['tree']['closure'])) {
if (!$class = $this->getRelatedClassName($meta, $classMapping['tree']['closure'])) {
throw new InvalidMappingException("Tree closure class: {$classMapping['tree']['closure']} does not exist.");
}
$config['closure'] = $class;
}
}
if (isset($mapping['id'])) {
foreach($mapping['id'] as $field => $fieldMapping) {
if (isset($fieldMapping['gedmo'])) {
if (in_array('treePathSource', $fieldMapping['gedmo'])) {
if (!$validator->isValidFieldForPathSource($meta, $field)) {
throw new InvalidMappingException(
"Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->name}"
);
}
$config['path_source'] = $field;
}
}
}
}
if (isset($mapping['fields'])) {
foreach ($mapping['fields'] as $field => $fieldMapping) {
if (isset($fieldMapping['gedmo'])) {
if (in_array('treeLeft', $fieldMapping['gedmo'])) {
if (!$validator->isValidField($meta, $field)) {
throw new InvalidMappingException("Tree left field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}");
}
$config['left'] = $field;
} elseif (in_array('treeRight', $fieldMapping['gedmo'])) {
if (!$validator->isValidField($meta, $field)) {
throw new InvalidMappingException("Tree right field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}");
}
$config['right'] = $field;
} elseif (in_array('treeLevel', $fieldMapping['gedmo'])) {
if (!$validator->isValidField($meta, $field)) {
throw new InvalidMappingException("Tree level field - [{$field}] type is not valid and must be 'integer' in class - {$meta->name}");
}
$config['level'] = $field;
} elseif (in_array('treeRoot', $fieldMapping['gedmo'])) {
if (!$validator->isValidFieldForRoot($meta, $field)) {
throw new InvalidMappingException("Tree root field - [{$field}] type is not valid and must be any of the 'integer' types or 'string' in class - {$meta->name}");
}
$config['root'] = $field;
} elseif (in_array('treePath', $fieldMapping['gedmo']) || isset($fieldMapping['gedmo']['treePath'])) {
if (!$validator->isValidFieldForPath($meta, $field)) {
throw new InvalidMappingException("Tree Path field - [{$field}] type is not valid. It must be string or text in class - {$meta->name}");
}
$treePathInfo = isset($fieldMapping['gedmo']['treePath']) ? $fieldMapping['gedmo']['treePath'] :
$fieldMapping['gedmo'][array_search('treePath', $fieldMapping['gedmo'])];
if (is_array($treePathInfo) && isset($treePathInfo['separator'])) {
$separator = $treePathInfo['separator'];
} else {
$separator = '|';
}
if (strlen($separator) > 1) {
throw new InvalidMappingException("Tree Path field - [{$field}] Separator {$separator} is invalid. It must be only one character long.");
}
if (is_array($treePathInfo) && isset($treePathInfo['appendId'])) {
$appendId = $treePathInfo['appendId'];
} else {
$appendId = null;
}
if (is_array($treePathInfo) && isset($treePathInfo['startsWithSeparator'])) {
$startsWithSeparator = $treePathInfo['startsWithSeparator'];
} else {
$startsWithSeparator = false;
}
if (is_array($treePathInfo) && isset($treePathInfo['endsWithSeparator'])) {
$endsWithSeparator = $treePathInfo['endsWithSeparator'];
} else {
$endsWithSeparator = true;
}
$config['path'] = $field;
$config['path_separator'] = $separator;
$config['path_append_id'] = $appendId;
$config['path_starts_with_separator'] = $startsWithSeparator;
$config['path_ends_with_separator'] = $endsWithSeparator;
} elseif (in_array('treePathSource', $fieldMapping['gedmo'])) {
if (!$validator->isValidFieldForPathSource($meta, $field)) {
throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->name}");
}
$config['path_source'] = $field;
} elseif (in_array('treePathHash', $fieldMapping['gedmo'])) {
if (!$validator->isValidFieldForPathSource($meta, $field)) {
throw new InvalidMappingException("Tree PathHash field - [{$field}] type is not valid and must be 'string' in class - {$meta->name}");
}
$config['path_hash'] = $field;
} elseif (in_array('treeLockTime', $fieldMapping['gedmo'])) {
if (!$validator->isValidFieldForLocktime($meta, $field)) {
throw new InvalidMappingException("Tree LockTime field - [{$field}] type is not valid. It must be \"date\" in class - {$meta->name}");
}
$config['lock_time'] = $field;
} elseif (in_array('treeParent', $fieldMapping['gedmo'])) {
$config['parent'] = $field;
}
}
}
}
if (isset($config['activate_locking']) && $config['activate_locking'] && !isset($config['lock_time'])) {
throw new InvalidMappingException("You need to map a date|datetime|timestamp field as the tree lock time field to activate locking support.");
}
if (isset($mapping['manyToOne'])) {
foreach ($mapping['manyToOne'] as $field => $relationMapping) {
if (isset($relationMapping['gedmo'])) {
if (in_array('treeParent', $relationMapping['gedmo'])) {
if (!$rel = $this->getRelatedClassName($meta, $relationMapping['targetEntity'])) {
throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->name}");
}
$config['parent'] = $field;
}
if (in_array('treeRoot', $relationMapping['gedmo'])) {
if (!$rel = $this->getRelatedClassName($meta, $relationMapping['targetEntity'])) {
throw new InvalidMappingException("Unable to find root-descendant relation through root field - [{$field}] in class - {$meta->name}");
}
$config['root'] = $field;
}
}
}
}
if (!$meta->isMappedSuperclass && $config) {
if (isset($config['strategy'])) {
if (is_array($meta->identifier) && count($meta->identifier) > 1) {
throw new InvalidMappingException("Tree does not support composite identifiers in class - {$meta->name}");
}
$method = 'validate'.ucfirst($config['strategy']).'TreeMetadata';
$validator->$method($meta, $config);
} else {
throw new InvalidMappingException("Cannot find Tree type for class: {$meta->name}");
}
}
}
/**
* {@inheritDoc}
*/
protected function _loadMappingFile($file)
{
return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file));
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Tree\Mapping\Event\Adapter;
use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM;
use Gedmo\Tree\Mapping\Event\TreeAdapter;
/**
* Doctrine event adapter for ODM adapted
* for Tree behavior
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class ODM extends BaseAdapterODM implements TreeAdapter
{
// Nothing specific yet
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Tree\Mapping\Event\Adapter;
use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM;
use Gedmo\Tree\Mapping\Event\TreeAdapter;
/**
* Doctrine event adapter for ORM adapted
* for Tree behavior
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class ORM extends BaseAdapterORM implements TreeAdapter
{
// Nothing specific yet
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Gedmo\Tree\Mapping\Event;
use Gedmo\Mapping\Event\AdapterInterface;
/**
* Doctrine event adapter interface
* for Tree behavior
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface TreeAdapter extends AdapterInterface
{
}

View File

@@ -0,0 +1,240 @@
<?php
namespace Gedmo\Tree\Mapping;
use Gedmo\Exception\InvalidMappingException;
/**
* This is a validator for all mapping drivers for Tree
* behavioral extension, containing methods to validate
* mapping information
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @author <rocco@roccosportal.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Validator
{
/**
* List of types which are valid for tree fields
*
* @var array
*/
private $validTypes = array(
'integer',
'smallint',
'bigint',
'int',
);
/**
* List of types which are valid for the path (materialized path strategy)
*
* @var array
*/
private $validPathTypes = array(
'string',
'text',
);
/**
* List of types which are valid for the path source (materialized path strategy)
*
* @var array
*/
private $validPathSourceTypes = array(
'id',
'integer',
'smallint',
'bigint',
'string',
'int',
'float',
);
/**
* List of types which are valid for the path hash (materialized path strategy)
*
* @var array
*/
private $validPathHashTypes = array(
'string',
);
/**
* List of types which are valid for the path source (materialized path strategy)
*
* @var array
*/
private $validRootTypes = array(
'integer',
'smallint',
'bigint',
'int',
'string',
'guid',
);
/**
* Checks if $field type is valid
*
* @param object $meta
* @param string $field
*
* @return boolean
*/
public function isValidField($meta, $field)
{
$mapping = $meta->getFieldMapping($field);
return $mapping && in_array($mapping['type'], $this->validTypes);
}
/**
* Checks if $field type is valid for Path field
*
* @param object $meta
* @param string $field
*
* @return boolean
*/
public function isValidFieldForPath($meta, $field)
{
$mapping = $meta->getFieldMapping($field);
return $mapping && in_array($mapping['type'], $this->validPathTypes);
}
/**
* Checks if $field type is valid for PathSource field
*
* @param object $meta
* @param string $field
*
* @return boolean
*/
public function isValidFieldForPathSource($meta, $field)
{
$mapping = $meta->getFieldMapping($field);
return $mapping && in_array($mapping['type'], $this->validPathSourceTypes);
}
/**
* Checks if $field type is valid for PathHash field
*
* @param object $meta
* @param string $field
*
* @return boolean
*/
public function isValidFieldForPathHash($meta, $field)
{
$mapping = $meta->getFieldMapping($field);
return $mapping && in_array($mapping['type'], $this->validPathHashTypes);
}
/**
* Checks if $field type is valid for LockTime field
*
* @param object $meta
* @param string $field
*
* @return boolean
*/
public function isValidFieldForLockTime($meta, $field)
{
$mapping = $meta->getFieldMapping($field);
return $mapping && ($mapping['type'] === 'date' || $mapping['type'] === 'datetime' || $mapping['type'] === 'timestamp');
}
/**
* Checks if $field type is valid for Root field
*
* @param object $meta
* @param string $field
*
* @return boolean
*/
public function isValidFieldForRoot($meta, $field)
{
$mapping = $meta->getFieldMapping($field);
return $mapping && in_array($mapping['type'], $this->validRootTypes);
}
/**
* Validates metadata for nested type tree
*
* @param object $meta
* @param array $config
*
* @throws InvalidMappingException
*/
public function validateNestedTreeMetadata($meta, array $config)
{
$missingFields = array();
if (!isset($config['parent'])) {
$missingFields[] = 'ancestor';
}
if (!isset($config['left'])) {
$missingFields[] = 'left';
}
if (!isset($config['right'])) {
$missingFields[] = 'right';
}
if ($missingFields) {
throw new InvalidMappingException("Missing properties: ".implode(', ', $missingFields)." in class - {$meta->name}");
}
}
/**
* Validates metadata for closure type tree
*
* @param object $meta
* @param array $config
*
* @throws InvalidMappingException
*/
public function validateClosureTreeMetadata($meta, array $config)
{
$missingFields = array();
if (!isset($config['parent'])) {
$missingFields[] = 'ancestor';
}
if (!isset($config['closure'])) {
$missingFields[] = 'closure class';
}
if ($missingFields) {
throw new InvalidMappingException("Missing properties: ".implode(', ', $missingFields)." in class - {$meta->name}");
}
}
/**
* Validates metadata for materialized path type tree
*
* @param object $meta
* @param array $config
*
* @throws InvalidMappingException
*/
public function validateMaterializedPathTreeMetadata($meta, array $config)
{
$missingFields = array();
if (!isset($config['parent'])) {
$missingFields[] = 'ancestor';
}
if (!isset($config['path'])) {
$missingFields[] = 'path';
}
if (!isset($config['path_source'])) {
$missingFields[] = 'path_source';
}
if ($missingFields) {
throw new InvalidMappingException("Missing properties: ".implode(', ', $missingFields)." in class - {$meta->name}");
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Gedmo\Tree;
/**
* This interface is not necessary but can be implemented for
* Entities which in some cases needs to be identified as
* Tree Node
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface Node
{
// use now annotations instead of predefined methods, this interface is not necessary
/**
* @gedmo:TreeLeft
* to mark the field as "tree left" use property annotation @gedmo:TreeLeft
* it will use this field to store tree left value
*/
/**
* @gedmo:TreeRight
* to mark the field as "tree right" use property annotation @gedmo:TreeRight
* it will use this field to store tree right value
*/
/**
* @gedmo:TreeParent
* in every tree there should be link to parent. To identify a relation
* as parent relation to child use @Tree:Ancestor annotation on the related property
*/
/**
* @gedmo:TreeLevel
* level of node.
*/
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Gedmo\Tree;
/**
* This interface ensures a consistent api between repositories for the ORM and the ODM.
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface RepositoryInterface extends RepositoryUtilsInterface
{
/**
* Get all root nodes
*
* @param string $sortByField
* @param string $direction
*
* @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 $options - Options
* @param boolean $includeNode - Include node in results?
*
* @return array - Array of nodes
*/
public function getNodesHierarchy($node = null, $direct = false, 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 File

@@ -0,0 +1,183 @@
<?php
namespace Gedmo\Tree;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\Common\Persistence\ObjectManager;
use Gedmo\Exception\InvalidArgumentException;
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;
/**
* This index is used to hold the children of a node
* when using any of the buildTree related methods.
*
* @var string
*/
protected $childrenIndex = '__children';
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();
if ($node !== null) {
if ($node instanceof $meta->name) {
$wrapperClass = $this->om instanceof \Doctrine\ORM\EntityManagerInterface ?
'\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, $options, $includeNode);
return $this->repo->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 '';
}
$childrenIndex = $this->childrenIndex;
$build = function ($tree) use (&$build, &$options, $childrenIndex) {
$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[$childrenIndex]) > 0) {
$output .= $build($node[$childrenIndex]);
}
$output .= is_string($options['childClose']) ? $options['childClose'] : $options['childClose']($node);
}
return $output.(is_string($options['rootClose']) ? $options['rootClose'] : $options['rootClose']($tree));
};
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[$this->childrenIndex] = 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][$this->childrenIndex]);
$stack[$l - 1][$this->childrenIndex][$i] = $item;
$stack[] = &$stack[$l - 1][$this->childrenIndex][$i];
}
}
}
return $nestedTree;
}
/**
* {@inheritDoc}
*/
public function setChildrenIndex($childrenIndex)
{
$this->childrenIndex = $childrenIndex;
}
/**
* {@inheritDoc}
*/
public function getChildrenIndex()
{
return $this->childrenIndex;
}
}

View File

@@ -0,0 +1,74 @@
<?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
*
* @return array|string
*/
public function buildTree(array $nodes, array $options = array());
/**
* Process nodes and produce an array with the
* structure of the tree
*
* @param array $nodes - Array of nodes
*
* @return array - Array with tree structure
*/
public function buildTreeArray(array $nodes);
/**
* Sets the current children index.
*
* @param string $childrenIndex
*/
public function setChildrenIndex($childrenIndex);
/**
* Gets the current children index.
*
* @return string
*/
public function getChildrenIndex();
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Gedmo\Tree;
use Doctrine\Common\Persistence\ObjectManager;
use Gedmo\Mapping\Event\AdapterInterface;
interface Strategy
{
/**
* NestedSet strategy
*/
const NESTED = 'nested';
/**
* Closure strategy
*/
const CLOSURE = 'closure';
/**
* Materialized Path strategy
*/
const MATERIALIZED_PATH = 'materializedPath';
/**
* Get the name of strategy
*
* @return string
*/
public function getName();
/**
* Initialize strategy with tree listener
*
* @param TreeListener $listener
*/
public function __construct(TreeListener $listener);
/**
* Operations after metadata is loaded
*
* @param ObjectManager $om
* @param object $meta
*/
public function processMetadataLoad($om, $meta);
/**
* Operations on tree node insertion
*
* @param ObjectManager $om - object manager
* @param object $object - node
* @param AdapterInterface $ea - event adapter
*
* @return void
*/
public function processScheduledInsertion($om, $object, AdapterInterface $ea);
/**
* Operations on tree node updates
*
* @param ObjectManager $om - object manager
* @param object $object - node
* @param AdapterInterface $ea - event adapter
*
* @return void
*/
public function processScheduledUpdate($om, $object, AdapterInterface $ea);
/**
* Operations on tree node delete
*
* @param ObjectManager $om - object manager
* @param object $object - node
*
* @return void
*/
public function processScheduledDelete($om, $object);
/**
* Operations on tree node removal
*
* @param ObjectManager $om - object manager
* @param object $object - node
*
* @return void
*/
public function processPreRemove($om, $object);
/**
* Operations on tree node persist
*
* @param ObjectManager $om - object manager
* @param object $object - node
*
* @return void
*/
public function processPrePersist($om, $object);
/**
* Operations on tree node update
*
* @param ObjectManager $om - object manager
* @param object $object - node
*
* @return void
*/
public function processPreUpdate($om, $object);
/**
* Operations on tree node insertions
*
* @param ObjectManager $om - object manager
* @param object $object - node
* @param AdapterInterface $ea - event adapter
*
* @return void
*/
public function processPostPersist($om, $object, AdapterInterface $ea);
/**
* Operations on tree node updates
*
* @param ObjectManager $om - object manager
* @param object $object - node
* @param AdapterInterface $ea - event adapter
*
* @return void
*/
public function processPostUpdate($om, $object, AdapterInterface $ea);
/**
* Operations on tree node removals
*
* @param ObjectManager $om - object manager
* @param object $object - node
* @param AdapterInterface $ea - event adapter
*
* @return void
*/
public function processPostRemove($om, $object, AdapterInterface $ea);
/**
* Operations on the end of flush process
*
* @param ObjectManager $om - object manager
* @param AdapterInterface $ea - event adapter
*
* @return void
*/
public function onFlushEnd($om, AdapterInterface $ea);
}

View File

@@ -0,0 +1,539 @@
<?php
namespace Gedmo\Tree\Strategy;
use Gedmo\Tree\Strategy;
use Gedmo\Tree\TreeListener;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ODM\MongoDB\UnitOfWork as MongoDBUnitOfWork;
use Gedmo\Mapping\Event\AdapterInterface;
use Gedmo\Exception\RuntimeException;
use Gedmo\Exception\TreeLockingException;
/**
* This strategy makes tree using materialized path strategy
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @author <rocco@roccosportal.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
abstract class AbstractMaterializedPath implements Strategy
{
const ACTION_INSERT = 'insert';
const ACTION_UPDATE = 'update';
const ACTION_REMOVE = 'remove';
/**
* TreeListener
*
* @var AbstractTreeListener
*/
protected $listener = null;
/**
* Array of objects which were scheduled for path processes
*
* @var array
*/
protected $scheduledForPathProcess = array();
/**
* Array of objects which were scheduled for path process.
* This time, this array contains the objects with their ID
* already set
*
* @var array
*/
protected $scheduledForPathProcessWithIdSet = array();
/**
* Roots of trees which needs to be locked
*
* @var array
*/
protected $rootsOfTreesWhichNeedsLocking = array();
/**
* Objects which are going to be inserted (set only if tree locking is used)
*
* @var array
*/
protected $pendingObjectsToInsert = array();
/**
* Objects which are going to be updated (set only if tree locking is used)
*
* @var array
*/
protected $pendingObjectsToUpdate = array();
/**
* Objects which are going to be removed (set only if tree locking is used)
*
* @var array
*/
protected $pendingObjectsToRemove = array();
/**
* {@inheritdoc}
*/
public function __construct(TreeListener $listener)
{
$this->listener = $listener;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return Strategy::MATERIALIZED_PATH;
}
/**
* {@inheritdoc}
*/
public function processScheduledInsertion($om, $node, AdapterInterface $ea)
{
$meta = $om->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($om, $meta->name);
$fieldMapping = $meta->getFieldMapping($config['path_source']);
if ($meta->isIdentifier($config['path_source']) || $fieldMapping['type'] === 'string') {
$this->scheduledForPathProcess[spl_object_hash($node)] = $node;
} else {
$this->updateNode($om, $node, $ea);
}
}
/**
* {@inheritdoc}
*/
public function processScheduledUpdate($om, $node, AdapterInterface $ea)
{
$meta = $om->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($om, $meta->name);
$uow = $om->getUnitOfWork();
$changeSet = $ea->getObjectChangeSet($uow, $node);
if (isset($changeSet[$config['parent']]) || isset($changeSet[$config['path_source']])) {
if (isset($changeSet[$config['path']])) {
$originalPath = $changeSet[$config['path']][0];
} else {
$pathProp = $meta->getReflectionProperty($config['path']);
$pathProp->setAccessible(true);
$originalPath = $pathProp->getValue($node);
}
$this->updateNode($om, $node, $ea);
$this->updateChildren($om, $node, $ea, $originalPath);
}
}
/**
* {@inheritdoc}
*/
public function processPostPersist($om, $node, AdapterInterface $ea)
{
$oid = spl_object_hash($node);
if ($this->scheduledForPathProcess && array_key_exists($oid, $this->scheduledForPathProcess)) {
$this->scheduledForPathProcessWithIdSet[$oid] = $node;
unset($this->scheduledForPathProcess[$oid]);
if (empty($this->scheduledForPathProcess)) {
foreach ($this->scheduledForPathProcessWithIdSet as $oid => $node) {
$this->updateNode($om, $node, $ea);
unset($this->scheduledForPathProcessWithIdSet[$oid]);
}
}
}
$this->processPostEventsActions($om, $ea, $node, self::ACTION_INSERT);
}
/**
* {@inheritdoc}
*/
public function processPostUpdate($om, $node, AdapterInterface $ea)
{
$this->processPostEventsActions($om, $ea, $node, self::ACTION_UPDATE);
}
/**
* {@inheritdoc}
*/
public function processPostRemove($om, $node, AdapterInterface $ea)
{
$this->processPostEventsActions($om, $ea, $node, self::ACTION_REMOVE);
}
/**
* {@inheritdoc}
*/
public function onFlushEnd($om, AdapterInterface $ea)
{
$this->lockTrees($om, $ea);
}
/**
* {@inheritdoc}
*/
public function processPreRemove($om, $node)
{
$this->processPreLockingActions($om, $node, self::ACTION_REMOVE);
}
/**
* {@inheritdoc}
*/
public function processPrePersist($om, $node)
{
$this->processPreLockingActions($om, $node, self::ACTION_INSERT);
}
/**
* {@inheritdoc}
*/
public function processPreUpdate($om, $node)
{
$this->processPreLockingActions($om, $node, self::ACTION_UPDATE);
}
/**
* {@inheritdoc}
*/
public function processMetadataLoad($om, $meta)
{
}
/**
* {@inheritdoc}
*/
public function processScheduledDelete($om, $node)
{
$meta = $om->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($om, $meta->name);
$this->removeNode($om, $meta, $config, $node);
}
/**
* Update the $node
*
* @param ObjectManager $om
* @param object $node - target node
* @param AdapterInterface $ea - event adapter
*
* @return void
*/
public function updateNode(ObjectManager $om, $node, AdapterInterface $ea)
{
$oid = spl_object_hash($node);
$meta = $om->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($om, $meta->name);
$uow = $om->getUnitOfWork();
$parentProp = $meta->getReflectionProperty($config['parent']);
$parentProp->setAccessible(true);
$parent = $parentProp->getValue($node);
$pathProp = $meta->getReflectionProperty($config['path']);
$pathProp->setAccessible(true);
$pathSourceProp = $meta->getReflectionProperty($config['path_source']);
$pathSourceProp->setAccessible(true);
$path = (string) $pathSourceProp->getValue($node);
// We need to avoid the presence of the path separator in the path source
if (strpos($path, $config['path_separator']) !== false) {
$msg = 'You can\'t use the Path separator ("%s") as a character for your PathSource field value.';
throw new RuntimeException(sprintf($msg, $config['path_separator']));
}
$fieldMapping = $meta->getFieldMapping($config['path_source']);
// default behavior: if PathSource field is a string, we append the ID to the path
// path_append_id is true: always append id
// path_append_id is false: never append id
if ($config['path_append_id'] === true || ($fieldMapping['type'] === 'string' && $config['path_append_id'] !== false)) {
if (method_exists($meta, 'getIdentifierValue')) {
$identifier = $meta->getIdentifierValue($node);
} else {
$identifierProp = $meta->getReflectionProperty($meta->getSingleIdentifierFieldName());
$identifierProp->setAccessible(true);
$identifier = $identifierProp->getValue($node);
}
$path .= '-'.$identifier;
}
if ($parent) {
// Ensure parent has been initialized in the case where it's a proxy
$om->initializeObject($parent);
$changeSet = $uow->isScheduledForUpdate($parent) ? $ea->getObjectChangeSet($uow, $parent) : false;
$pathOrPathSourceHasChanged = $changeSet && (isset($changeSet[$config['path_source']]) || isset($changeSet[$config['path']]));
if ($pathOrPathSourceHasChanged || !$pathProp->getValue($parent)) {
$this->updateNode($om, $parent, $ea);
}
$parentPath = $pathProp->getValue($parent);
// if parent path not ends with separator
if ($parentPath[strlen($parentPath) - 1] !== $config['path_separator']) {
// add separator
$path = $pathProp->getValue($parent).$config['path_separator'].$path;
} else {
// don't add separator
$path = $pathProp->getValue($parent).$path;
}
}
if ($config['path_starts_with_separator'] && (strlen($path) > 0 && $path[0] !== $config['path_separator'])) {
$path = $config['path_separator'].$path;
}
if ($config['path_ends_with_separator'] && ($path[strlen($path) - 1] !== $config['path_separator'])) {
$path .= $config['path_separator'];
}
$pathProp->setValue($node, $path);
$changes = array(
$config['path'] => array(null, $path),
);
if (isset($config['path_hash'])) {
$pathHash = md5($path);
$pathHashProp = $meta->getReflectionProperty($config['path_hash']);
$pathHashProp->setAccessible(true);
$pathHashProp->setValue($node, $pathHash);
$changes[$config['path_hash']] = array(null, $pathHash);
}
if (isset($config['root'])) {
$root = null;
// Define the root value by grabbing the top of the current path
$rootFinderPath = explode($config['path_separator'], $path);
$rootIndex = $config['path_starts_with_separator'] ? 1 : 0;
$root = $rootFinderPath[$rootIndex];
// If it is an association, then make it an reference
// to the entity
if ($meta->hasAssociation($config['root'])) {
$rootClass = $meta->getAssociationTargetClass($config['root']);
$root = $om->getReference($rootClass, $root);
}
$rootProp = $meta->getReflectionProperty($config['root']);
$rootProp->setAccessible(true);
$rootProp->setValue($node, $root);
$changes[$config['root']] = array(null, $root);
}
if (isset($config['level'])) {
$level = substr_count($path, $config['path_separator']);
$levelProp = $meta->getReflectionProperty($config['level']);
$levelProp->setAccessible(true);
$levelProp->setValue($node, $level);
$changes[$config['level']] = array(null, $level);
}
if (!$uow instanceof MongoDBUnitOfWork) {
$ea->setOriginalObjectProperty($uow, $oid, $config['path'], $path);
$uow->scheduleExtraUpdate($node, $changes);
} else {
$ea->recomputeSingleObjectChangeSet($uow, $meta, $node);
}
if (isset($config['path_hash'])) {
$ea->setOriginalObjectProperty($uow, $oid, $config['path_hash'], $pathHash);
}
}
/**
* Update node's children
*
* @param ObjectManager $om
* @param object $node
* @param AdapterInterface $ea
* @param string $originalPath
*
* @return void
*/
public function updateChildren(ObjectManager $om, $node, AdapterInterface $ea, $originalPath)
{
$meta = $om->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($om, $meta->name);
$children = $this->getChildren($om, $meta, $config, $originalPath);
foreach ($children as $child) {
$this->updateNode($om, $child, $ea);
}
}
/**
* Process pre-locking actions
*
* @param ObjectManager $om
* @param object $node
* @param string $action
*
* @return void
*/
public function processPreLockingActions($om, $node, $action)
{
$meta = $om->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($om, $meta->name);
if ($config['activate_locking']) {
;
$parentProp = $meta->getReflectionProperty($config['parent']);
$parentProp->setAccessible(true);
$parentNode = $node;
while (!is_null($parent = $parentProp->getValue($parentNode))) {
$parentNode = $parent;
}
// In some cases, the parent could be a not initialized proxy. In this case, the
// "lockTime" field may NOT be loaded yet and have null instead of the date.
// We need to be sure that this field has its real value
if ($parentNode !== $node && $parentNode instanceof \Doctrine\ODM\MongoDB\Proxy\Proxy) {
$reflMethod = new \ReflectionMethod(get_class($parentNode), '__load');
$reflMethod->setAccessible(true);
$reflMethod->invoke($parentNode);
}
// If tree is already locked, we throw an exception
$lockTimeProp = $meta->getReflectionProperty($config['lock_time']);
$lockTimeProp->setAccessible(true);
$lockTime = $lockTimeProp->getValue($parentNode);
if (!is_null($lockTime)) {
$lockTime = $lockTime instanceof \MongoDate ? $lockTime->sec : $lockTime->getTimestamp();
}
if (!is_null($lockTime) && ($lockTime >= (time() - $config['locking_timeout']))) {
$msg = 'Tree with root id "%s" is locked.';
$id = $meta->getIdentifierValue($parentNode);
throw new TreeLockingException(sprintf($msg, $id));
}
$this->rootsOfTreesWhichNeedsLocking[spl_object_hash($parentNode)] = $parentNode;
$oid = spl_object_hash($node);
switch ($action) {
case self::ACTION_INSERT:
$this->pendingObjectsToInsert[$oid] = $node;
break;
case self::ACTION_UPDATE:
$this->pendingObjectsToUpdate[$oid] = $node;
break;
case self::ACTION_REMOVE:
$this->pendingObjectsToRemove[$oid] = $node;
break;
default:
throw new \InvalidArgumentException(sprintf('"%s" is not a valid action.', $action));
}
}
}
/**
* Process pre-locking actions
*
* @param ObjectManager $om
* @param AdapterInterface $ea
* @param object $node
* @param string $action
*
* @return void
*/
public function processPostEventsActions(ObjectManager $om, AdapterInterface $ea, $node, $action)
{
$meta = $om->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($om, $meta->name);
if ($config['activate_locking']) {
switch ($action) {
case self::ACTION_INSERT:
unset($this->pendingObjectsToInsert[spl_object_hash($node)]);
break;
case self::ACTION_UPDATE:
unset($this->pendingObjectsToUpdate[spl_object_hash($node)]);
break;
case self::ACTION_REMOVE:
unset($this->pendingObjectsToRemove[spl_object_hash($node)]);
break;
default:
throw new \InvalidArgumentException(sprintf('"%s" is not a valid action.', $action));
}
if (empty($this->pendingObjectsToInsert) && empty($this->pendingObjectsToUpdate) &&
empty($this->pendingObjectsToRemove)) {
$this->releaseTreeLocks($om, $ea);
}
}
}
/**
* Locks all needed trees
*
* @param ObjectManager $om
* @param AdapterInterface $ea
*
* @return void
*/
protected function lockTrees(ObjectManager $om, AdapterInterface $ea)
{
// Do nothing by default
}
/**
* Releases all trees which are locked
*
* @param ObjectManager $om
* @param AdapterInterface $ea
*
* @return void
*/
protected function releaseTreeLocks(ObjectManager $om, AdapterInterface $ea)
{
// Do nothing by default
}
/**
* Remove node and its children
*
* @param ObjectManager $om
* @param object $meta - Metadata
* @param object $config - config
* @param object $node - node to remove
*
* @return void
*/
abstract public function removeNode($om, $meta, $config, $node);
/**
* Returns children of the node with its original path
*
* @param ObjectManager $om
* @param object $meta - Metadata
* @param object $config - config
* @param string $originalPath - original path of object
*
* @return array|\Traversable
*/
abstract public function getChildren($om, $meta, $config, $originalPath);
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Gedmo\Tree\Strategy\ODM\MongoDB;
use Gedmo\Tree\Strategy\AbstractMaterializedPath;
use Doctrine\Common\Persistence\ObjectManager;
use Gedmo\Mapping\Event\AdapterInterface;
use Gedmo\Tool\Wrapper\AbstractWrapper;
/**
* This strategy makes tree using materialized path strategy
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class MaterializedPath extends AbstractMaterializedPath
{
/**
* {@inheritdoc}
*/
public function removeNode($om, $meta, $config, $node)
{
$uow = $om->getUnitOfWork();
$wrapped = AbstractWrapper::wrap($node, $om);
// Remove node's children
$results = $om->createQueryBuilder()
->find($meta->name)
->field($config['path'])->equals(new \MongoRegex('/^'.preg_quote($wrapped->getPropertyValue($config['path'])).'.?+/'))
->getQuery()
->execute();
foreach ($results as $node) {
$uow->scheduleForDelete($node);
}
}
/**
* {@inheritdoc}
*/
public function getChildren($om, $meta, $config, $originalPath)
{
return $om->createQueryBuilder()
->find($meta->name)
->field($config['path'])->equals(new \MongoRegex('/^'.preg_quote($originalPath).'.+/'))
->sort($config['path'], 'asc') // This may save some calls to updateNode
->getQuery()
->execute();
}
/**
* {@inheritdoc}
*/
protected function lockTrees(ObjectManager $om, AdapterInterface $ea)
{
$uow = $om->getUnitOfWork();
foreach ($this->rootsOfTreesWhichNeedsLocking as $oid => $root) {
$meta = $om->getClassMetadata(get_class($root));
$config = $this->listener->getConfiguration($om, $meta->name);
$lockTimeProp = $meta->getReflectionProperty($config['lock_time']);
$lockTimeProp->setAccessible(true);
$lockTimeValue = new \MongoDate();
$lockTimeProp->setValue($root, $lockTimeValue);
$changes = array(
$config['lock_time'] => array(null, $lockTimeValue),
);
$ea->recomputeSingleObjectChangeSet($uow, $meta, $root);
}
}
/**
* {@inheritdoc}
*/
protected function releaseTreeLocks(ObjectManager $om, AdapterInterface $ea)
{
$uow = $om->getUnitOfWork();
foreach ($this->rootsOfTreesWhichNeedsLocking as $oid => $root) {
$meta = $om->getClassMetadata(get_class($root));
$config = $this->listener->getConfiguration($om, $meta->name);
$lockTimeProp = $meta->getReflectionProperty($config['lock_time']);
$lockTimeProp->setAccessible(true);
$lockTimeValue = null;
$lockTimeProp->setValue($root, $lockTimeValue);
$changes = array(
$config['lock_time'] => array(null, null),
);
$ea->recomputeSingleObjectChangeSet($uow, $meta, $root);
unset($this->rootsOfTreesWhichNeedsLocking[$oid]);
}
}
}

View File

@@ -0,0 +1,469 @@
<?php
namespace Gedmo\Tree\Strategy\ORM;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Version;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\Common\Persistence\ObjectManager;
use Gedmo\Tree\Strategy;
use Gedmo\Tree\TreeListener;
use Gedmo\Tool\Wrapper\AbstractWrapper;
use Gedmo\Exception\RuntimeException;
use Gedmo\Mapping\Event\AdapterInterface;
/**
* This strategy makes tree act like
* a closure table.
*
* @author Gustavo Adrian <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Closure implements Strategy
{
/**
* TreeListener
*
* @var TreeListener
*/
protected $listener = null;
/**
* List of pending Nodes, which needs to
* be post processed because of having a parent Node
* which requires some additional calculations
*
* @var array
*/
private $pendingChildNodeInserts = array();
/**
* List of nodes which has their parents updated, but using
* new nodes. They have to wait until their parents are inserted
* on DB to make the update
*
* @var array
*/
private $pendingNodeUpdates = array();
/**
* List of pending Nodes, which needs their "level"
* field value set
*
* @var array
*/
private $pendingNodesLevelProcess = array();
/**
* {@inheritdoc}
*/
public function __construct(TreeListener $listener)
{
$this->listener = $listener;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return Strategy::CLOSURE;
}
/**
* {@inheritdoc}
*/
public function processMetadataLoad($em, $meta)
{
$config = $this->listener->getConfiguration($em, $meta->name);
$closureMetadata = $em->getClassMetadata($config['closure']);
$cmf = $em->getMetadataFactory();
if (!$closureMetadata->hasAssociation('ancestor')) {
// create ancestor mapping
$ancestorMapping = array(
'fieldName' => 'ancestor',
'id' => false,
'joinColumns' => array(
array(
'name' => 'ancestor',
'referencedColumnName' => 'id',
'unique' => false,
'nullable' => false,
'onDelete' => 'CASCADE',
'onUpdate' => null,
'columnDefinition' => null,
),
),
'inversedBy' => null,
'targetEntity' => $meta->name,
'cascade' => null,
'fetch' => ClassMetadataInfo::FETCH_LAZY,
);
$closureMetadata->mapManyToOne($ancestorMapping);
if (Version::compare('2.3.0-dev') <= 0) {
$closureMetadata->reflFields['ancestor'] = $cmf
->getReflectionService()
->getAccessibleProperty($closureMetadata->name, 'ancestor')
;
}
}
if (!$closureMetadata->hasAssociation('descendant')) {
// create descendant mapping
$descendantMapping = array(
'fieldName' => 'descendant',
'id' => false,
'joinColumns' => array(
array(
'name' => 'descendant',
'referencedColumnName' => 'id',
'unique' => false,
'nullable' => false,
'onDelete' => 'CASCADE',
'onUpdate' => null,
'columnDefinition' => null,
),
),
'inversedBy' => null,
'targetEntity' => $meta->name,
'cascade' => null,
'fetch' => ClassMetadataInfo::FETCH_LAZY,
);
$closureMetadata->mapManyToOne($descendantMapping);
if (Version::compare('2.3.0-dev') <= 0) {
$closureMetadata->reflFields['descendant'] = $cmf
->getReflectionService()
->getAccessibleProperty($closureMetadata->name, 'descendant')
;
}
}
// create unique index on ancestor and descendant
$indexName = substr(strtoupper("IDX_".md5($closureMetadata->name)), 0, 20);
$closureMetadata->table['uniqueConstraints'][$indexName] = array(
'columns' => array(
$this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('ancestor')),
$this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('descendant')),
),
);
// this one may not be very useful
$indexName = substr(strtoupper("IDX_".md5($meta->name.'depth')), 0, 20);
$closureMetadata->table['indexes'][$indexName] = array(
'columns' => array('depth'),
);
if ($cacheDriver = $cmf->getCacheDriver()) {
$cacheDriver->save($closureMetadata->name."\$CLASSMETADATA", $closureMetadata, null);
}
}
/**
* {@inheritdoc}
*/
public function onFlushEnd($em, AdapterInterface $ea)
{
}
/**
* {@inheritdoc}
*/
public function processPrePersist($em, $node)
{
$this->pendingChildNodeInserts[spl_object_hash($em)][spl_object_hash($node)] = $node;
}
/**
* {@inheritdoc}
*/
public function processPreUpdate($em, $node)
{
}
/**
* {@inheritdoc}
*/
public function processPreRemove($em, $node)
{
}
/**
* {@inheritdoc}
*/
public function processScheduledInsertion($em, $node, AdapterInterface $ea)
{
}
/**
* {@inheritdoc}
*/
public function processScheduledDelete($em, $entity)
{
}
protected function getJoinColumnFieldName($association)
{
if (count($association['joinColumnFieldNames']) > 1) {
throw new RuntimeException('More association on field '.$association['fieldName']);
}
return array_shift($association['joinColumnFieldNames']);
}
/**
* {@inheritdoc}
*/
public function processPostUpdate($em, $entity, AdapterInterface $ea)
{
$meta = $em->getClassMetadata(get_class($entity));
$config = $this->listener->getConfiguration($em, $meta->name);
// Process TreeLevel field value
if (!empty($config)) {
$this->setLevelFieldOnPendingNodes($em);
}
}
/**
* {@inheritdoc}
*/
public function processPostRemove($em, $entity, AdapterInterface $ea)
{
}
/**
* {@inheritdoc}
*/
public function processPostPersist($em, $entity, AdapterInterface $ea)
{
$uow = $em->getUnitOfWork();
$emHash = spl_object_hash($em);
while ($node = array_shift($this->pendingChildNodeInserts[$emHash])) {
$meta = $em->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($em, $meta->name);
$identifier = $meta->getSingleIdentifierFieldName();
$nodeId = $meta->getReflectionProperty($identifier)->getValue($node);
$parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
$closureClass = $config['closure'];
$closureMeta = $em->getClassMetadata($closureClass);
$closureTable = $closureMeta->getTableName();
$ancestorColumnName = $this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('ancestor'));
$descendantColumnName = $this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('descendant'));
$depthColumnName = $em->getClassMetadata($config['closure'])->getColumnName('depth');
$entries = array(
array(
$ancestorColumnName => $nodeId,
$descendantColumnName => $nodeId,
$depthColumnName => 0,
),
);
if ($parent) {
$dql = "SELECT c, a FROM {$closureMeta->name} c";
$dql .= " JOIN c.ancestor a";
$dql .= " WHERE c.descendant = :parent";
$q = $em->createQuery($dql);
$q->setParameters(compact('parent'));
$ancestors = $q->getArrayResult();
foreach ($ancestors as $ancestor) {
$entries[] = array(
$ancestorColumnName => $ancestor['ancestor'][$identifier],
$descendantColumnName => $nodeId,
$depthColumnName => $ancestor['depth'] + 1,
);
}
if (isset($config['level'])) {
$this->pendingNodesLevelProcess[$nodeId] = $node;
}
} elseif (isset($config['level'])) {
$uow->scheduleExtraUpdate($node, array($config['level'] => array(null, 1)));
$ea->setOriginalObjectProperty($uow, spl_object_hash($node), $config['level'], 1);
$levelProp = $meta->getReflectionProperty($config['level']);
$levelProp->setValue($node, 1);
}
foreach ($entries as $closure) {
if (!$em->getConnection()->insert($closureTable, $closure)) {
throw new RuntimeException('Failed to insert new Closure record');
}
}
}
// Process pending node updates
if (!empty($this->pendingNodeUpdates)) {
foreach ($this->pendingNodeUpdates as $info) {
$this->updateNode($em, $info['node'], $info['oldParent']);
}
$this->pendingNodeUpdates = array();
}
// Process TreeLevel field value
$this->setLevelFieldOnPendingNodes($em);
}
/**
* Process pending entities to set their "level" value
*
* @param \Doctrine\Common\Persistence\ObjectManager $em
*/
protected function setLevelFieldOnPendingNodes(ObjectManager $em)
{
if (!empty($this->pendingNodesLevelProcess)) {
$first = array_slice($this->pendingNodesLevelProcess, 0, 1);
$first = array_shift($first);
$meta = $em->getClassMetadata(get_class($first));
unset($first);
$identifier = $meta->getIdentifier();
$mapping = $meta->getFieldMapping($identifier[0]);
$config = $this->listener->getConfiguration($em, $meta->name);
$closureClass = $config['closure'];
$closureMeta = $em->getClassMetadata($closureClass);
$uow = $em->getUnitOfWork();
foreach ($this->pendingNodesLevelProcess as $node) {
$children = $em->getRepository($meta->name)->children($node);
foreach ($children as $child) {
$this->pendingNodesLevelProcess[AbstractWrapper::wrap($child, $em)->getIdentifier()] = $child;
}
}
// Avoid type conversion performance penalty
$type = 'integer' === $mapping['type'] ? Connection::PARAM_INT_ARRAY : Connection::PARAM_STR_ARRAY;
// We calculate levels for all nodes
$sql = 'SELECT c.descendant, MAX(c.depth) + 1 AS levelNum ';
$sql .= 'FROM '.$closureMeta->getTableName().' c ';
$sql .= 'WHERE c.descendant IN (?) ';
$sql .= 'GROUP BY c.descendant';
$levelsAssoc = $em->getConnection()->executeQuery($sql, array(array_keys($this->pendingNodesLevelProcess)), array($type))->fetchAll(\PDO::FETCH_NUM);
//create key pair array with resultset
$levels = array();
foreach( $levelsAssoc as $level )
{
$levels[$level[0]] = $level[1];
}
$levelsAssoc = null;
// Now we update levels
foreach ($this->pendingNodesLevelProcess as $nodeId => $node) {
// Update new level
$level = $levels[$nodeId];
$levelProp = $meta->getReflectionProperty($config['level']);
$uow->scheduleExtraUpdate(
$node,
array($config['level'] => array(
$levelProp->getValue($node), $level,
))
);
$levelProp->setValue($node, $level);
$uow->setOriginalEntityProperty(spl_object_hash($node), $config['level'], $level);
}
$this->pendingNodesLevelProcess = array();
}
}
/**
* {@inheritdoc}
*/
public function processScheduledUpdate($em, $node, AdapterInterface $ea)
{
$meta = $em->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($em, $meta->name);
$uow = $em->getUnitOfWork();
$changeSet = $uow->getEntityChangeSet($node);
if (array_key_exists($config['parent'], $changeSet)) {
// If new parent is new, we need to delay the update of the node
// until it is inserted on DB
$parent = $changeSet[$config['parent']][1] ? AbstractWrapper::wrap($changeSet[$config['parent']][1], $em) : null;
if ($parent && !$parent->getIdentifier()) {
$this->pendingNodeUpdates[spl_object_hash($node)] = array(
'node' => $node,
'oldParent' => $changeSet[$config['parent']][0],
);
} else {
$this->updateNode($em, $node, $changeSet[$config['parent']][0]);
}
}
}
/**
* Update node and closures
*
* @param EntityManagerInterface $em
* @param object $node
* @param object $oldParent
*/
public function updateNode(EntityManagerInterface $em, $node, $oldParent)
{
$wrapped = AbstractWrapper::wrap($node, $em);
$meta = $wrapped->getMetadata();
$config = $this->listener->getConfiguration($em, $meta->name);
$closureMeta = $em->getClassMetadata($config['closure']);
$nodeId = $wrapped->getIdentifier();
$parent = $wrapped->getPropertyValue($config['parent']);
$table = $closureMeta->getTableName();
$conn = $em->getConnection();
// ensure integrity
if ($parent) {
$dql = "SELECT COUNT(c) FROM {$closureMeta->name} c";
$dql .= " WHERE c.ancestor = :node";
$dql .= " AND c.descendant = :parent";
$q = $em->createQuery($dql);
$q->setParameters(compact('node', 'parent'));
if ($q->getSingleScalarResult()) {
throw new \Gedmo\Exception\UnexpectedValueException("Cannot set child as parent to node: {$nodeId}");
}
}
if ($oldParent) {
$subQuery = "SELECT c2.id FROM {$table} c1";
$subQuery .= " JOIN {$table} c2 ON c1.descendant = c2.descendant";
$subQuery .= " WHERE c1.ancestor = :nodeId AND c2.depth > c1.depth";
$ids = $conn->executeQuery($subQuery, compact('nodeId'))->fetchAll(\PDO::FETCH_COLUMN);
if ($ids) {
// using subquery directly, sqlite acts unfriendly
$query = "DELETE FROM {$table} WHERE id IN (".implode(', ', $ids).")";
if (!empty($ids) && !$conn->executeQuery($query)) {
throw new RuntimeException('Failed to remove old closures');
}
}
}
if ($parent) {
$wrappedParent = AbstractWrapper::wrap($parent, $em);
$parentId = $wrappedParent->getIdentifier();
$query = "SELECT c1.ancestor, c2.descendant, (c1.depth + c2.depth + 1) AS depth";
$query .= " FROM {$table} c1, {$table} c2";
$query .= " WHERE c1.descendant = :parentId";
$query .= " AND c2.ancestor = :nodeId";
$closures = $conn->fetchAll($query, compact('nodeId', 'parentId'));
foreach ($closures as $closure) {
if (!$conn->insert($table, $closure)) {
throw new RuntimeException('Failed to insert new Closure record');
}
}
}
if (isset($config['level'])) {
$this->pendingNodesLevelProcess[$nodeId] = $node;
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Gedmo\Tree\Strategy\ORM;
use Gedmo\Tree\Strategy\AbstractMaterializedPath;
use Gedmo\Tool\Wrapper\AbstractWrapper;
/**
* This strategy makes tree using materialized path strategy
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class MaterializedPath extends AbstractMaterializedPath
{
/**
* {@inheritdoc}
*/
public function removeNode($om, $meta, $config, $node)
{
$uow = $om->getUnitOfWork();
$wrapped = AbstractWrapper::wrap($node, $om);
$path = addcslashes($wrapped->getPropertyValue($config['path']), '%');
// Remove node's children
$qb = $om->createQueryBuilder();
$qb->select('e')
->from($config['useObjectClass'], 'e')
->where($qb->expr()->like('e.'.$config['path'], $qb->expr()->literal($path.'%')));
if (isset($config['level'])) {
$lvlField = $config['level'];
$lvl = $wrapped->getPropertyValue($lvlField);
if (!empty($lvl)) {
$qb->andWhere($qb->expr()->gt('e.' . $lvlField, $qb->expr()->literal($lvl)));
}
}
$results = $qb->getQuery()
->execute();
foreach ($results as $node) {
$uow->scheduleForDelete($node);
}
}
/**
* {@inheritdoc}
*/
public function getChildren($om, $meta, $config, $path)
{
$path = addcslashes($path, '%');
$qb = $om->createQueryBuilder($config['useObjectClass']);
$qb->select('e')
->from($config['useObjectClass'], 'e')
->where($qb->expr()->like('e.'.$config['path'], $qb->expr()->literal($path.'%')))
->andWhere('e.'.$config['path'].' != :path')
->orderBy('e.'.$config['path'], 'asc'); // This may save some calls to updateNode
$qb->setParameter('path', $path);
return $qb->getQuery()
->execute();
}
}

View File

@@ -0,0 +1,718 @@
<?php
namespace Gedmo\Tree\Strategy\ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Gedmo\Exception\UnexpectedValueException;
use Doctrine\ORM\Proxy\Proxy;
use Gedmo\Tool\Wrapper\AbstractWrapper;
use Gedmo\Tree\Strategy;
use Gedmo\Tree\TreeListener;
use Gedmo\Mapping\Event\AdapterInterface;
/**
* This strategy makes the tree act like a nested set.
*
* This behavior can impact the performance of your application
* since nested set trees are slow on inserts and updates.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Nested implements Strategy
{
/**
* Previous sibling position
*/
const PREV_SIBLING = 'PrevSibling';
/**
* Next sibling position
*/
const NEXT_SIBLING = 'NextSibling';
/**
* Last child position
*/
const LAST_CHILD = 'LastChild';
/**
* First child position
*/
const FIRST_CHILD = 'FirstChild';
/**
* TreeListener
*
* @var TreeListener
*/
protected $listener = null;
/**
* The max number of "right" field of the
* tree in case few root nodes will be persisted
* on one flush for node classes
*
* @var array
*/
private $treeEdges = array();
/**
* Stores a list of node position strategies
* for each node by object hash
*
* @var array
*/
private $nodePositions = array();
/**
* Stores a list of delayed nodes for correct order of updates
*
* @var array
*/
private $delayedNodes = array();
/**
* {@inheritdoc}
*/
public function __construct(TreeListener $listener)
{
$this->listener = $listener;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return Strategy::NESTED;
}
/**
* Set node position strategy
*
* @param string $oid
* @param string $position
*/
public function setNodePosition($oid, $position)
{
$valid = array(
self::FIRST_CHILD,
self::LAST_CHILD,
self::NEXT_SIBLING,
self::PREV_SIBLING,
);
if (!in_array($position, $valid, false)) {
throw new \Gedmo\Exception\InvalidArgumentException("Position: {$position} is not valid in nested set tree");
}
$this->nodePositions[$oid] = $position;
}
/**
* {@inheritdoc}
*/
public function processScheduledInsertion($em, $node, AdapterInterface $ea)
{
/** @var ClassMetadata $meta */
$meta = $em->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($em, $meta->name);
$meta->getReflectionProperty($config['left'])->setValue($node, 0);
$meta->getReflectionProperty($config['right'])->setValue($node, 0);
if (isset($config['level'])) {
$meta->getReflectionProperty($config['level'])->setValue($node, 0);
}
if (isset($config['root']) && !$meta->hasAssociation($config['root']) && !isset($config['rootIdentifierMethod'])) {
$meta->getReflectionProperty($config['root'])->setValue($node, 0);
} else if (isset($config['rootIdentifierMethod']) && is_null($meta->getReflectionProperty($config['root'])->getValue($node))) {
$meta->getReflectionProperty($config['root'])->setValue($node, 0);
}
}
/**
* {@inheritdoc}
*/
public function processScheduledUpdate($em, $node, AdapterInterface $ea)
{
$meta = $em->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($em, $meta->name);
$uow = $em->getUnitOfWork();
$changeSet = $uow->getEntityChangeSet($node);
if (isset($config['root']) && isset($changeSet[$config['root']])) {
throw new \Gedmo\Exception\UnexpectedValueException("Root cannot be changed manually, change parent instead");
}
$oid = spl_object_hash($node);
if (isset($changeSet[$config['left']]) && isset($this->nodePositions[$oid])) {
$wrapped = AbstractWrapper::wrap($node, $em);
$parent = $wrapped->getPropertyValue($config['parent']);
// revert simulated changeset
$uow->clearEntityChangeSet($oid);
$wrapped->setPropertyValue($config['left'], $changeSet[$config['left']][0]);
$uow->setOriginalEntityProperty($oid, $config['left'], $changeSet[$config['left']][0]);
// set back all other changes
foreach ($changeSet as $field => $set) {
if ($field !== $config['left']) {
if (is_array($set) && array_key_exists(0, $set) && array_key_exists(1, $set)) {
$uow->setOriginalEntityProperty($oid, $field, $set[0]);
$wrapped->setPropertyValue($field, $set[1]);
} else {
$uow->setOriginalEntityProperty($oid, $field, $set);
$wrapped->setPropertyValue($field, $set);
}
}
}
$uow->recomputeSingleEntityChangeSet($meta, $node);
$this->updateNode($em, $node, $parent);
} elseif (isset($changeSet[$config['parent']])) {
$this->updateNode($em, $node, $changeSet[$config['parent']][1]);
}
}
/**
* {@inheritdoc}
*/
public function processPostPersist($em, $node, AdapterInterface $ea)
{
$meta = $em->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($em, $meta->name);
$parent = $meta->getReflectionProperty($config['parent'])->getValue($node);
$this->updateNode($em, $node, $parent, self::LAST_CHILD);
}
/**
* {@inheritdoc}
*/
public function processScheduledDelete($em, $node)
{
$meta = $em->getClassMetadata(get_class($node));
$config = $this->listener->getConfiguration($em, $meta->name);
$uow = $em->getUnitOfWork();
$wrapped = AbstractWrapper::wrap($node, $em);
$leftValue = $wrapped->getPropertyValue($config['left']);
$rightValue = $wrapped->getPropertyValue($config['right']);
if (!$leftValue || !$rightValue) {
return;
}
$rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null;
$diff = $rightValue - $leftValue + 1;
if ($diff > 2) {
$qb = $em->createQueryBuilder();
$qb->select('node')
->from($config['useObjectClass'], 'node')
->where($qb->expr()->between('node.' . $config['left'], '?1', '?2'))
->setParameters(array(1 => $leftValue, 2 => $rightValue));
if (isset($config['root'])) {
$qb->andWhere($qb->expr()->eq('node.' . $config['root'], ':rid'));
$qb->setParameter('rid', $rootId);
}
$q = $qb->getQuery();
// get nodes for deletion
$nodes = $q->getResult();
foreach ((array)$nodes as $removalNode) {
$uow->scheduleForDelete($removalNode);
}
}
$this->shiftRL($em, $config['useObjectClass'], $rightValue + 1, -$diff, $rootId);
}
/**
* {@inheritdoc}
*/
public function onFlushEnd($em, AdapterInterface $ea)
{
// reset values
$this->treeEdges = array();
}
/**
* {@inheritdoc}
*/
public function processPreRemove($em, $node)
{
}
/**
* {@inheritdoc}
*/
public function processPrePersist($em, $node)
{
}
/**
* {@inheritdoc}
*/
public function processPreUpdate($em, $node)
{
}
/**
* {@inheritdoc}
*/
public function processMetadataLoad($em, $meta)
{
}
/**
* {@inheritdoc}
*/
public function processPostUpdate($em, $entity, AdapterInterface $ea)
{
}
/**
* {@inheritdoc}
*/
public function processPostRemove($em, $entity, AdapterInterface $ea)
{
}
/**
* Update the $node with a diferent $parent
* destination
*
* @param EntityManagerInterface $em
* @param object $node - target node
* @param object $parent - destination node
* @param string $position
*
* @throws \Gedmo\Exception\UnexpectedValueException
*/
public function updateNode(EntityManagerInterface $em, $node, $parent, $position = 'FirstChild')
{
$wrapped = AbstractWrapper::wrap($node, $em);
/** @var ClassMetadata $meta */
$meta = $wrapped->getMetadata();
$config = $this->listener->getConfiguration($em, $meta->name);
$root = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null;
$identifierField = $meta->getSingleIdentifierFieldName();
$nodeId = $wrapped->getIdentifier();
$left = $wrapped->getPropertyValue($config['left']);
$right = $wrapped->getPropertyValue($config['right']);
$isNewNode = empty($left) && empty($right);
if ($isNewNode) {
$left = 1;
$right = 2;
}
$oid = spl_object_hash($node);
if (isset($this->nodePositions[$oid])) {
$position = $this->nodePositions[$oid];
}
$level = 0;
$treeSize = $right - $left + 1;
$newRoot = null;
if ($parent) { // || (!$parent && isset($config['rootIdentifierMethod']))
$wrappedParent = AbstractWrapper::wrap($parent, $em);
$parentRoot = isset($config['root']) ? $wrappedParent->getPropertyValue($config['root']) : null;
$parentOid = spl_object_hash($parent);
$parentLeft = $wrappedParent->getPropertyValue($config['left']);
$parentRight = $wrappedParent->getPropertyValue($config['right']);
if (empty($parentLeft) && empty($parentRight)) {
// parent node is a new node, but wasn't processed yet (due to Doctrine commit order calculator redordering)
// We delay processing of node to the moment parent node will be processed
if (!isset($this->delayedNodes[$parentOid])) {
$this->delayedNodes[$parentOid] = array();
}
$this->delayedNodes[$parentOid][] = array('node' => $node, 'position' => $position);
return;
}
if (!$isNewNode && $root === $parentRoot && $parentLeft >= $left && $parentRight <= $right) {
throw new UnexpectedValueException("Cannot set child as parent to node: {$nodeId}");
}
if (isset($config['level'])) {
$level = $wrappedParent->getPropertyValue($config['level']);
}
switch ($position) {
case self::PREV_SIBLING:
if (property_exists($node, 'sibling')) {
$wrappedSibling = AbstractWrapper::wrap($node->sibling, $em);
$start = $wrappedSibling->getPropertyValue($config['left']);
$level++;
} else {
$newParent = $wrappedParent->getPropertyValue($config['parent']);
if (is_null($newParent) && ((isset($config['root']) && $config['root'] == $config['parent']) || $isNewNode)) {
throw new UnexpectedValueException("Cannot persist sibling for a root node, tree operation is not possible");
} else if (is_null($newParent) && (isset($config['root']) || $isNewNode)) {
// root is a different column from parent (pointing to another table?), do nothing
} else {
$wrapped->setPropertyValue($config['parent'], $newParent);
}
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node);
$start = $parentLeft;
}
break;
case self::NEXT_SIBLING:
if (property_exists($node, 'sibling')) {
$wrappedSibling = AbstractWrapper::wrap($node->sibling, $em);
$start = $wrappedSibling->getPropertyValue($config['right']) + 1;
$level++;
} else {
$newParent = $wrappedParent->getPropertyValue($config['parent']);
if (is_null($newParent) && ((isset($config['root']) && $config['root'] == $config['parent']) || $isNewNode)) {
throw new UnexpectedValueException("Cannot persist sibling for a root node, tree operation is not possible");
} else if (is_null($newParent) && (isset($config['root']) || $isNewNode)) {
// root is a different column from parent (pointing to another table?), do nothing
} else {
$wrapped->setPropertyValue($config['parent'], $newParent);
}
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node);
$start = $parentRight + 1;
}
break;
case self::LAST_CHILD:
$start = $parentRight;
$level++;
break;
case self::FIRST_CHILD:
default:
$start = $parentLeft + 1;
$level++;
break;
}
$this->shiftRL($em, $config['useObjectClass'], $start, $treeSize, $parentRoot);
if (!$isNewNode && $root === $parentRoot && $left >= $start) {
$left += $treeSize;
$wrapped->setPropertyValue($config['left'], $left);
}
if (!$isNewNode && $root === $parentRoot && $right >= $start) {
$right += $treeSize;
$wrapped->setPropertyValue($config['right'], $right);
}
$newRoot = $parentRoot;
} elseif (!isset($config['root']) ||
($meta->isSingleValuedAssociation($config['root']) && ($newRoot = $meta->getFieldValue($node, $config['root'])))) {
if (!isset($this->treeEdges[$meta->name])) {
$this->treeEdges[$meta->name] = $this->max($em, $config['useObjectClass'], $newRoot) + 1;
}
$level = 0;
$parentLeft = 0;
$parentRight = $this->treeEdges[$meta->name];
$this->treeEdges[$meta->name] += 2;
switch ($position) {
case self::PREV_SIBLING:
if (property_exists($node, 'sibling')) {
$wrappedSibling = AbstractWrapper::wrap($node->sibling, $em);
$start = $wrappedSibling->getPropertyValue($config['left']);
} else {
$wrapped->setPropertyValue($config['parent'], null);
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node);
$start = $parentLeft + 1;
}
break;
case self::NEXT_SIBLING:
if (property_exists($node, 'sibling')) {
$wrappedSibling = AbstractWrapper::wrap($node->sibling, $em);
$start = $wrappedSibling->getPropertyValue($config['right']) + 1;
} else {
$wrapped->setPropertyValue($config['parent'], null);
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node);
$start = $parentRight;
}
break;
case self::LAST_CHILD:
$start = $parentRight;
break;
case self::FIRST_CHILD:
default:
$start = $parentLeft + 1;
break;
}
$this->shiftRL($em, $config['useObjectClass'], $start, $treeSize, null);
if (!$isNewNode && $left >= $start) {
$left += $treeSize;
$wrapped->setPropertyValue($config['left'], $left);
}
if (!$isNewNode && $right >= $start) {
$right += $treeSize;
$wrapped->setPropertyValue($config['right'], $right);
}
} else {
$start = 1;
if (isset($config['rootIdentifierMethod'])) {
$method = $config['rootIdentifierMethod'];
$newRoot = $node->$method();
$repo = $em->getRepository($config['useObjectClass']);
$criteria = new Criteria();
$criteria->andWhere(Criteria::expr()->notIn($wrapped->getMetadata()->identifier[0], [$wrapped->getIdentifier()]));
$criteria->andWhere(Criteria::expr()->eq($config['root'], $node->$method()));
$criteria->andWhere(Criteria::expr()->isNull($config['parent']));
$criteria->andWhere(Criteria::expr()->eq($config['level'], 0));
$criteria->orderBy([$config['right'] => Criteria::ASC]);
$roots = $repo->matching($criteria)->toArray();
$last = array_pop($roots);
$start = ($last) ? $meta->getFieldValue($last, $config['right']) + 1 : 1;
} else if ($meta->isSingleValuedAssociation($config['root'])) {
$newRoot = $node;
} else {
$newRoot = $wrapped->getIdentifier();
}
}
$diff = $start - $left;
if (!$isNewNode) {
$levelDiff = isset($config['level']) ? $level - $wrapped->getPropertyValue($config['level']) : null;
$this->shiftRangeRL(
$em,
$config['useObjectClass'],
$left,
$right,
$diff,
$root,
$newRoot,
$levelDiff
);
$this->shiftRL($em, $config['useObjectClass'], $left, -$treeSize, $root);
} else {
$qb = $em->createQueryBuilder();
$qb->update($config['useObjectClass'], 'node');
if (isset($config['root'])) {
$qb->set('node.' . $config['root'], ':rid');
$qb->setParameter('rid', $newRoot);
$wrapped->setPropertyValue($config['root'], $newRoot);
$em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['root'], $newRoot);
}
if (isset($config['level'])) {
$qb->set('node.' . $config['level'], $level);
$wrapped->setPropertyValue($config['level'], $level);
$em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['level'], $level);
}
if (isset($newParent)) {
$wrappedNewParent = AbstractWrapper::wrap($newParent, $em);
$newParentId = $wrappedNewParent->getIdentifier();
$qb->set('node.' . $config['parent'], ':pid');
$qb->setParameter('pid', $newParentId);
$wrapped->setPropertyValue($config['parent'], $newParent);
$em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['parent'], $newParent);
}
$qb->set('node.' . $config['left'], $left + $diff);
$qb->set('node.' . $config['right'], $right + $diff);
// node id cannot be null
$qb->where($qb->expr()->eq('node.' . $identifierField, ':id'));
$qb->setParameter('id', $nodeId);
$qb->getQuery()->getSingleScalarResult();
$wrapped->setPropertyValue($config['left'], $left + $diff);
$wrapped->setPropertyValue($config['right'], $right + $diff);
$em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['left'], $left + $diff);
$em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['right'], $right + $diff);
}
if (isset($this->delayedNodes[$oid])) {
foreach ($this->delayedNodes[$oid] as $nodeData) {
$this->updateNode($em, $nodeData['node'], $node, $nodeData['position']);
}
}
}
/**
* Get the edge of tree
*
* @param EntityManagerInterface $em
* @param string $class
* @param integer $rootId
*
* @return integer
*/
public function max(EntityManagerInterface $em, $class, $rootId = 0)
{
$meta = $em->getClassMetadata($class);
$config = $this->listener->getConfiguration($em, $meta->name);
$qb = $em->createQueryBuilder();
$qb->select($qb->expr()->max('node.' . $config['right']))
->from($config['useObjectClass'], 'node');
if (isset($config['root']) && $rootId) {
$qb->where($qb->expr()->eq('node.' . $config['root'], ':rid'));
$qb->setParameter('rid', $rootId);
}
$query = $qb->getQuery();
$right = $query->getSingleScalarResult();
return intval($right);
}
/**
* Shift tree left and right values by delta
*
* @param EntityManager $em
* @param string $class
* @param integer $first
* @param integer $delta
* @param EntityManagerInterface $em
* @param string $class
* @param integer $first
* @param integer $delta
* @param integer|string $root
*/
public function shiftRL(EntityManagerInterface $em, $class, $first, $delta, $root = null)
{
$meta = $em->getClassMetadata($class);
$config = $this->listener->getConfiguration($em, $class);
$sign = ($delta >= 0) ? ' + ' : ' - ';
$absDelta = abs($delta);
$qb = $em->createQueryBuilder();
$qb->update($config['useObjectClass'], 'node')
->set('node.' . $config['left'], "node.{$config['left']} {$sign} {$absDelta}")
->where($qb->expr()->gte('node.' . $config['left'], $first));
if (isset($config['root'])) {
$qb->andWhere($qb->expr()->eq('node.' . $config['root'], ':rid'));
$qb->setParameter('rid', $root);
}
$qb->getQuery()->getSingleScalarResult();
$qb = $em->createQueryBuilder();
$qb->update($config['useObjectClass'], 'node')
->set('node.' . $config['right'], "node.{$config['right']} {$sign} {$absDelta}")
->where($qb->expr()->gte('node.' . $config['right'], $first));
if (isset($config['root'])) {
$qb->andWhere($qb->expr()->eq('node.' . $config['root'], ':rid'));
$qb->setParameter('rid', $root);
}
$qb->getQuery()->getSingleScalarResult();
// update in memory nodes increases performance, saves some IO
foreach ($em->getUnitOfWork()->getIdentityMap() as $className => $nodes) {
// for inheritance mapped classes, only root is always in the identity map
if ($className !== $meta->rootEntityName) {
continue;
}
foreach ($nodes as $node) {
if ($node instanceof Proxy && !$node->__isInitialized__) {
continue;
}
$nodeMeta = $em->getClassMetadata(get_class($node));
if (!array_key_exists($config['left'], $nodeMeta->getReflectionProperties())) {
continue;
}
$oid = spl_object_hash($node);
$left = $meta->getReflectionProperty($config['left'])->getValue($node);
$currentRoot = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($node) : null;
if ($currentRoot === $root && $left >= $first) {
$meta->getReflectionProperty($config['left'])->setValue($node, $left + $delta);
$em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['left'], $left + $delta);
}
$right = $meta->getReflectionProperty($config['right'])->getValue($node);
if ($currentRoot === $root && $right >= $first) {
$meta->getReflectionProperty($config['right'])->setValue($node, $right + $delta);
$em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['right'], $right + $delta);
}
}
}
}
/**
* Shift range of right and left values on tree
* depending on tree level difference also
*
* @param EntityManagerInterface $em
* @param string $class
* @param integer $first
* @param integer $last
* @param integer $delta
* @param integer|string $root
* @param integer|string $destRoot
* @param integer $levelDelta
*/
public function shiftRangeRL(EntityManagerInterface $em, $class, $first, $last, $delta, $root = null, $destRoot = null, $levelDelta = null)
{
$meta = $em->getClassMetadata($class);
$config = $this->listener->getConfiguration($em, $class);
$sign = ($delta >= 0) ? ' + ' : ' - ';
$absDelta = abs($delta);
$levelSign = ($levelDelta >= 0) ? ' + ' : ' - ';
$absLevelDelta = abs($levelDelta);
$qb = $em->createQueryBuilder();
$qb->update($config['useObjectClass'], 'node')
->set('node.' . $config['left'], "node.{$config['left']} {$sign} {$absDelta}")
->set('node.' . $config['right'], "node.{$config['right']} {$sign} {$absDelta}")
->where($qb->expr()->gte('node.' . $config['left'], $first))
->andWhere($qb->expr()->lte('node.' . $config['right'], $last));
if (isset($config['root'])) {
$qb->set('node.' . $config['root'], ':drid');
$qb->setParameter('drid', $destRoot);
$qb->andWhere($qb->expr()->eq('node.' . $config['root'], ':rid'));
$qb->setParameter('rid', $root);
}
if (isset($config['level'])) {
$qb->set('node.' . $config['level'], "node.{$config['level']} {$levelSign} {$absLevelDelta}");
}
$qb->getQuery()->getSingleScalarResult();
// update in memory nodes increases performance, saves some IO
foreach ($em->getUnitOfWork()->getIdentityMap() as $className => $nodes) {
// for inheritance mapped classes, only root is always in the identity map
if ($className !== $meta->rootEntityName) {
continue;
}
foreach ($nodes as $node) {
if ($node instanceof Proxy && !$node->__isInitialized__) {
continue;
}
$nodeMeta = $em->getClassMetadata(get_class($node));
if (!array_key_exists($config['left'], $nodeMeta->getReflectionProperties())) {
continue;
}
$left = $meta->getReflectionProperty($config['left'])->getValue($node);
$right = $meta->getReflectionProperty($config['right'])->getValue($node);
$currentRoot = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($node) : null;
if ($currentRoot === $root && $left >= $first && $right <= $last) {
$oid = spl_object_hash($node);
$uow = $em->getUnitOfWork();
$meta->getReflectionProperty($config['left'])->setValue($node, $left + $delta);
$uow->setOriginalEntityProperty($oid, $config['left'], $left + $delta);
$meta->getReflectionProperty($config['right'])->setValue($node, $right + $delta);
$uow->setOriginalEntityProperty($oid, $config['right'], $right + $delta);
if (isset($config['root'])) {
$meta->getReflectionProperty($config['root'])->setValue($node, $destRoot);
$uow->setOriginalEntityProperty($oid, $config['root'], $destRoot);
}
if (isset($config['level'])) {
$level = $meta->getReflectionProperty($config['level'])->getValue($node);
$meta->getReflectionProperty($config['level'])->setValue($node, $level + $levelDelta);
$uow->setOriginalEntityProperty($oid, $config['level'], $level + $levelDelta);
}
}
}
}
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Gedmo\Tree\Traits;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
/**
* MaterializedPath Trait
*
* @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
trait MaterializedPath
{
/**
* @var string
*/
protected $path;
/**
* @var self
*/
protected $parent;
/**
* @var integer
*/
protected $level;
/**
* @var Collection|self[]
*/
protected $children;
/**
* @var string
*/
protected $hash;
/**
* @param self $parent
*
* @return self
*/
public function setParent(self $parent = null)
{
$this->parent = $parent;
return $this;
}
/**
* @return self
*/
public function getParent()
{
return $this->parent;
}
/**
* @param string $path
*
* @return self
*/
public function setPath($path)
{
$this->path = $path;
return $this;
}
/**
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* @return integer
*/
public function getLevel()
{
return $this->level;
}
/**
* @param string $hash
*
* @return self
*/
public function setHash($hash)
{
$this->hash = $hash;
return $this;
}
/**
* @return string
*/
public function getHash()
{
return $this->hash;
}
/**
* @param Collection|self[] $children
*
* @return self
*/
public function setChildren($children)
{
$this->children = $children;
return $this;
}
/**
* @return Collection|self[]
*/
public function getChildren()
{
return $this->children = $this->children ?: new ArrayCollection();
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Gedmo\Tree\Traits;
/**
* NestedSet Trait, usable with PHP >= 5.4
*
* @author Renaat De Muynck <renaat.demuynck@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
trait NestedSet
{
/**
* @var integer
*/
private $root;
/**
* @var integer
*/
private $level;
/**
* @var integer
*/
private $left;
/**
* @var integer
*/
private $right;
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Gedmo\Tree\Traits;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* NestedSet Trait, usable with PHP >= 5.4
*
* @author Renaat De Muynck <renaat.demuynck@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
trait NestedSetEntity
{
/**
* @var integer
* @Gedmo\TreeRoot
* @ORM\Column(name="root", type="integer", nullable=true)
*/
private $root;
/**
* @var integer
* @Gedmo\TreeLevel
* @ORM\Column(name="lvl", type="integer")
*/
private $level;
/**
* @var integer
* @Gedmo\TreeLeft
* @ORM\Column(name="lft", type="integer")
*/
private $left;
/**
* @var integer
* @Gedmo\TreeRight
* @ORM\Column(name="rgt", type="integer")
*/
private $right;
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Gedmo\Tree\Traits;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* NestedSet Trait with UUid, usable with PHP >= 5.4
*
* @author Benjamin Lazarecki <benjamin.lazarecki@sensiolabs.com>
*
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
trait NestedSetEntityUuid
{
use NestedSetEntity;
/**
* @var string
* @Gedmo\TreeRoot
* @ORM\Column(name="root", type="string", nullable=true)
*/
private $root;
}

View File

@@ -0,0 +1,285 @@
<?php
namespace Gedmo\Tree;
use Doctrine\Common\EventArgs;
use Gedmo\Mapping\MappedEventSubscriber;
use Doctrine\Common\Persistence\ObjectManager;
/**
* The tree listener handles the synchronization of
* tree nodes. Can implement different
* strategies on handling the tree.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class TreeListener extends MappedEventSubscriber
{
/**
* Tree processing strategies for object classes
*
* @var array
*/
private $strategies = array();
/**
* List of strategy instances
*
* @var array
*/
private $strategyInstances = array();
/**
* List of used classes on flush
*
* @var array
*/
private $usedClassesOnFlush = array();
/**
* Specifies the list of events to listen
*
* @return array
*/
public function getSubscribedEvents()
{
return array(
'prePersist',
'preRemove',
'preUpdate',
'onFlush',
'loadClassMetadata',
'postPersist',
'postUpdate',
'postRemove',
);
}
/**
* Get the used strategy for tree processing
*
* @param ObjectManager $om
* @param string $class
*
* @return Strategy
*/
public function getStrategy(ObjectManager $om, $class)
{
if (!isset($this->strategies[$class])) {
$config = $this->getConfiguration($om, $class);
if (!$config) {
throw new \Gedmo\Exception\UnexpectedValueException("Tree object class: {$class} must have tree metadata at this point");
}
$managerName = 'UnsupportedManager';
if ($om instanceof \Doctrine\ORM\EntityManagerInterface) {
$managerName = 'ORM';
} elseif ($om instanceof \Doctrine\ODM\MongoDB\DocumentManager) {
$managerName = 'ODM\\MongoDB';
}
if (!isset($this->strategyInstances[$config['strategy']])) {
$strategyClass = $this->getNamespace().'\\Strategy\\'.$managerName.'\\'.ucfirst($config['strategy']);
if (!class_exists($strategyClass)) {
throw new \Gedmo\Exception\InvalidArgumentException($managerName." TreeListener does not support tree type: {$config['strategy']}");
}
$this->strategyInstances[$config['strategy']] = new $strategyClass($this);
}
$this->strategies[$class] = $config['strategy'];
}
return $this->strategyInstances[$this->strategies[$class]];
}
/**
* Looks for Tree objects being updated
* for further processing
*
* @param EventArgs $args
*/
public function onFlush(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$uow = $om->getUnitOfWork();
// check all scheduled updates for TreeNodes
foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
$meta = $om->getClassMetadata(get_class($object));
if ($this->getConfiguration($om, $meta->name)) {
$this->usedClassesOnFlush[$meta->name] = null;
$this->getStrategy($om, $meta->name)->processScheduledInsertion($om, $object, $ea);
$ea->recomputeSingleObjectChangeSet($uow, $meta, $object);
}
}
foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
$meta = $om->getClassMetadata(get_class($object));
if ($this->getConfiguration($om, $meta->name)) {
$this->usedClassesOnFlush[$meta->name] = null;
$this->getStrategy($om, $meta->name)->processScheduledUpdate($om, $object, $ea);
}
}
foreach ($ea->getScheduledObjectDeletions($uow) as $object) {
$meta = $om->getClassMetadata(get_class($object));
if ($this->getConfiguration($om, $meta->name)) {
$this->usedClassesOnFlush[$meta->name] = null;
$this->getStrategy($om, $meta->name)->processScheduledDelete($om, $object);
}
}
foreach ($this->getStrategiesUsedForObjects($this->usedClassesOnFlush) as $strategy) {
$strategy->onFlushEnd($om, $ea);
}
}
/**
* Updates tree on Node removal
*
* @param EventArgs $args
*/
public function preRemove(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$object = $ea->getObject();
$meta = $om->getClassMetadata(get_class($object));
if ($this->getConfiguration($om, $meta->name)) {
$this->getStrategy($om, $meta->name)->processPreRemove($om, $object);
}
}
/**
* Checks for persisted Nodes
*
* @param EventArgs $args
*/
public function prePersist(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$object = $ea->getObject();
$meta = $om->getClassMetadata(get_class($object));
if ($this->getConfiguration($om, $meta->name)) {
$this->getStrategy($om, $meta->name)->processPrePersist($om, $object);
}
}
/**
* Checks for updated Nodes
*
* @param EventArgs $args
*/
public function preUpdate(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$object = $ea->getObject();
$meta = $om->getClassMetadata(get_class($object));
if ($this->getConfiguration($om, $meta->name)) {
$this->getStrategy($om, $meta->name)->processPreUpdate($om, $object);
}
}
/**
* Checks for pending Nodes to fully synchronize
* the tree
*
* @param EventArgs $args
*/
public function postPersist(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$object = $ea->getObject();
$meta = $om->getClassMetadata(get_class($object));
if ($this->getConfiguration($om, $meta->name)) {
$this->getStrategy($om, $meta->name)->processPostPersist($om, $object, $ea);
}
}
/**
* Checks for pending Nodes to fully synchronize
* the tree
*
* @param EventArgs $args
*/
public function postUpdate(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$object = $ea->getObject();
$meta = $om->getClassMetadata(get_class($object));
if ($this->getConfiguration($om, $meta->name)) {
$this->getStrategy($om, $meta->name)->processPostUpdate($om, $object, $ea);
}
}
/**
* Checks for pending Nodes to fully synchronize
* the tree
*
* @param EventArgs $args
*/
public function postRemove(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$object = $ea->getObject();
$meta = $om->getClassMetadata(get_class($object));
if ($this->getConfiguration($om, $meta->name)) {
$this->getStrategy($om, $meta->name)->processPostRemove($om, $object, $ea);
}
}
/**
* Mapps additional metadata
*
* @param EventArgs $eventArgs
*/
public function loadClassMetadata(EventArgs $eventArgs)
{
$ea = $this->getEventAdapter($eventArgs);
$om = $ea->getObjectManager();
$meta = $eventArgs->getClassMetadata();
$this->loadMetadataForObjectClass($om, $meta);
if (isset(self::$configurations[$this->name][$meta->name]) && self::$configurations[$this->name][$meta->name]) {
$this->getStrategy($om, $meta->name)->processMetadataLoad($om, $meta);
}
}
/**
* {@inheritDoc}
*/
protected function getNamespace()
{
return __NAMESPACE__;
}
/**
* Get the list of strategy instances used for
* given object classes
*
* @param array $classes
*
* @return Strategy[]
*/
protected function getStrategiesUsedForObjects(array $classes)
{
$strategies = array();
foreach ($classes as $name => $opt) {
if (isset($this->strategies[$name]) && !isset($strategies[$this->strategies[$name]])) {
$strategies[$this->strategies[$name]] = $this->strategyInstances[$this->strategies[$name]];
}
}
return $strategies;
}
}