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