2025-07-11 19:40:30 +03:00

555 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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