diff --git a/.gitignore b/.gitignore index c7b2739..e1118fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2220,3 +2220,4 @@ FodyWeavers.xsd /composer.lock /vendor/goodboyalex/php_components_pack/ /.idea/php-test-framework.xml +/.idea/inspectionProfiles/Project_Default.xml diff --git a/sources/classes/ConditionBuilder.php b/sources/classes/ConditionBuilder.php index ccd7e39..7de805a 100644 --- a/sources/classes/ConditionBuilder.php +++ b/sources/classes/ConditionBuilder.php @@ -194,9 +194,10 @@ } /** - * Собирает условие в виде строки. + * Собирает условие в виде пригодном для SQL. Также возвращает массив параметров защиты от SQL-инъекций. * - * @return string Возвращает условие в виде, пригодном для SQL. + * @return Tuple (string, array) Возвращает условие в виде, пригодном для SQL и массив параметров для защиты от + * SQL-инъекций. */ public function Build (): Tuple { diff --git a/sources/classes/Database.php b/sources/classes/Database.php index 1264817..e50ddc7 100644 --- a/sources/classes/Database.php +++ b/sources/classes/Database.php @@ -9,11 +9,13 @@ use Closure; use goodboyalex\php_db_components_pack\enums\DBDriver; use goodboyalex\php_db_components_pack\models\DBConfig; + use goodboyalex\php_db_components_pack\traits\Database\DatabaseCountExist; use goodboyalex\php_db_components_pack\traits\Database\DatabaseGet; use goodboyalex\php_db_components_pack\traits\Database\DatabaseInsert; use goodboyalex\php_db_components_pack\traits\Database\DatabaseQueryExecute; use goodboyalex\php_db_components_pack\traits\Database\DatabaseSpecial; use goodboyalex\php_db_components_pack\traits\Database\DatabaseTransactions; + use goodboyalex\php_db_components_pack\traits\Database\DatabaseUpdate; use PDO; use PDOException; @@ -23,7 +25,7 @@ * Используется класс PDO для подключения к базе данных. * * @author Александр Бабаев - * @package php_components_pack + * @package php_db_components_pack * @version 1.0 * @since 1.0 * @see PDO @@ -47,7 +49,9 @@ private string $DBSignClose; /** - * @var Closure $OnException Обработчик исключений. + * @var Closure $OnException Обработчик исключений. Анонимная функция формата: + * + * function (Exception $e, bool $isTerminate): void */ private Closure $OnException; @@ -57,10 +61,12 @@ private DBConfig $Config; /** - * Конструктор. Подключает базу данных + * Конструктор. * * @param DBConfig $config Конфигурация подключения к базе данных. - * @param callable $onException Обработчик исключений. + * @param callable $onException Обработчик исключений. Анонимная функция формата: + * + * function (Exception $e, bool $isTerminate): void */ public function __construct (DBConfig $config, callable $onException) { @@ -126,40 +132,6 @@ $this->DataBaseHandle = null; } - /** - * Получает набор строк в массиве данных, удовлетворяющий выборке - * - * @param string $table Имя таблицы - * @param array $columns Колонки, которые нужно включить в запрос - * @param array $where Параметры выборки - * - * @return false|array Строка в формате массива или false в случае ошибки - * - * @see Query - * @see QueryFirst - * @see QueryLast - * @see GetRow - */ - public function GetRows (string $table, array $columns = [], array $where = []): false|array - { - // Задаю массив параметров - $params = []; - - // Получаю SQL запрос - $sql = $this->PrepareSQLForRowsQuery($table, $columns, $where, $params); - - // Получаю строки на основании запроса - $queryResult = $this->Query($sql, $params); - - // Если строки не получены - if ($queryResult === false) - // - то выдаю ошибку - return false; - - // Получаю значение строк - return $queryResult; - } - // Транзакции use DatabaseTransactions; @@ -172,220 +144,15 @@ // Получение данных use DatabaseGet; + // Проверка существования и получение количества строк + use DatabaseCountExist; + + // Обновление записей + use DatabaseUpdate; + // Приватные методы use DatabaseSpecial; - - - /** - * Получает колонку в массиве данных - * - * @param string $table Имя таблицы - * @param string $column Имя колонки - * @param array $where Параметры запроса - * - * @return false|array Ассоциированный массив с результатом запроса или false в случае ошибки - * - * @see Query - */ - public function GetCol (string $table, string $column, array $where = []): false|array - { - // Задаю параметры - $params = []; - - // Строковая интерпретация массива условий - $sql_where = $this->PrepareQueryWhere($where, $params); - - // Создаю запрос - $sql = "SELECT $this->DBSignOpen$column$this->DBSignClose FROM $this->DBSignOpen$table$this->DBSignClose"; - - // Если заданы where-параметры - if (count($where) > 0) { - // - то добавляю их - $sql .= " WHERE $sql_where"; - } - - // Получаю столбец на основании запроса - $queryResult = $this->Query($sql, $params); - - // Если строка не получена или пуста - if ($queryResult === false) - // - то выдаю ошибку - return false; - - // Создаю результат - $result = []; - - // Для каждого результата запроса - foreach ($queryResult as $row) - // - передаю его в результат - $result[] = $row[$column]; - - // Вывожу результат - return $result; - } - - /** - * Получение значение единичного поля - * - * @param string $table Имя таблицы - * @param string $column Требуемый столбец - * @param array $where Параметры запроса - * - * @return mixed|null Результат запроса или null в случае ошибки - */ - public function GetValue (string $table, string $column, array $where = []): mixed - { - // Задаю параметры - $params = []; - - // Строковая интерпретация массива условий - $sql_where = $this->PrepareQueryWhere($where, $params); - - // Создаю запрос - $sql = "SELECT $this->DBSignOpen$column$this->DBSignClose FROM $this->DBSignOpen$table$this->DBSignClose"; - - // Если заданы where-параметры - if (count($where) > 0) { - // - то добавляю их - $sql .= " WHERE $sql_where"; - } - - // Получаю строку на основании запроса - $queryResult = $this->QueryScalar($sql, $params); - - // Если строка не получена или пуста - if ($queryResult === false || count($queryResult) == 0) - // - то выдаю результат null - return null; - - // Получаю значение колонки - return $queryResult[$column]; - } - - /** - * Заменяет данные в строке базы данных - * - * @param string $table Имя таблицы - * @param array $set Массив данных для замены - * @param array $where Массив условий - * - * @return bool Результат выполнения - */ - public function Update (string $table, array $set, array $where = []): bool - { - // Создаю массив параметров - $params_set = []; - - // Строковая интерпретация массива для изменения - $sql_set = ""; - - // Для каждых данных для изменения - foreach ($set as $key => $value) { - // - получаю ключ 100%-но без ":" в начале - $set_key = $key[0] == ":" ? substr($key, 1) : $key; - - // - добавляю префикс для 2 или более итерации - $prefix = $sql_set == "" ? "" : ", "; - - // - добавляю данные в sql_set - $sql_set .= "$prefix$this->DBSignOpen$set_key$this->DBSignClose=:$set_key"; - - // - добавляю данные в параметры - $params_set[":" . $set_key] = $value; - } - - // Обработанные параметры - $params_where = []; - - // Строковая интерпретация массива условий - $sql_where = $this->PrepareQueryWhere(where: $where, params: $params_where); - - // Создаю параметры - $params = array_merge($params_set, $params_where); - - // Создаю запрос - $sql = "UPDATE $this->DBSignOpen$table$this->DBSignClose SET $sql_set"; - - // Если заданы where-параметры - if (count($where) > 0) - // - то добавляю их - $sql .= " WHERE $sql_where"; - - // Выполняю запрос - $count = $this->Execute($sql, $params); - - // Если результат - false - if ($count === false) - // - то и общий результат - false - return false; - - // Если изменено 0 строк - if ($count === 0) - // - то и общий результат - false - return false; - - // Вывожу результат -- успех - return true; - } - - /** - * Проверяет, существует ли запись в таблице. - * - * @param string $table Имя таблицы - * @param array $where Массив условий - * - * @return bool Результат проверки - */ - public function IsExist (string $table, array $where = []): bool - { - // Вывожу результат - return $this->Count($table, $where) > 0; - } - - /** - * Подсчитывает количество строк, удовлетворяющих условию. - * - * @param string $table Имя таблицы - * @param array $where Массив условий выборки - * - * @return int Количество строк или -1, в случае ошибки - */ - public function Count (string $table, array $where = []): int - { - // Параметры - $params = []; - - // Строковая интерпретация массива условий - $sql_where = $this->PrepareQueryWhere($where, $params); - - // Создаю запрос - $sql = "SELECT COUNT(*) FROM $this->DBSignOpen$table$this->DBSignClose"; - - // Если заданы where-параметры - if (count($where) > 0) { - // - то добавляю их - $sql .= ' WHERE ' . $sql_where; - } - - // Выполняю запрос - $countResult = $this->Query($sql, $params); - - // Если запрос выполнен с ошибкой - if ($countResult === false) - // - то в результат идёт -1 - return -1; - - // Получаю секцию - $section = match ($this->Config->Driver) { - DBDriver::MySQL, DBDriver::SQLite => "COUNT(*)", - DBDriver::MSSQL, DBDriver::OracleDB, DBDriver::PostgreSQL => "" - }; - - // Вывожу количество - return isset($countResult[0][$section]) ? (int)$countResult[0][$section] : -1; - } - /** * Удаляет строки по условию. * diff --git a/sources/traits/Database/DatabaseCountExist.php b/sources/traits/Database/DatabaseCountExist.php new file mode 100644 index 0000000..a994f2b --- /dev/null +++ b/sources/traits/Database/DatabaseCountExist.php @@ -0,0 +1,80 @@ +Build(); + + // Создаю запрос + $sql = "SELECT COUNT(*) FROM $this->DBSignOpen$table$this->DBSignClose"; + + // Если заданы where-параметры + if ($where->Count() > 0) + // - то добавляю их + $sql .= ' WHERE ' . $sql_where; + + // Выполняю запрос + $countResult = $this->Query($sql, $params); + + // Если запрос выполнен с ошибкой + if ($countResult === false) + // - то в результат идёт -1 + return -1; + + // Получаю секцию + $section = match ($this->Config->Driver) { + DBDriver::MySQL, DBDriver::SQLite => "COUNT(*)", + DBDriver::MSSQL, DBDriver::OracleDB, DBDriver::PostgreSQL => "" + }; + + // Вывожу количество + return isset($countResult[0][$section]) ? (int)$countResult[0][$section] : -1; + } + + /** + * Проверяет, существует ли запись в таблице. + * + * @param string $table Имя таблицы. + * @param ConditionBuilder $where Условия выборки. + * + * @return bool Результат проверки. + */ + public function IsExist (string $table, ConditionBuilder $where): bool + { + // Вывожу результат + return $this->Count($table, $where) > 0; + } + } \ No newline at end of file diff --git a/sources/traits/Database/DatabaseGet.php b/sources/traits/Database/DatabaseGet.php index 339e9de..ab6c871 100644 --- a/sources/traits/Database/DatabaseGet.php +++ b/sources/traits/Database/DatabaseGet.php @@ -1,20 +1,20 @@ false в случае ошибки. */ - public function GetRowColumns (string $table, array $columns, ConditionBuilder $where): false|array + public function GetRow (string $table, ConditionBuilder $where, array $columns = [], + string $className = "\\StdClass"): object|false + { + // Задаю массив параметров + $params = []; + + // Формируем SQL-запрос + $sql = $this->PrepareSQLForRowsQuery($table, $columns, $where, $params); + + // Добавляю лимит + $sql .= " LIMIT 1"; + + // Получаю строку + $row = $this->Query($sql, $params); + + // Если не получено + if ($row === false) + // - то вывожу false + return false; + + // Получаю объект + $item = $this->RestoreItem($row, $className, DBOperation::Get); + + // Если при получении возникла ошибка + if ($item === null) + // - то возвращаю false + return false; + + // Возвращаю элемент + return $item; + } + + /** + * Получает набор строк в массиве данных, удовлетворяющий выборке. + * + * @param string $table Имя таблицы. + * @param ConditionBuilder $where Where-условия выборки. + * @param array $columns Колонки, которые нужно включить в запрос. + * @param string $className Полное имя класса, реализуемого интерфейсом IDBItem. + * + * @return false|ObjectArray Массив найденных классов или false в случае ошибки. + */ + public function GetRows (string $table, ConditionBuilder $where, array $columns = [], + string $className = "\\StdClass"): false|ObjectArray { // Задаю массив параметров $params = []; @@ -44,78 +82,99 @@ // Получаю SQL запрос $sql = $this->PrepareSQLForRowsQuery($table, $columns, $where, $params); - // Получаю строку на основании запроса - return $this->QueryScalar($sql, $params); + // Получаю строки на основании запроса + $queryResult = $this->Query($sql, $params); + + // Если строки не получены + if ($queryResult === false) + // - то выдаю ошибку + return false; + + // Создаю массив объектов + $result = new ObjectArray(); + + // Для каждого элемента + foreach ($queryResult as $row) { + // - пытаюсь восстановить объект + $item = $this->RestoreItem($row, $className, DBOperation::Get); + + // - если не получилось + if ($item === null) + // -- то пропускаю элемент + continue; + + // - добавляю элемент в массив + $result[] = $item; + } + + // Выдаю массив объектов + return $result; } /** - * Извлекает одну запись из базы данных и создает соответствующий объект класса + * Получает колонку в массиве данных. * - * @param string $table Название таблицы - * @param ConditionBuilder $condition Условия выборки - * @param string $className Полное имя класса, реализуемого интерфейсом IDBItem + * @param string $table Имя таблицы. + * @param string $column Имя колонки. + * @param ConditionBuilder $where Параметры запроса. * - * @return object Заполненный объект класса + * @return false|array Ассоциированный массив с результатом запроса или false в случае ошибки. + * + * @see Query */ - public function GetRow (string $table, ConditionBuilder $condition, string $className): object + public function GetCol (string $table, string $column, ConditionBuilder $where): false|array { - try { - // Строим условие WHERE для SQL-запроса - $whereClause = $condition->Build(); - - // Формируем SQL-запрос - $sql = "SELECT * FROM $table WHERE $whereClause LIMIT 1;"; - - // Подготовленное выражение - $stmt = $this->pdo->prepare($sql); - - // Присваивание параметров - foreach ($params as $col => $val) { - $stmt->bindParam(":$col", $val); - } - - // Выполнение запроса - $stmt->execute(); - - // Получаем первую строку результатов - $row = $stmt->fetch(PDO::FETCH_ASSOC); - - if (!$row) { - throw new Exception("No data found with the given conditions."); - } - - // Создание объекта желаемого класса - $obj = new $className(); - - // Заполняем поля объекта соответствующими значениями из БД - foreach ($row as $column => $value) { - // Анализируем свойства класса и находим соответствующее свойство - $properties = (new ReflectionClass($obj))->getProperties(ReflectionProperty::IS_PUBLIC); - foreach ($properties as $prop) { - // Проверяем, соответствует ли данное свойство указанному полю - $attributes = $prop->getAttributes(FieldName::class); - if ($attributes && $attributes[0]->getArguments()['name'] === $column) { - // Применяем процедуру конвертации (если указана) - $convertAttrs = $prop->getAttributes(ConvertToDB::class); - if ($convertAttrs) { - $converter = $convertAttrs[0]->getArguments()['fromDb']; - if ($converter !== null) { - $value = call_user_func($converter, $value); - } - } - - // Устанавливаем значение свойства - $obj->{$prop->getName()} = $value; - break; - } - } - } - - return $obj; - } - catch (\PDOException|\Exception $e) { - error_log("Error fetching data: " . $e->getMessage()); - return null; - } + /** + * Интерпретирую условия. + * + * @var string $sql_where Строка запроса. + * @var array $params Массив параметров строки запроса. + */ + [$sql_where, $params] = $where->Build(); + + // Создаю запрос + $sql = "SELECT $this->DBSignOpen$column$this->DBSignClose FROM $this->DBSignOpen$table$this->DBSignClose"; + + // Если заданы where-параметры + if ($where->Count() > 0) + // - то добавляю их + $sql .= " WHERE $sql_where"; + + // Получаю столбец на основании запроса + $queryResult = $this->Query($sql, $params); + + // Если строка не получена или пуста + if ($queryResult === false) + // - то выдаю ошибку + return false; + + // Создаю результат + $result = []; + + // Для каждого результата запроса + foreach ($queryResult as $row) + // - передаю его в результат + $result[] = $row[$column]; + + // Вывожу результат + return $result; + } + + /** + * Получение значение единичного поля. + * + * @param string $table Имя таблицы. + * @param string $column Требуемый столбец. + * @param ConditionBuilder $where Параметры запроса. + * + * @return mixed|null Результат запроса или null в случае ошибки. + */ + public function GetValue (string $table, string $column, ConditionBuilder $where): mixed + { + // Получаю колонку по условию из таблицы + $result = $this->GetCol($table, $column, $where); + + // Если результат получен, то выдаю первый элемент, а если нет, то null + return $result[0] ?? null; } } \ No newline at end of file diff --git a/sources/traits/Database/DatabaseQueryExecute.php b/sources/traits/Database/DatabaseQueryExecute.php index 22570f2..03541a3 100644 --- a/sources/traits/Database/DatabaseQueryExecute.php +++ b/sources/traits/Database/DatabaseQueryExecute.php @@ -6,6 +6,7 @@ namespace goodboyalex\php_db_components_pack\traits\Database; + use Exception; use PDO; use PDOException; @@ -13,7 +14,7 @@ * Трейт для работы с запросами к базе данных типа Query и Execute. * * @author Александр Бабаев - * @package php_components_pack + * @package php_db_components_pack * @version 1.0 * @since 1.0 * @see PDO @@ -21,162 +22,138 @@ trait DatabaseQueryExecute { /** - * Запрос строк из базы данных. + * Выполнить запрос к базе данных и вернуть ассоциированный массив. * - * @param string $query Запрос - * @param array $params Параметры запроса + * @param string $query Запрос SQL. + * @param array $params Параметры запроса. * - * @return false|array Ассоциированный массив с результатом запроса или false в случае ошибки + * @return array|false Ассоциативный массив с результатом запроса или false в случае ошибки. */ - public function Query (string $query, array $params = []): false|array + public function Query (string $query, array $params = []): array|false { - // По умолчанию, результат пуст - $result = false; + // Проверяем, что это именно запрос SELECT + if (!preg_match('/^\s*SELECT\s+/i', trim($query))) { + $this->HandleException(new Exception("Некорректный запрос для метода Query() / Incorrect request for the Query() method: '$query'. Используйте метод Execute() для небезопасных операций / Use the Execute() method for unsafe operations."), + false); + return false; + } + + // Подготавливаем запрос + $stmt = $this->DataBaseHandle->prepare($query, [PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]); + + // Если запрос не подготовлен + if ($stmt === false) { + // - обрабатываю ошибку + $this->HandleException(new Exception("Ошибка подготовки SQL-запроса / SQL query preparation error"), + false); + + // - прерываю выполнение + return false; + } try { - // Подготавливаю запрос - $STH = $this->DataBaseHandle->prepare($query, [PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]); - - // Выполняю запрос - $STH->execute($params); - - // Указываю, что данные, которые я хочу получить, должны быть в ассоциативном массиве - $STH->setFetchMode(PDO::FETCH_ASSOC); - - // Получаю все данные - $result = $STH->fetchAll(); + // Выполняем запрос + $stmt->execute($params); } catch (PDOException $e) { - $this->HandleException($e); + // - в случае ошибки, обрабатываю её + $this->HandleException(new Exception("Ошибка выполнения SQL-запроса / SQL query execution error: " . + $e->getMessage()), false); + + // - прерываю выполнение + return false; } - // Вывожу результат - return $result; + // Если получены данные, то возвращаю их, в противном случае возвращаю пустой массив + return ($data = $stmt->fetchAll(PDO::FETCH_ASSOC)) !== [] ? $data : []; } /** - * Выполняем запрос на получение одной строки (аналог QueryFirst) + * Получить первую строку результата запроса. * - * @param string $query Запрос - * @param array $params Параметры запроса + * @param string $query Запрос SQL. + * @param array $params Параметры запроса. * - * @return false|array Строка в формате массива или false в случае ошибки - * - * @see Query - * @see QueryFirst - * @see QueryLast - * @see GetRow + * @return array|false Первая строка результата запроса или false в случае ошибки. */ - public function QueryScalar (string $query, array $params = []): false|array + public function QueryFirst (string $query, array $params = []): array|false { - return $this->QueryFirst($query, $params); + // Выполняем запрос + $rows = $this->Query($query, $params); + + // Если получены данные, то возвращаю первую строку, в противном случае возвращаю false + return is_array($rows) ? reset($rows) : false; } /** - * Выполняем запрос на получение первой строки + * Получить последнюю строку результата запроса. * - * @param string $query Запрос - * @param array $params Параметры запроса + * @param string $query Запрос SQL. + * @param array $params Параметры запроса. * - * @return false|array Строка в формате массива или false в случае ошибки - * - * @see Query - * @see QueryLast - * @see QueryScalar + * @return array|false Последняя строка результата запроса или false в случае ошибки. */ - public function QueryFirst (string $query, array $params = []): false|array + public function QueryLast (string $query, array $params = []): array|false { - // Выполняю запрос - $result = $this->Query($query, $params); + // Выполняем запрос + $rows = $this->Query($query, $params); - // Если в результате запроса получили ошибку или количество строк = 0 - if ($result === false || count($result) == 0) - // - то возвращаем ошибку - return false; - - // Получаю первый ключ массива - $firstKey = array_key_first($result); - - // Возвращаем первую строку - return $result[$firstKey]; + // Если получены данные, то возвращаю последнюю строку, в противном случае возвращаю false + return is_array($rows) ? end($rows) : false; } /** - * Выполняем запрос на получение последней строки + * Выполнить запрос, который изменяет данные в базе данных (INSERT, UPDATE, DELETE). * - * @param string $query Запрос - * @param array $params Параметры запроса + * @param string $query Запрос SQL. + * @param array $params Параметры запроса. * - * @return false|array Строка в формате массива или false в случае ошибки - * - * @see Query - * @see QueryFirst - * @see QueryScalar - */ - public function QueryLast (string $query, array $params = []): false|array - { - // Выполняю запрос - $result = $this->Query($query, $params); - - // Если в результате запроса получили ошибку или количество строк = 0 - if ($result === false || count($result) == 0) - // - то возвращаем ошибку - return false; - - // Получаю последний ключ массива - $lastKey = array_key_last($result); - - // Возвращаем первую строку - return $result[$lastKey]; - } - - - /** - * Выполнение запроса. Обычно используется для операций, - * которые не возвращают никаких данных, кроме количества - * затронутых ими записей. Например, - * - * $db->Execute('DELETE FROM table WHERE id=1'); - * - * @param string $query Запрос - * @param array $params Параметры запроса - * - * @return int|false Количество затронутых строк или false в случае ошибки + * @return int|false Количество затронутых строк или false в случае ошибки. */ public function Execute (string $query, array $params = []): int|false { - // По умолчанию результат false - $result = false; + // Проверяем, что это не запрос SELECT + if (preg_match('/^\s*SELECT\s+/i', trim($query))) { + // - выдаю ошибку + $this->HandleException(new Exception("Некорректный запрос для метода Execute() / Incorrect request for the Execute() method: '$query'. Используйте метод Query() для безопасного выбора данных / Use the Query() method to select data safely."), + false); + + // - аннулирую результат + return false; + } + try { - // Если параметры не заданы - if (count($params) == 0) { - // - то выполняю запрос - $result = $this->DataBaseHandle->exec($query); - } + // Если это простой запрос без параметров + if (empty($params)) + // - то просто выполняю его + return $this->DataBaseHandle->exec($query); else { - // - в противном случае - // -- подготавливаю запрос - $STH = $this->DataBaseHandle->prepare($query, [PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]); + // - в противном случае, подготавливаем запрос с параметрами + $stmt = $this->DataBaseHandle->prepare($query, [PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]); - // -- выполняю запрос - $opResult = $STH->execute($params); + // - если подготовили неуспешно + if (!$stmt) { + // -- то формируем ошибку + $this->HandleException(new PDOException("Ошибка подготовки SQL-запроса / SQL query preparation error"), + false); + + // -- возвращаем неудачу + return false; + } - // -- и если выполнение успешное, - if ($opResult) - // --- то в результат пойдёт количество строк - $result = $STH->rowCount(); + // - выполняем запрос + $executed = $stmt->execute($params); + + // - если успешно, то возвращаем количество изменённых строк, иначе - false + return ($executed) ? $stmt->rowCount() : false; } } catch (PDOException $e) { - $this->HandleException($e); - } - - // Если в результате false - if ($result === false) - // - то возвращаю его + // - в случае ошибки, обрабатываю её + $this->HandleException($e, false); + + // - прерываю выполнение return false; - - // Возвращаю результат - return $result; + } } } \ No newline at end of file diff --git a/sources/traits/Database/DatabaseSpecial.php b/sources/traits/Database/DatabaseSpecial.php index e331c85..3153606 100644 --- a/sources/traits/Database/DatabaseSpecial.php +++ b/sources/traits/Database/DatabaseSpecial.php @@ -10,6 +10,7 @@ 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; @@ -29,32 +30,6 @@ */ trait DatabaseSpecial { - /** - * Подготавливает массив параметров для запроса. - * - * @param array $array Массив. - * - * @return array Массив с подготовленными параметрами. - */ - private static function PrepareArray (array $array): array - { - // Результирующий массив - $result = []; - - // Для каждого элемента массива - foreach ($array as $key => $value) - // - если ключ начинается с ":" - if ($key[0] == ":") - // - то сразу добавляем его в результирующий массив - $result[$key] = $value; - else - // - в противном случае, предварительно добавим в имя ключа ":" - $result[':' . $key] = $value; - - // Возвращаем результат - return $result; - } - /** * Находит атрибут в массиве. * @@ -69,16 +44,112 @@ } /** - * Проверяет, является ли свойство публичным. + * Получает публичные свойства класса и их значения. * - * @param object $obj Объект. - * @param string $propertyName Имя свойства. + * @param object $obj Класс. * - * @return bool Возвращает true, если свойство публичное, иначе false. + * @return array Массив публичных свойств и их значений. */ - private static function IsPublicProperty (object $obj, string $propertyName): bool + private static function GetPublicProperties (object $obj): array { - return property_exists($obj, $propertyName) && new ReflectionProperty($obj, $propertyName)->isPublic(); + // Создаю массив свойств + $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; } /** @@ -94,14 +165,10 @@ $result = []; // Получаю массив свойств - $properties = get_class_vars(get_class($source)); + $properties = self::GetPublicProperties($source); // Для каждого свойства foreach ($properties as $key => $value) { - // - пропускаю не публичные свойства - if (!self::IsPublicProperty($source, $key)) - continue; - // Получаю рефлексию // - для класса $reflectedClass = new ReflectionClass(get_class($source)); @@ -169,20 +236,135 @@ 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(); + + // Получаем публичные свойства класса + 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 Прерывать выполнение (true) или просто зафиксировать + * (false). * * @return void */ - private function HandleException (Exception $exception): void + private function HandleException (Exception $exception, bool $terminate = true): void { // Выбираю обработчик исключений - $onException = $this->OnException ?? fn (Exception $e) => die($e->getMessage()); + $onException = $this->OnException ?? function (Exception $e, bool $isTerminate): void + { + // Если требуется прерывать выполнение + if ($isTerminate) + // - то прерываем + die($e->getMessage()); + else + // - в противном случае, выводим сообщение + echo $e->getMessage(); + }; // Выполняю обработчик исключений - $onException($exception); + $onException($exception, $terminate); } /** @@ -231,19 +413,21 @@ * * @param string $table Имя таблицы * @param array $columns Колонки, которые нужно включить в запрос - * @param array $where Параметры выборки + * @param ConditionBuilder $whereConditions Параметры выборки * @param array $params Параметры и их значения * * @return string SQL-запрос */ - private function PrepareSQLForRowsQuery (string $table, array $columns, ConditionBuilder $where, + private function PrepareSQLForRowsQuery (string $table, array $columns, ConditionBuilder $whereConditions, array &$params = []): string { - // Очищаю параметры - $params = []; - - // Строковая интерпретация массива условий - $sql_where = $this->PrepareQueryWhere($where, $params); + /** + * Собираю условие в виде пригодном для 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)) : "*"; @@ -252,50 +436,11 @@ $sql = "SELECT $sql_columns FROM $this->DBSignOpen$table$this->DBSignClose"; // Если заданы where-параметры - if (count($where) > 0) { + if ($whereConditions->Count() > 0) // - то добавляю их $sql .= " WHERE $sql_where"; - } // Возвращаю запрос return $sql; } - - /** - * Готовит выражение для WHERE-запроса - * - * @param array $where Массив условий - * @param array $params Очищенные параметры - * - * @return string Строка WHERE-запроса - */ - private function PrepareQueryWhere (array $where, array &$params): string - { - // Очищаю параметры - $params = []; - - // Задаю результат - $result = ""; - - // Если массив условий не пуст - if (count($where) > 0) { - // - то для каждого условия - foreach ($where as $key => $value) { - // -- получаю ключ 100%-но без ":" в начале - $where_key = $key[0] == ":" ? substr($key, 1) : $key; - - // -- добавляю префикс для 2 или более итерации - $prefix = $result == "" ? "" : " AND "; - - // -- добавляю данные в $sql_where - $result .= $prefix . $this->DBSignOpen . $where_key . $this->DBSignClose . " = :" . $where_key; - - // -- добавляю данные в параметры - $params[$where_key] = "$value"; - } - } - - // Вывожу результат - return $result; - } } \ No newline at end of file diff --git a/sources/traits/Database/DatabaseUpdate.php b/sources/traits/Database/DatabaseUpdate.php new file mode 100644 index 0000000..d062eba --- /dev/null +++ b/sources/traits/Database/DatabaseUpdate.php @@ -0,0 +1,165 @@ + $value) { + // - получаю ключ 100%-но без ":" в начале + $set_key = $key[0] == ":" ? substr($key, 1) : $key; + + // - добавляю префикс для 2 или более итерации + $prefix = $sql_set == "" ? "" : ", "; + + // - добавляю данные в sql_set + $sql_set .= "$prefix$this->DBSignOpen$set_key$this->DBSignClose=:$set_key"; + + // - добавляю данные в параметры + $params_set[":" . $set_key] = $value; + } + + // Обработанные параметры + $params_where = []; + + // Строковая интерпретация массива условий + $sql_where = $this->PrepareQueryWhere(where: $where, params: $params_where); + + // Создаю параметры + $params = array_merge($params_set, $params_where); + + // Создаю запрос + $sql = "UPDATE $this->DBSignOpen$table$this->DBSignClose SET $sql_set"; + + // Если заданы where-параметры + if (count($where) > 0) + // - то добавляю их + $sql .= " WHERE $sql_where"; + + // Выполняю запрос + $count = $this->Execute($sql, $params); + + // Если результат - false + if ($count === false) + // - то и общий результат - false + return false; + + // Если изменено 0 строк + if ($count === 0) + // - то и общий результат - false + return false; + + // Вывожу результат -- успех + return true; + } + + public function Update (string $table, IDBItem $item): bool + { + $primaryKeys = $this->FindPrimaryKeys($item, DBOperation::Update); + + $pk_keys = array_keys($primaryKeys); + + $where = new ConditionBuilder(); + + for ($i = 0; $i < count($primaryKeys); $i++) { + if ($i > 0) + $where = $where->And(); + + $whereKey = $pk_keys[$i]; + + $whereValue = $primaryKeys[$whereKey]; + + $where = $where->WhereEquals($whereKey, $whereValue); + } + + $dbItem = $this->GetRow($table, $where, className: get_class($item)); + + // Создаю массив параметров + $params_set = []; + + // Строковая интерпретация массива для изменения + $sql_set = ""; + + // Для каждых данных для изменения + foreach ($set as $key => $value) { + // - получаю ключ 100%-но без ":" в начале + $set_key = $key[0] == ":" ? substr($key, 1) : $key; + + // - добавляю префикс для 2 или более итерации + $prefix = $sql_set == "" ? "" : ", "; + + // - добавляю данные в sql_set + $sql_set .= "$prefix$this->DBSignOpen$set_key$this->DBSignClose=:$set_key"; + + // - добавляю данные в параметры + $params_set[":" . $set_key] = $value; + } + + // Обработанные параметры + $params_where = []; + + // Строковая интерпретация массива условий + $sql_where = $this->PrepareQueryWhere(where: $where, params: $params_where); + + // Создаю параметры + $params = array_merge($params_set, $params_where); + + // Создаю запрос + $sql = "UPDATE $this->DBSignOpen$table$this->DBSignClose SET $sql_set"; + + // Если заданы where-параметры + if (count($where) > 0) + // - то добавляю их + $sql .= " WHERE $sql_where"; + + // Выполняю запрос + $count = $this->Execute($sql, $params); + + // Если результат - false + if ($count === false) + // - то и общий результат - false + return false; + + // Если изменено 0 строк + if ($count === 0) + // - то и общий результат - false + return false; + + // Вывожу результат -- успех + return true; + } + } \ No newline at end of file