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

Поделиться с друзьями
-->

Комментарии (5)


  1. smple
    29.08.2016 14:41
    +2

    конфигурация с помощью массива, где надо помнить имена ключей или подсматривать документацию.


    попробуйте сделать interface


    interface ConfigurationInterface
    {
        public getArray();
    }

    И сделать классы для конфигурации которые будут реализовывать данный интерфейс и уже иметь методы для изменения.
    Ну и везде где аргументом ожидается массив добавить возможность передавать ConfigurationInterface
    Мне кажется это бы упростило использование и позволило использовать autocomplete для различных имен.


    К сожалению большинство фреймворков конфигурируются различными магическими массивами и приходится заучивать имена ключей.


    1. nazarpc
      29.08.2016 14:49
      -4

      Конфигурация пишется однажды при создании класса и на основании того, какая у вас структура таблицы. Здесь проблемы нет.


      Для чтения и записи данных вы можете сделать удобные обертки со специфичным для конкретной ситуации интерфейсом, заучивать ключи совершенно не нужно.
      Это могут быть просто аргументы:


      public function set (int $id, string $title, string $content) {
          return $this->update($id, $title, $content);
      }

      Или value object если пожелаете:


      public function set (PostInterface $post) {
          return $this->update($post->toArray());
      }

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


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


  1. MSerega
    30.08.2016 14:06
    +1

    У меня глаза кровоточат от исходников. Чувак, ну почитай уже книги чистый или совершенный код. Как этим пользоваться? Почему не camelCase в стилях? Твой пример $Mail->send_to(....) — тут переменная с большой буквы, а имя метода почему-то с маленькой. Пакеты иду идут с маленькой буквы, потом уякс — с большой. методы почти везде по 40+ строк, рефакторь, разделяй логику. Удали нафиг .idea из репы, зачем она там? Ты уверен в смысле фразы … fast… framework? Пишешь здесь в примере на одной строчке в одинарных кавычках, а в следующей в двойных, которые заведомо медленнее в силу специфики обработки.

    Залез в первый случайный файл /core/traits/Permission/Any.php — чувак, это п**здец. Потерял зрения сразу от встроенного SQL запроса. Все, дальше не выносимо смотреть. Со скоростью уверен там будут большие проблемы.

    В общем вердикт — изучай глубже язык, оптимизируй структуры, обязательна к прочтению книга «Чистый Код» Мартин Р., обязательно больше читать про архитектуру проектов, слабую связанность компонентов.. Больше практики на простых проектах, а не замахиваться на то, в чем слабое понимание.

    Если так хочется сделать фрэймворк, то сделай проще, но с какой-то супер идеей, то, что очень нужно и просто реализуется, но мало где есть или вообще пока такого не сделали, в общем какой-то креатив нужен и тогда народ потянется.

    И принимай во внимание местные комменты, они в 99% говорят чего реально не хватает или нужно переделать, и не нужно спорить. Если нет реальных аргументов, то переделай — будет больше пользы.


    1. nazarpc
      30.08.2016 14:32
      -3

      У меня глаза кровоточат от исходников. Чувак, ну почитай уже книги чистый или совершенный код.

      Вы не поверите, я читал Макконнелла. От корки до корки.


      Как этим пользоваться?

      Читать документацию для начала, наверное. Статьи можно здесь, на хабре, если с английским туго.


      Почему не camelCase в стилях?

      В стилях? Потому что 1) Мне не нравится camelCase в целом 2) В CSS более обще принято разделять дефисами и не это кажется эстетически более привлекательным.


      Твой пример $Mail->send_to(....) — тут переменная с большой буквы, а имя метода почему-то с маленькой.

      А с чего бы им быть одинаковыми? Объекты почти всегда с большой, методы/функции и свойста/переменные почти всегда маленькими буквами. В чём проблема-то?


      Пакеты иду идут с маленькой буквы, потом уякс — с большой.

      Пакеты О_о?


      методы почти везде по 40+ строк, рефакторь, разделяй логику.

      У вас религия не позволяет иметь 40+ строк в методе? У меня таких проблем нет.


      Удали нафиг .idea из репы, зачем она там?

      А вы зайдите в папку и посмотрите что за файлы там лежат. Не будете задавать глупых вопросов и считать меня дураком.


      Ты уверен в смысле фразы … fast… framework? Пишешь здесь в примере на одной строчке в одинарных кавычках, а в следующей в двойных, которые заведомо медленнее в силу специфики обработки.

      Конечно уверен, я в одной статье даже независимые бенчмарки приводил. Если в вашем коде есть измеримая разница в производительности после замены двойных кавычек на одинарные с конкатенацией — то примеры в студию, иначе это глупости и пустые разговоры. А вот читабельность в подобных примерах сильно выигрывает от двойных кавычек.


      Залез в первый случайный файл /core/traits/Permission/Any.php — чувак, это п**здец. Потерял зрения сразу от встроенного SQL запроса. Все, дальше не выносимо смотреть.

      Могу приписать почитать какую-нибудь книжку по SQL, облегчит последствия и следующий раз не потеряете сознание.


      Со скоростью уверен там будут большие проблемы.

      Это уже даже смешно становится:) Раз так уверены, то давайте пример того, что вы считаете быстрым full-stack фреймворком? Только не пишите подобную чушь, давайте бенчмарки, какие-то подтверждения.


      В общем вердикт — изучай глубже язык, оптимизируй структуры, обязательна к прочтению книга «Чистый Код» Мартин Р., обязательно больше читать про архитектуру проектов, слабую связанность компонентов… Больше практики на простых проектах, а не замахиваться на то, в чем слабое понимание.

      Я вам тоже советую изучать язык. Это может помочь в будущем не писать чушь про кавычки.


      Если так хочется сделать фрэймворк, то сделай проще, но с какой-то супер идеей, то, что очень нужно и просто реализуется, но мало где есть или вообще пока такого не сделали, в общем какой-то креатив нужен и тогда народ потянется.

      Давайте вы сначала посмотрите документацию, потом сравните с, предположим, Symfony, а потом вернетесь и мы поговорим предметно что проще и что быстрее предметно, а не зазубренными книжными фразами. Контакты мои найти очень просто, всегда рад пообщаться.


      И принимай во внимание местные комменты, они в 99% говорят чего реально не хватает или нужно переделать, и не нужно спорить. Если нет реальных аргументов, то переделай — будет больше пользы.

      Не нужно спорить? Серьезно, завязывайте свои догмы транслировать, включите мышление, проявите упомянутый вами креатив и осмысление получаемой извне информации.


      1. MSerega
        30.08.2016 16:08

        Нет слов.