555 lines
20 KiB
PHP
555 lines
20 KiB
PHP
<?php
|
||
|
||
namespace goodboyalex\php_components_pack\tests\data;
|
||
|
||
use goodboyalex\php_components_pack\classes\ActionState;
|
||
use goodboyalex\php_components_pack\classes\ObjectArray;
|
||
use goodboyalex\php_components_pack\extensions\GUIDExtension;
|
||
use goodboyalex\php_components_pack\interfaces\IDuplicated;
|
||
use goodboyalex\php_components_pack\interfaces\ISerializable;
|
||
use IteratorAggregate;
|
||
use Traversable;
|
||
|
||
/**
|
||
* Класс списка элементов меню.
|
||
*
|
||
* @author Александр Бабаев
|
||
* @package freecms
|
||
* @version 0.1
|
||
* @since 0.1
|
||
*/
|
||
final class MenuItems implements ISerializable, IteratorAggregate, IDuplicated
|
||
{
|
||
/**
|
||
* @var ObjectArray Переменная для хранения списка.
|
||
*/
|
||
private ObjectArray $_items;
|
||
|
||
/**
|
||
* Конструктор.
|
||
*
|
||
* @param ObjectArray|array $items Список элементов меню.
|
||
*/
|
||
public function __construct (ObjectArray|array $items)
|
||
{
|
||
$this->_items = is_array($items) ? new ObjectArray($items) : $items;
|
||
}
|
||
|
||
/**
|
||
* Добавляет список элементов.
|
||
*
|
||
* @param ObjectArray|array $items Список элементов меню.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function AddItems (ObjectArray|array $items): void
|
||
{
|
||
// Получаю список
|
||
$itemsToAdd = is_array($items) ? new ObjectArray($items) : $items;
|
||
|
||
/**
|
||
* @var MenuItemModel $item Элемент меню
|
||
*/
|
||
foreach ($itemsToAdd as $item)
|
||
// - добавляю элемент
|
||
$this->AddItem($item);
|
||
}
|
||
|
||
/**
|
||
* Добавляет элемент меню в список.
|
||
*
|
||
* @param MenuItemModel $item Элемент меню.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function AddItem (MenuItemModel $item): void
|
||
{
|
||
// Добавляю
|
||
$this->_items[] = $item;
|
||
}
|
||
|
||
/**
|
||
* Содержится ли элемент в списке.
|
||
*
|
||
* @param string $id Идентификатор элемента меню.
|
||
*
|
||
* @return bool Содержится ли элемент в списке.
|
||
*/
|
||
public function ContainsItem (string $id): bool
|
||
{
|
||
// Проверяю, что идентификатор не пустой
|
||
if (GUIDExtension::IsNotValidOrEmpty($id))
|
||
// - возвращаю отрицательный результат
|
||
return false;
|
||
|
||
// Получаю, что элемент с таким идентификатором существует
|
||
return $this->_items->IsExist(fn (MenuItemModel $item) => $item->Id == $id);
|
||
}
|
||
|
||
/**
|
||
* Вычисляет количество элементов меню.
|
||
*
|
||
* @param callable|null $predicate Выражение, уточняющее детали.
|
||
*
|
||
* @return int Количество элементов меню.
|
||
*/
|
||
public function Count (?callable $predicate = null): int
|
||
{
|
||
return $this->_items->Count($predicate);
|
||
}
|
||
|
||
/**
|
||
* Очищает список элементов.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function Clear (): void
|
||
{
|
||
// Очищаю
|
||
$this->_items->Clear();
|
||
}
|
||
|
||
/**
|
||
* Перемещает элемент меню вверх.
|
||
*
|
||
* @param string $id Идентификатор элемента меню.
|
||
*
|
||
* @return ActionState Результат выполнения.
|
||
*/
|
||
public function MoveUp (string $id): ActionState
|
||
{
|
||
// Создаю результат
|
||
$result = new ActionState();
|
||
|
||
// Проверяю, что идентификатор не пустой
|
||
if (GUIDExtension::IsNotValidOrEmpty($id)) {
|
||
// - то выдаю ошибку
|
||
$result->AddError("Неверный идентификатор (GUID)");
|
||
|
||
// - и прерываю
|
||
return $result;
|
||
}
|
||
|
||
// Загружаю элемент
|
||
$item = $this->GetItem($id);
|
||
|
||
// Если элемент не загружен
|
||
if ($item == null) {
|
||
// - то выдаю ошибку
|
||
$result->AddError("Пункт меню с идентификатором 0%s не найден!");
|
||
|
||
// - и прерываю
|
||
return $result;
|
||
}
|
||
|
||
// Получаю его текущий порядковый номер
|
||
$oldOrder = $item->Order;
|
||
|
||
// Если порядковый номер не больше 1
|
||
if ($oldOrder <= 1) {
|
||
// - то выдаю предупреждение
|
||
$result->AddWarning("Пункт меню находится на первом месте. Перемещение не требуется!");
|
||
|
||
// - и прерываю
|
||
return $result;
|
||
}
|
||
|
||
// Получаю новый порядковый номер
|
||
$newOrder = $oldOrder - 1;
|
||
|
||
// Получаю элемент, находящийся на новом порядковом номере (или GuidExEmpty, если нет элемента)
|
||
$oldPlaceItem = $this->_items->GetRow(fn (MenuItemModel $item) => $item->Order == $newOrder);
|
||
|
||
// Присваиваю новый порядковый номер изменяемого элемента
|
||
$item->Order = $newOrder;
|
||
|
||
// Обновляю список элементов
|
||
$this->UpdateItem($item);
|
||
|
||
// И если на новом порядковом номере существовал элемент
|
||
if ($oldPlaceItem !== false) {
|
||
// - то присваиваю ему старый порядковый номер
|
||
$oldPlaceItem->Order = $oldOrder;
|
||
|
||
// - и обновляю список
|
||
$this->UpdateItem($oldPlaceItem);
|
||
}
|
||
|
||
// Возвращаю результат
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Получает элемент меню.
|
||
*
|
||
* @param string $id Идентификатор элемента меню.
|
||
*
|
||
* @return MenuItemModel|null Элемент меню или null, если элемент не найден.
|
||
*/
|
||
public function GetItem (string $id): ?MenuItemModel
|
||
{
|
||
// Проверяю, что идентификатор не пустой
|
||
if (GUIDExtension::IsNotValidOrEmpty($id))
|
||
// - то возвращаю null
|
||
return null;
|
||
|
||
// Получаю элемент
|
||
$item = $this->_items->GetRow(fn (MenuItemModel $item) => $item->Id == $id);
|
||
|
||
// Если элемент не найден, то возвращаю null, иначе возвращаю найденный элемент.
|
||
return ($item === false) ? null : $item;
|
||
}
|
||
|
||
/**
|
||
* Обновляет элемент меню.
|
||
*
|
||
* @param MenuItemModel $item Обновлённый элемент меню.
|
||
*
|
||
* @return void
|
||
*/
|
||
public function UpdateItem (MenuItemModel $item): void
|
||
{
|
||
// Если элемент существует
|
||
if ($this->_items->IsExist(fn (MenuItemModel $itemM) => $itemM->Id == $item->Id))
|
||
// - то удаляю его
|
||
$this->RemoveItem($item->Id);
|
||
|
||
// Добавляю новый
|
||
$this->AddItem($item);
|
||
}
|
||
|
||
/**
|
||
* Удаление элемента меню.
|
||
*
|
||
* @param string $id Идентификатор элемента меню.
|
||
*
|
||
* @return bool Статус удаления.
|
||
*/
|
||
public function RemoveItem (string $id): bool
|
||
{
|
||
// Проверяю, что идентификатор не пустой
|
||
if (GUIDExtension::IsNotValidOrEmpty($id))
|
||
// - возвращаю отрицательный результат
|
||
return false;
|
||
|
||
return $this->_items->Delete(fn (MenuItemModel $item) => $item->Id == $id);
|
||
}
|
||
|
||
/**
|
||
* Перемещает элемент меню вниз.
|
||
*
|
||
* @param string $id Идентификатор элемента меню.
|
||
*
|
||
* @return ActionState Результат выполнения.
|
||
*/
|
||
public function MoveDown (string $id): ActionState
|
||
{
|
||
// Создаю результат
|
||
$result = new ActionState();
|
||
|
||
// Проверяю, что идентификатор не пустой
|
||
if (GUIDExtension::IsNotValidOrEmpty($id)) {
|
||
// - то выдаю ошибку
|
||
$result->AddError("Неверный идентификатор (GUID)");
|
||
|
||
// - и прерываю
|
||
return $result;
|
||
}
|
||
|
||
// Загружаю элемент
|
||
$item = $this->GetItem($id);
|
||
|
||
// Если элемент не загружен
|
||
if ($item == null) {
|
||
// - то выдаю ошибку
|
||
$result->AddError("Пункт меню с идентификатором 0%s не найден!");
|
||
|
||
// - и прерываю
|
||
return $result;
|
||
}
|
||
|
||
// Получаю его текущий порядковый номер
|
||
$oldOrder = $item->Order;
|
||
|
||
// Если порядковый номер не больше 1
|
||
if ($oldOrder >= $this->GetLastItemOrder($item->ParentId)) {
|
||
// - то выдаю предупреждение
|
||
$result->AddWarning("Пункт меню находится на последнем месте. Перемещение не требуется!");
|
||
|
||
// - и прерываю
|
||
return $result;
|
||
}
|
||
|
||
// Получаю новый порядковый номер
|
||
$newOrder = $oldOrder + 1;
|
||
|
||
// Получаю элемент, находящийся на новом порядковом номере (или GuidExEmpty, если нет элемента)
|
||
$oldPlaceItem = $this->_items->GetRow(fn (MenuItemModel $item) => $item->Order == $newOrder);
|
||
|
||
// Присваиваю новый порядковый номер изменяемого элемента
|
||
$item->Order = $newOrder;
|
||
|
||
// Обновляю список элементов
|
||
$this->UpdateItem($item);
|
||
|
||
// И если на новом порядковом номере существовал элемент
|
||
if ($oldPlaceItem !== false) {
|
||
// - то присваиваю ему старый порядковый номер
|
||
$oldPlaceItem->Order = $oldOrder;
|
||
|
||
// - и обновляю список
|
||
$this->UpdateItem($oldPlaceItem);
|
||
}
|
||
|
||
// Возвращаю результат
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Получает порядковый номер последнего элемента в списке.
|
||
*
|
||
* @param string $parentItemId Идентификатор родителя.
|
||
*
|
||
* @return int Порядковый номер последнего элемента в списке.
|
||
*/
|
||
public function GetLastItemOrder (string $parentItemId): int
|
||
{
|
||
// Проверяю, что идентификатор не пустой
|
||
if (GUIDExtension::IsNotValidOrEmpty($parentItemId) && $parentItemId !== GUIDExtension::GUIDEmpty)
|
||
// - возвращаю отрицательный результат
|
||
return 0;
|
||
|
||
// Получаю последний элемент
|
||
$item = $this->GetSubItems($parentItemId)->Last(false);
|
||
|
||
// Если элемент не найден
|
||
if ($item === false)
|
||
// - возвращаю нулевой результат
|
||
return 0;
|
||
|
||
// Возвращаю порядок последнего элемента
|
||
return $item->Order;
|
||
}
|
||
|
||
/**
|
||
* Получает список вложенных элементов.
|
||
*
|
||
* @param string $parentItemId Идентификатор родителя.
|
||
* @param bool $sortByOrder Нужно ли сортировать по порядку элементов.
|
||
*
|
||
* @return ObjectArray Список вложенных элементов.
|
||
*/
|
||
public function GetSubItems (string $parentItemId, bool $sortByOrder = true): ObjectArray
|
||
{
|
||
// Проверяю, что идентификатор корректен
|
||
if (GUIDExtension::IsNotValidOrEmpty($parentItemId) && $parentItemId !== GUIDExtension::GUIDEmpty)
|
||
// - возвращаю отрицательный результат
|
||
return new ObjectArray([]);
|
||
|
||
// Получаю список элементов
|
||
$result = $this->_items->GetRows(fn (MenuItemModel $item) => $item->ParentId == $parentItemId);
|
||
|
||
// Если нужно сортировать
|
||
if ($sortByOrder)
|
||
// - сортирую
|
||
$result->Sort("Order");
|
||
|
||
// Возвращаю результат
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Изменяет родителя у элемента.
|
||
*
|
||
* @param string $id Идентификатор элемента.
|
||
* @param string $newParentId Идентификатор нового родителя.
|
||
*
|
||
* @return ActionState Результат выполнения.
|
||
*/
|
||
public function ChangeParent (string $id, string $newParentId): ActionState
|
||
{
|
||
// Создаю результат
|
||
$result = new ActionState();
|
||
|
||
// Проверяю, что идентификатор не пустой
|
||
if (GUIDExtension::IsNotValidOrEmpty($id) || GUIDExtension::IsNotValidOrEmpty($newParentId)) {
|
||
// - то выдаю ошибку
|
||
$result->AddError("Неверный идентификатор (GUID)");
|
||
|
||
// - и прерываю
|
||
return $result;
|
||
}
|
||
|
||
// Если имеет дочерние
|
||
if ($this->HasSubItems($id)) {
|
||
// - то выдаю ошибку
|
||
$result->AddError(
|
||
"Пункт меню с идентификатором 0%s имеет дочерние пункты (подпункты). Пожалуйста, сперва переместите их, а потом будет возможно сменить родителя у данного элемента!");
|
||
|
||
// - и прерываю
|
||
return $result;
|
||
}
|
||
|
||
// Загружаю элемент
|
||
$item = $this->GetItem($id);
|
||
|
||
// Если элемент не загружен
|
||
if ($item == null) {
|
||
// - то выдаю ошибку
|
||
$result->AddError("Пункт меню с идентификатором 0%s не найден!");
|
||
|
||
// - и прерываю
|
||
return $result;
|
||
}
|
||
|
||
// Получаю список свободных порядковых номеров
|
||
$freeOrdersList = $this->GetFreeOrders($newParentId);
|
||
|
||
// Получаю первый свободный порядковый номер
|
||
$newOrder = count($freeOrdersList) > 0 ? min($freeOrdersList) : $this->GetLastItemOrder($newParentId) + 1;
|
||
|
||
// Присваиваю элементу идентификатор нового родителя
|
||
$item->ParentId = $newParentId;
|
||
|
||
// Устанавливаю его порядок
|
||
$item->Order = $newOrder;
|
||
|
||
// Обновляю список элементов
|
||
$this->UpdateItem($item);
|
||
|
||
// Возвращаю результат
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Проверяет, имеет ли элемент дочерние.
|
||
*
|
||
* @param string $id Идентификатор элемента меню.
|
||
*
|
||
* @return bool Имеет ли элемент дочерние.
|
||
*/
|
||
public function HasSubItems (string $id): bool
|
||
{
|
||
// Проверяю, что идентификатор не пустой
|
||
if (GUIDExtension::IsNotValidOrEmpty($id))
|
||
// - возвращаю отрицательный результат
|
||
return false;
|
||
|
||
// Проверяю, существует ли элемент, у которого идентификатор родителя совпадает
|
||
// с идентификатором указанного элемента
|
||
return $this->_items->IsExist(fn (MenuItemModel $item) => $item->ParentId == $id);
|
||
}
|
||
|
||
/**
|
||
* Получает порядковый номер отсутствующего элемента в списке между первым и последним.
|
||
*
|
||
* @param string $parentItemId Идентификатор родителя.
|
||
*
|
||
* @return array Список порядковых номеров, которые не используются.
|
||
*/
|
||
public function GetFreeOrders (string $parentItemId): array
|
||
{
|
||
// Проверяю, что идентификатор не пустой
|
||
if (GUIDExtension::IsNotValidOrEmpty($parentItemId) && $parentItemId !== GUIDExtension::GUIDEmpty)
|
||
// - возвращаю отрицательный результат
|
||
return [];
|
||
|
||
// Получаю список элементов
|
||
$items = $this->GetSubItems($parentItemId);
|
||
|
||
// Создаю список-результат
|
||
$result = [];
|
||
|
||
// Получаю минимальный порядок элементов
|
||
$min = $this->GetFirstItemOrder($parentItemId);
|
||
|
||
// Получаю максимальный порядок элементов
|
||
$max = $this->GetLastItemOrder($parentItemId);
|
||
|
||
// Если произошёл уникальный случай, когда нет элементов
|
||
if ($min == 0 || $max == 0)
|
||
return $result;
|
||
|
||
// Заношу в список все до минимума
|
||
if ($min > 0)
|
||
for ($ind = 1; $ind < $min; $ind++)
|
||
$result[] = $ind;
|
||
|
||
// Прохожу "занятые" элементы
|
||
for ($ind = $min; $ind <= $max; $ind++)
|
||
// - и проверяю их на свободные
|
||
if (!$items->IsExist(fn (MenuItemModel $item) => $item->Order == $ind))
|
||
// -- заношу свободные в список
|
||
$result[] = $ind;
|
||
|
||
// Сортирую список
|
||
sort($result, SORT_NUMERIC);
|
||
|
||
// Возвращаю результат
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Получает порядковый номер первого элемента в списке.
|
||
*
|
||
* @param string $parentItemId Идентификатор родителя.
|
||
*
|
||
* @return int Порядковый номер первого элемента в списке.
|
||
*/
|
||
public function GetFirstItemOrder (string $parentItemId): int
|
||
{
|
||
// Проверяю, что идентификатор не пустой
|
||
if (GUIDExtension::IsNotValidOrEmpty($parentItemId) && $parentItemId !== GUIDExtension::GUIDEmpty)
|
||
// - возвращаю отрицательный результат
|
||
return 0;
|
||
|
||
// Получаю первый элемент
|
||
$item = $this->GetSubItems($parentItemId)->First(false);
|
||
|
||
// Если элемент не найден
|
||
if ($item === false)
|
||
// - возвращаю нулевой результат
|
||
return 0;
|
||
|
||
// Возвращаю порядок первого элемента
|
||
return $item->Order;
|
||
}
|
||
|
||
/**
|
||
* @inheritdoc
|
||
*/
|
||
public function getIterator (): Traversable
|
||
{
|
||
return $this->_items->getIterator();
|
||
}
|
||
|
||
/**
|
||
* @inheritdoc
|
||
*/
|
||
public function Duplicate (): MenuItems
|
||
{
|
||
return new MenuItems($this->_items);
|
||
}
|
||
|
||
/**
|
||
* @inheritdoc
|
||
*/
|
||
public
|
||
function Serialize (): string
|
||
{
|
||
return $this->_items->Serialize();
|
||
}
|
||
|
||
/**
|
||
* @inheritdoc
|
||
*/
|
||
public
|
||
function UnSerialize (string $serialized): void
|
||
{
|
||
// Создаю новый класс списка элементов
|
||
$this->_items = new ObjectArray([]);
|
||
|
||
// Восстанавливаю данные
|
||
$this->_items->UnSerialize($serialized);
|
||
}
|
||
} |