CRUD і пов'язані операції в CleverStyle Framework

Після статті з описом базових інтерфейсів для роботи з БД було достатньо коментарів з пропозицією більш високорівневих інструментів для роботи. У 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',
'рядок_значення' => 'text',
'text_value' => 'html'
]
],
'images' => [
'data_model' => [
'id' => 'int',
'image' => 'text'
]
],
'відео' => [
'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 репозиторій
» Документація по фреймфорку
» Релевантні тести в якості додаткових прикладів всіх можливих сценаріїв використання
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.