File: /home/accemeff/vendor/craftcms/cms/src/services/Structures.php
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/
namespace craft\services;
use Craft;
use craft\base\Element;
use craft\base\ElementInterface;
use craft\db\Query;
use craft\db\Table;
use craft\errors\StructureNotFoundException;
use craft\events\MoveElementEvent;
use craft\models\Structure;
use craft\records\Structure as StructureRecord;
use craft\records\StructureElement;
use yii\base\Component;
use yii\base\Exception;
/**
* Structures service.
* An instance of the Structures service is globally accessible in Craft via [[\craft\base\ApplicationTrait::getStructures()|`Craft::$app->structures`]].
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 3.0
*/
class Structures extends Component
{
// Constants
// =========================================================================
/**
* @event MoveElementEvent The event that is triggered before an element is moved.
*/
const EVENT_BEFORE_MOVE_ELEMENT = 'beforeMoveElement';
/**
* @event MoveElementEvent The event that is triggered after an element is moved.
*/
const EVENT_AFTER_MOVE_ELEMENT = 'afterMoveElement';
// Properties
// =========================================================================
/**
* @var int The timeout to pass to [[\yii\mutex\Mutex::acquire()]] when acquiring a lock on the structure.
*/
public $mutexTimeout = 0;
/**
* @var
*/
private $_rootElementRecordsByStructureId;
// Public Methods
// =========================================================================
// Structure CRUD
// -------------------------------------------------------------------------
/**
* Returns a structure by its ID.
*
* @param int $structureId
* @param bool $withTrashed
* @return Structure|null
*/
public function getStructureById(int $structureId, bool $withTrashed = false)
{
$query = (new Query())
->select([
'id',
'maxLevels',
'uid'
])
->from([Table::STRUCTURES])
->where(['id' => $structureId]);
if (!$withTrashed) {
$query->andWhere(['dateDeleted' => null]);
}
$result = $query->one();
return $result ? new Structure($result) : null;
}
/**
* Returns a structure by its UID.
*
* @param string $structureUid
* @param bool $withTrashed
* @return Structure|null
*/
public function getStructureByUid(string $structureUid, bool $withTrashed = false)
{
$query = (new Query())
->select([
'id',
'maxLevels',
'uid'
])
->from([Table::STRUCTURES])
->where(['uid' => $structureUid]);
if (!$withTrashed) {
$query->andWhere(['dateDeleted' => null]);
}
$result = $query->one();
return $result ? new Structure($result) : null;
}
/**
* Saves a structure
*
* @param Structure $structure
* @return bool Whether the structure was saved successfully
* @throws StructureNotFoundException if $structure->id is invalid
*/
public function saveStructure(Structure $structure): bool
{
if ($structure->id) {
$structureRecord = StructureRecord::findWithTrashed()
->andWhere(['id' => $structure->id])
->one();
if (!$structureRecord) {
throw new StructureNotFoundException("No structure exists with the ID '{$structure->id}'");
}
} else {
$structureRecord = new StructureRecord();
}
$structureRecord->maxLevels = $structure->maxLevels;
$structureRecord->uid = $structure->uid;
if ($structureRecord->dateDeleted) {
$success = $structureRecord->restore();
} else {
$success = $structureRecord->save();
}
if ($success) {
$structure->id = $structureRecord->id;
} else {
$structure->addErrors($structureRecord->getErrors());
}
return $success;
}
/**
* Deletes a structure by its ID.
*
* @param int $structureId
* @return bool
*/
public function deleteStructureById(int $structureId): bool
{
if (!$structureId) {
return false;
}
$affectedRows = Craft::$app->getDb()->createCommand()
->softDelete(Table::STRUCTURES, [
'id' => $structureId
])
->execute();
return (bool)$affectedRows;
}
/**
* Returns the descendant level delta for a given element.
*
* @param int $structureId
* @param ElementInterface $element
* @return int
*/
public function getElementLevelDelta(int $structureId, ElementInterface $element): int
{
$elementRecord = $this->_getElementRecord($structureId, $element);
/** @var StructureElement $deepestDescendant */
$deepestDescendant = $elementRecord
->children()
->orderBy(['level' => SORT_DESC])
->one();
if ($deepestDescendant) {
return $deepestDescendant->level - $elementRecord->level;
}
return 0;
}
// Moving elements around
// -------------------------------------------------------------------------
/**
* Prepends an element to another within a given structure.
*
* @param int $structureId
* @param ElementInterface $element
* @param ElementInterface $parentElement
* @param string $mode Whether this is an "insert", "update", or "auto".
* @return bool
* @throws Exception
*/
public function prepend(int $structureId, ElementInterface $element, ElementInterface $parentElement, string $mode = 'auto'): bool
{
$parentElementRecord = $this->_getElementRecord($structureId, $parentElement);
if ($parentElementRecord === null) {
throw new Exception('There was a problem getting the parent element.');
}
return $this->_doIt($structureId, $element, $parentElementRecord, 'prependTo', $mode);
}
/**
* Appends an element to another within a given structure.
*
* @param int $structureId
* @param ElementInterface $element
* @param ElementInterface $parentElement
* @param string $mode Whether this is an "insert", "update", or "auto".
* @return bool
* @throws Exception
*/
public function append(int $structureId, ElementInterface $element, ElementInterface $parentElement, string $mode = 'auto'): bool
{
$parentElementRecord = $this->_getElementRecord($structureId, $parentElement);
if ($parentElementRecord === null) {
throw new Exception('There was a problem getting the parent element.');
}
return $this->_doIt($structureId, $element, $parentElementRecord, 'appendTo', $mode);
}
/**
* Prepends an element to the root of a given structure.
*
* @param int $structureId
* @param ElementInterface $element
* @param string $mode Whether this is an "insert", "update", or "auto".
* @return bool
* @throws Exception
*/
public function prependToRoot(int $structureId, ElementInterface $element, string $mode = 'auto'): bool
{
$parentElementRecord = $this->_getRootElementRecord($structureId);
if ($parentElementRecord === null) {
throw new Exception('There was a problem getting the parent element.');
}
return $this->_doIt($structureId, $element, $parentElementRecord, 'prependTo', $mode);
}
/**
* Appends an element to the root of a given structure.
*
* @param int $structureId
* @param ElementInterface $element
* @param string $mode Whether this is an "insert", "update", or "auto".
* @return bool
* @throws Exception
*/
public function appendToRoot(int $structureId, ElementInterface $element, string $mode = 'auto'): bool
{
$parentElementRecord = $this->_getRootElementRecord($structureId);
if ($parentElementRecord === null) {
throw new Exception('There was a problem getting the parent element.');
}
return $this->_doIt($structureId, $element, $parentElementRecord, 'appendTo', $mode);
}
/**
* Moves an element before another within a given structure.
*
* @param int $structureId
* @param ElementInterface $element
* @param ElementInterface $nextElement
* @param string $mode Whether this is an "insert", "update", or "auto".
* @return bool
* @throws Exception
*/
public function moveBefore(int $structureId, ElementInterface $element, ElementInterface $nextElement, string $mode = 'auto'): bool
{
$nextElementRecord = $this->_getElementRecord($structureId, $nextElement);
if ($nextElementRecord === null) {
throw new Exception('There was a problem getting the next element.');
}
return $this->_doIt($structureId, $element, $nextElementRecord, 'insertBefore', $mode);
}
/**
* Moves an element after another within a given structure.
*
* @param int $structureId
* @param ElementInterface $element
* @param ElementInterface $prevElement
* @param string $mode Whether this is an "insert", "update", or "auto".
* @return bool
* @throws Exception
*/
public function moveAfter(int $structureId, ElementInterface $element, ElementInterface $prevElement, string $mode = 'auto'): bool
{
$prevElementRecord = $this->_getElementRecord($structureId, $prevElement);
if ($prevElementRecord === null) {
throw new Exception('There was a problem getting the previous element.');
}
return $this->_doIt($structureId, $element, $prevElementRecord, 'insertAfter', $mode);
}
// Private Methods
// =========================================================================
/**
* Returns a structure element record from given structure and element IDs.
*
* @param int $structureId
* @param ElementInterface $element
* @return StructureElement|null
*/
private function _getElementRecord(int $structureId, ElementInterface $element)
{
/** @var Element $element */
$elementId = $element->id;
if ($elementId) {
return StructureElement::findOne([
'structureId' => $structureId,
'elementId' => $elementId
]);
}
return null;
}
/**
* Returns the root node for a given structure ID, or creates one if it doesn't exist.
*
* @param int $structureId
* @return StructureElement
*/
private function _getRootElementRecord(int $structureId): StructureElement
{
if (!isset($this->_rootElementRecordsByStructureId[$structureId])) {
$elementRecord = StructureElement::find()
->where(['structureId' => $structureId])
->roots()
->one();
if (!$elementRecord) {
// Create it
$elementRecord = new StructureElement();
$elementRecord->structureId = $structureId;
$elementRecord->makeRoot();
}
$this->_rootElementRecordsByStructureId[$structureId] = $elementRecord;
}
return $this->_rootElementRecordsByStructureId[$structureId];
}
/**
* Updates a ElementInterface with the new structure attributes from a StructureElement record.
*
* @param int $structureId
* @param ElementInterface $element
* @param StructureElement $targetElementRecord
* @param string $action
* @param string $mode
* @return bool Whether it was done
* @throws \Throwable if reasons
*/
private function _doIt($structureId, ElementInterface $element, StructureElement $targetElementRecord, $action, $mode): bool
{
// Get a lock or bust
$lockName = 'structure:' . $structureId;
$mutex = Craft::$app->getMutex();
if (!$mutex->acquire($lockName, $this->mutexTimeout)) {
throw new Exception('Unable to acquire a lock for the structure ' . $structureId);
}
$elementRecord = null;
/** @var Element $element */
// Figure out what we're doing
if ($mode !== 'insert') {
// See if there's an existing structure element record
$elementRecord = $this->_getElementRecord($structureId, $element);
if ($elementRecord !== null) {
$mode = 'update';
}
}
if ($elementRecord === null) {
$elementRecord = new StructureElement();
$elementRecord->structureId = $structureId;
$elementRecord->elementId = $element->id;
$mode = 'insert';
}
if ($mode === 'update' && $this->hasEventHandlers(self::EVENT_BEFORE_MOVE_ELEMENT)) {
// Fire a 'beforeMoveElement' event
$this->trigger(self::EVENT_BEFORE_MOVE_ELEMENT, new MoveElementEvent([
'structureId' => $structureId,
'element' => $element,
]));
}
// Tell the element about it
if (!$element->beforeMoveInStructure($structureId)) {
$mutex->release($lockName);
return false;
}
$transaction = Craft::$app->getDb()->beginTransaction();
try {
if (!$elementRecord->$action($targetElementRecord)) {
$transaction->rollBack();
$mutex->release($lockName);
return false;
}
// Update the element with the latest values.
// todo: we should be able to pull these from $elementRecord - https://github.com/creocoder/yii2-nested-sets/issues/114
$values = (new Query())
->select(['root', 'lft', 'rgt', 'level'])
->from(Table::STRUCTUREELEMENTS)
->where([
'structureId' => $structureId,
'elementId' => $element->id,
])
->one();
$element->root = $values['root'];
$element->lft = $values['lft'];
$element->rgt = $values['rgt'];
$element->level = $values['level'];
// Tell the element about it
$element->afterMoveInStructure($structureId);
$transaction->commit();
} catch (\Throwable $e) {
$transaction->rollBack();
$mutex->release($lockName);
throw $e;
}
$mutex->release($lockName);
if ($mode === 'update' && $this->hasEventHandlers(self::EVENT_AFTER_MOVE_ELEMENT)) {
// Fire an 'afterMoveElement' event
$this->trigger(self::EVENT_AFTER_MOVE_ELEMENT, new MoveElementEvent([
'structureId' => $structureId,
'element' => $element,
]));
}
return true;
}
}