20250711 1.1.1 Beta 3

This commit is contained in:
2025-07-11 19:40:30 +03:00
parent 83a76dc8e2
commit 3fbb7dc81c
7 changed files with 1001 additions and 10 deletions

555
tests/data/MenuItems.php Normal file
View File

@@ -0,0 +1,555 @@
<?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);
}
}