20250712 1.1.1 Beta 4
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -78,4 +78,5 @@ fabric.properties | |||||||
| .idea/caches/build_file_checksums.ser | .idea/caches/build_file_checksums.ser | ||||||
|  |  | ||||||
| .idea/ | .idea/ | ||||||
| vendor/ | vendor/ | ||||||
|  | /tests/extensions/class.txt | ||||||
|   | |||||||
							
								
								
									
										179
									
								
								sources/extensions/TypeExtension.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								sources/extensions/TypeExtension.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | |||||||
|  | # Описание класса TypeExtension | ||||||
|  |  | ||||||
|  | ## Информация о версии | ||||||
|  |  | ||||||
|  | Версия класса: 1.0 | ||||||
|  |  | ||||||
|  | Впервые введено в пакет с версии: 1.1.1 | ||||||
|  |  | ||||||
|  | Описание класса: Расширение для любого типа. | ||||||
|  |  | ||||||
|  | ## Публичные свойства и константы класса | ||||||
|  |  | ||||||
|  | К публичным свойствам можно отнести следующие статичные функции: `DEFAULT_TO_ARRAY_ON_CLASS` и | ||||||
|  | `DEFAULT_FROM_ARRAY_ON_CLASS`. Они описывают методы обработки классов по умолчанию для методов `ToArray` и `FromArray` | ||||||
|  | соответственно. | ||||||
|  |  | ||||||
|  | ### Метод `DEFAULT_TO_ARRAY_ON_CLASS` | ||||||
|  |  | ||||||
|  | Этот метод возвращает `Closure`, который можно использовать (и он используется при значении параметра `null`) во втором | ||||||
|  | параметре метода `ToArray`. | ||||||
|  |  | ||||||
|  | ### Метод `DEFAULT_FROM_ARRAY_ON_CLASS` | ||||||
|  |  | ||||||
|  | Этот метод возвращает `Closure`, который можно использовать (и он используется при значении параметра `null`) во втором | ||||||
|  | параметре метода `FromArray`. | ||||||
|  |  | ||||||
|  | ## Методы и функции | ||||||
|  |  | ||||||
|  | ### Метод `ToArray` | ||||||
|  |  | ||||||
|  | Этот _статический_ метод переводит класс в массив. Он содержит **1 обязательный параметр**: | ||||||
|  |  | ||||||
|  | * `object $class` - класс, который нужно перевести в массив | ||||||
|  |  | ||||||
|  | и **1 необязательный параметр**: | ||||||
|  |  | ||||||
|  | * `?callable $onClass = null` - метод обработки классов. Если передаётся `null`, то используется | ||||||
|  |   `DEFAULT_TO_ARRAY_ON_CLASS`. По умолчанию, `null`. | ||||||
|  |  | ||||||
|  | В качестве метода обработки классов `onClass` подойдет любое `Closure` вида | ||||||
|  |  | ||||||
|  |     fn (object $class): array => <ДЕЙСТВИЕ>; | ||||||
|  |  | ||||||
|  | Метод возвращает `array` - массив публичных свойств. | ||||||
|  |  | ||||||
|  | Синтаксис: | ||||||
|  |  | ||||||
|  |     public static function ToArray (object $class, ?callable $onClass = null): array | ||||||
|  |  | ||||||
|  | Пример, | ||||||
|  |  | ||||||
|  |     // Создаём тестовый класс (см. TypeExtensionTest) | ||||||
|  |     $class = new D ( | ||||||
|  |       'test_string', | ||||||
|  |       12345, | ||||||
|  |       true, | ||||||
|  |       new A("test_string_A", 6789, false), | ||||||
|  |       new B("test_string_B", 9876, "false"), | ||||||
|  |       new C("test_string_C", 54321, true) | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // Создаём метод обработки классов | ||||||
|  |     // - воспользуемся тем, что класс C реализует интерфейс ISerialize | ||||||
|  |     $c_closure = fn (C $class): array => | ||||||
|  |       [ | ||||||
|  |         "type_class" => C::class, | ||||||
|  |         "type_value" => $class->Serialize() | ||||||
|  |       ]; | ||||||
|  |      | ||||||
|  |     // - зададим метод обработки | ||||||
|  |     $closure = fn (object $class): array => $class instanceof C | ||||||
|  |       ? $c_closure($class) | ||||||
|  |       : TypeExtension::ToArray($class); | ||||||
|  |  | ||||||
|  |     // Преобразуем в массив | ||||||
|  |     try { | ||||||
|  |       $array = TypeExtension::ToArray($class, $closure); | ||||||
|  |     } | ||||||
|  |     catch (TypeException $e) { | ||||||
|  |       // - если ошибка, то выводим её и завершаем работу | ||||||
|  |       die($e->getMessage()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Вывожу в файл | ||||||
|  |     file_put_contents(__DIR__ . "/class.txt", json_encode($array, JSON_PRETTY_PRINT)); | ||||||
|  |  | ||||||
|  | В результате содержимое файла: | ||||||
|  |  | ||||||
|  |     { | ||||||
|  |       "type_class": "goodboyalex\\php_components_pack\\tests\\data\\D", | ||||||
|  |       "stringD": "test_string", | ||||||
|  |       "intD": 12345, | ||||||
|  |       "boolD": true, | ||||||
|  |       "a": { | ||||||
|  |         "type_class": "goodboyalex\\php_components_pack\\tests\\data\\A", | ||||||
|  |         "a": "test_string_A", | ||||||
|  |         "b": 6789, | ||||||
|  |         "c": false | ||||||
|  |       }, | ||||||
|  |       "b": { | ||||||
|  |         "type_class": "goodboyalex\\php_components_pack\\tests\\data\\B", | ||||||
|  |         "a": "test_string_B", | ||||||
|  |         "b": 9876, | ||||||
|  |         "d": "false" | ||||||
|  |       }, | ||||||
|  |       "c": { | ||||||
|  |         "type_class": "goodboyalex\\php_components_pack\\tests\\data\\C", | ||||||
|  |         "type_value": "{\"string\":\"test_string_C\",\"int\":54321,\"bool\":true}" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | #### Выбрасываемые исключения | ||||||
|  |  | ||||||
|  | Этот метод выбрасывает исключение типа `TypeException` в случае, если объект `$class` (обязательный параметр метода) не является классом. | ||||||
|  |  | ||||||
|  | ### Метод `FromArray` | ||||||
|  |  | ||||||
|  | Этот _статический_ метод переводит массив в класс. Он содержит **1 обязательный параметр**: | ||||||
|  |  | ||||||
|  | * `array $array` - массив публичных свойств | ||||||
|  |  | ||||||
|  | и **1 необязательный параметр**: | ||||||
|  |  | ||||||
|  | * `?callable $onClass = null` - метод обработки классов. Если передаётся `null`, то используется | ||||||
|  |   `DEFAULT_FROM_ARRAY_ON_CLASS`. По умолчанию, `null`. | ||||||
|  |  | ||||||
|  | В качестве метода обработки классов `onClass` подойдет любое `Closure` вида | ||||||
|  |  | ||||||
|  |     fn (array $classArray): object => <ДЕЙСТВИЕ>; | ||||||
|  |  | ||||||
|  | Метод возвращает `object` - экземпляр класса. | ||||||
|  |  | ||||||
|  | Синтаксис: | ||||||
|  |  | ||||||
|  |     public static function FromArray (array $array, ?callable $onClass = null): object | ||||||
|  |  | ||||||
|  | Пример, | ||||||
|  |  | ||||||
|  |     // Считываем данные из файла и преобразуем в массив | ||||||
|  |     $array = json_decode(file_get_contents(__DIR__ . "/class.txt"), true); | ||||||
|  |  | ||||||
|  |     // Создаём метод обработки классов | ||||||
|  |     // - воспользуемся тем, что класс C реализует интерфейс ISerialize | ||||||
|  |     $c_closure = function (string $serializedC): C | ||||||
|  |     { | ||||||
|  |       $classC = new C(); | ||||||
|  |       $classC->UnSerialize($serializedC); | ||||||
|  |       return $classC; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // - зададим метод обработки | ||||||
|  |     $closure = fn (array $classArray): object => | ||||||
|  |       $classArray['type_class'] == C::class | ||||||
|  |         ? $c_closure($classArray["type_value"]) | ||||||
|  |         : TypeExtension::FromArray($classArray); | ||||||
|  |  | ||||||
|  |     // Преобразуем в объект | ||||||
|  |     try { | ||||||
|  |       $class = TypeExtension::FromArray($array, D::CLOSURE_FROM_ARRAY()); | ||||||
|  |     } catch (TypeException $e) { | ||||||
|  |       // - если ошибка, то выводим её и завершаем работу | ||||||
|  |       die($e->getMessage()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Вывожу тестовую строку | ||||||
|  |     echo $class->c->stringC; | ||||||
|  |  | ||||||
|  | В результате на экране отобразится: | ||||||
|  |  | ||||||
|  |     test_string_C | ||||||
|  |  | ||||||
|  | #### Выбрасываемые исключения | ||||||
|  |  | ||||||
|  | Этот метод выбрасывает исключение типа `TypeException` в случаях, если: | ||||||
|  |  | ||||||
|  | * массив `$array` не создан через метод `ToArray`; | ||||||
|  | * при попытке создать объект, он не создаётся; | ||||||
|  | * дата передана не корректного формата; | ||||||
|  | * пытается передать `null` не `nullable` типу. | ||||||
| @@ -28,20 +28,24 @@ final class TypeExtension | |||||||
|      */ |      */ | ||||||
|     public static function DEFAULT_TO_ARRAY_ON_CLASS (): Closure |     public static function DEFAULT_TO_ARRAY_ON_CLASS (): Closure | ||||||
|     { |     { | ||||||
|         return fn (object $class) => self::ToArray($class, self::DEFAULT_TO_ARRAY_ON_CLASS()); |         return fn (object $class): array => self::ToArray($class); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Переводит объект в массив. |      * Переводит объект в массив. | ||||||
|      * |      * | ||||||
|      * @param object $class Объект. |      * @param object $class Объект. | ||||||
|      * @param callable $onClass Метод обработки классов. |      * @param callable|null $onClass Метод обработки классов. Если передаётся null, то используется | ||||||
|  |      *     <code>DEFAULT_TO_ARRAY_ON_CLASS</code>. По умолчанию, null. | ||||||
|      * |      * | ||||||
|      * @return array Массив свойств типа. |      * @return array Массив свойств типа. | ||||||
|      * @throws TypeException Исключение, если объект не является классом. |      * @throws TypeException Исключение, если объект не является классом. | ||||||
|      */ |      */ | ||||||
|     public static function ToArray (object $class, callable $onClass): array |     public static function ToArray (object $class, ?callable $onClass = null): array | ||||||
|     { |     { | ||||||
|  |         // Проверяю, что метод обработки классов не null и если null, то присваиваю дефолтный метод | ||||||
|  |         $onClass ??= self::DEFAULT_TO_ARRAY_ON_CLASS(); | ||||||
|  |  | ||||||
|         // Создаю массив результата |         // Создаю массив результата | ||||||
|         $result = []; |         $result = []; | ||||||
|  |  | ||||||
| @@ -102,16 +106,28 @@ final class TypeExtension | |||||||
|         return $result; |         return $result; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Дефолтный метод обработки классов для FromArray. | ||||||
|  |      * | ||||||
|  |      * @return Closure Метод обработки классов для FromArray. | ||||||
|  |      */ | ||||||
|  |     public static function DEFAULT_FROM_ARRAY_ON_CLASS (): Closure | ||||||
|  |     { | ||||||
|  |         return fn (array $classArray): object => self::FromArray($classArray); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Переводит массив в объект. |      * Переводит массив в объект. | ||||||
|      * |      * | ||||||
|      * @param array $array Массив свойств. |      * @param array $array Массив свойств. | ||||||
|  |      * @param callable|null $onClass Метод обработки классов. Если передаётся null, то используется | ||||||
|  |      *     <code>DEFAULT_FROM_ARRAY_ON_CLASS</code>. По умолчанию, null. | ||||||
|      * |      * | ||||||
|      * @return mixed Объект типа $className. |      * @return mixed Восстановленный объект. | ||||||
|      * @throws TypeException Исключение, если массив не создан через ToArray, объект не создан, дата не корректного |      * @throws TypeException Исключение, если массив не создан через ToArray, объект не создан, дата не корректного | ||||||
|      *     формата или пытается передать null не nullable типу. |      *     формата или пытается передать null не nullable типу. | ||||||
|      */ |      */ | ||||||
|     public function FromArray (array $array): object |     public static function FromArray (array $array, ?callable $onClass = null): object | ||||||
|     { |     { | ||||||
|         // Проверяю, что массив является массивом, созданным через ToArray, то есть содержит ключ type_class |         // Проверяю, что массив является массивом, созданным через ToArray, то есть содержит ключ type_class | ||||||
|         if (!array_key_exists("type_class", $array)) |         if (!array_key_exists("type_class", $array)) | ||||||
| @@ -186,7 +202,7 @@ final class TypeExtension | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             // - если значение является NULL |             // - если значение является NULL | ||||||
|             if ($value = "null") { |             if (is_string($value) && $value == "null") { | ||||||
|                 // -- присваиваю NULL |                 // -- присваиваю NULL | ||||||
|                 try { |                 try { | ||||||
|                     $instance->$key = null; |                     $instance->$key = null; | ||||||
| @@ -203,7 +219,7 @@ final class TypeExtension | |||||||
|             // - если значение является классом |             // - если значение является классом | ||||||
|             if (is_object($instance->$key) && is_array($array[$key]) && array_key_exists("type_class", $array[$key])) { |             if (is_object($instance->$key) && is_array($array[$key]) && array_key_exists("type_class", $array[$key])) { | ||||||
|                 // -- добавляю в массив через рекурсию |                 // -- добавляю в массив через рекурсию | ||||||
|                 $instance->$key = self::FromArray($array[$key]); |                 $instance->$key = $onClass($array[$key]); | ||||||
|  |  | ||||||
|                 // -- следующий элемент |                 // -- следующий элемент | ||||||
|                 continue; |                 continue; | ||||||
|   | |||||||
							
								
								
									
										56
									
								
								tests/data/D.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								tests/data/D.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace goodboyalex\php_components_pack\tests\data; | ||||||
|  |  | ||||||
|  | use Closure; | ||||||
|  | use goodboyalex\php_components_pack\extensions\TypeExtension; | ||||||
|  |  | ||||||
|  | class D | ||||||
|  | { | ||||||
|  |     public string $stringD; | ||||||
|  |     public int $intD; | ||||||
|  |     public bool $boolD; | ||||||
|  |     public A $a; | ||||||
|  |     public B $b; | ||||||
|  |     public C $c; | ||||||
|  |  | ||||||
|  |     public function __construct (string $string = "", int $int = 0, bool $bool = false, ?A $a = null, ?B $b = null, | ||||||
|  |         ?C $c = null) | ||||||
|  |     { | ||||||
|  |         $this->stringD = $string; | ||||||
|  |         $this->intD = $int; | ||||||
|  |         $this->boolD = $bool; | ||||||
|  |  | ||||||
|  |         $this->a = $a ?? new A(); | ||||||
|  |         $this->b = $b ?? new B(); | ||||||
|  |         $this->c = $c ?? new C(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static function CLOSURE_FROM_ARRAY (): Closure | ||||||
|  |     { | ||||||
|  |         return fn (array $classArray): object | ||||||
|  |             => $classArray['type_class'] == C::class | ||||||
|  |             ? self::CLOSURE_FROM_FOR_C($classArray["type_value"]) : TypeExtension::FromArray($classArray); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static function CLOSURE_FROM_FOR_C (string $serializedC): object | ||||||
|  |     { | ||||||
|  |         $classC = new C(); | ||||||
|  |         $classC->UnSerialize($serializedC); | ||||||
|  |         return $classC; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static function CLOSURE_TO_ARRAY (): Closure | ||||||
|  |     { | ||||||
|  |         return fn (object $class): array => $class instanceof C ? self::CLOSURE_TO_FOR_C($class) | ||||||
|  |             : TypeExtension::ToArray($class); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static function CLOSURE_TO_FOR_C (C $class): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             "type_class" => C::class, | ||||||
|  |             "type_value" => $class->Serialize() | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,39 +0,0 @@ | |||||||
| <?php |  | ||||||
|  |  | ||||||
| namespace goodboyalex\php_components_pack\tests\data; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Модель, описывающая css классы, используемые в меню. |  | ||||||
|  * |  | ||||||
|  * @author Александр Бабаев |  | ||||||
|  * @package freecms |  | ||||||
|  * @version 0.1 |  | ||||||
|  * @since 0.1 |  | ||||||
|  */ |  | ||||||
| final class MenuCssClassModel |  | ||||||
| { |  | ||||||
|     /** |  | ||||||
|      * @var string $MenuClass Класс всего списка меню. |  | ||||||
|      */ |  | ||||||
|     public string $MenuClass = ""; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var string $ItemClass Класс элемента меню. |  | ||||||
|      */ |  | ||||||
|     public string $ItemClass = ""; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var string $ItemSubMenuClass Класс элемента, вызывающего вложенный список. |  | ||||||
|      */ |  | ||||||
|     public string $ItemSubMenuClass = ""; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var string $SubMenuClass Класс вложенного списка. |  | ||||||
|      */ |  | ||||||
|     public string $SubMenuClass = ""; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var string $SubItemClass Класс элемента вложенного списка. |  | ||||||
|      */ |  | ||||||
|     public string $SubItemClass = ""; |  | ||||||
| } |  | ||||||
| @@ -1,100 +0,0 @@ | |||||||
| <?php |  | ||||||
|  |  | ||||||
| namespace goodboyalex\php_components_pack\tests\data; |  | ||||||
|  |  | ||||||
| use Exception; |  | ||||||
| use goodboyalex\php_components_pack\classes\ClassMapper; |  | ||||||
| use goodboyalex\php_components_pack\extensions\GUIDExtension; |  | ||||||
| use goodboyalex\php_components_pack\interfaces\IDuplicated; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Модель элемента меню. |  | ||||||
|  * |  | ||||||
|  * @author Александр Бабаев |  | ||||||
|  * @package freecms |  | ||||||
|  * @version 0.1 |  | ||||||
|  * @since 0.1 |  | ||||||
|  */ |  | ||||||
| final class MenuItemModel implements IDuplicated |  | ||||||
| { |  | ||||||
|     /** |  | ||||||
|      * @var string $Id Идентификатор элемента. |  | ||||||
|      */ |  | ||||||
|     public string $Id { |  | ||||||
|         get { |  | ||||||
|             return $this->Id ?? GUIDExtension::GUIDEmpty; |  | ||||||
|         } |  | ||||||
|         set { |  | ||||||
|             // Проверка идентификатора |  | ||||||
|             if (!GUIDExtension::Validate($value)) |  | ||||||
|                 // - исключение |  | ||||||
|                 throw new Exception("Неверный идентификатор (GUID)"); |  | ||||||
|  |  | ||||||
|             // Установка идентификатора |  | ||||||
|             $this->Id = $value; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var string $Name Имя элемента. |  | ||||||
|      */ |  | ||||||
|     public string $Name; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var string|null $Description Описание элемента. |  | ||||||
|      */ |  | ||||||
|     public ?string $Description; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var string $URL Адрес URL элемента. |  | ||||||
|      */ |  | ||||||
|     public string $URL; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var bool $OpenInNewWindow Открывать ли в новом окне (добавлять к ссылке target="_blank"). |  | ||||||
|      */ |  | ||||||
|     public bool $OpenInNewWindow; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var string|null $IconClass Класс иконки. |  | ||||||
|      */ |  | ||||||
|     public ?string $IconClass; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var string $ParentId Идентификатор родительского элемента. |  | ||||||
|      */ |  | ||||||
|     public string $ParentId { |  | ||||||
|         get { |  | ||||||
|             return $this->ParentId ?? GUIDExtension::GUIDEmpty; |  | ||||||
|         } |  | ||||||
|         set { |  | ||||||
|             // Проверка идентификатора |  | ||||||
|             if (!GUIDExtension::Validate($value)) |  | ||||||
|                 // - исключение |  | ||||||
|                 throw new Exception("Неверный идентификатор (GUID)"); |  | ||||||
|  |  | ||||||
|             // Установка идентификатора |  | ||||||
|             $this->ParentId = $value; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var int $Order Порядок. |  | ||||||
|      */ |  | ||||||
|     public int $Order; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @inheritdoc |  | ||||||
|      */ |  | ||||||
|     public function Duplicate (): MenuItemModel |  | ||||||
|     { |  | ||||||
|         // Создание дубликата модели |  | ||||||
|         $model = new MenuItemModel(); |  | ||||||
|  |  | ||||||
|         // Копирование свойств |  | ||||||
|         ClassMapper::MapClass($this, $model); |  | ||||||
|  |  | ||||||
|         // Возврат дубликата |  | ||||||
|         return $model; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,555 +0,0 @@ | |||||||
| <?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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,139 +0,0 @@ | |||||||
| <?php |  | ||||||
|  |  | ||||||
| namespace goodboyalex\php_components_pack\tests\data; |  | ||||||
|  |  | ||||||
| use Exception; |  | ||||||
| use goodboyalex\php_components_pack\classes\ClassMapper; |  | ||||||
| use goodboyalex\php_components_pack\extensions\GUIDExtension; |  | ||||||
| use goodboyalex\php_components_pack\interfaces\IDuplicated; |  | ||||||
| use goodboyalex\php_components_pack\interfaces\IStoredAtSQL; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Модель меню. |  | ||||||
|  * |  | ||||||
|  * @author Александр Бабаев |  | ||||||
|  * @package freecms |  | ||||||
|  * @version 0.1 |  | ||||||
|  * @since 0.1 |  | ||||||
|  */ |  | ||||||
| final class MenuModel implements IDuplicated, IStoredAtSQL |  | ||||||
| { |  | ||||||
|     /** |  | ||||||
|      * @var string $Id Идентификатор меню. |  | ||||||
|      */ |  | ||||||
|     public string $Id { |  | ||||||
|         get { |  | ||||||
|             return $this->Id ?? GUIDExtension::GUIDEmpty; |  | ||||||
|         } |  | ||||||
|         set { |  | ||||||
|             // Проверка идентификатора |  | ||||||
|             if (!GUIDExtension::Validate($value)) |  | ||||||
|                 // - исключение |  | ||||||
|                 throw new Exception("Неверный идентификатор (GUID)"); |  | ||||||
|  |  | ||||||
|             // Установка идентификатора |  | ||||||
|             $this->Id = $value; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var string $Name Имя меню. |  | ||||||
|      */ |  | ||||||
|     public string $Name = ""; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var string $Description Описание меню. |  | ||||||
|      */ |  | ||||||
|     public string $Description = ""; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var MenuItems Элементы меню. |  | ||||||
|      */ |  | ||||||
|     public MenuItems $Items; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var bool $AllowSubMenu Допустимо ли вложенное меню. |  | ||||||
|      */ |  | ||||||
|     public bool $AllowSubMenu = true; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @var MenuCssClassModel $Css Модель стилей оформления меню. |  | ||||||
|      */ |  | ||||||
|     public MenuCssClassModel $Css; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Конструктор. |  | ||||||
|      */ |  | ||||||
|     public function __construct () |  | ||||||
|     { |  | ||||||
|         $this->Items = new MenuItems([]); |  | ||||||
|         $this->Css = new MenuCssClassModel(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @inheritdoc |  | ||||||
|      */ |  | ||||||
|     public function Duplicate (): MenuModel |  | ||||||
|     { |  | ||||||
|         // Создаю новый класс |  | ||||||
|         $model = new MenuModel(); |  | ||||||
|  |  | ||||||
|         // Копирую свойства класса |  | ||||||
|         ClassMapper::MapClass($this, $model); |  | ||||||
|  |  | ||||||
|         // Возвращаю класс |  | ||||||
|         return $model; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @inheritDoc |  | ||||||
|      */ |  | ||||||
|     public function ToSQL (bool $withId = true): array |  | ||||||
|     { |  | ||||||
|         // Создаем результат |  | ||||||
|         $result = []; |  | ||||||
|  |  | ||||||
|         // Добавляем значения |  | ||||||
|         $result['Name'] = $this->Name; |  | ||||||
|         $result['Description'] = $this->Description; |  | ||||||
|         $result['Items'] = $this->Items->Serialize(); |  | ||||||
|         $result['AllowSubMenu'] = $this->AllowSubMenu ? 1 : 0; |  | ||||||
|         $result['Css'] = serialize($this->Css); |  | ||||||
|  |  | ||||||
|         // Если требуется идентификатор |  | ||||||
|         if ($withId) |  | ||||||
|             // - то добавляем его |  | ||||||
|             $result['Id'] = $this->Id; |  | ||||||
|  |  | ||||||
|         // Возвращаем результат |  | ||||||
|         return $result; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @inheritDoc |  | ||||||
|      */ |  | ||||||
|     public function FromSQL (array $sqlData): MenuModel |  | ||||||
|     { |  | ||||||
|         // Создаем результат |  | ||||||
|         $result = new MenuModel(); |  | ||||||
|  |  | ||||||
|         // Устанавливаем значения |  | ||||||
|         if (isset($sqlData['Id'])) |  | ||||||
|             $result->Id = $sqlData['Id']; |  | ||||||
|         if (isset($sqlData['Name'])) |  | ||||||
|             $result->Name = $sqlData['Name']; |  | ||||||
|         if (isset($sqlData['Description'])) |  | ||||||
|             $result->Description = $sqlData['Description']; |  | ||||||
|         $this->Items = new MenuItems([]); |  | ||||||
|         if (isset($sqlData['Items'])) |  | ||||||
|             $result->Items->UnSerialize($sqlData['Items']); |  | ||||||
|         if (isset($sqlData['AllowSubMenu'])) |  | ||||||
|             $result->AllowSubMenu = $sqlData['AllowSubMenu'] == 1; |  | ||||||
|         $this->Css = new MenuCssClassModel(); |  | ||||||
|         if (isset($sqlData['Css'])) |  | ||||||
|             $result->Css = unserialize($sqlData['Css']); |  | ||||||
|  |  | ||||||
|         // Возвращаем массив |  | ||||||
|         return $result; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -2,70 +2,90 @@ | |||||||
|  |  | ||||||
| namespace goodboyalex\php_components_pack\tests\extensions; | namespace goodboyalex\php_components_pack\tests\extensions; | ||||||
|  |  | ||||||
| use goodboyalex\php_components_pack\extensions\GUIDExtension; | use goodboyalex\php_components_pack\exceptions\TypeException; | ||||||
| use goodboyalex\php_components_pack\extensions\TypeExtension; | use goodboyalex\php_components_pack\extensions\TypeExtension; | ||||||
| use goodboyalex\php_components_pack\tests\data\MenuCssClassModel; | use goodboyalex\php_components_pack\tests\data\A; | ||||||
| use goodboyalex\php_components_pack\tests\data\MenuItemModel; | use goodboyalex\php_components_pack\tests\data\B; | ||||||
| use goodboyalex\php_components_pack\tests\data\MenuItems; | use goodboyalex\php_components_pack\tests\data\C; | ||||||
| use goodboyalex\php_components_pack\tests\data\MenuModel; | use goodboyalex\php_components_pack\tests\data\D; | ||||||
| use PHPUnit\Framework\TestCase; | use PHPUnit\Framework\TestCase; | ||||||
|  |  | ||||||
| class TypeExtensionTest extends TestCase | class TypeExtensionTest extends TestCase | ||||||
| { | { | ||||||
|     public function testFromArray () |     public function testFromArray () | ||||||
|     { |     { | ||||||
|  |         // Подготавливаем данные к тестированию | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public function testToArray () |  | ||||||
|     { |  | ||||||
|         $this->PrepareForTest(); |         $this->PrepareForTest(); | ||||||
|  |  | ||||||
|         $menu = new MenuModel(); |         // Зададим имя файла | ||||||
|         $menu->Id = GUIDExtension::Generate(); |         $fileName = __DIR__ . "/class.txt"; | ||||||
|         $menu->Name = 'Menu'; |  | ||||||
|         $menu->Description = 'Description'; |  | ||||||
|         $menu->AllowSubMenu = true; |  | ||||||
|         $menu->Css = new MenuCssClassModel(); |  | ||||||
|         $menu->Css->MenuClass = "menuClass"; |  | ||||||
|         $menu->Css->ItemClass = "itemClass"; |  | ||||||
|         $menu->Css->SubItemClass = "subItemClass"; |  | ||||||
|         $menu->Css->ItemSubMenuClass = "itemSubMenuClass"; |  | ||||||
|  |  | ||||||
|         $menuItem1 = new MenuItemModel(); |         // Если файл не найден | ||||||
|         $menuItem1->Id = GUIDExtension::Generate(); |         if (!file_exists($fileName)) | ||||||
|         $menuItem1->Name = 'MenuItem1'; |             // - то завершаем работу | ||||||
|         $menuItem1->Description = 'Description'; |             die("The test data file could not be found or has not yet passed the ToArray test! / Файл с данными для тестирования не найден или не пройдено ещё тестирование ToArray!"); | ||||||
|         $menuItem1->Order = 1; |  | ||||||
|         $menuItem1->ParentId = GUIDExtension::GUIDEmpty; |  | ||||||
|         $menuItem1->URL = 'https://www.google.com'; |  | ||||||
|         $menu->Items->AddItem($menuItem1); |  | ||||||
|         $menuItem2 = new MenuItemModel(); |  | ||||||
|         $menuItem2->Id = GUIDExtension::Generate(); |  | ||||||
|         $menuItem2->Name = 'MenuItem2'; |  | ||||||
|         $menuItem2->Description = 'Description'; |  | ||||||
|         $menuItem2->ParentId = $menuItem1->Id; |  | ||||||
|         $menuItem2->Order = 1; |  | ||||||
|         $menuItem2->URL = 'https://www.google.ru'; |  | ||||||
|         $menu->Items->AddItem($menuItem2); |  | ||||||
|  |  | ||||||
|         $closure = fn (object $class) => $class::class == MenuItems::class ? $class->Serialize() |         // Считываем данные из файла и преобразуем в массив | ||||||
|             : TypeExtension::ToArray($class, TypeExtension::DEFAULT_TO_ARRAY_ON_CLASS()); |         $array = json_decode(file_get_contents($fileName), true); | ||||||
|  |  | ||||||
|         $array = TypeExtension::ToArray($menu, $closure); |         // Преобразуем в объект | ||||||
|  |         try { | ||||||
|  |             $class = TypeExtension::FromArray($array, D::CLOSURE_FROM_ARRAY()); | ||||||
|  |         } | ||||||
|  |         catch (TypeException $e) { | ||||||
|  |             // - если ошибка, то выводим её и завершаем работу | ||||||
|  |             die($e->getMessage()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         file_put_contents(__DIR__ . "/class.txt", json_encode($array, JSON_PRETTY_PRINT)); |         // Проверяем | ||||||
|         var_dump($array); |         $this->assertEquals(D::class, $class::class); | ||||||
|         die(); |         $this->assertEquals(A::class, $class->a::class); | ||||||
|  |         $this->assertEquals(B::class, $class->b::class); | ||||||
|  |         $this->assertEquals(9876, $class->b->b); | ||||||
|  |         $this->assertEquals(C::class, $class->c::class); | ||||||
|  |         $this->assertEquals(54321, $class->c->intC); | ||||||
|  |         $this->assertEquals('test_string', $class->stringD); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private function PrepareForTest (): void |     private function PrepareForTest (): void | ||||||
|     { |     { | ||||||
|         require_once __DIR__ . '/../../sources/exceptions/TypeException.php'; |         require_once __DIR__ . '/../../sources/exceptions/TypeException.php'; | ||||||
|         require_once __DIR__ . '/../../sources/extensions/TypeExtension.php'; |         require_once __DIR__ . '/../../sources/extensions/TypeExtension.php'; | ||||||
|         require_once __DIR__ . '/../data/MenuItemModel.php'; |         require_once __DIR__ . '/../data/A.php'; | ||||||
|         require_once __DIR__ . '/../data/MenuCssClassModel.php'; |         require_once __DIR__ . '/../data/B.php'; | ||||||
|         require_once __DIR__ . '/../data/MenuItems.php'; |         require_once __DIR__ . '/../data/C.php'; | ||||||
|         require_once __DIR__ . '/../data/MenuModel.php'; |         require_once __DIR__ . '/../data/D.php'; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function testToArray () | ||||||
|  |     { | ||||||
|  |         // Подготавливаем данные к тестированию | ||||||
|  |         $this->PrepareForTest(); | ||||||
|  |  | ||||||
|  |         // Создаём тестовый класс | ||||||
|  |         $class = new D ('test_string', 12345, true, new A("test_string_A", 6789, false), | ||||||
|  |             new B("test_string_B", 9876, "false"), new C("test_string_C", 54321, true)); | ||||||
|  |  | ||||||
|  |         // Преобразуем в массив | ||||||
|  |         try { | ||||||
|  |             $array = TypeExtension::ToArray($class, D::CLOSURE_TO_ARRAY()); | ||||||
|  |         } | ||||||
|  |         catch (TypeException $e) { | ||||||
|  |             // - если ошибка, то выводим её и завершаем работу | ||||||
|  |             die($e->getMessage()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Сохраняем массив в файл (для тестирования FromArray) | ||||||
|  |         file_put_contents(__DIR__ . "/class.txt", json_encode($array, JSON_PRETTY_PRINT)); | ||||||
|  |  | ||||||
|  |         // Проверяем | ||||||
|  |         $this->assertEquals(D::class, $array['type_class']); | ||||||
|  |         $this->assertEquals(A::class, $array['a']['type_class']); | ||||||
|  |         $this->assertEquals(B::class, $array['b']['type_class']); | ||||||
|  |         $this->assertEquals(9876, $array['b']['b']); | ||||||
|  |         $this->assertEquals(C::class, $array['c']['type_class']); | ||||||
|  |         $this->assertEquals('test_string', $array['stringD']); | ||||||
|  |         $this->assertEquals(12345, $array['intD']); | ||||||
|  |         $this->assertTrue($array['boolD']); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user