478 lines
21 KiB
PHP
478 lines
21 KiB
PHP
<?php
|
||
|
||
/**
|
||
* @noinspection SqlNoDataSourceInspection
|
||
* @noinspection PhpMultipleClassDeclarationsInspection
|
||
*/
|
||
|
||
namespace goodboyalex\php_db_components_pack\traits\Database;
|
||
|
||
use Exception;
|
||
use goodboyalex\php_components_pack\classes\ObjectArray;
|
||
use goodboyalex\php_components_pack\classes\Tuple;
|
||
use goodboyalex\php_db_components_pack\attributes\AutoIncrement;
|
||
use goodboyalex\php_db_components_pack\attributes\Check;
|
||
use goodboyalex\php_db_components_pack\attributes\Compare;
|
||
use goodboyalex\php_db_components_pack\attributes\ConvertToDB;
|
||
use goodboyalex\php_db_components_pack\attributes\DataType;
|
||
use goodboyalex\php_db_components_pack\attributes\DefaultValue;
|
||
use goodboyalex\php_db_components_pack\attributes\FieldName;
|
||
use goodboyalex\php_db_components_pack\attributes\ForeignKey;
|
||
use goodboyalex\php_db_components_pack\attributes\IgnoredInDB;
|
||
use goodboyalex\php_db_components_pack\attributes\NotNull;
|
||
use goodboyalex\php_db_components_pack\attributes\PrimaryKey;
|
||
use goodboyalex\php_db_components_pack\attributes\Unique;
|
||
use goodboyalex\php_db_components_pack\classes\ConditionBuilder;
|
||
use goodboyalex\php_db_components_pack\enums\DBOperation;
|
||
use goodboyalex\php_db_components_pack\enums\DBType;
|
||
use goodboyalex\php_db_components_pack\interfaces\IDBItem;
|
||
use goodboyalex\php_db_components_pack\models\DataBaseColumn;
|
||
use goodboyalex\php_db_components_pack\models\DBItemProperty;
|
||
use PDO;
|
||
use ReflectionClass;
|
||
use ReflectionException;
|
||
|
||
/**
|
||
* Трейт для функций поддержки для работы с базой данных.
|
||
*
|
||
* @author Александр Бабаев
|
||
* @package php_components_pack
|
||
* @version 1.0
|
||
* @since 1.0
|
||
* @see PDO
|
||
*/
|
||
trait DatabaseSpecial
|
||
{
|
||
/**
|
||
* Находит атрибут в массиве.
|
||
*
|
||
* @param array $attrs Массив атрибутов.
|
||
* @param string $className Имя класса.
|
||
*
|
||
* @return object|null Объект атрибута или <code>null</code>
|
||
*/
|
||
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;
|
||
|
||
/**
|
||
* Сравнение значений.
|
||
*
|
||
* @var Compare|null $compareAttr Атрибут сравнения значений.
|
||
*/
|
||
$compareAttr = self::FindAttribute($attributes, Compare::class);
|
||
|
||
// - получаю функцию сравнения
|
||
$compareFunc = $compareAttr?->Function;
|
||
|
||
// - получаем свойства столбца
|
||
/**
|
||
* Атрибут первичного ключа.
|
||
*
|
||
* @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, $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->Column->Name;
|
||
|
||
// - преобразую тип
|
||
$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 Экземпляр класса объекта или <code>null</code>, если ошибка.
|
||
*/
|
||
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->Column->Name;
|
||
|
||
// - добавляю в массив отношений
|
||
$propertiesRelationship[$fieldName] = [$property->Name, $property->ConvertFromDB];
|
||
}
|
||
|
||
// Проходим элементы массива из БД
|
||
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 Прерывать выполнение (<code>true</code>) или просто зафиксировать
|
||
* (<code>false</code>).
|
||
*
|
||
* @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;
|
||
}
|
||
} |