2025-08-05 18:33:19 +03:00

467 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* @noinspection SqlNoDataSourceInspection
*/
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\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\classes\DataBaseColumn;
use goodboyalex\php_db_components_pack\classes\DBItemProperty;
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 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;
$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 Экземпляр класса объекта или <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->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 Прерывать выполнение (<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;
}
}