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