Эволюция DBAPI

В новых версиях MODX Evolution ожидается существенная переделка ядра. Ключевым улучшением станет полноценное внедрение паттерна Dependency Injection. Ну, а как этим пользоваться, я покажу на примере DBAPI. Но для начала расскажу с чего все начиналось, чтобы было понятно зачем…

Начало

Один из двух самых значимых классов для MODX Evolution это DBAPI. Для тех, кто в танке, этот класс нужен для работы с базой данных. Так вот, этот самый класс DBAPI немного хардкорного синтаксического сахара для mysqli. В нем определены методы, как select/update/insert/save/delete/etc… Видя подобные названия знающие люди сразу начинают думать в сторону ORM или ActiveRecord. Но не тут то было. Эти методы не имеют ничего общего с вышеназванными паттернами. Более того, методы работают с параметрами насколько по разному, что зачастую не совсем понятно какой же в итоге SQL запрос получится.

Давайте на примере
->select('id', 'site_content', array('id > 10', 'pagetitle = "test"'));
//SELECT id FROM site_content WHERE id > 10 pagetitle = "test"

По логике, тут не хватает AND. Но нет, параметр where делает просто implode массива с пробелом. Хорошо. Запомнили. Смотрим следующий метод — update
->update(array('pagetitle'=> 'new'), 'site_content', array('id > 10', 'AND pagetitle = "test"'));

А тут уже нельзя использовать массив в аргументе where. НО! У нас есть метод save который так же может принимать параметр where и пробрасывать его в select запрос или update в зависимости от результатов.
->save(array('pagetitle'=> 'new'), 'site_content', array('id > 10', 'AND pagetitle = "test"'));

Если в базе не окажется документов с ID > 10 и pagetitle = «test», то будет вызван метод insert и как следствие, добавится новая запись у которой pagetitle будет установлен в значение new. В противном случае мы получим ошибку
trim() expects parameter 1 to be string, array given in


Помимо этого мы имеем кучу старевшего кода, в стиле _getFromStringFromArray, который ничем не отличается от
implode(' ', $tables)


Эксперимент
Исходя из вышеперечисленного захотелось попробовать немного причесать код и проверить — получился ли в результате сохранить обратную совместимость. Я взял класс DBAPI разрезал на части. После чего привел вызовы к единому стилю и тут стало очевидно, что завязка на mysqli не такая уж и жесткая. А это значит, что мы можем теоретически использовать pdo драйвер и скрестить MODX с любой базой данных.

Так, получилось самодостаточное решение, которое идеально подойдет для тех, кто не умеет работать с базой вне MODX. Покрыв код тестами почти на 90% пришла в голову мысль не изобретать велосипед, а прокинуть Illuminate\Database от Laravel в качестве альтернативы для mysqli драйвера. Вы не поверите, но получилось добиться почти полной совместимости (за исключением метода dataSeek, но ни в ядре, ни в компонентах этот метод не используется).

Что получилось
— Полная совместимость пакета со стандартным DBAPI классом
— Возможность использовать привычный синтаксис DBAPI вне MODX
— Поддержка полноценной ORM Eloquent для плавного переноса сайта на Laravel. Либо, как альтернатива поделке MODxAPI, которая входит в состав DocLister

6 комментариев

avatar
А это значит, что мы можем теоретически использовать pdo драйвер и скрестить MODX с любой базой данных.
Я ставил эксперимент с PDO, и упёрся в то что не получается escape функцию сделать такой же как она есть в MySQLi. Потому что PDO::quote всё оборачивает в кавычки. А в ядре где-то было такое, что число обернули в кавычки, а потом кинули в $modx->db->escape. Как итог всё падало.
avatar
Код покрыт тестами с проверкой ответа от mysqli и pdo. По итогу полная совместимость. В том числе и на escape.
Но вообще, PDO действительно экскейпит по другому. Поэтому пришлось сделать такой хак для обратной совместимости. Но замечу, что этот хак работает только в случае если вы пытаетесь использовать PDO в режиме DBAPI. Для Eloquent стиля все работает как и задумано в Laravel.
Комментарий отредактирован 2018-06-16 11:11:27 пользователем Agel_Nash
avatar
Понятно, что если делать через Eloquent, то там и escape в целом не очень то нужен, вроде как PDO сам этим займётся.
Мне 64j , тоже посоветовал в случае чего вырезать эти кавычки. Но я что-то забросил идею с PDO.
avatar
Я не работал в больших проектах. Всё по мелочам. Поэтому моё мнение может быть ошибочным. Но когда начинал изучать Lavarel или Yii или еще какой фреймворк часто казалось что в них работа с базой данных (ActiveRecord) как-то переусложнена. DBAPI мне нравиться, шлеп шлеп и в продакшен. Но раз есть обратная совместимость то всё круто. Можно будет пробовать Eloquent, возможно и я пойму преимущество такого подхода :) Спасибо за проделанную работу.
avatar
На сайтах шлеп-шлеп это и не нужно. Разве только ради самообразования.
Данное решение ориентировано в первую очередь на разработчиков дополнений.

