260 lines
9 KiB
PHP
260 lines
9 KiB
PHP
<?php
|
||
namespace {
|
||
if (!function_exists('sqliteLower')) {
|
||
function sqliteLower($str)
|
||
{
|
||
return mb_strtolower($str, 'UTF-8');
|
||
}
|
||
}
|
||
}
|
||
|
||
namespace ctiso {
|
||
|
||
use PDO;
|
||
use ctiso\Database\Statement;
|
||
use ctiso\Database\PDOStatement;
|
||
use ctiso\Database\IdGenerator;
|
||
|
||
/**
|
||
* Класс оболочка для PDO для замены Creole
|
||
* @phpstan-type DSN = array{phptype: string, hostspec: string, database: string, username: string, password: string}
|
||
*/
|
||
class Database extends PDO
|
||
{
|
||
|
||
/** @var DSN */
|
||
public $dsn;
|
||
|
||
/**
|
||
* Создает соединение с базой данных
|
||
* @param string $dsn - DSN
|
||
* @param string|null $username - имя пользователя
|
||
* @param string|null $password - пароль
|
||
*/
|
||
public function __construct($dsn, $username = null, $password = null)
|
||
{
|
||
parent::__construct($dsn, $username, $password);
|
||
$this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||
$this->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS, [PDOStatement::class, []]);
|
||
}
|
||
|
||
function prepare(string $sql, array $options = []): PDOStatement|false
|
||
{
|
||
/** @var PDOStatement $result */
|
||
$result = parent::prepare($sql, $options);
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Возвращает DSN
|
||
* @return DSN
|
||
*/
|
||
public function getDSN()
|
||
{
|
||
return $this->dsn;
|
||
}
|
||
|
||
/**
|
||
* Возвращает true, если база данных Postgres
|
||
* @return bool
|
||
*/
|
||
public function isPostgres()
|
||
{
|
||
return ($this->dsn["phptype"] == "pgsql");
|
||
}
|
||
/**
|
||
* Создает соединение с базой данных
|
||
* @param array $dsn - DSN
|
||
* @return Database|null
|
||
*/
|
||
static function getConnection(array $dsn)
|
||
{
|
||
|
||
/** @var ?Database */
|
||
$connection = null;
|
||
if ($dsn['phptype'] == 'pgsql' || $dsn['phptype'] == 'mysql') {
|
||
$port = (isset($dsn['port'])) ? "port={$dsn['port']};" : "";
|
||
$connection = new self("{$dsn['phptype']}:host={$dsn['hostspec']}; $port dbname={$dsn['database']}", $dsn['username'], $dsn['password']);
|
||
if ($dsn['phptype'] == 'pgsql') {
|
||
$connection->query('SET client_encoding="UTF-8"');
|
||
}
|
||
|
||
if (isset($dsn['schema'])) {
|
||
$connection->query('SET search_path TO ' . $dsn['schema']);
|
||
}
|
||
} elseif ($dsn['phptype'] == 'sqlite::memory') {
|
||
$connection = new self("{$dsn['phptype']}:");
|
||
$connection->sqliteCreateFunction('LOWER', 'sqliteLower', 1);
|
||
} elseif ($dsn['phptype'] == 'sqlite') {
|
||
$connection = new self("{$dsn['phptype']}:{$dsn['database']}");
|
||
$connection->setAttribute(PDO::ATTR_TIMEOUT, 5);
|
||
$mode = defined('SQLITE_JOURNAL_MODE') ? \SQLITE_JOURNAL_MODE : 'WAL';
|
||
$connection->query("PRAGMA journal_mode=$mode");
|
||
$connection->sqliteCreateFunction('LOWER', 'sqliteLower', 1);
|
||
}
|
||
$connection->dsn = $dsn;
|
||
return $connection;
|
||
}
|
||
|
||
/**
|
||
* Выполняет запрос к базе данных
|
||
* @param string $query - запрос
|
||
* @param ?array<string, mixed> $values - значения
|
||
*/
|
||
public function executeQuery($query, $values = null): PDOStatement|bool
|
||
{
|
||
$stmt = $this->prepare($query);
|
||
|
||
$stmt->execute($values);
|
||
$stmt->cache = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||
return $stmt;
|
||
}
|
||
|
||
/**
|
||
* Создает подготовленный запрос
|
||
* @param string $query - запрос
|
||
* @return Statement
|
||
*/
|
||
public function prepareStatement($query)
|
||
{
|
||
return new Statement($query, $this);
|
||
}
|
||
|
||
/**
|
||
* Извлекает из базы все элементы по запросу (Для совместимости со старым представлением баз данных CIS)
|
||
* @param string $query - запрос
|
||
* @param ?array<string, mixed> $values - значения
|
||
* @return list<array<string, mixed>>
|
||
*/
|
||
public function fetchAllArray($query, $values = null)
|
||
{
|
||
$sth = $this->prepare($query);
|
||
$prep = $this->prepareValues($values);
|
||
$sth->execute($prep);
|
||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||
}
|
||
|
||
/**
|
||
* Извлекает из базы первый элемент по запросу
|
||
* @param string $query - запрос
|
||
* @param ?array<string, mixed> $values - значения
|
||
* @return array<string, mixed>|false
|
||
*/
|
||
public function fetchOneArray($query, $values = null)
|
||
{
|
||
$sth = $this->prepare($query);
|
||
$prep = $this->prepareValues($values);
|
||
$sth->execute($prep);
|
||
return $sth->fetch(PDO::FETCH_ASSOC);
|
||
}
|
||
|
||
/**
|
||
* Преобразует значения в подготовленные значения
|
||
* @param array $values - значения
|
||
* @return ?array<string, string>
|
||
*/
|
||
private function prepareValues($values)
|
||
{
|
||
if (!$values) {
|
||
return null;
|
||
}
|
||
$pg = $this->isPostgres();
|
||
$prep = [];
|
||
foreach ($values as $key => $value) {
|
||
$result = null;
|
||
if (is_bool($value)) {
|
||
if ($pg) {
|
||
$result = $value ? 'true' : 'false';
|
||
} else {
|
||
$result = $value ? 1 : 0;
|
||
}
|
||
} else {
|
||
$result = $value;
|
||
}
|
||
$prep[":" . $key] = $result;
|
||
}
|
||
return $prep;
|
||
}
|
||
|
||
/**
|
||
* Создает INSERT запрос
|
||
* @param string $table - таблица
|
||
* @param array $values - значения
|
||
* @param bool $return_id - возвращать id
|
||
* @param string $index - индекс
|
||
* @return int|mixed
|
||
*/
|
||
function insertQuery($table, array $values, $return_id = false, $index = null)
|
||
{
|
||
$prep = $this->prepareValues($values);
|
||
|
||
$sql = "INSERT INTO $table (" . implode(",", array_keys($values))
|
||
. ") VALUES (" . implode(",", array_keys($prep)) . ")";
|
||
if ($return_id) {
|
||
if ($this->isPostgres()) {
|
||
$sql .= " RETURNING $index";
|
||
}
|
||
}
|
||
$stmt = $this->prepare($sql);
|
||
$stmt->setFetchMode(PDO::FETCH_ASSOC);
|
||
$stmt->execute($prep);
|
||
$result = $stmt->fetch();
|
||
if ($return_id) {
|
||
if ($this->isPostgres()) {
|
||
return $result[$index];
|
||
} else {
|
||
$result = $this->fetchOneArray("SELECT $index AS lastid FROM $table WHERE OID = last_insert_rowid()");
|
||
return $result['lastid'];
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Создает UPDATE запрос
|
||
* @param string $table - таблица
|
||
* @param array $values - значения
|
||
* @param string $cond - условие
|
||
*/
|
||
function updateQuery($table, array $values, $cond): void
|
||
{
|
||
$prep = $this->prepareValues($values);
|
||
$sql = "UPDATE $table SET " . implode(
|
||
",",
|
||
array_map(function ($k, $v) {
|
||
return $k . "=" . $v; }, array_keys($values), array_keys($prep))
|
||
) . " WHERE $cond";
|
||
|
||
$stmt = $this->prepare($sql);
|
||
$stmt->setFetchMode(PDO::FETCH_ASSOC);
|
||
$stmt->execute($prep);
|
||
}
|
||
|
||
/**
|
||
* Создает генератор идентификаторов
|
||
* @return IdGenerator
|
||
*/
|
||
function getIdGenerator()
|
||
{
|
||
return new IdGenerator($this);
|
||
}
|
||
|
||
/**
|
||
* Замечание: Только для Postgres SQL
|
||
* @param string $seq Имя последовательности для ключа таблицы
|
||
* @return int Идентефикатор следующей записи
|
||
*/
|
||
function getNextId($seq)
|
||
{
|
||
$result = $this->fetchOneArray("SELECT nextval('$seq')");
|
||
return $result['nextval'];
|
||
}
|
||
|
||
/**
|
||
* Закрывает соединение с базой данных
|
||
*/
|
||
function close(): void
|
||
{
|
||
}
|
||
}
|
||
}
|