§ Программный код

Редакция: 5 мар 2024
use mysqli;

class DatabaseConnector {

    /** @var mysqli $db */
    protected $db;

    protected $db_host;
    protected $db_user;
    protected $db_pass;
    protected $db_name;
    protected $db_port;
    protected $db_codepage;

    // ================ БАЗОВЫЕ ============================

    /**
     * db constructor.
     * @param mixed $cdb
     * @param string $codepage
     */
    public function __construct($cdb, string $codepage = 'utf8') {

        $this->db_port = null;

        // Коннект по массиву
        if (is_array($cdb)) {

            $this->db_host = $cdb['host'] ?? '';
            $this->db_user = $cdb['username'] ?? '';
            $this->db_pass = $cdb['password'] ?? '';
            $this->db_name = $cdb['database'] ?? '';
            $codepage      = $cdb['charset'] ?? $codepage;
        }
        // Коннект по строке
        else if (preg_match('~-h\s+([^\s+]+)\s+-u\s+([^\s+]+)\s+-p([^\s+]*)\s+([^\s+]+)~', $cdb, $c)) {
            list(, $this->db_host, $this->db_user, $this->db_pass, $this->db_name) = $c;
        }
        // SphinxConnect: 127.0.0.1:9
        else if (preg_match('~^([0-9.]+):(\d+)$~', $cdb, $c)) {

            $this->db_host = $c[1];
            $this->db_user = '';
            $this->db_pass = '';
            $this->db_name = '';
            $this->db_port = $c[2];
        }
        else {
            die('['.date('Y-m-d H:i:s')." Invalid MySQL connection string\n");
        }

        $this->db_codepage = $codepage;
    }

    // Подключиться к БД
    public function connect() {

        if (empty($this->db)) {

            $err_cstr = "$this->db_host:$this->db_user:$this->db_name";
            try {
                $this->db = new mysqli($this->db_host, $this->db_user, $this->db_pass, $this->db_name, $this->db_port);
            } catch (\Exception $e) {
                die("CONNECT ERROR: " . $e->getMessage() . " [$err_cstr]");
            }

            if ($this->db->connect_error) {
                die('['.date('Y-m-d H:i:s')." Can't connect to Db... [$err_cstr]\n");
            }

            $this->db->query("SET NAMES $this->db_codepage");
        }
    }

    // Отключиться от БД
    public function close() {

        if ($this->db) {
            $this->db->close();
            $this->db = null;
        }
    }

    // Переключить БД
    public function use($dbname) {

        $this->connect();
        $this->query("USE $dbname");
        return $this;
    }

    // Выполнить запрос $sql
    public function query($sql, $param = "") {

        $this->connect();
        $q = $this->db->query($sql);

        if ($this->db->errno) {
            throw new \Exception("Error in mysql: " . $this->db->error . " [$sql]");
        }

        $param = explode(':', $param);

        // Выдать все запрошенные строки
        if ($param[0] == 'rows') {

            $rows    = [];
            $keyname = $param[1] ?? "";
            foreach ($q as $item) $rows[] = $keyname ? $item[$keyname] : $item;
            return $rows;
        }

        return $q;
    }

    /** @return mixed */
    public function affected_rows() {
        return $this->db->affected_rows;
    }

    /** @return mysqli */
    public function db() {
        return $this->db;
    }

    // Обезопасить строку
    public function escape($s) {

        $this->connect();

        if (is_null($s))
            return 'NULL';
        else if (is_integer($s) || is_float($s) || is_double($s))
            return $s;
        else if (is_object($s) || is_array($s))
            return "'".$this->db->real_escape_string(json_encode($s, JSON_UNESCAPED_SLASHES))."'";
        else
            return "'".$this->db->real_escape_string($s)."'";
    }

    // Обезопасить массив входящих данных
    public function escape_arr(array $data) {

        foreach ($data as $id => $value)
            $data[$id] = $this->escape($value);

        return $data;
    }

    // Запросы WHERE
    // [f1=> ..., f2 => ...] Одиночный запрос
    // [[f1 => ..., f2 => ...], .... ] Множественный запрос
    protected function where($where) {

        $arr = [];
        foreach ($where as $k => $v)
            if (is_array($v))
                foreach ($v as $a => $b) $arr[$k][] = "$a = ".$this->escape($b);
            else
                $arr[0][] = "$k = ".$this->escape($v);

        return join(" OR ", array_map(function ($a) { return '(' . join(" AND ", $a) . ')'; }, $arr));
    }

