Пример использования standalone actions в Yii2

от автора

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

В данной статье будет приведен пример как при использовании функционала Standalone actions красиво организовать единообразную архитектуру, которую можно использовать во всех частях приложения.

Коротко, что это: возможность создать один раз реализацию action-а и привязывать их к произвольным контроллерам. Так базовый SiteController приложения на основе basic application template реализует два action-а — для обработки ошибок и проверки captcha:

прикрепление action-ов к SiteController

<?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,             ],         ];     } } 

Что нам нужно

  1. ListAction — standalone action, реализует связь между запросом и моделями поиска.
  2. DataProvider — прослойка над запросами, реализует постраничную навигацию, сортировку.
  3. 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *