null */ private static function FindAttribute (array $attrs, string $className): ?object { return array_find($attrs, fn ($attr) => $attr->getName() === $className); } /** * Разбирает объект на свойства. * * @param IDBItem $source Объект со свойствами. * @param DBOperation $operation Текущая операция. * * @return ObjectArray Массив свойств и их значений класса, который реализует интерфейс IDBItem. */ private static function GetProperties (IDBItem $source, DBOperation $operation): ObjectArray { // Создаю массив свойств $result = new ObjectArray(); // Получаю массив свойств $properties = get_class_vars(get_class($source)); // Для каждого свойства foreach ($properties as $key => $value) { // - пропускаю не свойства if (!property_exists($source, $key)) // -- пропускаю continue; // - получаю рефлексию класса $class = new ReflectionClass(get_class($source)); try { // - получаю рефлексию свойства $property = $class->getProperty($key); } catch (ReflectionException) { // - если ошибка, то вывожу пустой массив return $result; } // - пропускаю не публичные свойства if (!$property->isPublic()) // -- пропускаю continue; // - получаю атрибуты $attributes = $property->getAttributes(); /** * Фильтруем поля, игнорируемые для данной операции * * @var IgnoredInDB|null $ignoreAttr Атрибут игнорирования. */ $ignoreAttr = self::FindAttribute($attributes, IgnoredInDB::class); // - если поле игнорируется $isIgnore = $ignoreAttr !== null && $ignoreAttr->IgnoredOperations->IsExist(fn (DBOperation $oper) => $oper == $operation); /** * Получаю значение имени поля * * @var FieldName|null $fieldNameAttr Атрибут имени поля. */ $fieldNameAttr = self::FindAttribute($attributes, FieldName::class); // Если есть атрибут имени поля, то беру его имя, иначе беру имя свойства $fieldName = $fieldNameAttr !== null ? $fieldNameAttr->FieldName : ""; /** * Преобразование типа. * * @var ConvertToDB|null $convertAttr Атрибут конвертации. */ $convertAttr = self::FindAttribute($attributes, ConvertToDB::class); // - получаю функцию конвертации и сравнения $converterToDB = $convertAttr?->ConvertToDB; $converterFromDB = $convertAttr?->ConvertFromDB; $compareFunc = $convertAttr?->Compare; // - получаем свойства столбца /** * Атрибут первичного ключа. * * @var PrimaryKey|null $pkAttr Атрибут первичного ключа. */ $pkAttr = self::FindAttribute($attributes, PrimaryKey::class); // -- это первичный ключ? $isPrimary = $pkAttr !== null; /** * Тип данных поля. * * @var DataType|null $dtAttr Атрибут типа данных */ $dtAttr = self::FindAttribute($attributes, DataType::class); // - тип данных $dataType = $dtAttr !== null ? new Tuple($dtAttr->Type, $dtAttr->Size) : new Tuple(self::GetDBTypeForType($property->getType()->getName()), 0); /** * Атрибут "не пустое значение". * * @var NotNull|null $nnAttr Атрибут нет пустому значению. */ $nnAttr = self::FindAttribute($attributes, NotNull::class); // -- это первичный ключ? $isNotNull = $nnAttr !== null; /** * Атрибут "уникальное значение". * * @var Unique|null $unqAttr Атрибут уникального значения. */ $unqAttr = self::FindAttribute($attributes, Unique::class); // -- это уникальный ключ? $isUnique = $unqAttr !== null; /** * Ключ для связывания поля. * * @var ForeignKey|null $chkAttr Атрибут связывания. */ $chkAttr = self::FindAttribute($attributes, ForeignKey::class); // - связывание с другой таблицей $foreignWith = $chkAttr !== null ? new Tuple($chkAttr->TableName, $chkAttr->FieldName) : new Tuple(null, null); /** * Атрибут проверки поля. * * @var Check|null $chkAttr Атрибут проверки поля. */ $chkAttr = self::FindAttribute($attributes, Check::class); // - проверка данных поля $checkFunc = $chkAttr !== null ? $chkAttr->Condition : new ConditionBuilder(); /** * Атрибут значения по умолчанию поля. * * @var DefaultValue|null $dvAttr Атрибут значения по умолчанию поля. */ $dvAttr = self::FindAttribute($attributes, DefaultValue::class); // - значение по умолчанию $default = $dvAttr?->Value; /** * Атрибут "автоматической генерации". * * @var AutoIncrement|null $aiAttr Атрибут "автоматической генерации". */ $aiAttr = self::FindAttribute($attributes, AutoIncrement::class); // -- это атрибут "автоматической генерации"? $isAutoIncrement = $aiAttr !== null; // - создаю заголовок $columnHeader = new DataBaseColumn($fieldName, $dataType, $isNotNull, $isUnique, $isPrimary, $foreignWith, $checkFunc, $default, $isAutoIncrement); // - создаю объект свойства $item = new DBItemProperty($key, $value, $fieldName, $columnHeader, $isIgnore, $converterToDB, $converterFromDB, $compareFunc); // - добавляю в массив $result[] = $item; } // Возвращаю результат return $result; } /** * Получает тип из базы данных по типу переменной. * * @param string $type Тип. * * @return DBType Тип из базы данных. */ private static function GetDBTypeForType (string $type): DBType { /** * @noinspection SpellCheckingInspection Отключаю проверку из-за того, что многие типы будут в lower case */ return match (strtolower($type)) { "int", "integer" => DBType::INT, "float", "double" => DBType::FLOAT, "bool", "boolean" => DBType::BOOL, "dateonly", "timeonly", "datetime", "datetimeimmutable" => DBType::DATE, "array" => DBType::ARRAY, default => DBType::STRING }; } /** * Подготавливает массив параметров * * @param IDBItem $source Объект со свойствами. * @param DBOperation $operation Текущая операция. * * @return array|false Подготовленный массив параметров или false в случае ошибки */ private function PrepareParamsArray (IDBItem $source, DBOperation $operation): array|false { $result = []; // Получаю массив свойств $properties = self::GetProperties($source, $operation); /** * Для каждого свойства... * * @var DBItemProperty $property Свойство. */ foreach ($properties as $property) { // - пропускаю игнорируемые поля if ($property->IsIgnored) continue; // - получаю значение имени поля $fieldName = $property->FieldName; // - преобразую тип $value = call_user_func($property->ConvertToDB, $property->Value); // - добавляю в массив $result[$fieldName] = $value; } // Возвращаю результат return $result; } /** * Восстанавливает объект из БД. * * @param array $row Строка БД. * @param string $className Имя класса объекта. * @param DBOperation $operation Операция. * * @return object|null Экземпляр класса объекта или null, если ошибка. */ private function RestoreItem (array $row, string $className, DBOperation $operation): ?object { // Если целевой класс не реализует интерфейс IDBItem if (in_array(IDBItem::class, class_implements($className))) // - то прерываем и возвращаем null return null; // Создаём объект желаемого класса $class = new $className(); // Получаю свойства класса $properties = self::GetProperties($class, $operation); // Массив отношений, содержащий имя поля, имя свойства, метод преобразования $propertiesRelationship = []; /** * Для каждого свойства... * * @var DBItemProperty $property Свойство. */ foreach ($properties as $property) { // - пропускаю игнорируемые поля if ($property->IsIgnored) continue; // - получаю имя поля $fieldName = $property->FieldName; // - добавляю в массив отношений $propertiesRelationship[$fieldName] = [$property->Name, $property->ConverterFromDB]; } // Проходим элементы массива из БД foreach ($row as $column => $value) { // - если свойство не найдено if (!array_key_exists($column, $propertiesRelationship)) // -- то пропускаем continue; // - получаю имя свойства и процедуру конвертации [$property, $converter] = $propertiesRelationship[$column]; // - применяем процедуру конвертации (если указана) if ($converter !== null) // - выполняю функцию конвертации $value = call_user_func($converter, $value); // - устанавливаем значение свойства $class->$property = $value; } // Возвращаю класс return $class; } /** * Обрабатывает исключение. * * @param Exception $exception Исключение. * @param bool $terminate Прерывать выполнение (true) или просто зафиксировать * (false). * * @return void */ private function HandleException (Exception $exception, bool $terminate = true): void { // Выбираю обработчик исключений $onException = $this->OnException ?? function (Exception $e, bool $isTerminate): void { // Если требуется прерывать выполнение if ($isTerminate) // - то прерываем die($e->getMessage()); else // - в противном случае, выводим сообщение echo $e->getMessage(); }; // Выполняю обработчик исключений $onException($exception, $terminate); } /** * Подготавливает массив столбцов для использования в базе данных * * @param array $columns Массив колонок. * * @return array Массив преобразованных колонок. */ private function PrepareColumn (array $columns): array { return array_map(function ($item) { // Результирующая строка $result = ""; // Если длинна строки > 0 if (strlen($item) > 0) { // - первый символ $firstLetter = substr($item, 0, 1); // - последний символ $lastLetter = substr($item, -1); // - если первый символ не $this->DBSignOpen if ($firstLetter !== $this->DBSignOpen) // -- то добавляем $result .= $this->DBSignOpen; // - добавляем строку $result .= $item; // - если последний символ не $this->DBSignClose if ($lastLetter !== $this->DBSignClose) // -- то добавляем $result .= $this->DBSignClose; } // Возвращаем результат return $result; }, $columns); } /** * Генерирует SQL запрос выборки строк. * * @param string $table Имя таблицы * @param array $columns Колонки, которые нужно включить в запрос * @param ConditionBuilder $whereConditions Параметры выборки * @param array $params Параметры и их значения * * @return string SQL-запрос */ private function PrepareSQLForRowsQuery (string $table, array $columns, ConditionBuilder $whereConditions, array &$params = []): string { /** * Собираю условие в виде пригодном для SQL * * @var string $sql_where where-запрос SQL * @var array $params Параметры и их значения (для защиты от SQL-инъекции) */ [$sql_where, $params] = $whereConditions->Build(); // Колонки $sql_columns = count($columns) > 0 ? implode(', ', $this->PrepareColumn($columns)) : "*"; // Создаю запрос $sql = "SELECT $sql_columns FROM $this->DBSignOpen$table$this->DBSignClose"; // Если заданы where-параметры if ($whereConditions->Count() > 0) // - то добавляю их $sql .= " WHERE $sql_where"; // Возвращаю запрос return $sql; } }