    // =============================== ОДИНОЧНЫЕ ЗАПРОСЫ ===============================

    // Выбор из таблицы по заданным полям
    public function select($table, $where) {

        $param = explode(':', $table);
        $table = $param[0];
        $field = $param[1] ?? '*';

        return $this->query("SELECT $field FROM $table WHERE " . $this->where($where));
    }

    // Вставить новую строку $data в $table
    public function insert($table, array $data, $ignore = 'IGNORE') {

        $data = $this->escape_arr($data);
        $sql  = "INSERT $ignore INTO $table (`".join("`, `", array_keys($data))."`) ";
        $sql .= "VALUES (".join(", ", array_values($data)).")";
        $this->query($sql);

        return (int) $this->db->insert_id;
    }

    // Обновить данные $data в $where
    public function update($table, array $data, array $where) {

        $sets = [];
        foreach ($this->escape_arr($data) as $id => $value) $sets[] = "`$id` = $value";
        $this->query("UPDATE $table SET ".join(', ', $sets)." WHERE " . $this->where($where));
    }

    // =============================== ГРУППОВЫЕ ЗАПРОСЫ ===============================

    // Групповой запрос к таблице по ключу, $table:[$key=id]:[$fields=*]]
    // Если *key, то получается ассоциативный массив по ключу
    public function wherein($tablekey, $arr, $where = '') {

        $rows   = [];
        $r      = explode(':', $tablekey);
        $table  = $r[0];
        $key    = ($r[1] ?? '') ?: 'id';
        $field  = ($r[2] ?? '') ?: '*';

        if ($arr = $this->escape_arr(array_unique($arr))) {

            $ext = $key[0];
            $key = ltrim($key, '*');
            $sql = "SELECT $field FROM $table WHERE $key IN (".join(',', $arr).")" . ($where ? " AND $where" : "");
            foreach ($this->query($sql) as $row) {
                if ($ext == '*') {
                    $kval = $row[$key]; unset($row[$key]); $rows[$kval] = $row;
                } else {
                    $rows[] = $row;
                }
            }
        }

        return $rows;
    }

    // Пакетное удаление строк, table[:keyname=id]
    public function deletein($tablekey, $arr) {

        $r      = explode(':', $tablekey);
        $table  = $r[0];
        $key    = ($r[1] ?? '') ?: 'id';

        $this->query("DELETE FROM $table WHERE $key IN (".join(',', $this->escape_arr($arr)).")");
        return $this->db->affected_rows;
    }

    // Добавление пачки строк за один запрос
    // У всех строк ДОЛЖЕН БЫТЬ один формат полей, и идти в одном порядке
    // Если задан $names, то порядок имен идет оттуда
    public function batch_insert($table, $rows, $names = []) {

        if (empty($rows)) return 0;

        $names = $names ?: array_keys(current($rows));
        $sql = "INSERT IGNORE INTO $table (`".join("`,`", $names)."`) VALUES ";
        foreach ($rows as $id => $row) {
            $rows[$id] = is_array($row) ? '('.join(",", array_values($this->escape_arr($row))).')' : '('.$this->escape($row).')';
        }
        $this->query($sql . join(',', $rows));
        return $this->db->affected_rows;
    }

    // Обновление пачкой $table = "tablename[:id:[pluck]]": $rows[key] => data
    public function batch_update($table, $rows, $only_sql = false) {

        if (empty($rows)) return 0;

        $param = explode(':', $table);
        $table = $param[0];
        $key   = ($param[1] ?? '') ?: 'id';
        $pluck = ($param[2] ?? '') ?: '';

        $sets   = [];
        $keys   = [];
        $update = [];

        foreach ($rows as $id => $row) {

            // Обновление только одного заданного поля
            if ($pluck) $row = [$pluck => $row];

            $keys[] = ($id = $this->escape($id));
            foreach ($this->escape_arr($row) as $name => $v)
                $update[$name][$id] = $v;
        }

        foreach ($update as $name => $values) {

            $sets[$name] = "$name = CASE\n";
            foreach ($values as $id => $value) $sets[$name] .= " WHEN $key = $id THEN $value\n";
            $sets[$name] .= "ELSE $name END";
        }

        $sql = "UPDATE $table SET " . join(', ', $sets) . " WHERE $key IN (".join(',', $keys).")";
        if ($only_sql) {
            return $sql;
        } else {
            $this->query($sql);
            return (int) $this->db->affected_rows;
        }
    }

    // =============================== РАСШИРЕННЫЕ ===============================

