540 lines
17 KiB
PHP
540 lines
17 KiB
PHP
<?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);
|
|
}
|