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