Что общего у комментариев к статье на Хабре и дополнительных опций при покупке машины?
![](https://habrastorage.org/r/w1560/webt/4x/ex/_2/4xex_2tbdcc_iwesc63zed-dqzm.png)
С точки зрения моделирования данных, и то, и другое — “вложенные” сущности, которые не имеют самостоятельного значения в отрыве от родительского объекта.
В Yii (php framework) есть Gii — встроенный генератор кода, который позволяет в несколько кликов мышкой создавать базовые CRUD-интерфейсы по модели данных, которые значительно ускоряют разработку, но применимы только для самостоятельных сущностей, как статья или машина в примерах выше.
Было бы здорово, чтобы можно было сгенерировать что-то подобное для “вложенных” объектов данных, верно? Теперь — можно, добро пожаловать под кат за подробностями.
Для самых нетерпеливых в конце статьи дана инструкция по быстрому старту.
А для интересующихся в статье рассмотрены аспекты от бизнес-применения до внутреннего устройства:
Возможно, комментарии на хабре и плохой пример, т.к. часто бывают полезнее самой статьи, но, в любом случае, при разработке приложения часто встречается ситуации, когда определенный объект модели данных мало интересен пользователю, как самостоятельная сущность.
Рассмотрим упрощенную бизнес-задачу: сделать сайт для публикации сообщений, сгруппированных по различным темам.
Сайт должен иметь следующие интерфейсы:
Довольно стандартно, верно?
Посмотрим на модель данных:
![](https://habrastorage.org/r/w1560/webt/f0/db/dp/f0dbdpmz9gbbxhsmxhu7vmzurem.png)
Тоже никаких сюрпризов. Два класса моделей будут содержать нашу бизнес-логику:
Приложение будет обслуживаться двумя контроллерами:
Потенциально можно также сгенерировать PostController — для целей администрирования и/или копи-паста кусков кода в кастомные виджеты, но оставим это за рамками данной статьи.
До текущего момента большую часть кода можно сгенерировать с помощью gii, что ускоряет разработку и снижает риски (меньше ручного кода = меньше шансов допустить ошибку).
Остаются два вопроса:
Если удастся решить их с помощью автоматического генератора — это будет солидным достижением.
Главная страница, обслуживаемая адресом site/index, должна содержать список тем, отфильтрованных по заранее определенному критерию. Критерий фильтрации, как часть бизнес-логики, мы включили в модель.
Для отображения же есть несколько вариантов реализации.
Первый, грязный и быстрый — все сделать прямо в файле представления (views/site/index.php):
Можно пойти немного дальше и упаковать все это в отдельный файл представления, что-то вроде views/site/_topic-list-widget.php, вызвав его рендер из главного файла. Это даст немного больше управляемости и расширяемости, но все равно выглядит довольно грязно.
Большинство из нас, скорее всего, создадут отдельный виджет по всем правилам, в отдельном пространстве имен (app\widgets или app\components для шаблона basic — в зависимости от версии, которую вы используете), где инкапсулируют создание ActiveDataProvider по модели и отображение в самостоятельном представлении. Дальше останется только вызвать этот виджет с главной страницы. Это решение наиболее верное с точки зрения декомпозиции классов, управляемости и расширяемости кода.
Но не возникает ли ощущение, что код этого виджета будет очень во многом повторять код TopicController в части обработки actionIndex()? И так обидно писать этот код вручную.
Гораздо лучше было бы сгенерировать этот код автоматически и потом просто вызвать готовый виджет:
По аналогии с отфильтрованным списком тем у нас есть практически те же самые варианты.
Первый — все сделать в коде файла представления для просмотра темы (views/topic/view.php):
Второй — изолировать этот код в отдельный файл представления: views/topic/_posts-list-widget.php, просто, чтобы не мозолил глаза — переиспользовать его где-либо все равно не получится.
Третий — полноценный виджет, который будет во многом дублировать код условного PostController в части actionIndex(), но написанный вручную.
Или сгенерировать код автоматически и вызвать готовый виджет:
Бизнес-задача определена, требования к генерируемому виджету обрисованы, разберемся с тем, как именно это будем генерировать. В Gii уже есть генератор для CRUD-контроллера. Для CRUD-виджета нам понадобится создать новый генератор на основе существующего.
Пара ссылок на документацию перед стартом — будет также полезно, если вы решите написать собственное расширение:
Очевидно, вся функциональность упакована в расширение Yii, которое устанавливается через composer и попадает в папку vendor вашего проекта.
Расширение состоит из трех частей:
![](https://habrastorage.org/r/w1560/webt/uh/le/4c/uhle4cnzxzmubqgtoitn3ox5b_u.png)
Расширение должно генерировать код, поэтому центральной его частью является генератор Gii.
Изначально предполагалось, что для реализации расширения будет достаточно написать свой шаблон для встроенного генератора CRUD-Controller. К слову, именно поэтому директория называется templates, а не generators. Но вышло так, что генератор CRUD-Controller проводит весьма интенсивную валидацию введенных данных, которая не позволяла реализовать многие требования, например, изменить класс для наследования. Поэтому расширение содержит полноценный генератор, а не только шаблон.
Генератор gii состоит из следующих частей (все лежат внутри директории templates/crud):
Файлы Generator.php и form.php содержат в основном косметические правки относительно оригинальных из CRUD-генератора: имена файлов, валидация, тексты описаний и подсказок и т.п.
Файлы шаблона отвечают за генерируемое представление и сам код виджета. В первую очередь важен файл templates/crud/default/controller.php, который отвечает за генерацию непосредственно класса виджета, соответствующего классу контроллера из оригинального генератора.
Виджет должен иметь те же действия (actions), что и CRUD-контроллер, но генерируются они немного иначе. В примерах ниже показан результат генерации с комментариями:
Наконец, файлы представления содержат следующие основные правки:
Когда генератор закончит свою работу, он создаст класс виджета в пространстве имен приложения. Цепочка наследования выглядит так: виджеты, сгенерированные для приложения, наследуются от базового виджета расширения, класса \ianikanov\wce\Widget, который, в свою очередь, наследуется от базового виджета Yii, класса \yii\base\Widget.
Базовый класс виджета расширения решает следующие задачи:
Этот контроллер регистрируется в карте приложения через файл конфигурации и содержит только один метод — actionIndex, который выполняет следующие действия:
Пожалуй, важнее указать, чего этот контроллер НЕ делает:
Такой подход позволяет сохранить универсальность фасада, оставив реализацию бизнес требований, включая требования к безопасности, прикладному коду приложения.
Установка расширения производится с помощью composer:
Далее надо внести несколько изменений в файл конфигурации приложения.
Во-первых, добавить указание на генератор gii:
Во-вторых, добавить встроенный контроллер-фасад в карту:
На этом установка и конфигурация завершена.
Чтобы сгенерировать виджет надо:
Далее для использования виджета, его надо вызвать, указав action и params — практически также, как вызывается контроллер.
Виджет просмотра списка моделей:
Виджет просмотра одной модели:
Виджет создания модели (кнопка + форма, обернутая в Modal):
Виджет изменения модели (кнопка + форма, обернутая в Modal):
Виджет удаления модели (кнопка):
Код виджета и всех представлений принадлежит приложению и может быть легко изменен — все точно также, как при генерации контроллера.
Пару слов о том, как расширение будет поддерживаться и развиваться. У меня есть основная работа и несколько своих “побочных” проектов (pet-projects). Так вот, это расширение — это побочный проект от моих побочных проектов, так что улучшения к нему я буду разрабатывать только под нужды своих проектов.
В лучших традициях open source, код доступен на гитхабе, и я буду его поддерживать в части исправления багов, а также постараюсь делать своевременные ревью, если кто захочет отправить pull request, так что, кому интересно — присоединяйтесь.
![](https://habrastorage.org/webt/4x/ex/_2/4xex_2tbdcc_iwesc63zed-dqzm.png)
С точки зрения моделирования данных, и то, и другое — “вложенные” сущности, которые не имеют самостоятельного значения в отрыве от родительского объекта.
В Yii (php framework) есть Gii — встроенный генератор кода, который позволяет в несколько кликов мышкой создавать базовые CRUD-интерфейсы по модели данных, которые значительно ускоряют разработку, но применимы только для самостоятельных сущностей, как статья или машина в примерах выше.
Было бы здорово, чтобы можно было сгенерировать что-то подобное для “вложенных” объектов данных, верно? Теперь — можно, добро пожаловать под кат за подробностями.
Для самых нетерпеливых в конце статьи дана инструкция по быстрому старту.
А для интересующихся в статье рассмотрены аспекты от бизнес-применения до внутреннего устройства:
- Бизнес-кейс: публикация сообщений по темам
- Список тем на главной
- Список сообщений по теме
- Под капотом: генератор gii на базе CRUD
- Шаблон генератора Gii
- Базовый класс виджета
- Встроенный контроллер-фасад
- Быстрый старт
- О поддержке и развитии
Бизнес-кейс: публикация сообщений по темам
Возможно, комментарии на хабре и плохой пример, т.к. часто бывают полезнее самой статьи, но, в любом случае, при разработке приложения часто встречается ситуации, когда определенный объект модели данных мало интересен пользователю, как самостоятельная сущность.
Рассмотрим упрощенную бизнес-задачу: сделать сайт для публикации сообщений, сгруппированных по различным темам.
Сайт должен иметь следующие интерфейсы:
- Главная страница — должна в будущем поддерживать различные виджеты, но на текущем этапе реализации только один: список тем, отфильтрованных по какому-то критерию.
- Полный список тем — полный список тем в табличном виде;
- Страница темы — информация о теме и список сообщений, опубликованных в ней.
Довольно стандартно, верно?
Посмотрим на модель данных:
![](https://habrastorage.org/webt/f0/db/dp/f0dbdpmz9gbbxhsmxhu7vmzurem.png)
Тоже никаких сюрпризов. Два класса моделей будут содержать нашу бизнес-логику:
- Класс Topic — данные по теме, валидация, список постов в ней, а также отдельный метод, возвращающий список тем, отфильтрованных по критерию для виджета на главной странице.
- Класс Post — только данные и валидация.
Приложение будет обслуживаться двумя контроллерами:
- SiteController — стандартные страницы (о нас, контакты и т.п.), авторизация (не требуется по ТЗ, но мы-то знаем) и index — главная страница. Т.к. мы предполагаем в будущем множество разнообразных виджетов, главную страницу имеет смысл оставить в этом контроллере, а не переносить в специфичный для одной модели.
- TopicController — стандартный набор действий: список, создание, редактирование, просмотр и удаление тем.
Потенциально можно также сгенерировать PostController — для целей администрирования и/или копи-паста кусков кода в кастомные виджеты, но оставим это за рамками данной статьи.
До текущего момента большую часть кода можно сгенерировать с помощью gii, что ускоряет разработку и снижает риски (меньше ручного кода = меньше шансов допустить ошибку).
Остаются два вопроса:
- Как вывести отфильтрованный список тем на главной странице?
- Как вывести список сообщений по теме?
Если удастся решить их с помощью автоматического генератора — это будет солидным достижением.
Список тем на главной
Главная страница, обслуживаемая адресом site/index, должна содержать список тем, отфильтрованных по заранее определенному критерию. Критерий фильтрации, как часть бизнес-логики, мы включили в модель.
Для отображения же есть несколько вариантов реализации.
Первый, грязный и быстрый — все сделать прямо в файле представления (views/site/index.php):
- Создать ActiveDataProvider;
- Заполнить его данными из модели Topic;
- Отобразить с использованием стандартного виджета ListView / GridView, указав необходимые поля вручную.
Можно пойти немного дальше и упаковать все это в отдельный файл представления, что-то вроде views/site/_topic-list-widget.php, вызвав его рендер из главного файла. Это даст немного больше управляемости и расширяемости, но все равно выглядит довольно грязно.
Большинство из нас, скорее всего, создадут отдельный виджет по всем правилам, в отдельном пространстве имен (app\widgets или app\components для шаблона basic — в зависимости от версии, которую вы используете), где инкапсулируют создание ActiveDataProvider по модели и отображение в самостоятельном представлении. Дальше останется только вызвать этот виджет с главной страницы. Это решение наиболее верное с точки зрения декомпозиции классов, управляемости и расширяемости кода.
Но не возникает ли ощущение, что код этого виджета будет очень во многом повторять код TopicController в части обработки actionIndex()? И так обидно писать этот код вручную.
Гораздо лучше было бы сгенерировать этот код автоматически и потом просто вызвать готовый виджет:
<?= \app\widgets\TopicControllerWidget::widget([
'action' => 'index',
'params' => [
'query' => app\models\Topic::findBySomeSpecificCriteria()
],
]) ?>
Список сообщений по теме
Страница просмотра темы, обслуживаемая адресом topic/view, должна содержать информацию о самой теме и список сообщений, опубликованных в ней. Список сообщений для темы мы получаем в модели автоматически, если у нас правильно настроены связи между таблицами, так что остается только вопрос отображения.По аналогии с отфильтрованным списком тем у нас есть практически те же самые варианты.
Первый — все сделать в коде файла представления для просмотра темы (views/topic/view.php):
- Создать ActiveDataProvider;
- Заполнить его данными из модели $model->getPosts();
- Отобразить с использованием стандартного виджета ListView / GridView, указав необходимые поля вручную.
Второй — изолировать этот код в отдельный файл представления: views/topic/_posts-list-widget.php, просто, чтобы не мозолил глаза — переиспользовать его где-либо все равно не получится.
Третий — полноценный виджет, который будет во многом дублировать код условного PostController в части actionIndex(), но написанный вручную.
Или сгенерировать код автоматически и вызвать готовый виджет:
<?= app\widgets\PostControllerWidget::widget([
'action' => 'index',
'params' => [
'query' => $model->getPosts(),
],
]) ?>
Под капотом: генератор gii на базе CRUD
Бизнес-задача определена, требования к генерируемому виджету обрисованы, разберемся с тем, как именно это будем генерировать. В Gii уже есть генератор для CRUD-контроллера. Для CRUD-виджета нам понадобится создать новый генератор на основе существующего.
Пара ссылок на документацию перед стартом — будет также полезно, если вы решите написать собственное расширение:
Очевидно, вся функциональность упакована в расширение Yii, которое устанавливается через composer и попадает в папку vendor вашего проекта.
Расширение состоит из трех частей:
- Директория templates/crud, содержащая шаблон генератора gii;
- Файл Controller.php — встроенный контроллер-фасад для вызовов виджетов;
- Файл Widget.php — базовый класс для всех генерируемых виджетов.
![](https://habrastorage.org/webt/uh/le/4c/uhle4cnzxzmubqgtoitn3ox5b_u.png)
Шаблон генератора Gii
Расширение должно генерировать код, поэтому центральной его частью является генератор Gii.
Изначально предполагалось, что для реализации расширения будет достаточно написать свой шаблон для встроенного генератора CRUD-Controller. К слову, именно поэтому директория называется templates, а не generators. Но вышло так, что генератор CRUD-Controller проводит весьма интенсивную валидацию введенных данных, которая не позволяла реализовать многие требования, например, изменить класс для наследования. Поэтому расширение содержит полноценный генератор, а не только шаблон.
Генератор gii состоит из следующих частей (все лежат внутри директории templates/crud):
- Директория default — это шаблон, где и происходит вся магия: каждый файл в этой директории будет соответствовать одному сгенерированному файлу в вашем проекте;
- Файл form.php — как можно догадаться из названия, это форма для ввода параметров генерации (имена классов и т.п.);
- Файл Generator.php — оркестратор генерации, который получает данные из формы, проводит их валидацию, а потом последовательно вызывает файлы шаблона для создания результата.
Файлы Generator.php и form.php содержат в основном косметические правки относительно оригинальных из CRUD-генератора: имена файлов, валидация, тексты описаний и подсказок и т.п.
Файлы шаблона отвечают за генерируемое представление и сам код виджета. В первую очередь важен файл templates/crud/default/controller.php, который отвечает за генерацию непосредственно класса виджета, соответствующего классу контроллера из оригинального генератора.
Виджет должен иметь те же действия (actions), что и CRUD-контроллер, но генерируются они немного иначе. В примерах ниже показан результат генерации с комментариями:
- actionIndex — вместо безусловного вывода всех моделей метод принимает параметр $query;
public function actionIndex($query) { $dataProvider = new ActiveDataProvider([ 'query' => $query, ]); return $this->render('index', [ 'dataProvider' => $dataProvider, ]); }
- actionCreate и actionUpdate — в случае успеха вместо редиректа просто возвращают код успеха, дальнейшая обработка обеспечивается встроенным контроллером-фасадом;
public function actionCreate() { $model = new Post(); if ($model->load(Yii::$app->request->post()) && $model->save()) { return 'success'; } return $this->render('create', [ 'model' => $model, ]); }
- actionDelete — поддерживает GET метод для отображения виджета удаления (по умолчанию — одна кнопка) и POST для выполнения действия; в случае успеха также не выполняет редирект, а возвращает код.
public function actionDelete($id) { $model = $this->findModel($id); if (Yii::$app->request->method == 'GET') { return $this->render('delete', [ 'model' => $model, ]); } else { $model->delete(); return 'success'; } }
Наконец, файлы представления содержат следующие основные правки:
- Все заголовки переведены в h2 вместо h1;
- Убран код, отвечающий за вывод title страницы и за хлебные крошки — виджет не должен влиять на эти вещи;
- Создание и редактирование моделей происходит с помощью модального окна (встроенный виджет Modal);
- Добавлен шаблон виджета удаления — с одной большой красной кнопкой.
Базовый класс виджета
Когда генератор закончит свою работу, он создаст класс виджета в пространстве имен приложения. Цепочка наследования выглядит так: виджеты, сгенерированные для приложения, наследуются от базового виджета расширения, класса \ianikanov\wce\Widget, который, в свою очередь, наследуется от базового виджета Yii, класса \yii\base\Widget.
Базовый класс виджета расширения решает следующие задачи:
- Определяет два основных поля: $action и $params, через которые виджету передается управление из вызывающего представления;
- Определяет ряд стандартных параметров, которые можно переопределить в сгенерированном классе, таких как путь к файлам представления виджета, имя и путь к контроллеру-фасаду (о нем ниже) и сообщения об ошибках;
- Определяет стандартные параметры при рендере представлений: render и renderFile;
- Обеспечивает инфраструктуру событий, аналогичную инфраструктуре контроллера, чтобы работали стандартные фильтры, такие как AccessControl и VerbFilter;
- Определяет метод run, который и собирает все это вместе.
Встроенный контроллер-фасад
С отображением данных проблем нет — виджеты для того и предназначены. А вот для редактирования, как ни крути, нужен контроллер. Генерировать уникальный контроллер для каждого виджета — теряется вся его суть. Использовать стандартный CRUD — не всегда актуально, да и не хочется зависеть от дополнительного запуска gii. Поэтому был использован вариант с универсальным, встроенным контроллером-фасадом.Этот контроллер регистрируется в карте приложения через файл конфигурации и содержит только один метод — actionIndex, который выполняет следующие действия:
- Принимает запрос от клиента;
- Передает управление соответствующему классу виджета;
- Обрабатывает бизнес-ошибки в результате работы виджета;
- Осуществляет перенаправление обратно основному приложению.
Пожалуй, важнее указать, чего этот контроллер НЕ делает:
- Он не проверяет уровни доступа — эта логика принадлежит конкретным виджетам;
- Он не производит никаких манипуляций с вводом — параметры передаются виджету, как есть;
- Он не производит никаких манипуляций с выводом, кроме проверки на заранее определенный код успеха.
Такой подход позволяет сохранить универсальность фасада, оставив реализацию бизнес требований, включая требования к безопасности, прикладному коду приложения.
Быстрый старт
Бизнес-задача ясна, готовы начать? Использование расширения состоит из четырех шагов:- Установка;
- Конфигурация;
- Генерация;
- Применение.
Установка расширения производится с помощью composer:
php composer.phar require --prefer-dist ianikanov/yii2-wce "dev-master"
Далее надо внести несколько изменений в файл конфигурации приложения.
Во-первых, добавить указание на генератор gii:
if (YII_ENV_DEV) {
$config['modules']['gii'] = [
'class' => 'yii\gii\Module',
'allowedIPs' => ['127.0.0.1', '::1', '192.168.0.*', '192.168.178.20'],
'generators' => [ //here
'widgetCrud' => [
'class' => '\ianikanov\wce\templates\crud\Generator',
'templates' => [
'WCE' => '@vendor/ianikanov/yii2-wce/templates/crud/default', // template name
],
],
],
];
}
Во-вторых, добавить встроенный контроллер-фасад в карту:
$config = [
...
'controllerMap' => [
'wce-embed' => '\ianikanov\wce\Controller',
],
...
];
На этом установка и конфигурация завершена.
Чтобы сгенерировать виджет надо:
- Открыть gii;
- Выбрать «CRUD Controller Widget»;
- Заполнить поля формы;
- Просмотреть и сгенерировать код.
Далее для использования виджета, его надо вызвать, указав action и params — практически также, как вызывается контроллер.
Виджет просмотра списка моделей:
<?= app\widgets\PostControllerWidget::widget([
'action' => 'index',
'params' => [
'query' => $otherModel->getPosts(),
],
]) ?>
Виджет просмотра одной модели:
<?= app\widgets\PostControllerWidget::widget(['action' => 'view', 'params' => ['id' => $post_id]]) ?>
Виджет создания модели (кнопка + форма, обернутая в Modal):
<?= app\widgets\PostControllerWidget::widget(['action' => 'create']) ?>
Виджет изменения модели (кнопка + форма, обернутая в Modal):
<?= app\widgets\PostControllerWidget::widget(['action' => 'update', 'params'=>['id' => $post_id]]) ?>
Виджет удаления модели (кнопка):
<?= app\widgets\PostControllerWidget::widget(['action' => 'delete', 'params'=>['id' => $post_id]]) ?>
Код виджета и всех представлений принадлежит приложению и может быть легко изменен — все точно также, как при генерации контроллера.
О поддержке и развитии
Пару слов о том, как расширение будет поддерживаться и развиваться. У меня есть основная работа и несколько своих “побочных” проектов (pet-projects). Так вот, это расширение — это побочный проект от моих побочных проектов, так что улучшения к нему я буду разрабатывать только под нужды своих проектов.
В лучших традициях open source, код доступен на гитхабе, и я буду его поддерживать в части исправления багов, а также постараюсь делать своевременные ревью, если кто захочет отправить pull request, так что, кому интересно — присоединяйтесь.