[], 'query' => [], 'time' => 0];
public $statEnabled = false;
/** @var string|bool Тип cache драйвера, false - не использовать кеширование */
protected $cacheStore = false;
/** @var bool */
protected $cacheLock = false;
/** @var Manager */
protected $manager = null;
public function __construct()
{
if (! extension_loaded('pdo')) {
throw new Exception('bff\db\Database: PDO extension is not loaded');
}
$debug = bff('debug');
$this->statEnabled = $debug;
if ($debug) {
$this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
$this->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
$this->setAttribute(PDO::ATTR_PERSISTENT, false);
}
/**
* Reset state
*/
public function onNewRequest($request)
{
$this->stats = [
'cache' => [], 'query' => [], 'time' => 0,
];
$this->result = null;
$this->trans = false;
$this->auto = true;
$this->rows = 0;
$this->crypt = false;
$this->cryptKey = '';
$this->tag = false;
$this->force = 0;
}
/**
* Деструктор
*/
public function __destruct()
{
$this->disconnect();
}
/**
* Установка параметров подключения
* @param string|array $config префикс настроек подключения(string) или массив настроек(array)
* @param bool $checkSlave проверить возможность slave подключения
* @return bool
*/
public function connectionConfig($config, $checkSlave = true)
{
if (is_string($config)) {
$config = config::prefixed($config);
}
if (empty($config) || !isset($config['type'])) {
throw new Exception('bff\db\Database: Connection settings are wrong');
}
switch ($config['type']) {
case 'pgsql':
$this->dns = 'pgsql:host=' . $config['host'] . ' port=' . $config['port'] . ' dbname=' . $config['name'];
break;
case 'mysql':
default:
$this->dns = 'mysql:host=' . $config['host'] . ';port=' . $config['port'] . ';dbname=' . $config['name'] . ';charset=' . $config['charset'];
break;
}
$this->user = $config['user'];
$this->pass = $config['pass'];
$this->charset = $config['charset'];
$this->backend = $config['type'];
$this->dbname = $config['name'];
if (!empty($config['cache'])) {
$this->setCacheStore($config['cache']);
}
if (isset($config['cache.lock'])) {
$this->setCacheLock($config['cache.lock']);
}
if ($checkSlave && ! empty($config['slave.host'])) {
$configSlave = config::prefixed('db.slave');
foreach (['type', 'host', 'port', 'name', 'user', 'pass', 'charset', 'cache'] as $v) {
if (! isset($configSlave[$v]) && isset($config[$v])) {
$configSlave[$v] = $config[$v];
}
}
try {
$this->slave = bff('database_factory');
$this->slave->connectionConfig($configSlave, false);
$this->slave->connect();
} catch (Exception $e) {
$this->slave->disconnect();
$this->slave = null;
}
}
return true;
}
/**
* Выполнение подлючения к базе данных
* @param bool $manager выполнять подключение менеджера
* @return bool
* @throws Exception
*/
public function connect($manager = true)
{
if (isset($this->pdo)) {
return false;
}
try {
if ($this->backend == 'mysql') {
# correct rowCount() (since php.version 5.3)
$this->setAttribute(PDO::MYSQL_ATTR_FOUND_ROWS, true);
$this->setAttribute(PDO::MYSQL_ATTR_INIT_COMMAND, 'SET NAMES ' . $this->charset . ', sql_mode = ""');
}
$this->pdo = new PDO($this->dns, $this->user, $this->pass, $this->attr);
if ($this->backend == 'pgsql') {
$this->pdo->exec('SET CLIENT_ENCODING TO ' . $this->charset);
}
if ($manager) {
$this->manager();
}
} catch (\PDOException $e) {
$sError = sprintf('Error connecting to the database %s - %s', Request::host(), $e->getMessage());
if (class_exists('\bff\base\HH', false)) {
HH::i()->error('db_connect', $sError, [
'DI' => 'database',
'method' => 'checkHH',
]);
}
$this->error($sError, false);
throw new Exception($sError, $e->getCode(), $e);
}
}
/**
* Проверка возможности подключения к БД
*/
public function checkHH()
{
if (! $this->isConnected()) {
return;
}
bff::hh()->resolve('db_connect');
}
/**
* Проверка режима работы MySQL сервера
*/
public function checkMysqlMode()
{
if (! $this->isMySQL()) {
return;
}
$hh = bff::hh();
$version = $this->one_data('SELECT VERSION();');
if (empty($version)) {
//$hh->notice('mysql_version_detect', _t('system','Failed to determine MySQL version'), ['DI' => 'database', 'method' => __FUNCTION__]);
return;
}
if (version_compare($version, '5.6') < 0) {
$hh->resolve('mysql_sql_mode');
return;
}
$mode = $this->one_array('SHOW VARIABLES WHERE Variable_name = "sql_mode"');
if (! empty($mode) && $mode['Variable_name'] == 'sql_mode' && empty($mode['Value'])) {
$hh->resolve('mysql_sql_mode');
return;
}
$msg = _t('system', 'Check your MySQL settings, setting "sql_mode" has an invalid value [val], instead of the required "" (empty string).', [
'val' => $mode['Value'] ? $mode['Value'] : '',
], false);
$hh->error('mysql_sql_mode', $msg, ['DI' => 'database', 'method' => __FUNCTION__]);
}
/**
* Данные о версии MySQL сервера
* @return array
*/
public function mysqlVersionInfo()
{
if (! $this->isMySQL()) {
return [];
}
$data = $this->select('SHOW VARIABLES WHERE Variable_name LIKE "version%"');
$result = [];
foreach ($data as $v) {
if (! isset($v['Variable_name']) || ! isset($v['Value'])) {
continue;
}
$result[ $v['Variable_name'] ] = $v['Value'];
}
return $result;
}
/**
* Проверка статуса подключения
* @return bool
*/
public function isConnected()
{
return isset($this->pdo);
}
/**
* Завершение подключения
*/
public function disconnect()
{
if ($this->manager) {
$this->manager->disconnect();
$this->manager = null;
}
$this->pdo = null;
if ($cache = $this->cache()) {
# todo: memcached + pnctl_fork
# https://github.com/php-memcached-dev/php-memcached/issues/195
if (method_exists($cache, 'quit')) {
$cache->quit();
}
}
return true;
}
/**
* Поддержка шифрования
* @param bool|null $cryptEnabled bool - включить/выключить поддержку шифрования; null - получить текущие настройки шифрования
* @param string $cryptKey - новый ключ шифрования
* @return string предыдущий ключ шифрования
*/
public function crypt($cryptEnabled = null, $cryptKey = '')
{
$currentCryptKey = $this->cryptKey;
if (is_bool($cryptEnabled)) {
$this->crypt = $cryptEnabled;
$this->cryptKey = $cryptKey;
}
return $currentCryptKey;
}
/**
* Шифрование: шифрованние данных
* @param string $data данные
* @param bool $base64 использовать base64 обвертку
* @return string
*/
public function crypt_encrypt($data, $base64 = true)
{
if (! is_scalar($data)) {
if (is_resource($data)) {
throw new Exception(static::class . ': Unable to encrypt recource data');
} else {
$data = serialize($data);
}
}
$result = $this->one_data('SELECT BFF_ENCRYPT(:data)', [':data' => $data]);
return ( $base64 ? base64_encode($result) : $result );
}
/**
* Шифрование: шифрование данных с ключем
* @param string $data данные
* @param string $cryptKey ключ шифрования
* @param bool $base64 использовать base64 обвертку
* @return mixed
*/
public function crypt_encrypt_key($data, $cryptKey, $base64 = true)
{
$prevEnabled = $this->crypt;
$prevKey = $this->crypt(true, $cryptKey);
$data = $this->crypt_encrypt($data, $base64);
$this->crypt($prevEnabled, $prevKey);
return $data;
}
/**
* Шифрование: расшифровка данных
* @param string $data данные
* @param bool $base64 использовать base64 обвертку
* @return mixed
*/
public function crypt_decrypt($data, $base64 = true)
{
$result = $this->one_data('SELECT BFF_DECRYPT(:data)', [':data' => ( $base64 ? base64_decode($data) : $data)]);
if (mb_stripos($result, 'a:') === 0) { // serialized Array
$result = func::unserialize($result);
} elseif (mb_stripos($result, 'O:') === 0) { // serialized Object
$result = unserialize($result);
}
return $result;
}
/**
* Шифрование: расшифровка данных с ключем
* @param string $data данные
* @param string $cryptKey ключ шифрования
* @param bool $base64 использовать base64 обвертку
* @return mixed
*/
public function crypt_decrypt_key($data, $cryptKey, $base64 = true)
{
$prevEnabled = $this->crypt;
$prevKey = $this->crypt(true, $cryptKey);
$data = $this->crypt_decrypt($data, $base64);
$this->crypt($prevEnabled, $prevKey);
return $data;
}
/**
* Логирование ошибки
* @param string $sMessage текст ошибки
* @param bool $debug получать backtrace
*/
protected function error($sMessage = '', $debug = false)
{
if ($debug) {
$backtrace = debug_backtrace(false);
foreach ($backtrace as $v) {
if (!empty($v['file']) && !empty($v['line']) && !empty($v['class'])) {
$sMessage .= "\n
{$v['file']} [{$v['line']}]";
}
}
}
trigger_error($sMessage, E_USER_ERROR);
}
/**
* Проверка SQLSTATE на наличие ошибок
* @param mixed $pdoQuery
* @return bool
*/
protected function errorCheck($pdoQuery = false, array $opts = [])
{
# Проверяем SQLSTATE
foreach ([$this->pdo, $pdoQuery] as $obj) {
if ($obj !== false && $obj->errorCode() != PDO::ERR_NONE) {
if ($this->trans && $this->auto) {
$this->rollback();
}
$error = $obj->errorInfo();
$message = '[SQL Error] ( ' . (@$error[0] . '.' . (isset($error[1]) ? $error[1] : '?')) . ' : ' . (isset($error[2]) ? $error[2] : '');
if (isset($opts['query'])) {
$message .= PHP_EOL . print_r($opts['query'], true);
}
if (isset($opts['bind'])) {
$message .= PHP_EOL . print_r($opts['bind'], true);
}
$message .= ' )';
$this->error($message, true);
return false;
}
}
return true;
}
/**
* Устанавливаем тег для ближайшего запроса
* @param string $key ключ тега
* @param array $data доп. данные
* @return $this
*/
public function tag($key, array $data = [])
{
$hook = 'db.query.' . $key . '.data';
if (bff::hooksAdded($hook)) {
$data = bff::filter($hook, $data);
}
$this->tag = ['key' => $key, 'data' => &$data];
return $this;
}
/**
* Форсировать выполнение ближайшего запроса на мастер сервере
*/
public function master()
{
$this->force = static::FORCE_MASTER;
}
/**
* Форсировать выполнение ближайшего запроса на слейв сервере
*/
public function slave()
{
$this->force = static::FORCE_SLAVE;
}
/**
* Установка типа cache драйвера, false - выключить кеширование
* @param string|bool $name
*/
public function setCacheStore($name)
{
$this->cacheStore = $name;
}
/**
* Инициализация Cache
* @param string|null $name
* @param array|null $tags
* @return \Psr\SimpleCache\CacheInterface|\Illuminate\Cache\MemcachedStore|bool
*/
public function cache($name = null, ?array $tags = null)
{
if (empty($name)) {
$name = $this->cacheStore;
}
if (empty($name)) {
return false;
}
try {
if (! empty($tags) && is_string($tags)) {
return Cache::group($tags, $name);
}
$cache = Cache::store($name);
if ($tags && $cache && method_exists($cache->getStore(), 'tags')) {
return $cache->tags($tags);
}
return $cache;
} catch (Exception $e) {
return false;
}
}
/**
* Генератор кеш ключа на основе строки
* @param string $str строка
* @return string
*/
public function cacheKey($str)
{
return 'db.cache.' . str_pad(base_convert(sprintf('%u', crc32($str)), 10, 36), 7, '0', STR_PAD_LEFT);
}
/**
* Установка lock режима работы кеша
* @param bool $lock
*/
public function setCacheLock($lock)
{
$this->cacheLock = $lock;
}
// ------------------------------------------------------------------------
// Управление транзакцией
/**
* Стартуем SQL транзакцию
* @param bool $auto
*/
public function begin($auto = false)
{
if (!$this->pdo) {
$this->connect();
}
$this->pdo->beginTransaction();
$this->trans = true;
$this->auto = $auto;
}
/**
* Откатываем SQL транзакцию
* @return void
*/
public function rollback()
{
if (!$this->pdo) {
$this->connect();
}
$this->pdo->rollback();
$this->trans = false;
$this->auto = true;
}
/**
* Коммитим SQL транзакцию
* @return void
*/
public function commit()
{
if (!$this->pdo) {
$this->connect();
}
$this->pdo->commit();
$this->trans = false;
$this->auto = true;
}
// ------------------------------------------------------------------------
// Собираем статистику
/**
* Статистика: кол-во выполненных запросов
* @param bool $bTotalCount true - возвращать общее кол-во, false - возвращать список выполненных запросов
* @return int|array
*/
public function statQueryCnt($totalCount = true)
{
if ($totalCount) {
$total = 0;
foreach ($this->stats['query'] as $v) {
$total += $v['n'];
}
return $total;
} else {
return $this->stats['query'];
}
}
/**
* Статистика: вывод статистики запросов на экран
* @param bool $bDebug использовать debug-метод
*/
public function statPrint($debug = false)
{
if ($debug) {
debug($this->stats);
}
echo 'query cnt: ' . $this->statQueryCnt() . '
total time: ' . number_format($this->stats['time'], 4) . ' ';
}
/**
* Статистика: получение статистики запросов
* @return array
*/
public function statGet()
{
return $this->stats;
}
/**
* Сбор статистики
* @param string $query текст запроса к базе
* @param int $timeStart время старта выполнения запроса
* @param bool $cached результат берется из кеша
* @param int $backtraceLevel backtrace-уровень
*/
protected function stat($query, $timeStart, $cached = false, $backtraceLevel = 2)
{
$timeProcessed = (microtime(true) - $timeStart);
$this->stats['time'] += $timeProcessed;
if (!isset($this->stats['query'][$query])) {
$this->stats['query'][$query] = ['n' => 0, 't' => [], 'tt' => 0, 'c' => 0, 'ctt' => 0];
}
$data = &$this->stats['query'][$query];
$data['n']++;
if ($cached) {
$data['c']++;
$data['ctt'] += $timeProcessed;
return;
}
$data['t'][] = number_format($timeProcessed, 4);
$data['tt'] += $timeProcessed;
if (bff('debug') && false) {
$aBacktrace = debug_backtrace(false);
if (isset($aBacktrace[$backtraceLevel]['file'])) {
$data['file'] = $aBacktrace[$backtraceLevel]['file'];
$data['line'] = $aBacktrace[$backtraceLevel]['line'];
}
}
}
/**
* Запретить сбор статистики
* @return void
*/
public function statDisable()
{
$this->statEnabled = false;
if ($this->manager) {
$this->manager->disableQueryLog();
}
}
/**
* Разрешить сбор статистики
* @return void
*/
public function statEnable()
{
$this->statEnabled = true;
if ($this->manager) {
$this->manager->enableQueryLog();
}
}
/**
* Расчет места занимаемого БД
* @return mixed
*/
public function usage()
{
if ($this->backend == 'mysql') {
return (int)$this->one_data('SELECT SUM(data_length + index_length) AS sum FROM information_schema.TABLES WHERE TABLE_SCHEMA = :db', [
':db' => $this->dbname,
]);
}
return false;
}
/**
* Данные о таблицах в БД приблизительное количество записей и размер на диске
* @return array
*/
public function tables()
{
if ($this->backend == 'mysql') {
return $this->select('SELECT TABLE_NAME AS `table`, TABLE_ROWS AS `rows`, data_length, index_length FROM information_schema.TABLES WHERE TABLE_SCHEMA = :db', [
':db' => $this->dbname,
]);
}
return false;
}
// ------------------------------------------------------------------------
// Обрабатываем SQL запросы
/**
* Обрабатываем (выполняем) SQL запросы
* @param string|array $query запросы
* @param array $bind аргументы
* @param int $ttl срок годности кеш версии в секундах
* @param int|bool $fetchType PDO::FETCH_NUM, PDO::FETCH_ASSOC, PDO::FETCH_BOTH, PDO::FETCH_OBJ
* @param string $fetchFunc
* @param array $prepareOptions
* @return array
*/
public function exec($query, array $bind = null, $ttl = 0, $fetchType = false, $fetchFunc = 'fetchAll', array $prepareOptions = [])
{
do {
if (! $this->slave) {
break;
}
$force = $this->force;
$this->force = 0;
if ($force == static::FORCE_MASTER) {
break;
}
if ($force == static::FORCE_SLAVE) {
return $this->slave->exec($query, $bind, $ttl, $fetchType, $fetchFunc, $prepareOptions);
}
if (! $ttl) {
break;
}
if (! is_string($query)) {
break;
}
if (preg_match('/\s*(?:INSERT|UPDATE|DELETE|TABLE|CREATE|ALTER|DROP|SHOW)\s/i', $query)) {
break;
}
return $this->slave->exec($query, $bind, $ttl, $fetchType, $fetchFunc, $prepareOptions);
} while (false);
if (!$this->pdo) {
$this->connect();
}
$batch = is_array($query);
if ($batch) {
if (!$this->trans && $this->auto) {
$this->begin(true);
}
if (is_null($bind)) {
$bind = [];
for ($i = 0; $i < count($query); $i++) {
$bind[] = null;
}
}
} else {
$query = [$query];
$bind = [$bind];
}
foreach (array_combine($query, $bind) as $cmd => $arg) {
if ($this->tag !== false) {
$cmd = bff::filter('db.query.' . $this->tag['key'], $cmd, ['bind' => &$arg, 'data' => &$this->tag['data']]);
}
if ($this->crypt) {
$cmd = preg_replace('/BFF\_(EN|DE)CRYPT\(([^\)]+)\)/xisu', 'AES_${1}CRYPT(${2}, :bff_crypt_key)', $cmd, -1, $crypts);
if ($crypts > 0 && !isset($arg[':bff_crypt_key'])) {
$arg[':bff_crypt_key'] = $this->cryptKey;
}
}
$cmd = bff::filter('db.query', $cmd, ['bind' => &$arg, 'result' => &$this->result, 'rows' => &$this->rows]);
if ($cmd === null) {
continue;
}
$time = microtime(true);
if ($ttl && ($cache = $this->cache())) {
$cacheKey = $this->cacheKey($cmd . var_export($arg, true));
if (($this->result = $cache->get($cacheKey)) !== null) {
if ($this->statEnabled) {
$this->stat($cmd, $time, true, 3);
}
continue;
}
$cacheLock = false;
if ($this->cacheLock && method_exists($cache->getStore(), 'lock')) {
$cacheLock = $cache->lock($cacheKey . '.lock', 15);
if (! $cacheLock->get()) {
try {
$cacheLock->block(15);
} catch (LockTimeoutException $e) {
// Unable to acquire lock...
}
if (($this->result = $cache->get($cacheKey)) !== null) {
if ($this->statEnabled) {
$this->stat($cmd, $time, true, 3);
}
$cacheLock->forceRelease();
continue;
}
}
}
}
if (is_null($arg)) {
$query = $this->pdo->query($cmd);
} else {
$query = $this->pdo->prepare($cmd, $prepareOptions);
if (is_object($query)) {
foreach ($arg as $key => $value) {
if (
!(is_array($value) ?
$query->bindValue($key, $value[0], $value[1]) :
$query->bindValue($key, $value, $this->type($value))
)
) {
break;
}
}
$query->execute();
}
}
# Проверяем SQLSTATE
if (!$this->errorCheck($query, ['query' => $cmd, 'bind' => $arg])) {
if ($this->tag !== false) {
$this->tag = false;
}
return false;
}
if ($fetchType !== false || preg_match('/^\s*(?:SELECT|PRAGMA|SHOW|EXPLAIN)\s/i', $cmd)) {
if ($fetchFunc !== false) {
$this->result = $query->$fetchFunc($fetchType);
$this->rows = $query->rowCount();
$query = null;
} else {
$this->result = $query;
}
} else {
$this->rows = $this->result = $query->rowCount();
$query = null;
}
if ($ttl && $cache && $fetchFunc !== false) {
$cache->set($cacheKey, $this->result, $ttl);
if ($cacheLock) {
$cacheLock->forceRelease();
}
}
HH::i()->statMysqlQuery($cmd, $time);
# Считаем кол-во выполненных запросов
if ($this->statEnabled) {
$this->stat($cmd, $time, false, 3);
}
}
if ($batch || $this->trans && $this->auto) {
$this->commit();
}
if ($this->tag !== false) {
$this->tag = false;
}
return $this->result;
}
/**
* Выполняем INSERT запрос
* @param string $table название таблицы
* @param array $fields массив параметров для вставки
* @param string|array|bool $returnID возвращать ID вновь добавленной записи (pgsql: несколько данных)
* @param array $bind доп. параметры запроса для bind'a
* @param array $cryptKeys ключи параметров, требующие шифрования
* @return int ID добавленной записи
*/
public function insert($table, array $fields = [], $returnID = 'id', $bind = [], array $cryptKeys = [])
{
$f = $v = '';
foreach ($fields as $field => $val) {
$f .= ($f ? ',' : '') . $this->wrapColumn($field);
$v .= ($v ? ',' : '') . ($this->crypt && in_array($field, $cryptKeys) ? 'BFF_ENCRYPT(:' . $field . ')' : ':' . $field);
if (is_array($val)) {
$val = json_encode($val);
}
$bind[':' . $field] = [$val, $this->type($val)];
}
if (!$bind) {
return false;
}
$query = 'INSERT INTO ' . $table . ' (' . $f . ') VALUES (' . $v . ')';
if ($returnID !== false) {
if ($this->isPgSQL()) {
# RETURNING expression (pgsql)
if (is_string($returnID)) {
$query .= ' RETURNING ' . $this->wrapColumn($returnID);
list($fetchType, $fetchFunc) = [0, 'fetchColumn']; // one_data
} elseif (is_array($returnID)) {
$query .= ' RETURNING ' . join(', ', $this->wrapColumn($returnID));
list($fetchType, $fetchFunc) = [PDO::FETCH_ASSOC, 'fetch']; // one_array
}
return $this->exec($query, $bind, 0, $fetchType, $fetchFunc);
} else {
# lastInsertId (mysql, ...)
$this->exec($query, $bind);
return $this->insert_id($table, $returnID);
}
} else {
return $this->exec($query, $bind);
}
}
/**
* Получаем ID последней добавленной записи
* @param string $table название таблицы
* @param string $columnName название поля ID
* @param string $sequencePostfix окончание в названии последовательности(sequence) (tableName_FieldName)
* @return int
*/
public function insert_id($table = '', $columnName = 'id', $sequencePostfix = '_seq')
{
$result = (int)$this->pdo->lastInsertId($table . ($columnName ? '_' . $columnName : '') . $sequencePostfix);
return (empty($result) ? 0 : $result);
}
/**
* Выполняем UPDATE запрос
* @param string $table название таблицы
* @param array $fields массив параметров для обновления
* @param array|string|int $conditions условия WHERE
* @param array $bind доп. параметры для подстановки в запрос
* @param array $cryptKeys ключи параметров, требующие шифрования
* @param array $options:
* string|array 'orderBy' порядок сортировки
* string|array|int 'limit' условие LIMIT
* bool 'returnQuery' возвращать запрос без выполнения
* @return bool|mixed
*/
public function update($table, array $fields = [], $conditions = '', $bind = [], array $cryptKeys = [], array $options = [])
{
$set = '';
foreach ($fields as $field => $val) {
if (is_int($field) && is_string($val)) {
$set .= ($set ? ',' : '') . $val;
} else {
$set .= ($set ? ',' : '') . $this->wrapColumn($field) . '=' . ($this->crypt && in_array($field, $cryptKeys) ? 'BFF_ENCRYPT(:' . $field . ')' : ':' . $field);
if (is_array($val)) {
$val = json_encode($val);
}
$bind[':' . $field] = [$val, $this->type($val)];
}
}
if ($set) {
if (is_numeric($conditions)) {
$bind[':id'] = [$conditions, $this->type($conditions)];
$conditions = 'id=:id'; # ID condition
} elseif (is_array($conditions)) {
$where = [];
foreach ($conditions as $field => $val) {
if (is_array($val)) {
$where[] = Model::condition($field, $val, $bind);
} else {
if (is_int($field) && is_string($val)) {
$where[] = $val; # special conditions (multi conditions)
} else {
$bindKey = ':' . $field;
while (isset($bind[$bindKey])) {
# исключаем повторение с bind-данными из $fields
$bindKey .= 'A';
}
$where[] = $this->wrapColumn($field) . '=' . $bindKey; # field condition
$bind[$bindKey] = [$val, $this->type($val)];
}
}
}
$conditions = join(' AND ', $where);
} else {
# string - raw condition
}
# order by
if (! empty($options['orderBy'])) {
$orderBy = $options['orderBy'];
if (is_array($orderBy)) {
$orderBy = join(', ', $orderBy);
}
$orderBy = ' ORDER BY ' . strval($orderBy);
} else {
$orderBy = '';
}
# limit
if (! empty($options['limit'])) {
$limit = $options['limit'];
if (is_array($limit)) {
if (! empty($limit)) {
$limit = $this->prepareLimit(false, reset($limit));
} else {
$limit = '';
}
} elseif (is_numeric($limit)) {
$limit = $this->prepareLimit(false, strval($limit));
}
}
$query = 'UPDATE ' . $table . ' SET ' . $set . ($conditions ? (' WHERE ' . $conditions) : '') . $orderBy . (!empty($limit) ? ' ' . $limit : '');
if (! empty($options['returnQuery'])) {
return [
'query' => $query,
'bind' => $bind,
];
}
return $this->exec($query, $bind);
}
return false;
}
/**
* Выполняем DELETE запрос
* @param string $table название таблицы
* @param array|int $conditions условия WHERE или ID (в таком случае WHERE id = ID)
* @param array $bind доп. параметры запроса для bind'a
* @return bool
*/
public function delete($table, $conditions = [], array $bind = [])
{
$where = '';
if (is_int($conditions)) {
$where .= 'id = :id'; # ID condition
$bind[':id'] = [$conditions, $this->type($conditions)];
} elseif (is_array($conditions)) {
foreach ($conditions as $field => $val) {
if (is_array($val)) {
$where .= ($where ? ' AND ' : '') . $this->prepareIN($field, $val); # IN - only ints
} else {
if (is_int($field) && is_string($val)) {
$where .= ($where ? ' AND ' : '') . $val; # special conditions (multi conditions)
} else {
$where .= ($where ? ' AND ' : '') . $this->wrapColumn($field) . '=:' . $field; # field condition
$bind[':' . $field] = [$val, $this->type($val)];
}
}
}
}
if ($where) {
return $this->exec('DELETE FROM ' . $table . ' WHERE ' . $where, $bind);
}
return false;
}
/**
* Получаем несколько строк из таблицы
* @param string $query текст запроса
* @param array|null $bindParams параметры запроса или null
* @param int $cache кешируем (в секундах)
* @param int $fetchType PDO::FETCH_NUM, PDO::FETCH_ASSOC, PDO::FETCH_BOTH, PDO::FETCH_OBJ
* @param string $fetchFunc
* @return mixed {@see exec}
*/
public function select($query, $bindParams = null, $cache = 0, $fetchType = PDO::FETCH_ASSOC, $fetchFunc = 'fetchAll')
{
return $this->exec($query, $bindParams, $cache, $fetchType, $fetchFunc);
}
/**
* Получаем несколько строк из таблицы построчно, с последующей обработкой в callback-функции
* @param string $query текст запроса
* @param array $bindParams параметры запроса
* @param callable $callback функция обработчик
* @param array|int $options [
* bool 'exclusive' => true Использовать новое подключение к БД
* int 'fetchType' => PDO::FETCH_ASSOC | PDO::FETCH_NUM | PDO::FETCH_BOTH | PDO::FETCH_OBJ
* ]
*/
public function select_iterator($query, array $bindParams, callable $callback, $options = [])
{
if (is_numeric($options)) {
$options = ['fetchType' => $options];
}
func::array_defaults($options, [
'fetchType' => PDO::FETCH_ASSOC,
'exclusive' => true,
]);
$exclusive = ! empty($options['exclusive']);
$fetchType = $options['fetchType'];
$disconnect = false;
if ($exclusive) {
if ($this->slave && $this->force == static::FORCE_SLAVE) {
$this->force = 0;
$self = $this->slave;
$self->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
} else {
/** @var Database $self */
$self = bff('database_factory');
$self->connectionConfig('db', false);
$self->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$self->connect(false);
$disconnect = true;
}
} else {
$self = $this;
$self->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
}
$stmt = $self->exec($query, $bindParams, 0, $fetchType, false/*PDOStatement*/, [
PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL,
]);
if ($stmt !== false) {
while ($row = $stmt->fetch($fetchType, PDO::FETCH_ORI_NEXT)) {
if ($callback($row) === false) {
break;
}
}
$stmt->closeCursor();
$stmt = null;
}
if ($disconnect) {
$self->disconnect();
} else {
$self->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
}
}
/**
* Получаем несколько строк из таблицы, с последующей группировкой по столбцу
* @param string $query текст запроса
* @param string $key поле для ключа
* @param array|null $bindParams параметры запроса или null
* @param int $cache кешируем (в секундах)
* @return mixed {@see exec}
*/
public function select_key($query, $key = 'id', $bindParams = null, $cache = 0)
{
$tmp = $this->select($query, $bindParams, $cache);
if (empty($key)) {
return $tmp;
}
$data = [];
if (!empty($tmp)) {
foreach ($tmp as $d) {
$data[$d[$key]] = $d;
}
unset($tmp);
}
return $data;
}
/**
* Получаем один столбец из таблицы
* @param string $query текст запроса
* @param array|null $bindParams параметры запроса или null
* @param int $cache кешируем (в секундах)
* @return mixed {@see exec}
*/
public function select_one_column($query, $bindParams = null, $cache = 0)
{
return $this->exec($query, $bindParams, $cache, PDO::FETCH_COLUMN, 'fetchAll');
}
/**
* Формирование SELECT запроса из одной таблицы
* @param string $table название таблицы
* @param array $fields список столбцов
* @param string|array $where условия WHERE
* @param array $options:
* string 'prefix' префикс таблицы
* string|array 'groupBy' порядок группировки
* string|array 'orderBy' порядок сортировки
* string|array|int 'limit' условие LIMIT
* array 'cryptKeys' шифруемые столбцы
* @return array
*/
public function select_prepare($table, array $fields = [], $where = [], array $options = [])
{
$tablePrefix = (!empty($options['prefix']) ? $options['prefix'] : '');
# cryptKeys:
$cryptKeys = (!empty($options['cryptKeys']) ? $options['cryptKeys'] : []);
# select
$select = [];
if (sizeof($fields) === 1 && current($fields) === '*' && !empty($cryptKeys)) {
$fields = array_merge($fields, $cryptKeys);
}
foreach ($fields as $field) {
$select[] = ($this->crypt && in_array($field, $cryptKeys) && $field != '*' ? 'BFF_DECRYPT(' . $field . ') as ' . $field : $field);
}
$select = ($select ? join(', ', $this->wrapColumn($select)) : '*');
# where
$filter = Model::filter((is_array($where) ? $where : [$where]), $tablePrefix);
# group by
if (! empty($options['groupBy'])) {
$groupBy = $options['groupBy'];
if (is_array($groupBy)) {
$groupBy = join(', ', $groupBy);
}
$groupBy = ' GROUP BY ' . $groupBy;
} else {
$groupBy = '';
}
# order by
if (! empty($options['orderBy'])) {
$orderBy = $options['orderBy'];
if (is_array($orderBy)) {
$orderBy = join(', ', $orderBy);
}
$orderBy = ' ORDER BY ' . strval($orderBy);
} else {
$orderBy = '';
}
# limit
if (! empty($options['limit'])) {
$limit = $options['limit'];
if (is_array($limit)) {
if (sizeof($limit) == 1) {
$limit = $this->prepareLimit(0, reset($limit));
} elseif (sizeof($limit) > 1) {
$limit = $this->prepareLimit(reset($limit), next($limit));
} else {
$limit = '';
}
} elseif (is_numeric($limit)) {
$limit = $this->prepareLimit(0, strval($limit));
}
}
return [
'query' => 'SELECT ' . $select . ' FROM ' . $table . ' ' . $tablePrefix . ' ' . $filter['where'] . $groupBy . $orderBy . (!empty($limit) ? ' ' . $limit : ''),
'bind' => $filter['bind'],
];
}
/**
* Получаем несколько строк из таблицы, с последующей группировкой по столбцу
* @param string $table название таблицы
* @param array $fields список столбцов
* @param string|array $where условия WHERE
* @param string|array $orderBy порядок сортировки
* @param string $limit условие LIMIT
* @param array $cryptKeys шифруемые столбцы
* @param int $cache кешируем (в секундах)
* @return mixed {@see exec}
*/
public function select_rows($table, array $fields = [], $where = [], $orderBy = '', $limit = '', array $cryptKeys = [], $cache = 0)
{
$select = $this->select_prepare($table, $fields, $where, ['orderBy' => $orderBy, 'limit' => $limit, 'cryptKeys' => $cryptKeys]);
return $this->exec($select['query'], $select['bind'], $cache, PDO::FETCH_ASSOC, 'fetchAll');
}
/**
* Получаем все строки из таблицы пошагово, с последующей обработкой в callback-функции
* @param string $table название таблицы
* @param array $fields список столбцов
* @param array $where условия WHERE
* @param string|array $orderBy порядок сортировки
* @param array $cryptKeys шифруемые столбцы
* @param callback $callback функция обработчик
* @param int $step кол-во обрабатываемых записей за шаг
* @return int общее кол-во записей в таблице
*/
public function select_rows_chunked($table, array $fields = [], array $where = [], $orderBy = '', array $cryptKeys = [], $callback = 0, $step = 100)
{
$total = $this->select_rows_count($table, $where);
if ($total > 0 && is_callable($callback)) {
if ($total < $step) {
$rows = $this->select_rows($table, $fields, $where, $orderBy, '', $cryptKeys);
if (! empty($rows)) {
$callback($rows);
}
} else {
for ($i = 0; $i <= $total; $i += $step) {
$rows = $this->select_rows($table, $fields, $where, $orderBy, [$i, $step], $cryptKeys);
if (! empty($rows)) {
if ($callback($rows) === false) {
break;
}
}
}
}
}
return $total;
}
/**
* Получаем все строки из таблицы построково, с последующей обработкой в callback-функции
* @param string $table название таблицы
* @param array $fields список столбцов
* @param array $where условия WHERE
* @param string|array $orderBy порядок сортировки
* @param string $limit условие LIMIT
* @param array $cryptKeys шифруемые столбцы
* @param callback|null $callback функция обработчик
*/
public function select_rows_iterator($table, array $fields = [], array $where = [], $orderBy = '', $limit = '', array $cryptKeys = [], $callback = null)
{
if (is_callable($callback)) {
$select = $this->select_prepare($table, $fields, $where, [
'orderBy' => $orderBy,
'limit' => $limit,
'cryptKeys' => $cryptKeys,
]);
$this->select_iterator($select['query'], $select['bind'], $callback);
}
}
/**
* Получаем несколько строк из таблицы, с последующей группировкой по столбцу
* @param string $table название таблицы
* @param string $key столбец, по которому выполняется группировка
* @param array $fields список столбцов
* @param string|array $where условия WHERE
* @param string|array $orderBy порядок сортировки
* @param string $limit условие LIMIT
* @param array $opts [
* array 'cryptKeys' - шифруемые столбцы
* int 'cache' - кешируем (в секундах)
* ]
* @return mixed @see self::select_key @method
*/
public function select_rows_key($table, $key, array $fields = [], $where = [], $orderBy = '', $limit = '', array $opts = [])
{
$opts['orderBy'] = $orderBy;
$opts['limit'] = $limit;
$select = $this->select_prepare($table, $fields, $where, $opts);
return $this->select_key($select['query'], $key, $select['bind'], $opts['cache'] ?? 0);
}
/**
* Получаем один столбец из таблицы
* @param string $table название таблицы
* @param string $column название столбца
* @param array $where условия WHERE
* @param array $opts [
* int|array 'limit' => 0, # условие LIMIT или [LIMIT, OFFSET]
* int 'cache' => 0, # кешируем (в секундах)
* bool 'crypt' => false, # столбец шифруется
* string|array 'groupBy' порядок группировки
* string|array 'orderBy' порядок сортировки
* ... {@see select_prepare}
* ]
* @return mixed {@see exec}
*/
public function select_rows_column(string $table, string $column, array $where = [], array $opts = [])
{
if (!empty($opts['crypt'])) {
$opts['cryptKeys'] = [$column];
}
$select = $this->select_prepare($table, [$column], $where, $opts);
if (!empty($opts['returnQuery'])) {
return $select;
}
return $this->exec($select['query'], $select['bind'], ($opts['cache'] ?? 0), PDO::FETCH_COLUMN, 'fetchAll');
}
/**
* Получаем кол-во записей в таблице
* @param string $table название таблицы
* @param array $where условия WHERE
* @param array|int $options доп. параметры или ttl
* @return mixed {@see exec}
*/
public function select_rows_count($table, array $where = [], $options = 0)
{
if (is_numeric($options)) {
$options = ['ttl' => intval($options)];
} elseif (!isset($options['ttl'])) {
$options['ttl'] = 0;
}
$select = $this->select_prepare($table, ['COUNT(*)'], $where, $options);
if (!empty($options['returnQuery'])) {
return $select;
}
return (int)$this->exec($select['query'], $select['bind'], $options['ttl'], 0, 'fetchColumn');
}
/**
* Получаем строку из таблицы
* @param string $table название таблицы
* @param array $fields список столбцов
* @param string|array $where условия WHERE
* @param string|array $orderBy порядок сортировки
* @param string $limit условие LIMIT
* @param array $cryptKeys шифруемые столбцы
* @param int $cache кешируем (в секундах)
* @return mixed {@see exec}
*/
public function select_row($table, array $fields = [], $where = [], $orderBy = '', $limit = '', array $cryptKeys = [], $cache = 0)
{
$select = $this->select_prepare($table, $fields, $where, [
'orderBy' => $orderBy,
'limit' => $limit,
'cryptKeys' => $cryptKeys,
]);
return $this->exec($select['query'], $select['bind'], $cache, PDO::FETCH_ASSOC, 'fetch');
}
/**
* Получаем данные из таблицы из одного столбца
* @param string $table название таблицы
* @param string $field название столбца
* @param string|array $where условия WHERE
* @param string|array $orderBy порядок сортировки
* @param string $limit условие LIMIT
* @param bool $cryptedField столбец шифруется
* @param int $cache кешируем (в секундах)
* @return mixed {@see exec}
*/
public function select_data($table, $field, $where = [], $orderBy = '', $limit = '', $cryptedField = false, $cache = 0)
{
$field = (is_array($field) ? array_slice($field, 0, 1) : [strval($field)]);
$select = $this->select_prepare($table, $field, $where, [
'orderBy' => $orderBy,
'limit' => $limit,
'cryptKeys' => ($cryptedField ? $field : []),
]);
return $this->exec($select['query'], $select['bind'], $cache, 0, 'fetchColumn');
}
/**
* Получаем данные из таблицы из одного поля
* @param string $query текст запроса
* @param array $bindParams параметры запроса
* @param int $cache кешируем (в секундах)
* @return mixed {@see exec}
*/
public function one_data($query, $bindParams = null, $cache = 0)
{
return $this->exec($query, $bindParams, $cache, 0, 'fetchColumn');
}
/**
* Получаем строку из таблицы
* @param string $query текст запроса
* @param array|null $bindParams параметры подставляемые в запрос или null
* @param int $cache кешируем (в секундах)
* @param int $fetchType PDO::FETCH_NUM; PDO::FETCH_ASSOC; PDO::FETCH_BOTH; PDO::FETCH_OBJ
* @return mixed {@see exec}
*/
public function one_array($query, $bindParams = null, $cache = 0, $fetchType = PDO::FETCH_ASSOC)
{
return $this->exec($query, $bindParams, $cache, $fetchType, 'fetch');
}
/**
* Возвращаем кол-во строк затронутых последним запросом
* @return int
*/
public function rows()
{
return $this->rows;
}
/**
* Возвращаем тип данных PDO, определенный по значению
* @param mixed $val значение
* @return int
*/
public function type($val)
{
foreach (
[
'null' => 'NULL',
'bool' => 'BOOL',
'string' => 'STR',
'int' => 'INT',
'float' => 'STR',
] as $php => $pdo
) {
if (call_user_func('is_' . $php, $val)) {
return constant('PDO::PARAM_' . $pdo);
}
}
return PDO::PARAM_LOB;
}
/**
* Получаем схему таблицы
* @param string $table имя таблицы
* @param int $cache кешируем (в секундах)
* @return array|bool
*/
public function schema($table, $cache = 0)
{
$cmd = [
'mysql' => [
'SHOW columns FROM `' . $this->dbname . '`.' . $table . ';',
'Field',
'Key',
'PRI',
'Type',
],
'mssql|sybase|dblib|pgsql|ibm|odbc' => [
'SELECT c.column_name AS field,' .
'c.data_type AS type,t.constraint_type AS pkey ' .
'FROM information_schema.columns AS c ' .
'LEFT OUTER JOIN ' .
'information_schema.key_column_usage AS k ON ' .
'c.table_name=k.table_name AND ' .
'c.column_name=k.column_name ' .
($this->dbname ?
('AND ' .
(preg_match('/^pgsql$/', $this->backend) ?
'c.table_catalog=k.table_catalog' :
'c.table_schema=k.table_schema') . ' ') : '') .
'LEFT OUTER JOIN ' .
'information_schema.table_constraints AS t ON ' .
'k.table_name=t.table_name AND ' .
'k.constraint_name=t.constraint_name ' .
($this->dbname ?
('AND ' .
(preg_match('/pgsql/', $this->backend) ?
'k.table_catalog=t.table_catalog' :
'k.table_schema=t.table_schema') . ' ') : '') .
'WHERE ' .
'c.table_name=\'' . $table . '\'' .
($this->dbname ?
('AND ' .
(preg_match('/pgsql/', $this->backend) ?
'c.table_catalog' : 'c.table_schema') .
'=\'' . $this->dbname . '\'') : '') .
';',
'field',
'pkey',
'PRIMARY KEY',
'type',
],
'sqlite2?' => [
'PRAGMA table_info(' . $table . ');',
'name',
'pk',
1,
'type',
],
];
$match = false;
foreach ($cmd as $backend => $val) {
if (preg_match('/' . $backend . '/', $this->backend)) {
$match = true;
break;
}
}
if (!$match) {
$this->error(_t('system', 'This type of database is not supported'), false);
return false;
}
$result = $this->exec($val[0], null, $cache);
if (!$result) {
$this->error(_t('system', 'Failed to get schema for this table "[table]"', ['table' => $table]), false);
return false;
}
return [
'result' => $result,
'field' => $val[1],
'pkname' => $val[2],
'pkval' => $val[3],
'type' => $val[4],
];
}
/**
* Изменение кодировки и Engine для таблицы
* @param string $table имя таблицы
* @param string|null $charset новая кодировка 'utf8mb4'
* @param string|null $engine новая engine 'InnoDB'
* @return bool
*/
public function changeTable($table, ?string $charset = null, ?string $engine = null)
{
if (empty($table)) {
return false;
}
$data = $this->one_array('SELECT TABLE_NAME, TABLE_COLLATION, ENGINE FROM `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA` = :db_name AND `TABLE_NAME` = :table_name', [
':db_name' => $this->dbname,
':table_name' => $table,
]);
if (empty($data)) {
return false;
}
if (! empty($charset) && $data['TABLE_COLLATION'] !== mb_strtolower($charset) . '_unicode_ci') {
$this->exec('ALTER TABLE `' . $data['TABLE_NAME'] . '` CONVERT TO CHARACTER SET ' . $charset . ' COLLATE ' . $charset . '_unicode_ci;');
}
if (! empty($engine) && $data['ENGINE'] !== mb_strtolower($engine)) {
$this->exec('ALTER TABLE `' . $data['TABLE_NAME'] . '` ENGINE=' . $engine);
}
return true;
}
// --------------------------------------------------------------
/**
* Проверка существования таблицы
* @param string $table название таблицы
* @return bool
*/
public function isTable($table)
{
switch ($this->getDriverName()) {
case 'pgsql':
$result = $this->select_one_column(
'SELECT table_name FROM information_schema.tables
WHERE table_schema = :shema',
[':shema' => 'public']
);
foreach ($result as $v) {
if ($v == $table) {
return true;
}
}
break;
case 'mysqli':
case 'mysql':
$result = $this->one_data('SHOW TABLES LIKE :table', [':table' => $table]);
if (!empty($result)) {
return true;
}
break;
}
return false;
}
/**
* Получаем текущую дату/время в SQL формате
* @param bool $quote выполнять квотирование
* @return string
*/
public function getNOW($quote = true)
{
$date = date('Y-m-d H:i:s');
return ($quote ? $this->str2sql($date) : $date);
}
/**
* Получаем текущую дату/время в SQL формате
* @param bool $quote выполнять квотирование
* @return string
*/
public function now($quote = false)
{
return $this->getNOW($quote);
}
/**
* Выполняем квотирование строки
* @param string $value
* @return string
*/
public function str2sql($value)
{
return $this->pdo->quote($value);
}
/**
* Выполняем квотирование названия колонки
* @param string|array $column
* @param bool $force
* @return string|array
*/
public function wrapColumn($column, bool $force = false)
{
if (is_array($column)) {
foreach ($column as $k => $v) {
$column[$k] = $this->wrapColumn($v);
}
return $column;
}
if (strripos($column, ' as ') !== false) {
$segments = preg_split('/(\s+as\s+)/i', $column, -1, PREG_SPLIT_OFFSET_CAPTURE);
if (($len = sizeof($segments)) > 2) {
$segments = [
mb_substr($column, 0, $segments[$len - 2][1] + mb_strlen($segments[$len - 2][0])),
$this->wrapColumn($segments[$len - 1][0], true),
];
} else {
$segments = [
$this->wrapColumn($segments[0][0]),
$this->wrapColumn($segments[1][0], true),
];
}
return join(' as ', $segments);
}
if (
! $force &&
(
$column === '*' ||
stripos($column, '.') !== false ||
stripos($column, '(') !== false
)
) {
return $column;
}
if ($force && stripos($column, '`') !== false) {
return $column;
}
return '`' . str_replace('`', '``', trim($column)) . '`'; # MySQL
}
/**
* Подготовка параметров UPDATE запроса (@deprecated, используем self::update)
* @param string $queryData @ref итоговый sql запрос
* @param array $data данные
* @param array $keysNotPrepare названия полей не требующих sql:prepare
* @return bool
*/
public function prepareUpdateQuery(&$queryData, $data, $keysNotPrepare = [])
{
if (empty($data)) {
return '';
}
$queryData = [];
foreach ($data as $key => $value) {
if (!empty($value) || $value == 0) {
$queryData[] = $key . ' = ' . (!empty($keysNotPrepare) && ($keysNotPrepare === true || in_array($key, $keysNotPrepare)) ? $value : $this->str2sql($value)) . ' ';
}
}
$queryData = join(', ', $queryData);
return !empty($queryData);
}
/**
* Подготовка параметров INSERT запроса (@deprecated, используем self::insert)
* @param string $fields @ref поля базы данных
* @param string $values @ref данные в виде sql запроса
* @param array $data данные
* @return bool
*/
public function prepareInsertQuery(&$fields, &$values, $data)
{
$fields = [];
$values = [];
foreach ($data as $key => $value) {
if ((!empty($value) || $value == 0) && !is_array($value)) {
$fields[] = $key;
$values[] = $this->str2sql($value);
}
}
$fields = join(', ', $fields);
$values = join(', ', $values);
return (!empty($fields) && !empty($values));
}
/**
* Подготовка LIMIT
* @param int|bool $offset
* @param int $limit
* @return string SQL
*/
public function prepareLimit($offset, $limit)
{
switch ($this->getDriverName()) {
case 'pgsql':
return " LIMIT " . (empty($limit) ? 'ALL' : $limit) . ($offset !== false ? " OFFSET $offset " : '');
break;
case 'mysqli':
case 'mysql':
default:
return ' LIMIT ' . ($offset !== false ? $offset . ', ' : '') . (empty($limit) ? '18446744073709551615' : $limit) . " ";
break;
}
}
/**
* Строит IN или NOT IN sql строку сравнения
* @param string $column название колонки для сравнения
* @param array $values массив значений - разрешенных (IN) или запрещенных (NOT IN)
* @param bool $not true: NOT IN (), false: IN ()
* @param bool $allowEmptySet true - разрешить массив $values быть пустым, эта функция вернет 1=1 или 1=0
* @param bool $ints приводит значения к int
* @return mixed
*/
public function prepareIN($column, $values, $not = false, $allowEmptySet = true, $ints = null)
{
if (is_null($ints)) {
$ints = is_numeric(reset($values));
}
if (! sizeof($values)) {
if (! $allowEmptySet) {
$this->error(_t('system', 'No values specified for SQL IN comparison'), true);
} else {
return (($not) ? '1=1' : '1=0');
}
}
if (! is_array($values)) {
$values = [$values];
}
if (sizeof($values) == 1) {
@reset($values);
return $this->wrapColumn($column) . ($not ? ' <> ' : ' = ') . ($ints ? intval(current($values)) : $this->str2sql(current($values)));
} else {
if ($ints) {
$values = array_map('intval', $values);
} else {
$values = array_map([$this, 'str2sql'], $values);
}
return $this->wrapColumn($column) . ($not ? ' NOT IN ' : ' IN ') . '(' . implode(',', $values) . ')';
}
}
/**
* Выполняет множественную вставку
* @param string $table таблица
* @param array $data многомерный массив для вставки
* @param array $cryptKeys ключи параметров, требующие шифрования
* @return bool|mixed false - если запрос не выполнялся.
*/
public function multiInsert($table, array $data, array $cryptKeys = [])
{
if (empty($data)) {
return false;
}
$aResult = [];
$bind = [];
$i = 1;
foreach ($data as $data2) {
# Если массив не многомерный выполняем нормальный INSERT запрос
if (! is_array($data2)) {
return $this->insert($table, $data, false, [], $cryptKeys);
}
$aPlaceholders = [];
foreach ($data2 as $key => $var) {
$key1 = ":$key$i"; #:name1
$aPlaceholders[] = ($this->crypt && in_array($key, $cryptKeys) ? 'BFF_ENCRYPT(' . $key1 . ')' : $key1);
$bind[$key1] = [$var, $this->type($var)];
$i++;
}
$aResult[] = '(' . join(',', $aPlaceholders) . ')';
}
if (empty($aResult)) {
return false;
}
$first = reset($data);
return $this->exec(
'INSERT INTO ' . $table . ' (' . join(',', $this->wrapColumn(array_keys($first))) . ')
VALUES ' . join(', ', $aResult),
$bind
);
}
/**
* Конструктор SQL запроса к таблице
* @param string $table название таблицы
* @param string $as
* @return QueryBuilder
*/
public function query($table, $as = null)
{
$manager = $this->manager();
return $manager::table($table, $as);
}
/**
* Менеджер по работе с базой данных
* @return Manager
*/
public function manager()
{
if ($this->manager === null) {
$this->manager = Manager::init($this->pdo);
}
return $this->manager;
}
/**
* Добавление столбца в таблицу, если столбца не существует
* @param string $table имя таблицы
* @param string $name имя столбца
* @param string $definition параметры столбца
* @return bool
*/
public function columnAdd($table, $name, $definition)
{
$success = false;
do {
if (empty($table)) {
$this->error('Добавление столбца. Не указано название таблицы');
break;
}
if (empty($name)) {
$this->error('Добавление столбца. Таблица "' . $table . '", не указано название столбца');
break;
}
if (empty($definition)) {
$this->error('Добавление столбца. Таблица "' . $table . '", для столбца "' . $name . '" не указаны параметры столбца');
break;
}
if (!$this->isTable($table)) {
$this->error('Добавление столбца. Таблица "' . $table . '" не найдена, добавление столбца "' . $name . '"');
break;
}
$schema = $this->schema($table);
if (empty($schema['result'])) {
$this->error('Добавление столбца. Схема для таблицы "' . $table . '" не найдена, добавление столбца "' . $name . '"');
break;
}
foreach ($schema['result'] as $v) {
if ($v['Field'] == $name) {
# столбец с таким наванием {$name} уже существует в таблице {$table}
break 2;
}
}
$this->exec('ALTER TABLE ' . $table . ' ADD `' . $name . '` ' . $definition);
$success = true;
} while (false);
return $success;
}
/**
* Удаление столбца из таблицы, если столбец существует
* @param string $table имя таблицы
* @param string $name имя столбца
* @return bool
*/
public function columnDelete($table, $name)
{
$success = false;
do {
if (empty($table)) {
$this->error('Удаление столбца. Не указано название таблицы');
break;
}
if (empty($name)) {
$this->error('Удаление столбца. Таблица "' . $table . '", не указано название столбца');
break;
}
if (!$this->isTable($table)) {
$this->error('Удаление столбца. Таблица "' . $table . '" не найдена, удаление столбца "' . $name . '"');
break;
}
$schema = $this->schema($table);
if (empty($schema['result'])) {
$this->error('Удаление столбца. Схема для таблицы "' . $table . '" не найдена');
break;
}
foreach ($schema['result'] as $v) {
if ($v['Field'] == $name) {
$this->exec('ALTER TABLE ' . $table . ' DROP `' . $name . '`');
$success = true;
break 2;
}
}
} while (false);
return $success;
}
/**
* Сортировка записей (для js-компонента tablednd)
* @param string $table название таблицы
* @param string $sAdditionalQuery дополнительные параметры запроса
* @param string $sIDField название id-поля
* @param string $sOrderField название num-поля (поля сортировки), например 'num'
* @param bool $bTree сортировке в дереве
* @param string $sPIDField название pid-поля
* @param string $sPrefix префикс входящих данных
* @return bool
*/
public function rotateTablednd($table, $sAdditionalQuery = '', $sIDField = 'id', $sOrderField = 'num', $bTree = false, $sPIDField = 'pid', $sPrefix = 'dnd-')
{
do {
/**
* dragged - перемещаемый елемент
* target - елемент 'до' или 'после' которого, оказался перемещаемый елемент (сосед)
* position - новая позиция перемещаемого елемента относительно 'target' елемента
*/
$nDraggedID = intval(str_replace($sPrefix, '', (!empty($_POST['dragged']) ? $_POST['dragged'] : '')));
if ($nDraggedID <= 0) {
break;
}
$nNeighboorID = intval(str_replace($sPrefix, '', (!empty($_POST['target']) ? $_POST['target'] : '')));
if ($nNeighboorID <= 0) {
break;
}
$sPosition = (isset($_POST['position']) ? trim($_POST['position']) : '');
if (empty($sPosition) || !in_array($sPosition, ['after', 'before'])) {
break;
}
# сортируем
$aNeighboorData = $this->one_array("SELECT $sIDField, $sOrderField" . ($bTree ? ", $sPIDField" : '') . " FROM $table WHERE $sIDField=$nNeighboorID $sAdditionalQuery LIMIT 1");
if (!$aNeighboorData) {
return false;
}
if ($sPosition == 'before') { # before
$this->exec(
"UPDATE $table SET $sOrderField = (CASE WHEN $sIDField=$nDraggedID THEN {$aNeighboorData[$sOrderField]} ELSE $sOrderField+1 END)
WHERE ($sOrderField>={$aNeighboorData[$sOrderField]} OR $sIDField=$nDraggedID)
" . ($bTree ? " AND $sPIDField = " . $aNeighboorData[$sPIDField] : '') . " $sAdditionalQuery"
);
} else { # after
$this->exec(
"UPDATE $table SET $sOrderField = (CASE WHEN $sIDField=$nDraggedID THEN {$aNeighboorData[$sOrderField]}+1 ELSE $sOrderField+1 END)
WHERE ($sOrderField>{$aNeighboorData[$sOrderField]} OR $sIDField=$nDraggedID)
" . ($bTree ? " AND $sPIDField = " . $aNeighboorData[$sPIDField] : '') . " $sAdditionalQuery"
);
}
return true;
} while (false);
return false;
}
/**
* Сортировка записей вверх, вниз
* @param string $table название таблицы
* @param int $id ID элемента
* @param string $direction направление 'up', 'down'
* @param array $opts параметры
* @return bool
*/
public function rotateUpDown(string $table, int $id, string $direction, array $opts = []): bool
{
func::array_defaults($opts, [
'fields' => [
'id' => 'id',
'order' => 'num',
'pid' => 'pid',
],
'tree' => false,
'filter' => [],
]);
if (empty($id)) {
return false;
}
$direction = mb_strtolower($direction);
if (! in_array($direction, ['up', 'down'])) {
return false;
}
$f_id = $opts['fields']['id'];
$f_num = $opts['fields']['order'];
$f_pid = $opts['fields']['pid'];
$fields = [$f_id, $f_num];
if ($opts['tree']) {
$fields[] = $f_pid;
}
$filter = $opts['filter'];
$filter[$f_id] = $id;
$data = $this->select_row($table, $fields, $filter, '', $this->prepareLimit(0, 1));
if (empty($data)) {
return false;
}
$filter = $opts['filter'];
if ($opts['tree']) {
$filter[$f_pid] = $data[$f_pid];
}
$order = '';
if ($direction === 'up') {
$filter[$f_num] = ['<', $data[$f_num]];
$order = $opts['fields']['order'] . ' DESC ';
} elseif ($direction === 'down') {
$filter[$f_num] = ['>', $data[$f_num]];
$order = $opts['fields']['order'] . ' ASC ';
}
$neighbor = $this->select_row($table, $fields, $filter, $order, $this->prepareLimit(0, 1));
if (empty($neighbor)) {
return true;
}
$this->update($table, [$f_num => $neighbor[$f_num] ], [$f_id => $data[$f_id] ]);
$this->update($table, [$f_num => $data[$f_num] ], [$f_id => $neighbor[$f_id] ]);
return true;
}
/**
* Конвертируем список строк в древовидную структуру (дерево)
* @param array $rows Двухуровневый массив строк, полученных из базы
* @param string $idName название id-поля
* @param string $pidName название pid-поля
* @param string $childrenName название ключа для вложенных элементов
* @return array конвертируем массив (дерево).
*/
public function transformRowsToTree($rows, $idName, $pidName, $childrenName = 'childnodes')
{
if (empty($rows)) {
return $rows;
}
$children = []; # children of each ID
$ids = [];
# Collect who are children of whom.
foreach ($rows as $i => $r) {
$row =& $rows[$i];
$id = $row[$idName];
if ($id === null) {
# Rows without an ID are totally invalid and makes the result tree to
# be empty (because PARENT_ID = null means "a root of the tree"). So
# skip them totally.
continue;
}
$pid = $row[$pidName];
if ($id == $pid) {
$pid = null;
}
$children[$pid][$id] =& $row;
if (!isset($children[$id])) {
$children[$id] = [];
}
$row[$childrenName] =& $children[$id];
$ids[$id] = true;
}
# Root elements are elements with non-found PIDs.
$tree = [];
foreach ($rows as $i => $r) {
$row =& $rows[$i];
$id = $row[$idName];
$pid = $row[$pidName];
if ($pid == $id) {
$pid = null;
}
if (!isset($ids[$pid])) {
$tree[$row[$idName]] =& $row;
}
//unset($row[$idName]);
//unset($row[$pidName]);
}
return $tree;
}
/**
* Получаем ID всех parent-записей в таблице со структурой дерева id-pid
* @param string $table название таблицы
* @param int $nID ID текущей записи (для которой необходимо получить parent-записи)
* @param int $nDepth ограничитель по глубине
* @param string $sIDField название id-поля
* @param string $sPIDField название pid-поля
* @return array
*/
public function getAdjacencyListParentsID($table, $nID, $nDepth = 0, $sIDField = 'id', $sPIDField = 'pid')
{
if (!$nDepth) {
$nDepth = 20;
}
$fields = [];
$joins = [];
$where = '';
for ($i = 0; $i < $nDepth; $i++) {
# Алиасы для таблицы.
$alias = 't' . sprintf("%02d", $i);
$aliasPrev = $i > 0 ? 't' . sprintf("%02d", $i - 1) : null;
# Список полей для алиаса.
$fields[] = "$alias.$sPIDField";
# LEFT JOIN только для второй и далее таблиц!
if ($aliasPrev) {
$joins[] = "LEFT JOIN $table $alias ON ($alias.$sIDField = $aliasPrev.$sPIDField)";
} else {
$joins[] = "$table $alias";
}
# Условие поиска.
if (!$i) {
$where = "$alias.$sIDField = $nID";
}
}
$query = 'SELECT ' . join(', ', $fields) . ' FROM ' . join(' ', $joins) . ' WHERE ' . $where;
$tmp = $this->exec($query, null, 0, PDO::FETCH_NUM, 'fetch'); //one_array
$res = [];
if (!empty($tmp)) {
foreach ($tmp as $v) {
if (!empty($v)) {
$res[] = $v;
} else {
break;
}
}
}
return $res;
}
/**
* Получаем ID всех child-записей в таблице со структурой дерева id-pid
* @param string $table название таблицы
* @param int $nID ID текущей записи (для которой необходимо получить child-записи)
* @param int $nDepth ограничитель по глубине
* @param string $sIDField название id-поля
* @param string $sPIDField название pid-поля
* @return array
*/
public function getAdjacencyListChildrensID($table, $nID, $nDepth = 0, $sIDField = 'id', $sPIDField = 'pid')
{
if (!$nDepth) {
$nDepth = 20;
}
$fields = [];
$joins = [];
$where = '';
for ($i = 0; $i < $nDepth; $i++) {
# Алиасы для таблицы.
$alias = 't' . sprintf("%02d", $i);
$aliasPrev = $i > 0 ? 't' . sprintf("%02d", $i - 1) : null;
# Список полей для алиаса.
$fields[] = "$alias.$sIDField";
# LEFT JOIN только для второй и далее таблиц!
if ($aliasPrev) {
$joins[] = "LEFT JOIN $table $alias ON ($alias.$sPIDField = $aliasPrev.$sIDField)";
} else {
$joins[] = "$table $alias";
}
# Условие поиска.
if (!$i) {
$where = "$alias.$sIDField = $nID";
}
}
$query = 'SELECT ' . join(', ', $fields) . ' FROM ' . join(' ', $joins) . ' WHERE ' . $where;
$tmp = $this->exec($query, null, 0, PDO::FETCH_NUM, 'fetchAll'); //select
$res = [];
if (!empty($tmp)) {
foreach ($tmp as $t) {
foreach ($t as $v) {
if (!empty($v)) {
if ($v != $nID) {
$res[] = $v;
}
} else {
break;
}
}
}
}
return $res;
}
/**
* Формирование FULLTEXT запроса для mysql
* @param string $sQ строка поиска
* @param mixed $fields строка перечисление полей, учествующих в поиске, например 'content,title,mdescription'
* @return string
*/
public function prepareFulltextQuery($sQ, $fields = false)
{
# избавляемся от знаков * "
$sQ = str_replace('*', '', $sQ);
$sQ = str_replace('"', '', $sQ);
# избавляемся от знаков -
if (strpos($sQ, '-') !== false) {
$sQ = str_replace('-', ' ', $sQ);
$sQ = preg_replace("/\s+/", " ", $sQ); // и от двойных пробелов
$sQ = rtrim($sQ); // и от последнего пробела
}
$phrase = $sQ;
# добавляем к каждому слову *
if (strpos($sQ, ' ') !== false) {
$aWords = explode(' ', $sQ);
$sQ = '';
foreach ($aWords as $v) {
if (strlen($v) > 2) {
$sQ .= $v . '* ';
}
}
} else {
$sQ .= '*';
$phrase = '';
}
if ($fields !== false) {
return " MATCH($fields) AGAINST (" . $this->str2sql($sQ . ($phrase ? ('>"' . $phrase . '"') : '')) . " IN BOOLEAN MODE) ";
} else {
return $sQ;
}
}
/**
* Проверка ключа / Формирование ключа исходя из заголовка
* @param string $sKeyword keyword
* @param string $sTitle заголовок
* @param string $table название таблицы
* @param int|null $nExceptRecordID исключая записи (ID)
* @param string $keywordField название keyword-поля в таблице
* @param string $sIDField название id-поля в таблице
* @return string ключ
*/
public function getKeyword($sKeyword = '', $sTitle = '', $table = false, $nExceptRecordID = null, $keywordField = 'keyword', $sIDField = 'id')
{
if (empty($sKeyword) && !empty($sTitle)) {
$sKeyword = mb_strtolower(func::translit($sTitle));
}
$sKeyword = preg_replace('/[^\p{L}\w0-9_\-]/iu', '', $sKeyword);
if (empty($sKeyword)) {
bff('errors')->set(_t('', 'Enter Keyword'));
} else {
if ($table !== false) {
$foundID = $this->isKeywordExists($sKeyword, $table, $nExceptRecordID, $keywordField, $sIDField);
if (!empty($foundID)) {
bff('errors')->set(_t('', 'This keyword is already in use'));
}
}
}
return $sKeyword;
}
/**
* Проверка существования ключа
* @param string $keyword keyword
* @param string $table название таблицы
* @param int|null $exceptRecordID исключая записи (ID)
* @param string $keywordField название keyword-поля в таблице
* @param string $idField название id-поля в таблице
* @return int
*/
public function isKeywordExists($keyword, $table, $exceptRecordID = null, $keywordField = 'keyword', $idField = 'id')
{
return $this->one_data(
'SELECT ' . $idField . '
FROM ' . $table . '
WHERE
' . (!empty($exceptRecordID) ? ' ' . $idField . '!=' . intval($exceptRecordID) . ' AND ' : '') . '
' . $keywordField . ' = :key
LIMIT 1',
[':key' => $keyword]
);
}
// ------------------------------------------------------------------------
// PostgreSQL Special Methods
/**
* Рестарт последовательности (SEQUENCE) в PostgreSQL
* @param string $table название таблицы
* @param string $columnName название столбца (для которого создана последовательность)
* @param string $sequencePostfix постфикс
* @param int $nRestartWith счетчик, с которого необходимо выполнить рестарт
*/
public function pgRestartSequence($table = '', $columnName = 'id', $sequencePostfix = '_seq', $nRestartWith = 1)
{
if (empty($nRestartWith) || $nRestartWith <= 0) {
$nRestartWith = 1;
}
$this->exec('ALTER SEQUENCE "' . $table . ($columnName ? '_' . $columnName : '') . $sequencePostfix . '" RESTART WITH ' . $nRestartWith);
}
/**
* Работа с ARRAY полем в PostgreSQL
* если $mData массив - формируем строку для сохранения в базу
* если $mData строка - формируем массив
* @param string|array $mData
* @param bool $bJavascript результа необходим для дальнейшей работы в Javascript
* @return array|string
*/
public function pgArrayInt($mData, $bJavascript = false)
{
if (is_array($mData)) {
if ($bJavascript) {
return '[' . join(',', $mData) . ']';
} else {
$result = [];
foreach ($mData as $v) {
if (is_array($v)) {
$result[] = $this->pgArrayInt($v, false);
} else {
if (!is_numeric($v)) { // quote only non-numeric values
$v = '"' . str_replace('"', '\\"', $v) . '"'; // escape double quote
}
$result[] = $v;
}
}
return '{' . implode(',', $result) . '}'; // format
}
} else {
if (!is_string($mData)) {
$mData = '';
}
$mData = (!empty($mData) ? trim($mData, '{}') : '');
return ($bJavascript ? ('[' . $mData . ']')
: ($mData != '' ? explode(',', $mData) : []));
}
}
// ------------------------------------------------------------------------
// Методы для работы с языковыми таблицами (данными)
/**
* Формирование и выполнение INSERT-запроса
* @param int|array $id ID записи или массив параметров
* @param array $data данные
* @param array $fields ключи допустимых языковых полей [field => filter, ...]
* @param string $table название языковой таблицы
* @param string $aLocales список ключей локализаций или false - все доступные
* @return bool|int
*/
public function langInsert($id, $data, $fields, $table, $aLocales = [])
{
if (empty($fields)) {
return false;
}
# Mark as nested arrays
foreach ($fields as $key => $filter) {
if (
(is_int($filter) && $filter >= TYPE_CONVERT_SINGLE) ||
(is_array($filter) && is_int(reset($filter)) && reset($filter) >= TYPE_CONVERT_SINGLE)
) {
$fields[$key] = true;
}
}
if (! array_intersect_key($data, $fields)) {
return false;
}
$res = 0;
$record = (is_array($id) ? $id : ['id' => $id]);
$aLocales = (empty($aLocales) ? bff::locale()->getLanguages() : $aLocales);
foreach ($aLocales as $lang) {
$insert = $record;
$insert['lang'] = $lang;
foreach ($fields as $key => $filter) {
if ($filter === true) {
$insert[$key] = [];
if (isset($data[$key]) && is_array($data[$key])) {
foreach ($data[$key] as $subKey => $value) {
$insert[$key][$subKey] = (is_array($value) ? $value[$lang] ?? '' : $value);
}
}
$insert[$key] = json_encode($insert[$key]);
} else {
$insert[$key] = $data[$key][$lang] ?? '';
}
}
if ($this->insert($table, $insert, false)) {
$res++;
}
}
return $res;
}
/**
* Формирование и выполнение UPDATE-запроса
* @param int|array $id ID записи или массив параметров
* @param array $data данные
* @param array $fields ключи допустимых языковых полей [field => filter, ...]
* @param string $table название языковой таблицы
* @return bool
*/
public function langUpdate($id, $data, $fields, $table)
{
if (empty($fields)) {
return false;
}
# Mark as nested arrays
foreach ($fields as $key => $filter) {
if (
(is_int($filter) && $filter >= TYPE_CONVERT_SINGLE) ||
(is_array($filter) && is_int(reset($filter)) && reset($filter) >= TYPE_CONVERT_SINGLE)
) {
$fields[$key] = true;
}
}
if (! array_intersect_key($data, $fields)) {
return false;
}
$newLocales = [];
$record = (is_array($id) ? $id : ['id' => $id]);
foreach (bff::locale()->getLanguages() as $lang) {
$update = [];
foreach ($fields as $key => $filter) {
if ($filter === true) {
$update[$key] = [];
if (isset($data[$key]) && is_array($data[$key])) {
foreach ($data[$key] as $subKey => $value) {
$update[$key][$subKey] = (is_array($value) ? $value[$lang] ?? '' : $value);
}
}
$update[$key] = json_encode($update[$key]);
} else {
$update[$key] = $data[$key][$lang] ?? '';
}
}
if (! empty($update)) {
$record['lang'] = $lang;
$res = $this->update($table, $update, $record);
if (empty($res)) {
$newLocales[] = $lang;
}
}
}
if (! empty($newLocales)) {
$this->langInsert($id, $data, $fields, $table, $newLocales);
}
return true;
}
/**
* Получение языковых данных (дополняем ими параметр $data)
* @param int|array $filter ID записи или массив параметров
* @param array $data @ref данные
* @param array $fields ключи допустимых языковых полей [field => filter, ...]
* @param string $table название языковой таблицы
* @param string|null $lang только данные для указанного языка
* @return void
*/
public function langSelect($filter, &$data, $fields, $table, ?string $lang = null)
{
if (empty($fields)) {
return;
}
$defaults = array_fill_keys(bff::locale()->getLanguages(), '');
foreach ($fields as $key => $fieldType) {
if (
(is_int($fieldType) && $fieldType >= TYPE_CONVERT_SINGLE) ||
(is_array($fieldType) && is_int(reset($fieldType)) && reset($fieldType) >= TYPE_CONVERT_SINGLE)
) {
$fields[$key] = true; # mark as nested array
$data[$key] = [];
} else {
$data[$key] = $defaults;
}
}
if (! is_array($filter)) {
$filter = ['id' => $filter];
}
if ($lang) {
$filter['lang'] = $lang;
}
$rows = $this->select_rows($table, array_merge(array_keys($fields), ['lang']), $filter);
foreach ($rows as $row) {
foreach ($fields as $key => $fieldType) {
if ($fieldType === true) {
$subData = json_decode($row[$key], true);
if (is_array($subData)) {
foreach ($subData as $subKey => $subValue) {
if ($lang) {
$data[$key][$subKey] = $subValue;
} else {
if (! isset($data[$key][$subKey])) {
$data[$key][$subKey] = $defaults;
}
$data[$key][$subKey][$row['lang']] = $subValue;
}
}
}
} else {
if ($lang) {
$data[$key] = $row[$key];
} else {
$data[$key][$row['lang']] = $row[$key];
}
}
}
}
}
/**
* Формирование "AND связки" с языковой таблицей
* @param bool $and добавлять "AND"
* @param string $tablePrefix префикс таблицы с которой связываем
* @param string $langPrefix префикс языковой таблицы
* @param mixed $lang keyword языка или false (текущий)
* @return string
*/
public function langAnd($and = true, $tablePrefix = 'I', $langPrefix = 'L', $lang = false)
{
return ($and ? ' AND ' : '') . $tablePrefix . '.id = ' . $langPrefix . '.id AND ' . $langPrefix . '.lang = ' . $this->pdo->quote(($lang === false ? bff::locale()->current() : $lang)) . ' ';
}
/**
* Формируем bind языковых данных для UPDATE/INSERT запроса
* В случае если языковые данные находятся в той же таблице что и основные данные
* @param array $data данные
* @param array $keys ключи допустимых языковых полей
* @param array $bind @ref результат формирования
* @return array
*/
public function langFieldsModify($data, array $keys, &$bind)
{
if (empty($data) || empty($keys)) {
return [];
}
$languages = bff::locale()->getLanguages();
$unbind = ($data === $bind);
foreach ($keys as $key => $type) {
if (isset($data[$key])) {
foreach ($languages as $lng) {
if (isset($data[$key][$lng])) {
$bind[$key . '_' . $lng] = ($type == TYPE_ARRAY || $type >= TYPE_CONVERT_SINGLE ? serialize($data[$key][$lng]) : $data[$key][$lng]);
}
}
# если $bind является ссылкой на $data
if ($unbind && isset($bind[$key])) {
unset($bind[$key]);
}
}
}
return $bind;
}
/**
* Преобразование языковых данных в массиве $data
* В случае если языковые данные находятся в той же таблице что и основные данные
* @param array $data @ref
* @param array $keys ключи c языковыми данными
* @return void
*/
public function langFieldsSelect(&$data, $keys)
{
if (empty($keys)) {
return;
}
if (empty($data)) {
$aLocales = array_fill_keys(bff::locale()->getLanguages(), '');
foreach ($keys as $key => $type) {
$data[$key] = $aLocales;
}
return;
}
foreach (bff::locale()->getLanguages() as $lng) {
foreach ($keys as $key => $type) {
$key2 = $key . '_' . $lng;
if (isset($data[$key2])) {
$data[$key][$lng] = ($type == TYPE_ARRAY || $type >= TYPE_CONVERT_SINGLE ? unserialize($data[$key2]) : $data[$key2]);
unset($data[$key2]);
} else {
$data[$key][$lng] = '';
}
}
}
}
// ------------------------------------------------------------------------
//PDO
public function getPDO()
{
return $this->pdo;
}
public function getPersistent()
{
return $this->pdo->getAttribute(PDO::ATTR_PERSISTENT);
}
public function setPersistent($value)
{
return $this->setAttribute(PDO::ATTR_PERSISTENT, $value);
}
/**
* Получаем имя текущего драйвера работы с базой
* @return string
*/
public function getDriverName()
{
return mb_strtolower($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME));
}
/**
* Является ли текущей база mysql
* @return bool
*/
public function isMySQL()
{
// mysql, mysqli
return (strpos($this->getDriverName(), 'mysql') == 0);
}
/**
* Является ли текущей база pgsql
* @return bool
*/
public function isPgSQL()
{
return ($this->getDriverName() == 'pgsql');
}
/**
* Устанавливаем PDO атрибут
* @param mixed $name ключ атрибута
* @param mixed $value значение
* @return bool
*/
public function setAttribute($name, $value)
{
if ($this->pdo instanceof PDO) {
return $this->pdo->setAttribute($name, $value);
}
$this->attr[$name] = $value;
return true;
}
/**
* Запуск миграций
* @param string $action 'migrate', 'rollback', 'seed', 'status'
* @param string $options ['configuration' => '', 'environment' => 'production', 'target', 'name', 'seed', ...]
* @param bool|string $output выводить результат на экран (при вызове из консоли)
* @return string
*/
public function migrations($action, array $options = [], $output = false)
{
$configDefault = bff()->corePath('db/migrations/config.php');
# array => file
if (isset($options['configuration']) && is_array($options['configuration'])) {
$configDefaultData = require $configDefault;
$config = config::prefixed('db');
$configDB = [
'adapter' => $config['type'],
'host' => $config['host'],
'name' => $config['name'],
'user' => $config['user'],
'pass' => $config['pass'],
'port' => $config['port'],
'charset' => $config['charset'],
];
$configDefaultData['environments']['production'] = $configDB;
$configDefaultData['environments']['development'] = $configDB;
$configFileName = Files::tempFile(false, 'dbmgrt');
file_put_contents($configFileName, ' $options['configuration'], '-p' => 'php',
'name' => $name,
];
} break;
case 'migrate': {
$command = [
$action,
'-e' => $options['environment'],
'-c' => $options['configuration'], '-p' => 'php',
];
if ($target) {
$command['-t'] = $target;
}
} break;
case 'rollback': {
$command = [
$action,
'-e' => $options['environment'],
'-c' => $options['configuration'], '-p' => 'php',
];
if (isset($target)) {
$command['-t'] = $target;
}
} break;
case 'seed': {
$command = [
$action,
'-e' => $options['environment'],
'-c' => $options['configuration'], '-p' => 'php',
];
if ($target) {
$command['-t'] = $target;
}
$seed = (isset($options['seed']) ? $options['seed'] : null);
if ($seed) {
$command ['-s'] = (array)$seed;
}
} break;
case 'seed-create': {
$name = (isset($options['name']) ? ucfirst($options['name']) : '?');
$command = [
'seed:create',
'-c' => $options['configuration'], '-p' => 'php',
'name' => $name,
];
} break;
case 'status': {
$command = [
$action,
'-e' => $options['environment'],
'-c' => $options['configuration'], '-p' => 'php',
'-f' => 'json',
];
} break;
default: {
$result = false;
} break;
}
if ($command) {
try {
if ($output === true) {
$phinx = new \Phinx\Console\PhinxApplication();
$phinx->run(
new \Symfony\Component\Console\Input\ArrayInput($command)
);
} else {
if (is_string($output) && !empty($output)) {
$streamFile = $output;
$streamFileKeep = true;
} else {
$streamFile = Files::tempFile(false, 'dbmgrt');
}
$stream = fopen($streamFile, 'w+');
$phinx = new \Phinx\Console\PhinxApplication();
$phinx->doRun(
new \Symfony\Component\Console\Input\ArrayInput($command),
new \Symfony\Component\Console\Output\StreamOutput($stream)
);
$result = stream_get_contents($stream, -1, 0);
}
} catch (Throwable $e) {
$result = $e->getMessage();
}
if (isset($stream)) {
fclose($stream);
if (! isset($streamFileKeep)) {
unlink($streamFile);
}
}
}
if (isset($configFileName)) {
unlink($configFileName);
}
return $result;
}
}