Skip to content

Instantly share code, notes, and snippets.

@temuri416
Last active October 15, 2017 20:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save temuri416/05b94d7e3491994993a997d150d32ea0 to your computer and use it in GitHub Desktop.
Save temuri416/05b94d7e3491994993a997d150d32ea0 to your computer and use it in GitHub Desktop.
<?php
namespace My\Model\Behaviour;
use
Phalcon\Events\Manager as EventsManager
, Phalcon\Mvc\Model\Behavior\NestedSet as phNeSe
, Phalcon\Events\ManagerInterface
, Phalcon\Events\EventsAwareInterface
, Phalcon\Mvc\Model
, Phalcon\Mvc\ModelInterface
, Phalcon\Mvc\Model\Behavior
, Phalcon\Mvc\Model\Exception
, Phalcon\Db\AdapterInterface
, Phalcon\Mvc\Model\BehaviorInterface
, Phalcon\Mvc\Model\ResultsetInterface
;
class NestedSet extends Behavior implements BehaviorInterface, EventsAwareInterface
{
const EVT_TYPE_QUERY = 'query';
const EVT_DESCENDANTS = 'descendants';
const EVT_ANCESTORS = 'ancestors';
const EVT_PARENT = 'parent';
const EVT_PREV = 'prev';
const EVT_NEXT = 'next';
const EVT_ROOTS = 'roots';
/**
* @var EventsManager
*/
protected $_eventsManager;
/**
* @var AdapterInterface|null
*/
private $db;
/**
* @var ModelInterface|null
*/
private $owner;
private $hasManyRoots = false;
private $rootAttribute = 'root';
private $leftAttribute = 'lft';
private $rightAttribute = 'rgt';
private $levelAttribute = 'level';
private $primaryKey = 'id';
private $ignoreEvent = false;
private $deleted = false;
public function __construct($options = null)
{
if (isset($options['db']) && $options['db'] instanceof AdapterInterface) {
$this->db = $options['db'];
}
if (isset($options['hasManyRoots'])) {
$this->hasManyRoots = (bool) $options['hasManyRoots'];
}
if (isset($options['rootAttribute'])) {
$this->rootAttribute = $options['rootAttribute'];
}
if (isset($options['leftAttribute'])) {
$this->leftAttribute = $options['leftAttribute'];
}
if (isset($options['rightAttribute'])) {
$this->rightAttribute = $options['rightAttribute'];
}
if (isset($options['levelAttribute'])) {
$this->levelAttribute = $options['levelAttribute'];
}
if (isset($options['primaryKey'])) {
$this->primaryKey = $options['primaryKey'];
}
if (isset($options['eventsManager'])) {
$this->_eventsManager = $options['eventsManager'];
}
}
/**
* @param string $eventType
* @param ModelInterface $model
* @throws Exception
*/
public function notify($eventType, ModelInterface $model)
{
switch ($eventType) {
case 'beforeCreate':
case 'beforeDelete':
case 'beforeUpdate':
if (!$this->ignoreEvent) {
throw new Exception(
sprintf(
'You should not use %s:%s when %s attached. Use the methods of behavior.',
get_class($model),
$eventType,
__CLASS__
)
);
}
break;
}
}
/**
* Calls a method when it's missing in the model
*
* @param ModelInterface $model
* @param string $method
* @param null $arguments
* @return mixed|null|string
* @throws Exception
*/
public function missingMethod(ModelInterface $model, $method, $arguments = null)
{
if (!method_exists($this, $method)) {
return null;
}
$this->getDbHandler($model);
$this->setOwner($model);
return call_user_func_array([$this, $method], $arguments);
}
/**
* @param EventsManager $eventsManager
* @return $this
*/
public function setEventsManager(ManagerInterface $eventsManager)
{
$this->_eventsManager = $eventsManager;
return $this;
}
/**
* @return EventsManager
*/
public function getEventsManager()
{
return $this->_eventsManager;
}
/**
* @return ModelInterface
*/
public function getOwner()
{
if (!$this->owner instanceof ModelInterface) {
trigger_error("Owner isn't a valid ModelInterface instance.", E_USER_WARNING);
}
return $this->owner;
}
public function setOwner(ModelInterface $owner)
{
$this->owner = $owner;
}
public function getIsNewRecord()
{
return $this->getOwner()->getDirtyState() == Model::DIRTY_STATE_TRANSIENT;
}
/**
* Returns if the current node is deleted.
*
* @return boolean whether the node is deleted.
*/
public function getIsDeletedRecord()
{
return $this->deleted;
}
/**
* Sets if the current node is deleted.
*
* @param boolean $value whether the node is deleted.
*/
public function setIsDeletedRecord($value)
{
$this->deleted = $value;
}
/**
* Determines if node is leaf.
*
* @return boolean whether the node is leaf.
*/
public function isLeaf()
{
$owner = $this->getOwner();
return $owner->{$this->rightAttribute} - $owner->{$this->leftAttribute} === 1;
}
/**
* Determines if node is root.
*
* @return boolean whether the node is root.
*/
public function isRoot()
{
return $this->getOwner()->{$this->leftAttribute} == 1;
}
/**
* Determines if node is descendant of subject node.
*
* @param \Phalcon\Mvc\ModelInterface $subj the subject node.
*
* @return boolean whether the node is descendant of subject node.
*/
public function isDescendantOf($subj)
{
$owner = $this->getOwner();
$result = ($owner->{$this->leftAttribute} > $subj->{$this->leftAttribute})
&& ($owner->{$this->rightAttribute} < $subj->{$this->rightAttribute});
if ($this->hasManyRoots) {
$result = $result && ($owner->{$this->rootAttribute} === $subj->{$this->rootAttribute});
}
return $result;
}
/**
* Named scope. Gets descendants for node.
*
* @param int $depth the depth.
* @param boolean $addSelf If TRUE - parent node will be added to result set.
* @return ResultsetInterface
*/
public function descendants($depth = null, $addSelf = false)
{
$owner = $this->getOwner();
$query = $owner::query()
->where($this->leftAttribute . '>' . ($addSelf ? '=' : null) . $owner->{$this->leftAttribute})
->andWhere($this->rightAttribute . '<' . ($addSelf ? '=' : null) . $owner->{$this->rightAttribute})
->orderBy($this->leftAttribute);
if ($depth !== null) {
$query = $query->andWhere($this->levelAttribute . '<=' . ($owner->{$this->levelAttribute} + $depth));
}
if ($this->hasManyRoots) {
$query = $query->andWhere($this->rootAttribute . '=' . $owner->{$this->rootAttribute});
}
if ($evt = $this->getEventsManager()) {
$evt->fire(self::EVT_TYPE_QUERY . ':' . self::EVT_DESCENDANTS, $query, $owner);
}
return $query->execute();
}
/**
* Named scope. Gets children for node (direct descendants only).
*
* @return ResultsetInterface
*/
public function children()
{
return $this->descendants(1);
}
/**
* Named scope. Gets ancestors for node.
*
* @param int $depth the depth.
* @return ResultsetInterface
*/
public function ancestors($depth = null)
{
$owner = $this->getOwner();
$query = $owner::query()
->where($this->leftAttribute . '<' . $owner->{$this->leftAttribute})
->andWhere($this->rightAttribute . '>' . $owner->{$this->rightAttribute})
->orderBy($this->leftAttribute);
if ($depth !== null) {
$query = $query->andWhere($this->levelAttribute . '>=' . ($owner->{$this->levelAttribute} - $depth));
}
if ($this->hasManyRoots) {
$query = $query->andWhere($this->rootAttribute . '=' . $owner->{$this->rootAttribute});
}
if ($evt = $this->getEventsManager()) {
$evt->fire(self::EVT_TYPE_QUERY . ':' . self::EVT_ANCESTORS, $query, $owner);
}
return $query->execute();
}
/**
* Named scope. Gets root node(s).
*
* @return ResultsetInterface
*/
public function roots()
{
$query = $owner::query()
->andWhere($this->leftAttribute . ' = 1')
;
if ($evt = $this->getEventsManager()) {
$evt->fire(self::EVT_TYPE_QUERY . ':' . self::EVT_ROOTS, $query, $owner);
}
return $owner::find($query->getParams());
}
/**
* Named scope. Gets parent of node.
*
* @return \Phalcon\Mvc\ModelInterface
*/
public function parent()
{
$owner = $this->getOwner();
$query = $owner::query()
->where($this->leftAttribute . '<' . $owner->{$this->leftAttribute})
->andWhere($this->rightAttribute . '>' . $owner->{$this->rightAttribute})
->orderBy($this->rightAttribute)
->limit(1);
if ($this->hasManyRoots) {
$query = $query->andWhere($this->rootAttribute . '=' . $owner->{$this->rootAttribute});
}
if ($evt = $this->getEventsManager()) {
$evt->fire(self::EVT_TYPE_QUERY . ':' . self::EVT_PARENT, $query, $owner);
}
return $query->execute()->getFirst();
}
/**
* Named scope. Gets previous sibling of node.
*
* @return ModelInterface
*/
public function prev()
{
$owner = $this->getOwner();
$query = $owner::query()
->where($this->rightAttribute . '=' . ($owner->{$this->leftAttribute} - 1));
if ($this->hasManyRoots) {
$query = $query->andWhere($this->rootAttribute . '=' . $owner->{$this->rootAttribute});
}
if ($evt = $this->getEventsManager()) {
$evt->fire(self::EVT_TYPE_QUERY . ':' . self::EVT_PREV, $query, $owner);
}
return $query->execute()->getFirst();
}
/**
* Named scope. Gets next sibling of node.
*
* @return ModelInterface
*/
public function next()
{
$owner = $this->getOwner();
$query = $owner::query()
->where($this->leftAttribute . '=' . ($owner->{$this->rightAttribute} + 1));
if ($this->hasManyRoots) {
$query = $query->andWhere($this->rootAttribute . '=' . $owner->{$this->rootAttribute});
}
if ($evt = $this->getEventsManager()) {
$evt->fire(self::EVT_TYPE_QUERY . ':' . self::EVT_NEXT, $query, $owner);
}
return $query->execute()->getFirst();
}
/**
* Prepends node to target as first child.
*
* @param ModelInterface $target the target
* @param array $attributes List of attributes.
* @return boolean
*/
public function prependTo(ModelInterface $target, array $attributes = null)
{
// Re-search $target
$target = $target::findFirst($target->{$this->primaryKey});
return $this->addNode($target, $target->{$this->leftAttribute} + 1, 1, $attributes);
}
/**
* Prepends target to node as first child.
*
* @param ModelInterface $target the target.
* @param array $attributes list of attributes.
* @return boolean
*/
public function prepend(ModelInterface $target, array $attributes = null)
{
return $target->prependTo($this->getOwner(), $attributes);
}
/**
* Appends node to target as last child.
*
* @param ModelInterface $target the target.
* @param array $attributes list of attributes.
* @return boolean
*/
public function appendTo(ModelInterface $target, array $attributes = null)
{
// Re-search $target
$target = $target::findFirst($target->{$this->primaryKey});
return $this->addNode($target, $target->{$this->rightAttribute}, 1, $attributes);
}
/**
* Appends target to node as last child.
*
* @param ModelInterface $target the target.
* @param array $attributes list of attributes.
* @return boolean
*/
public function append(ModelInterface $target, array $attributes = null)
{
return $target->appendTo($this->getOwner(), $attributes);
}
/**
* Inserts node as previous sibling of target.
*
* @param ModelInterface $target the target.
* @param array $attributes list of attributes.
* @return boolean
*/
public function insertBefore(ModelInterface $target, array $attributes = null)
{
return $this->addNode($target, $target->{$this->leftAttribute}, 0, $attributes);
}
/**
* Inserts node as next sibling of target.
*
* @param ModelInterface $target the target.
* @param array $attributes list of attributes.
* @return boolean
*/
public function insertAfter(ModelInterface $target, array $attributes = null)
{
return $this->addNode($target, $target->{$this->rightAttribute} + 1, 0, $attributes);
}
/**
* Move node as previous sibling of target.
*
* @param ModelInterface $target the target.
* @return boolean
*/
public function moveBefore(ModelInterface $target)
{
return $this->moveNode($target, $target->{$this->leftAttribute}, 0);
}
/**
* Move node as next sibling of target.
*
* @param ModelInterface $target the target.
* @return boolean
*/
public function moveAfter(ModelInterface $target)
{
return $this->moveNode($target, $target->{$this->rightAttribute} + 1, 0);
}
/**
* Move node as first child of target.
*
* @param ModelInterface $target the target.
* @return boolean
*/
public function moveAsFirst(ModelInterface $target)
{
return $this->moveNode($target, $target->{$this->leftAttribute} + 1, 1);
}
/**
* Move node as last child of target.
*
* @param ModelInterface $target the target.
* @return boolean
*/
public function moveAsLast(ModelInterface $target)
{
return $this->moveNode($target, $target->{$this->rightAttribute}, 1);
}
/**
* Move node as new root.
*
* @return boolean
* @throws Exception
*/
public function moveAsRoot()
{
$owner = $this->getOwner();
if (!$this->hasManyRoots) {
throw new Exception('Many roots mode is off.');
}
if ($this->getIsNewRecord()) {
throw new Exception('The node should not be new record.');
}
if ($this->getIsDeletedRecord()) {
throw new Exception('The node should not be deleted.');
}
if ($owner->isRoot()) {
throw new Exception('The node already is root node.');
}
$this->db->begin();
$left = $owner->{$this->leftAttribute};
$right = $owner->{$this->rightAttribute};
$levelDelta = 1 - $owner->{$this->levelAttribute};
$delta = 1 - $left;
$condition = $this->leftAttribute . '>=' . $left . ' AND ';
$condition .= $this->rightAttribute . '<=' . $right . ' AND ';
$condition .= $this->rootAttribute . '=' . $owner->{$this->rootAttribute};
$this->ignoreEvent = true;
foreach ($owner::find($condition) as $i) {
$arr = [
$this->leftAttribute => $i->{$this->leftAttribute} + $delta,
$this->rightAttribute => $i->{$this->rightAttribute} + $delta,
$this->levelAttribute => $i->{$this->levelAttribute} + $levelDelta,
$this->rootAttribute => $owner->{$this->primaryKey}
];
if ($i->update($arr) == false) {
$this->db->rollback();
$this->ignoreEvent = false;
return false;
}
}
$this->ignoreEvent = false;
$this->shiftLeftRight($right + 1, $left - $right - 1);
$this->db->commit();
return true;
}
/**
* Create root node if multiple-root tree mode. Update node if it's not new.
*
* @param array $attributes list of attributes.
* @param array $whiteList whether to perform validation.
* @return boolean
*/
public function saveNode(array $attributes = null, array $whiteList = null)
{
$owner = $this->getOwner();
$this->ignoreEvent = true;
if (!$owner->readAttribute($this->primaryKey)) {
$result = $this->makeRoot($attributes, $whiteList);
} else {
$result = $owner->update($attributes, $whiteList);
}
$this->ignoreEvent = false;
return $result;
}
/**
* Deletes node and it's descendants.
*
* @return boolean
* @throws Exception
*/
public function deleteNode()
{
$owner = $this->getOwner();
if ($this->getIsNewRecord()) {
throw new Exception('The node cannot be deleted because it is new.');
}
if ($this->getIsDeletedRecord()) {
throw new Exception('The node cannot be deleted because it is already deleted.');
}
$this->db->begin();
if ($owner->isLeaf()) {
$this->ignoreEvent = true;
if ($owner->delete() == false) {
$this->db->rollback();
$this->ignoreEvent = false;
return false;
}
} else {
$condition = $this->leftAttribute . '>=' . $owner->{$this->leftAttribute} . ' AND ';
$condition .= $this->rightAttribute . '<=' . $owner->{$this->rightAttribute};
if ($this->hasManyRoots) {
$condition .= ' AND ' . $this->rootAttribute . '=' . $owner->{$this->rootAttribute};
}
$this->ignoreEvent = true;
foreach ($owner::find($condition) as $i) {
if ($i->delete() == false) {
$this->db->rollback();
$this->ignoreEvent = false;
return false;
}
}
}
$key = $owner->{$this->rightAttribute} + 1;
$delta = $owner->{$this->leftAttribute} - $owner->{$this->rightAttribute} - 1;
$this->shiftLeftRight($key, $delta);
$this->ignoreEvent = false;
$this->db->commit();
return true;
}
/**
* Gets DB handler.
*
* @param ModelInterface $model
* @return AdapterInterface
* @throws Exception
*/
private function getDbHandler(ModelInterface $model)
{
if (!$this->db instanceof AdapterInterface) {
if ($model->getDi()->has('db')) {
$db = $model->getDi()->getShared('db');
if (!$db instanceof AdapterInterface) {
throw new Exception('The "db" service which was obtained from DI is invalid adapter.');
}
$this->db = $db;
} else {
throw new Exception('Undefined database handler.');
}
}
return $this->db;
}
/**
* @param ModelInterface $target
* @param int $key
* @param int $levelUp
*
* @return boolean
* @throws Exception
*/
private function moveNode(ModelInterface $target, $key, $levelUp)
{
$owner = $this->getOwner();
if ($this->getIsNewRecord()) {
throw new Exception('The node should not be new record.');
}
if ($this->getIsDeletedRecord()) {
throw new Exception('The node should not be deleted.');
}
if ($target->getIsDeletedRecord()) {
throw new Exception('The target node should not be deleted.');
}
if ($owner == $target) {
throw new Exception('The target node should not be self.');
}
if ($target->isDescendantOf($owner)) {
throw new Exception('The target node should not be descendant.');
}
if (!$levelUp && $target->isRoot()) {
throw new Exception('The target node should not be root.');
}
$this->db->begin();
$left = $owner->{$this->leftAttribute};
$right = $owner->{$this->rightAttribute};
$levelDelta = $target->{$this->levelAttribute} - $owner->{$this->levelAttribute} + $levelUp;
if ($this->hasManyRoots && $owner->{$this->rootAttribute} !== $target->{$this->rootAttribute}) {
$this->ignoreEvent = true;
// 1. Rebuild the target tree
foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) {
$condition = join(' AND ', [
$attribute . '>=' . $key,
$this->rootAttribute . '=' . $target->{$this->rootAttribute},
]);
foreach ($target::find($condition) as $i) {
$delta = $right - $left + 1;
/** @var ModelInterface $i */
if (!$i->update([$attribute => $i->{$attribute} + $delta])) {
$this->db->rollback();
$this->ignoreEvent = false;
return false;
}
}
}
$delta = $key - $left;
// 2. Rebuild the owner's tree of children (owner sub-tree)
$condition = $this->leftAttribute . '>=' . $left . ' AND ';
$condition .= $this->rightAttribute . '<=' . $right . ' AND ';
$condition .= $this->rootAttribute . '=' . $owner->{$this->rootAttribute};
foreach ($owner::find($condition) as $i) {
$arr = [
$this->leftAttribute => $i->{$this->leftAttribute} + $delta,
$this->rightAttribute => $i->{$this->rightAttribute} + $delta,
$this->levelAttribute => $i->{$this->levelAttribute} + $levelDelta,
$this->rootAttribute => $target->{$this->rootAttribute}
];
if ($i->update($arr) == false) {
$this->db->rollback();
$this->ignoreEvent = false;
return false;
}
}
// 3. Rebuild the owner tree
$this->shiftLeftRight($right + 1, $left - $right - 1, $owner);
$this->ignoreEvent = false;
$this->db->commit();
} else {
$delta = $right - $left + 1;
$this->ignoreEvent = true;
$this->shiftLeftRight($key, $delta);
if ($left >= $key) {
$left += $delta;
$right += $delta;
}
$condition = $this->leftAttribute . '>=' . $left . ' AND ' . $this->rightAttribute . '<=' . $right;
if ($this->hasManyRoots) {
$condition .= ' AND ' . $this->rootAttribute . '=' . $owner->{$this->rootAttribute};
}
foreach ($owner::find($condition) as $i) {
if ($i->update([$this->levelAttribute => $i->{$this->levelAttribute} + $levelDelta]) == false) {
$this->db->rollback();
$this->ignoreEvent = false;
return false;
}
}
foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) {
$condition = $attribute . '>=' . $left . ' AND ' . $attribute . '<=' . $right;
if ($this->hasManyRoots) {
$condition .= ' AND ' . $this->rootAttribute . '=' . $owner->{$this->rootAttribute};
}
foreach ($owner::find($condition) as $i) {
if ($i->update([$attribute => $i->{$attribute} + $key - $left]) == false) {
$this->db->rollback();
$this->ignoreEvent = false;
return false;
}
}
}
$this->shiftLeftRight($right + 1, -$delta);
$this->ignoreEvent = false;
$this->ignoreEvent = false;
$this->db->commit();
}
return true;
}
/**
* @param int $key
* @param int $delta
* @param ModelInterface $model
* @return boolean
*/
private function shiftLeftRight($key, $delta, ModelInterface $model = null)
{
$owner = $model ?: $this->getOwner();
foreach ([$this->leftAttribute, $this->rightAttribute] as $attribute) {
$condition = $attribute . '>=' . $key;
if ($this->hasManyRoots) {
$condition .= ' AND ' . $this->rootAttribute . '=' . $owner->{$this->rootAttribute};
}
foreach ($owner::find($condition) as $i) {
/** @var ModelInterface $i */
if ($i->update([$attribute => $i->{$attribute} + $delta]) == false) {
$this->db->rollback();
$this->ignoreEvent = false;
return false;
}
}
}
return true;
}
/**
* @param ModelInterface $target
* @param int $key
* @param int $levelUp
* @param array $attributes
*
* @return boolean
* @throws \Exception
*/
private function addNode(ModelInterface $target, $key, $levelUp, array $attributes = null)
{
$owner = $this->getOwner();
if (!$this->getIsNewRecord()) {
throw new Exception('The node cannot be inserted because it is not new.');
}
if ($this->getIsDeletedRecord()) {
throw new Exception('The node cannot be inserted because it is deleted.');
}
if ($target->getIsDeletedRecord()) {
throw new Exception('The node cannot be inserted because target node is deleted.');
}
if ($owner == $target) {
throw new Exception('The target node should not be self.');
}
if (!$levelUp && $target->isRoot()) {
throw new Exception('The target node should not be root.');
}
if ($this->hasManyRoots) {
$owner->{$this->rootAttribute} = $target->{$this->rootAttribute};
}
$db = $this->getDbHandler($owner);
$db->begin();
try {
$this->ignoreEvent = true;
$this->shiftLeftRight($key, 2);
$this->ignoreEvent = false;
$owner->{$this->leftAttribute} = $key;
$owner->{$this->rightAttribute} = $key + 1;
$owner->{$this->levelAttribute} = $target->{$this->levelAttribute} + $levelUp;
$this->ignoreEvent = true;
$result = $owner->create($attributes);
$this->ignoreEvent = false;
if (!$result) {
$db->rollback();
$this->ignoreEvent = false;
return false;
}
$db->commit();
} catch (\Exception $e) {
$db->rollback();
$this->ignoreEvent = false;
throw $e;
}
return true;
}
/**
* @param array $attributes
* @param array $whiteList
*
* @return boolean
* @throws Exception
*/
private function makeRoot($attributes, $whiteList)
{
$owner = $this->getOwner();
$owner->{$this->rootAttribute} = 0;
$owner->{$this->leftAttribute} = 1;
$owner->{$this->rightAttribute} = 2;
$owner->{$this->levelAttribute} = 1;
if ($this->hasManyRoots) {
$this->db->begin();
$this->ignoreEvent = true;
if ($owner->create($attributes, $whiteList) == false) {
$this->db->rollback();
$this->ignoreEvent = false;
return false;
}
$pk = $owner->{$this->rootAttribute} = $owner->{$this->primaryKey};
$owner::findFirst($pk)->update([$this->rootAttribute => $pk]);
$this->ignoreEvent = false;
$this->db->commit();
} else {
if (count($owner->roots())) {
throw new Exception('Cannot create more than one root in single root mode.');
}
if ($owner->create($attributes, $whiteList) == false) {
return false;
}
}
return true;
}
}
// And then, something like this in an abstract class that has NestedSet behavior:
$eventsManager = new EventsManager;
$eventsManager->attach(BehNestedSet::EVT_TYPE_QUERY, function(Event $event, Criteria $criteria, AbstractNode $node) {
if ($callback = $this->getQueryModificationCallback()) {
$callback($criteria);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment