После статьи с описанием базовых интерфейсов для работы с БД было достаточно комментариев с предложением более высокоуровневых инструментов для работы. В CleverStyle Framework есть подобного рода инструменты в виде трейтов cs\CRUD и cs\CRUD_helpers. Вместе они позволяют для достаточно типичных ситуаций заменить большую простыню шаблонного кода на один вызов функции. О том, что это такое, и какой набор задач позволяет решить и будет эта статья.


cs\CRUD — основы


Этот трейт имеет 4 основные метода для использования: create(), read(), update() и delete(). Что с ними делать понятно без комментариев, формат вызова следующий:


->create($arguments : mixed[])
->create(...$arguments : mixed[])

->read($id : int|int[]|string|string[])

->update($arguments : mixed[])
->update(...$arguments : mixed[])

->delete($id : int|int[]|string|string[])

Читать и удалять можно как одиночные элементы, так и массивы элементов. При создании и обновлении элементов можно использовать ряд аргументов либо один аргумент в виде массива (можно как индексированные, так и ассоциативные с произвольным порядком ключей). Ещё один нюанс при создании элементов: если количество аргументов (ключей массива) соответствует количеству элементов в модели таблицы, то значит идентификатор задан явно, если же на 1 меньше то идентификатор будет автоматически сгенерирован средствами БД.


Для того, чтобы всё это общалось с БД нужно определить абстрактный метод cdb(), который возвращает идентификатор БД, а так же свойства $table с именем таблицы и $data_model с описанием структуры таблицы (и связанных таблиц, если есть такие).


Опционально могут быть определены свойства $data_model_ml_group и $data_model_files_tag_prefix для поддержки многоязычности и загружаемых файлов соответственно.


$table


Название основной таблицы (так же используется как префикс для связанных таблиц), например:


    protected $table = '[prefix]shop_items';

$data_model


Проще всего описать примером:


    protected $data_model = [
        'id'         => 'int',
        'date'       => 'int',
        'category'   => 'int',
        'price'      => 'float',
        'in_stock'   => 'int',
        'listed'     => 'int:0..1',
        'attributes' => [
            'data_model' => [
                'id'            => 'int',
                'attribute'     => 'int',
                'numeric_value' => 'float',
                'string_value'  => 'text',
                'text_value'    => 'html'
            ]
        ],
        'images'     => [
            'data_model' => [
                'id'    => 'int',
                'image' => 'text'
            ]
        ],
        'videos'     => [
            'data_model' => [
                'id'     => 'int',
                'video'  => 'text',
                'poster' => 'text',
                'type'   => 'text'
            ]
        ],
        'tags'       => [
            'data_model'     => [
                'id'  => 'int',
                'tag' => 'html'
            ],
            'language_field' => 'lang'
        ]
    ];

Среди поддерживаемых типов есть числа (приводятся к числам при чтении из MySQL/PostgreSQL) с ограничением допустимого диапазона, строки с автоматической чисткой от XSS и обрезкой при превышении заданной длинны, JSON (при записи сериализируется, при чтении десериализируется обратно), а так же некоторые другие плюшки, подробнее про типы поддерживаемых полей можно почитать в документации.


Когда вместо типа указывается массив — значит мы имеем дело со связанными таблицами.


Связанные таблицы


Это такие таблицы, которые содержат вспомогательные данные со связями один к одному или один ко многим. В примере выше у нас основная таблица [prefix]shop_items, а связанная attributes в БД представлена как [prefix]shop_items_attributes.


Связанная таблица описывается во вложенном ключе data_model, так же опционально может быть ключ language_field, который указывает что связанные данные зависят от языка (само поле в модели не указывается), соответственно, чтение/обновление данных нужно делать с учетом текущего языка.


Пример вставки в такой конфигурации:


$id = $this->create(
    date(),
    $category,
    $price,
    $in_stock,
    $listed,
    [
        [$attr1_id, 1, '', ''], // Здесь id не указывается, он такой же как у будущего созданного элемента
        [$attr2_id, 2, '', '']  // (автоматически сгенерированный, ведь мы его явно не указывали)
    ],
    [
        'http://example.com/pic1.jpg', // Поле кроме id только одно, так что можем не заморачиваться с массивами
        'http://example.com/pic2.jpg'
    ],
    [], // Можем ничего не указывать, если нужно
    [
        'tag1', // Многоязычность обеспечивается автоматически, не нужно указывать язык явно в поле `lang`
        'tag2', // (подробнее об этом ниже)
        'tag3'
    ]
);

