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