    // Извлечение данных порциями
    // Пример использования foreach ($db->rows('tablename:[id=имя ключа]:[group_cnt=кол-во в группе выдачи]') as $row)
    public function chunk($table, $pt = null) {

        $request = explode(':', $table);
        $table   = $request[0];                     // Рабочая таблица
        $keyname = ($request[1] ?? '') ?: 'id';     // Ключ
        $grpcnt  = ($request[2] ?? '') ?: 0;        // Вместо одной строки, выдать группу из N строк

        // FIELD задает поля, которые выводятся по умолчанию все; JOIN подцепляет запросы
        $arr     = is_array($pt);
        $field   = $arr && isset($pt[0])   ? $pt[0] : '*';
        $where   = $arr && isset($pt['w']) ? $pt['w'] : (is_string($pt) ? $pt : '');
        $join    = $arr && isset($pt['j']) ? $pt['j'] : '';
        $steps   = $arr && isset($pt['s']) ? $pt['s'] : 5000;

        $gkeyname = $keyname;
        if ($grpcnt && preg_match('~\.(\w+)$~', $keyname, $c)) $gkeyname = $c[1];

        list($min, $max) = $this->query("SELECT MIN($keyname), MAX($keyname) FROM $table")->fetch_row();

        $step = (int)(($max - $min) / $steps);
        $step = max($step, 100);
        $rows = [];

        for ($id = $min; $id < $max; $id += $step) {

            $sql = "SELECT $field FROM $table $join ".
                "WHERE ($keyname >= $id AND $keyname < $id + $step)" . ($where ? " AND $where" : "");

            foreach ($this->query($sql) as $row) {
                if ($grpcnt) {
                    $rows[$row[$gkeyname]] = $row;
                    if (count($rows) >= $grpcnt) {
                        yield $rows;
                        $rows = [];
                    }
                } else {
                    yield $row;
                }
            }
        }

        if ($grpcnt && $rows) yield $rows;
    }

    // Извлечение из arr только необходимого поля
    public function pluck($rows, $name, $intval = false) {

        foreach ($rows as $id => $row) {
            $value = $row[$name] ?? null;
            $rows[$id] = $intval ? (int) $value : $value;
        }

        return $rows;
    }

    // Список таблиц в БД
    // Список таблиц в БД
    public function tables($filter = '') {

        $rows = [];
        foreach ($this->query("SELECT table_name, data_length + index_length AS size FROM information_schema.TABLES WHERE table_schema = '$this->db_name' ORDER BY `table_name` ASC") as $row) {
            $tb = $row['table_name'];
            if (!$filter || preg_match($filter, $tb))
                $rows[$tb] = $row['size'];
        }
        return $rows;
    }

    // Минимальное и максимальное значение поля
    public function minmax($table, $key = 'id') {
        return $this->query("SELECT MIN($key), MAX($key), COUNT(*) FROM $table")->fetch_row();
    }

    // Подсчет количества в таблице
    public function count($table, $where = '') {
        return $this->query("SELECT COUNT(*) FROM $table".($where ? " WHERE $where" : ""))->fetch_row()[0];
    }

    // Сокращеннаая запись
    public function kk($rows) { return join(',', $this->escape_arr(array_keys($rows))); }
    public function kv($rows) { return join(',', $this->escape_arr($rows)); }

    // =============================== HELPERS ===============================

    // Забронировать следующую порцию очереди в таблице
    // Формат запроса table:[id=key-name-field]:[status=status-field-name]; $quantity = количество элементов в одном треде
    public function reserve_queue($table, $thread, $quantity = 250) {

        $basic = explode(':', $table);
        $table = $basic[0];
        $field = ($basic[1] ?? '') ?: 'id';
        $key   = ($basic[2] ?? '') ?: 'status';
        $sql   = "SELECT $field FROM $table WHERE $key = -$thread";

        // Одиночный запрос
        if ($thread < 0) return [-$thread];

        if ($rows = $this->query($sql, "rows:$field")) {
            return $rows;
        } else {
            $this->query("UPDATE $table SET $key = -$thread WHERE $key = 0 LIMIT $quantity");
            return $this->query($sql, "rows:$field");
        }
    }

    // Получение статистики по `status`
    public function status_group($tablename, $status_field = 'status') {

        $rows = [];
        foreach ($this->query("SELECT COUNT(*) g, `$status_field` FROM $tablename GROUP BY `$status_field`") as $row) {
            $rows[$row[$status_field]] = (int) $row['g'];
        }
        return $rows;
    }
}