Можно т��лько представить, сколько нужно сделать запросов в БД, чтобы провернуть такую функциональность.
При чтении данные будут преобразованы обратно в такой же формат, то есть можно делать $this->update($changes + $this->read($id)).


Многоязычность контента в CleverStyle Framework


Перед тем, как подробнее описать устройство многоязычности контента в cs\CRUD нужно описать как это обычно решается во фреймворке в общем виде.


Для обеспечения многоязычности контента обычно используется системный класс cs\Text. Он, как и система разрешений, оперирует группами и метками. К примеру, в интерфейсе администрирования вещи, которые могут зависеть от языка (название сайта, подпись в отправляемых письмах и подобное) реализованы подобным образом:


$result = \cs\Text::instance()->set(
    \cs\Config::instance()->module('System')->db('texts'),
    'System/Config/core',
    'name',
    'New site name'
);

В $result будет строка вида {¶$id}. Эту строку потом можно передать в cs\Text::process() чтобы получить обратно текст на текущем языке (или на том, на котором вообще есть перевод).


$site_name = \cs\Text::instance()->process(
    \cs\Config::instance()->module('System')->db('texts'),
    $result
);

Под капотом же используется 2 таблицы в БД, индекс которой передается в первом параметре. Первая [prefix]texts ассоциирует group + label с уникальным идентификатором, а [prefix]texts_data содержит, собственно, переводы для каждого языка.


$data_model_ml_group


Теперь, когда мы имеем представление как в общем виде можно использовать встроенную многоязычность контента в CleverStyle Framework, становится понятно к чему $data_model_ml_group. Это ни что иное как второй аргумент в вызове cs\Text::set(). Но как же указать, что должно быть многоязычным, а что нет? Для этого используется префикс ml: в типе данных.


Если бы в примере выше категория зависела от языка, то мы бы изменили соответствующую строчку на:


        'category'   => 'ml:int',

В результате, при записи под капотом будет сделан вызов:


$category = \cs\Text::set(
    $this->cdb(), // Какую БД использовать
    $this->data_model_ml_group,
    'category',
    $category
);

А при чтении:


$category = \cs\Text::process(
    $this->cdb(),
    $category
);

Для связанных таблиц используется отдельное поле в связанной таблице (указанное в language_field), там данный механизм не используется.


Обработка загружаемых файлов в CleverStyle Framework


Перед тем, как подробнее описать как загружаемые файлы обрабатываются в cs\CRUD нужно описать как оно обычно решается во фреймворке в общем виде.


Фреймворк из коробки не имеет функциональности для загрузки файлов (такая функциональность обеспечивается сторонними модулями, один такой есть в репозитории), но определяет интерфейс для этой функциональности. Каждый загружаемый файл регистрируется в БД и может быть ассоциирован с определёнными тэгами. То есть на фронтенде файл загружается и в ответ прилетает абсолютный путь, по которому доступен загруженный файл. Но если файл не подписать хотя бы одним тэгом, то он будет удален по прошествию некоторого времени.


Для подпис�� файла нужно сгенерировать событие System/upload_files/add_tag, соответствующий модуль среагирует на событие и добавит тэг для файла. Пример как это используется для пользовательских аватарок (опция доступна на фронтенде только если установлен модуль, обеспечивающий такую функциональность):


\cs\Event::instance()->fire(
    'System/upload_files/add_tag',
    [
        'url' => $new_avatar,
        'tag' => "users/$user/avatar"
    ]
);

Если файл больше не используется — тэг нужно удалить, и файл тоже будет удален:


\cs\Event::instance()->fire(
    'System/upload_files/del_tag',
    [
        'url' => $old_avatar,
        'tag' => "users/$user/avatar"
    ]
);

$data_model_files_tag_prefix