А преимущества — чистота кода, стабильность и предсказуемость работы.
avatar
Нашел парочку небольших багов, о которых сразу не подумал. Фиксятся легко хаком через расширение класса.
1) Свойство $config не доступно для внешних обращений
2) При возникновении ошибки SQL запроса сайт падает из-за Exception. Это и хорошо и плохо. Хорошо, т.к. теперь можно оформить красивую страничку 500 ошибки. Плохо — по умолчанию не обрабатывается стандартными средствами движка. Как исправить смотрите ниже метод query().
3) Метод insert приводит параметр $fields по таким же правилам, как и в других методах. Соответственно из подобных запросов
$modx->getDatabase()->insert(
        array(
                'name'=>'',
                'description'=>'',
                'disabled'=>'',
                'moduleguid'=>'',
                'plugincode'=>'',
                'properties'=>'',
                'category'=>'',
                ), $modx->getDatabase()->getFullTableName('site_plugins'), // Insert into
        "CONCAT(name, ' {$_lang['duplicated_el_suffix']}{$count}') AS name, description, '1' AS disabled, moduleguid, plugincode, properties, category", $modx->getDatabase()->getFullTableName('site_plugins'), "id='{$id}'");


Мы получаем запрос на вставку данных с пустыми именами полей. Лечится так же подменой insertFrom метода.
Итого получается следующий класс
<?php namespace App;

use AgelxNash\Modx\Evo\Database\LegacyDatabase;
use AgelxNash\Modx\Evo\Database\Exceptions;

class Database extends LegacyDatabase
{
    public $config;

    public function query($sql)
    {
        try {
            return parent::query($sql);
        } catch (Exceptions\QueryException $exception) {
            $core = evolutionCMS();
            /**
             * Ошибка запроса лежит тут $exception->getMessage()
             * Код ошибки $exception->getCode()
             * Сам запрос $exception->getQuery()
             * Вместо $core->messageQuit оформляем свою страницу и делаем die();
             */
            $core->messageQuit($exception->getMessage());
        } catch (Exceptions\ConnectException $exception) {
            $core = evolutionCMS();
            /**
             * Ошибка подключения к базе. Текст ошибки $exception->getMessage()
             * Код ошибки $exception->getCode()
             * Вместо $core->messageQuit оформляем свою страницу и делаем die();
             */
            $core->messageQuit($exception->getMessage());
        } catch (\Exception $exception) {
            $core = evolutionCMS();
            $core->messageQuit($exception->getMessage());
        }
    }

    public function insertFrom(
        $fields,
        $table,
        $fromFields = '*',
        $fromTable = '',
        $where = '',
        $limit = ''
    ) {
        if (is_array($fields)) {
            $onlyKeys = true;
            foreach ($fields as $key => $value) {
                if (!empty($value)) {
                    $onlyKeys = false;
                    break;
                }
            }
            if ($onlyKeys) {
                $fields = array_keys($fields);
            }
        }

        return parent::insertFrom($fields, $table, $fromFields, $fromTable, $where, $limit);
    }
}


Именно App\Database::class нужно будет указать вместо AgelxNash\Modx\Evo\Database\LegacyDatabase::class при подмене
Комментарий отредактирован 2018-06-19 09:49:35 пользователем Agel_Nash
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.