После статьи с описанием базовых интерфейсов для работы с БД было достаточно комментариев с предложением более высокоуровневых инструментов для работы. В 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 репозиторий
» Документация по фреймфорку
» Релевантные тесты в качестве дополнительных примеров всех возможных сценариев использования
