📜 ⬆️ ⬇️

An example of using standalone actions in Yii2

When developing a site, an integral part is the collection of data. Sampling for certain conditions, pagination. Each time to write implementation in controllers is very boring. When you can once make an extensible implementation of frequently used functionality.

This article will give an example of how to use the Standalone actions functionality of the Yii2 framework to beautifully organize a uniform architecture that can be used in all parts of the application.

In short, what it is : the ability to create once the implementation of action-a and tie them to arbitrary controllers. So, the basic SiteController of an application based on a basic application template implements two action-a - for error handling and captcha checking:

attaching action s to 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, ], ]; } } 


')
What do we need

  1. ListAction - standalone action , realizes the connection between queries and search models.
  2. DataProvider - interlayer over requests, implements pagination navigation, sorting.
  3. Search Model - Model of search, accepts incoming data, performs validation and creates DataProvider with the necessary request.

The implementation of the last two things can be seen in the standard CRUDs generated by gii . It may seem unnecessary to take the data sample to a separate class, when it could be implemented by the method in the AR models themselves (as it was in yii1). But as it seems to me, the division of responsibility and the removal of the functional in a separate class gives more flexibility.

ListAction implementation

It is a class with a run method that is called when an action is requested. The class is inherited from yii \ base \ Action. Actions can be configured by binding to its controller by changing its properties. In our action, we pass the search model, inherited from the base abstract class and other optional settings for action, such as a custom view, data spacer method, method of obtaining pagination, etc.

class implementation with comments
 <?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,        - eg $_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; } } 


You also need to create a default view for the output if it is not an Ajax request.

default view
 <?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 ]); 



In this view, the default search form is implemented for the secure attributes of the search model and the search results are displayed using the GridView widget. Safe attributes are if they are specified in the script or if they have validation rules.

Basic Search Model

It is an abstract class from which the search models transmitted in ListAction should inherit. It implements the basis for the interaction of the model and the ListAction. The sampling logic is implemented in the inherited models.

abstract class implementation
 <?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; } } 


It remains to implement the search model and attach a ListAction to search for this model in an arbitrary controller. In the search model, it is mandatory to implement data sampling. Everything else depends on the requirements of a particular search model - validation, data composition logic, etc.

The data composition logic is overridden in the buildModels method.

Below is a simple example of a product search model with comments:

Search model
 <?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; } } 


It remains to attach the ListAction to the controller and transfer the product search model to it:

configuring the controller to search for products
 <?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, ] ]; } } 


So, when addressing an action using Ajax, we get JSON of approximately the following content:

sampling result
 { "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 } } 


If the validation fails, the array will contain a description of the error. With the usual request (not Ajax), we will see something like this:



For example, a small module was created based on the basic application template . It needs to be connected in the settings of the application Yii2 and start the migration with test data

 php yii migrate --migrationPath=modules/shop/migrations 


Summarizing all the above, this functionality makes it possible to create a uniform implementation of a sample of collections and any other repeating functionality.

As an example from reality, we use this functionality in the API, one action implements, depending on the request, the output of the response in JSON or a web interface for testing.

Source: https://habr.com/ru/post/208556/


All Articles