[], '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; } }