This commit is contained in:
Xes
2025-08-14 22:41:49 +02:00
parent 2de81ccc46
commit 8ce45119b6
39774 changed files with 4309466 additions and 0 deletions

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);
}
}
}
}
}
}