Теперь, когда мы имеем представление как загружаемые файлы обрабатываются в общем виде, становится понятно к чему $data_model_files_tag_prefix. Это не что иное, как общий префикс для тэгов при генерации события добавления/удаления тэгов для файлов.


При каждой вставке/изменении данных cs\CRUD анализирует все поля и поля всех связанных таблиц на наличие ссылок, сравнивает какие ссылки уже были, какие ещё нет, и генерирует под капотом вызовы аналогичные следующим:


$clang = \cs\Language:instance()->clang; // Краткий формат языка, например: en, ru, uk
$tag = "$this->data_model_files_tag_prefix/$id/$clang";
\cs\Event::instance()->fire(
    'System/upload_files/del_tag',
    [
        'tag' => $tag,
        'url' => $unused_file
    ]
);
\cs\Event::instance()->fire(
    'System/upload_files/add_tag',
    [
        'tag' => $tag,
        'url' => $new_file
    ]
);

Методы для поиска ссылок и подписи их тэгами могут быть повторно использованы отдельно, они так же доступны в cs\CRUD трейте и имеют следующий формат вызовов:


->find_urls($array : array) : string[] // Массив может быть произвольной глубины, можно передавать весь массив аргументов как есть
->update_files_tags($tag : string, $old_files : string[], $new_files : string[])

Резюме по cs\CRUD


cs\CRUD может снять с разработчика огромных пласт рутины даже в многоязычных конфигурациях, обеспечивая невероятно простые интерфейсы для множества типичных ситуаций. Тем не менее, иногда только часть функциональности может быть использована напрямую, но даже в этом случае польза, несомненно, будет.


cs\CRUD_helpers


Это вспомогательный трейт, который использует под капотом cs\CRUD и имеет те же требования.
Основное что этот метод предоставляет — это метод search() (который, впрочем, скорее фильтр) со следующим форматом вызова:


->search($search_parameters = [] : mixed[], $page = 1 : int, $count = 100 : int, $order_by = 'id' : string, $asc = false : bool) : false|int|int[]|string[]

Данный метод позволяет искать точные совпадения, совпадения из нескольких альтернативных значений, а так же диапазоны от… до для чисел. При этом в фильтре могут участвовать как поля основной таблицы, так и поля связанных таблиц. К тому же, при многоязычной конфигурации функция поиска будет учитывать это и сделает соответственный JOIN с таблицей [prefix]texts_data.


Так же есть возможность постраничного вывода результатов, возможность сортировки по колонкам (в том числе из связанных таблиц) и получение количества результатов поиск одним числом.


Выглядит это так:


$this->search([
    'argument'     => 'Value',              // Точно совпадение
    'argument2'    => ['Value1', 'Value2'], // Полное совпадение одного из нескольких вариантов
    'argument2'    => [                     // Поиск по диапазону, границы включаются в диапазон
        'from' => 2,                        // больше или равно, необязательно
        'to'   => 5                         // меньше или равно, необязательно
    ],
    'joined_table' => [                     // Связанные таблицы тоже поддерживаются, синтаксис такой же
        'argument' => 'Yes'
    ]
]);

Больше примеров и описание формата вызовов в документации, так же ещё более подробные примеры, включая некоторые крайние случаи можно увидеть в тестах (раз, два) (они очень хорошо покрывают оба трейта и всю работу с БД).


Так же cs\CRUD_helpers содержит ряд более атомарных методов для построения запроса и выполнения поиска, которые могут использоваться если вам не хватает того, что предоставляет cs\CRUD_heplers, но вы бы хотели повторно использовать уже существующие части механизма поиска. Но эти методы пока не документируются официально, так что используйте их с некоторой осторожностью.


В итоге


Если ваша задача вписывается в возможности cs\CRUD и cs\CRUD_helpers, то считайте что вам очень повезло.
Не придется писать ни одной строчки SQL, никаких проблем с многоязычностью и учетом загружаемых файлов, поиск сделать будет так же элементарно. Если же этого не достаточно, то по крайней мере можно изучить как оно устроено, либо даже повторно использовать часть го��овых методов.


» GitHub репозиторий
» Документация по фреймфорку
» Релевантные тесты в качестве дополнительных примеров всех возможных сценариев использования