В данной статье будет приведен пример как при использовании функционала Standalone actions красиво организовать единообразную архитектуру, которую можно использовать во всех частях приложения.
Коротко, что это: возможность создать один раз реализацию action-а и привязывать их к произвольным контроллерам. Так базовый SiteController приложения на основе basic application template реализует два action-а — для обработки ошибок и проверки captcha:
<?php namespace app\controllers; use Yii; use yii\web\Controller; class SiteController extends Controller { public function actions() { return [ 'error' => [ 'class' => 'yii\web\ErrorAction', ], 'captcha' => [ 'class' => 'yii\captcha\CaptchaAction', 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, ], ]; } }
Что нам нужно
- ListAction — standalone action, реализует связь между запросом и моделями поиска.
- DataProvider — прослойка над запросами, реализует постраничную навигацию, сортировку.
- Search Model — Модель поиска, принимает входящие данные, производит валидацию и создаёт DataProvider с нужным запросом.
Реализацию двух последних вещей можно увидеть в стандартных CRUD-ах генерируемых gii. Возможно покажется излишним вынос выборки данных в отдельный класс, когда как это можно было бы реализовать методом в самих моделях AR (как это было в yii1). Но как мне кажется, разделение ответственности и вынос функционала в отдельный класс даёт больше гибкости.
Реализация ListAction
Представляет собой класс с методом run, вызываемый при запросе к action-у. Класс наследуется от yii\base\Action. Action-ы можно настраивать при привязке к его к контроллеру, изменяя его свойства. В наш action мы передаем модель поиска, наследованный от базового абстрактного класса и прочие опционально настройки action-а, такие как кастомное представление (view), способ проставки данных, метод получения пагинации и т.п.
<?php namespace app\modules\shop\actions; use Yii; use yii\base; use yii\web\Response; use app\modules\shop\components\FilterModelBase; use yii\widgets\LinkPager; class ListAction extends base\Action { /** * Модель поиска * @var FilterModelBase */ protected $_filterModel; /** * Анонимная-функция запускаемая в случае ошибки валидации модели поиска * @var callable */ protected $_validationFailedCallback; /** * Метод вставки данных из запроса, * Если true, то данные в запросе должны быть в под-массиве e.g. $_GET/$_POST[SearchModel][attribute] * @var bool */ public $directPopulating = true; /** * Метод получение пагинации, если true, то получаем уже готовый html пагинации, * нужно для AJAX запросов * @var bool */ public $paginationAsHTML = false; /** * Тип запроса * @var string */ public $requestType = 'get'; /** * Пусть до представления * @var string */ public $view = '@app/modules/shop/views/list/index'; public function run() { if (!$this->_filterModel) { throw new base\ErrorException('Не указана модель поиска'); } $request = Yii::$app->request; if ($request->isAjax) { Yii::$app->response->format = Response::FORMAT_JSON; } // Проставляем данные $data = (strtolower($this->requestType) === 'post' && $request->isPost) ? $_POST : $_GET; $this->_filterModel->load(($this->directPopulating) ? $data : [$this->_filterModel->formName() => $data]); // Производим выборку в модели поиска $this->_filterModel->search(); // Если при поиске произошла ошибка валидации if ($this->_filterModel->hasErrors()) { /** * В зависимости от запроса решаем что делать, * если ajax то сбрасываем ошибку, иначе если входящих данных нет, очищаем ошибки */ if ($request->isAjax){ return (is_callable($this->_validationFailedCallback)) ? call_user_func($this->_validationFailedCallback, $this->_filterModel) : [ 'error' => current($this->_filterModel->getErrors()) ]; } if (empty($data)) { $this->_filterModel->clearErrors(); } } if (!($dataProvider = $this->_filterModel->getDataProvider())) { throw new base\ErrorException('Не проинициализирован DataProvider'); } if ($request->isAjax) { // Возвращаем корректно сформированную коллекцию объектов return [ 'list' => $this->_filterModel->buildModels(), 'pagination' => ($this->paginationAsHTML) ? LinkPager::widget([ 'pagination' => $dataProvider->getPagination() ]) : $dataProvider->getPagination() ]; } return $this->controller->render($this->view ?: $this->id, [ 'filterModel' => $this->_filterModel, 'dataProvider' => $dataProvider, 'requestType' => $this->requestType, 'directPopulating' => $this->directPopulating ]); } public function setFilterModel(FilterModelBase $model) { $this->_filterModel = $model; } public function setValidationFailedCallback(callable $callback) { $this->_validationFailedCallback = $callback; } }
Так же нужно создать представление по умолчанию для вывода данных если это не Ajax запрос.
<?php use yii\widgets\ActiveForm; use yii\helpers\Html; /** * @var \yii\web\View $this * @var \yii\data\DataProviderInterface $dataProvider * @var \app\modules\shop\components\FilterModelBase $filterModel * @var ActiveForm: $form * @var string $requestType * @var bool $directPopulating */ // Формируем форму для поиска по safe аттрибутам if (($safeAttributes = $filterModel->safeAttributes())) { echo Html::beginTag('div', ['class' => 'well']); $form = ActiveForm::begin([ 'method' => $requestType ]); foreach ($safeAttributes as $attribute) { echo $form->field($filterModel, $attribute)->textInput([ 'name' => (!$directPopulating) ? $attribute : null ]); } echo Html::submitInput('search', ['class' => 'btn btn-default']). Html::endTag('div'); ActiveForm::end(); } echo \yii\grid\GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $filterModel ]);
В данном представлении по умолчанию реализована форма поиска по безопасным атрибутам модели поиска и вывод результатов поиска с помощью виджета GridView. Безопасными атрибуты являются если они указаны в сценарии или же у них имеются правила валидации.
Базовая модель поиска
Представляет собой абстрактный класс, от которого должны наследоваться модели поиска передаваемые в ListAction. Реализует базу для взаимодействия модели и ListAction-а. Логика выборки реализуется в наследуемых моделях.
<?php namespace app\modules\shop\components; use yii\base\Model; use yii\data\DataProviderInterface; abstract class FilterModelBase extends Model { /** * @var DataProviderInterface */ protected $_dataProvider; /** * @return DataProviderInterface */ abstract public function search(); /** * Получение результатов выборки * Этот метод часто переобределяется моделями поиска, например сгруппировать в под-массивы по датам и т.д. * @return mixed */ public function buildModels() { return $this->_dataProvider->getModels(); } public function getDataProvider() { return $this->_dataProvider; } }
Осталось реализовать модель поиска и прикрепить ListAction для поиска по данной модели в произвольный контроллер. В модели поиска обязательным является реализация выборки данных. Всё остальное зависит требований той или иной модели поиска — валидация, логика компоновки данных и т.п.
Логика компоновки данных переопределяется в методе buildModels.
Ниже с комментариями приведен простой пример модели поиска продуктов:
<?php namespace app\modules\shop\models\search; use app\modules\shop; use yii\data\ActiveDataProvider; use yii\data\Pagination; class ProductSearch extends shop\components\FilterModelBase { /** * Принимаемые моделью входящие данные */ public $price; public $page_size = 20; /** * Правила валидации модели * @return array */ public function rules() { return [ // Обязательное поле ['price', 'required'], // Только числа, значение как минимум должна равняться единице ['page_size', 'integer', 'integerOnly' => true, 'min' => 1] ]; } /** * Реализация логики выборки * @return ActiveDataProvider|\yii\data\DataProviderInterface */ public function search() { // Создаём запрос на получение продуктов вместе категориями $query = shop\models\Product::find() ->with('categories'); /** * Создаём DataProvider, указываем ему запрос, настраиваем пагинацию */ $this->_dataProvider = new ActiveDataProvider([ 'query' => $query, 'pagination' => new Pagination([ 'pageSize' => $this->page_size ]) ]); // Если ошибок нет, фильтруем по цене if ($this->validate()) { $query->where('price <= :price', [':price' => $this->price]); } return $this->_dataProvider; } /** * Переопределяем метод компоновки моделей, * возвращаем так же категории * Это синтетический пример. * @return array|mixed */ public function buildModels() { $result = []; /** * @var shop\models\Product $product */ foreach ($this->_dataProvider->getModels() as $product) { $result[] = array_merge($product->getAttributes(), [ 'categories' => $product->categories ]); } return $result; } }
Осталось прикрепить ListAction к контроллеру и передать ему модель поиска продуктов:
<?php namespace app\modules\shop\controllers; use yii\web\Controller; use app\modules\shop\actions\ListAction; use app\modules\shop\models\search\ProductSearch; class ProductController extends Controller { public function actions() { return [ 'index' => [ 'class' => ListAction::className(), 'filterModel' => new ProductSearch(), 'directPopulating' => false, ] ]; } }
Так при обращении к action-у посредством Ajax мы получим JSON примерно такого содержания:
{ "list": [ { "id": "7", "price": "50", "title": "product title #7", "description": "product description #7", "create_time": "0", "update_time": "0", "categories": [ { "id": "1", "title": "category title #1", "description": "category description #1", "create_time": "0", "update_time": "0" } ] } ], "pagination": { "pageVar": "page", "forcePageVar": true, "route": null, "params": null, "urlManager": null, "validatePage": true, "pageSize": 20, "totalCount": 1 } }
При ошибке валидации массив будет содержать описание ошибки. При обычном запросе (не Ajax) мы увидим приблизительно такое:

Для примера был создан небольшой модуль на основе basic application template. Его нужно подключить в настройках приложения Yii2 и запустить миграцию с тестовыми данными
php yii migrate --migrationPath=modules/shop/migrations
Резюмируя всё выше сказанное, данный функционал даёт возможность создать единообразную реализацию выборки коллекций и любого другого повторяющегося функционала.
Как пример из реальности, мы используем этот функционал в API, один action реализует в зависимости от запроса вывод ответа в JSON или веб-интерфейс для тестирования.
ссылка на оригинал статьи http://habrahabr.ru/company/topic/blog/208556/
Добавить комментарий