2025-08-02 15:45:51 +03:00

446 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_db_components_pack\attributes\ConvertToDB;
use goodboyalex\php_db_components_pack\attributes\FieldName;
use goodboyalex\php_db_components_pack\attributes\IgnoredInDB;
use goodboyalex\php_db_components_pack\attributes\PrimaryKey;
use goodboyalex\php_db_components_pack\classes\ConditionBuilder;
use goodboyalex\php_db_components_pack\enums\DBOperation;
use goodboyalex\php_db_components_pack\interfaces\IDBItem;
use PDO;
use ReflectionClass;
use ReflectionException;
use ReflectionProperty;
/**
* Трейт для функций поддержки для работы с базой данных.
*
* @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 object $obj Класс.
*
* @return array Массив публичных свойств и их значений.
*/
private static function GetPublicProperties (object $obj): array
{
// Создаю массив свойств
$result = [];
// Получаю массив свойств
$properties = get_class_vars(get_class($obj));
// Для каждого свойства
foreach ($properties as $key => $value) {
// - пропускаю не свойства
if (!property_exists($obj, $key))
// -- пропускаю
continue;
// - получаю рефлексию
$rProperty = new ReflectionProperty($obj, $key);
// - пропускаю не публичные свойства
if (!$rProperty->isPublic())
// -- пропускаю
continue;
// - добавляю в массив свойство и его значение
$result[$key] = $value;
}
// Возвращаю результат
return $result;
}
/**
* Получает первичные ключи класса.
*
* @param IDBItem $source Класс.
* @param DBOperation $operation Текущая операция.
*
* @return array Массив первичных ключей и их значений.
*/
private function FindPrimaryKeys (IDBItem $source, DBOperation $operation): array
{
// Создаю массив первичных ключей
$pKeys = [];
// Получаю массив свойств
$properties = self::GetPublicProperties($source);
// Для каждого свойства
foreach ($properties as $key => $value) {
// - получаю рефлексию
// - для класса
$reflectedClass = new ReflectionClass(get_class($source));
try {
// - для свойства
$reflectionProperty = $reflectedClass->getProperty($key);
}
catch (ReflectionException) {
// - если ошибка, то вывожу пустой массив
return [];
}
// - получаю атрибуты
$attributes = $reflectionProperty->getAttributes();
/**
* Фильтруем поля, игнорируемые для данной операции
*
* @var PrimaryKey|null $pkAttr Атрибут первичного ключа.
*/
$pkAttr = self::FindAttribute($attributes, PrimaryKey::class);
// - если на свойстве нет атрибута первичного ключа
if ($pkAttr === null)
// -- то пропускаю
continue;
/**
* Фильтруем поля, игнорируемые для данной операции
*
* @var IgnoredInDB|null $ignoreAttr Атрибут игнорирования.
*/
$ignoreAttr = self::FindAttribute($attributes, IgnoredInDB::class);
// - если поле игнорируется
if ($ignoreAttr !== null) {
// -- то проверяю, игнорируется ли данное поле
$isIgnore = $ignoreAttr->IgnoredOperations->IsExist(fn (DBOperation $oper) => $oper == $operation);
// -- если игнорируется
if ($isIgnore)
// --- то пропускаю
continue;
}
// - добавляю первичный ключ и его значение
$pKeys[$key] = $value;
}
// Возвращаю найденные ключи
return $pKeys;
}
/**
* Подготавливает массив параметров
*
* @param IDBItem $source Объект со свойствами.
* @param DBOperation $operation Текущая операция.
*
* @return array|false Подготовленный массив параметров или false в случае ошибки
*/
private function PrepareParamsArray (IDBItem $source, DBOperation $operation): array|false
{
$result = [];
// Получаю массив свойств
$properties = self::GetPublicProperties($source);
// Для каждого свойства
foreach ($properties as $key => $value) {
// Получаю рефлексию
// - для класса
$reflectedClass = new ReflectionClass(get_class($source));
try {
// - для свойства
$reflectionProperty = $reflectedClass->getProperty($key);
}
catch (ReflectionException $e) {
// - если ошибка, то вывожу и выходим
$this->HandleException($e);
}
// - получаю атрибуты
$attributes = $reflectionProperty->getAttributes();
/**
* Фильтруем поля, игнорируемые для данной операции
*
* @var IgnoredInDB|null $ignoreAttr Атрибут игнорирования.
*/
$ignoreAttr = self::FindAttribute($attributes, IgnoredInDB::class);
// - если поле игнорируется
if ($ignoreAttr !== null) {
// -- то проверяю, игнорируется ли данное поле
$isIgnore = $ignoreAttr->IgnoredOperations->IsExist(fn (DBOperation $oper) => $oper == $operation);
// -- если игнорируется
if ($isIgnore)
// --- то пропускаю
continue;
}
/**
* Получаю значение имени поля
*
* @var FieldName|null $fieldNameAttr Атрибут имени поля.
*/
$fieldNameAttr = self::FindAttribute($attributes, FieldName::class);
// Если есть атрибут имени поля, то беру его имя, иначе беру имя свойства
$fieldName = $fieldNameAttr !== null ? $fieldNameAttr->FieldName : $key;
/**
* Преобразование типа.
*
* @var ConvertToDB|null $convertAttr Атрибут конвертации.
*/
$convertAttr = self::FindAttribute($attributes, ConvertToDB::class);
// Если есть атрибут конвертации
if ($convertAttr) {
// - то получаю значение функции конвертации
$converter = $convertAttr->ConvertToDB;
// - выполняю функцию конвертации
$value = call_user_func($converter, $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();
// Получаем публичные свойства класса
try {
$properties = new ReflectionClass($class)->getProperties(ReflectionProperty::IS_PUBLIC);
}
catch (ReflectionException $exception) {
// - в случае ошибки, обрабатываем её
$this->HandleException($exception, false);
// - и возвращаем null
return null;
}
// Массив отношений, содержащий имя поля, имя свойства, метод преобразования
$propertiesRelationship = [];
// Для каждого свойства
foreach ($properties as $property) {
// - получаю атрибуты
$attributes = $property->getAttributes();
/**
* Фильтруем поля, игнорируемые для данной операции
*
* @var IgnoredInDB|null $ignoreAttr Атрибут игнорирования.
*/
$ignoreAttr = self::FindAttribute($attributes, IgnoredInDB::class);
// - если поле игнорируется
if ($ignoreAttr !== null) {
// -- то проверяю, игнорируется ли данное поле
$isIgnore = $ignoreAttr->IgnoredOperations->IsExist(fn (DBOperation $oper) => $oper == $operation);
// -- если игнорируется
if ($isIgnore)
// --- то пропускаю
continue;
}
/**
* Получаю значение имени поля
*
* @var FieldName|null $fieldNameAttr Атрибут имени поля.
*/
$fieldNameAttr = self::FindAttribute($attributes, FieldName::class);
// - если есть атрибут имени поля, то беру его имя, иначе беру имя свойства
$fieldName = $fieldNameAttr !== null ? $fieldNameAttr->FieldName : $property->getName();
/**
* Преобразование типа.
*
* @var ConvertToDB|null $convertAttr Атрибут конвертации.
*/
$convertAttr = self::FindAttribute($attributes, ConvertToDB::class);
// - если есть атрибут конвертации, то получаю значение функции конвертации, иначе null
$converterFunc = ($convertAttr) ? $convertAttr->ConvertFromDB : null;
// - добавляю в массив отношений
$propertiesRelationship[$fieldName] = [$property->getName(), $converterFunc];
}
// Проходим элементы массива из БД
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;
}
}