Yii2 - Intercept Render Action - yii2

I'm writing Yii2 basic application. Sometimes I have to render the views when an action of a controller is accessed as partials, sometimes I have to render them with the layout. This is based on a GET parameter in the request so my code in an action looks like this:
public function actionIndex() {
$isApi = Yii::$app->request->get('api');
$dataProvider = new ActiveDataProvider([
'query' => Participant::find(),
]);
if ($isApi) {
return json_encode($this->renderPartial('index', [
'dataProvider' => $dataProvider,
]));
} else {
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}
}
I have exactly the same logic for each action in each controller. I have to first check the GET request for the api variable and then based on that I call either the render or renderPartial function.
Is there a way to intercept the render part in each action and write this code to work globally for each action instead of copy and pasting it everywhere?

You can write method for this:
public function actionIndex() {
$dataProvider = new ActiveDataProvider([
'query' => Participant::find(),
]);
return $this->renderForApi('index', [
'dataProvider' => $dataProvider,
]);
}
protected function renderForApi($view, $params = []) {
$isApi = Yii::$app->request->get('api');
if ($isApi) {
return $this->asJson($this->renderPartial($view, $params));
}
return $this->render($view, $params);
}
You can put renderForApi() method into parent controller in your app or put it into trait and use it in every controller.

Related

Error Calling unknown method: omgdef\multilingual\MultilingualQuery::sort()?

Good afternoon, help me figure it out, using the OmgDef/yii2-multilingual-behavior extension, swears at this line:
public function actionIndex()
{
$data = new ActiveDataProvider([
'query' => FaqLang::find()->multilingual()->sort(), //error here
]);
return $this->render('index', [
'data' => $data
]);
}
My overridden model that stores the sort() method
<?
namespace admin\base;
/**
* Base active query class for models
* #package admin\base
*/
class ActiveQuery extends \yii\db\ActiveQuery
{
/**
* Order by order_num
* #return $this
*/
public function sort()
{
$this->orderBy(['order_num' => SORT_ASC]);
return $this;
}
}
You could try using the sort property of the ActiveDataProvider.
$data = new ActiveDataProvider([
'query' => FaqLang::find()
->multilingual()
->select(['id','question','answer']),
'sort' => [
'defaultOrder' => ['order_num' => SORT_ASC],
],
]);
To select columns use the select() method of the ActiveQuery object.
If you are using GridView to display results, you can also use it's columns property.

How can I display in view multiple GridViews with one DataProvider group it by some field?

A have a table 'operations' with finance operations for all users, including 'id', 'user_id', 'sum', 'name', 'date_picked' etc.
I have created one ActiveDataProvider that gets all operations for concrete User. Now I would like to display in view data from this dataprovider - not in one GridView, but in several GridViews which data grouped by 'date_picked' value.
I know that I can make a new dataprovider for the each 'date_picked' but there will be a large amount of requests to database.
Is there a way to display grouped data in several GridViews based on one dataprovider's data?
Controller:
/** #var OperationComponent $comp */
$comp = Yii::$app->operation;
$userId = Yii::$app->user->id;
$filterModel = $comp->getOperationSearch();
$operations = $comp->getSearchProvider($userId, Yii::$app->request->get());
OperationComponent:
public function getOperationSearch()
{
return new OperationSearch();
}
public function getSearchProvider($user_id, $params)
{
$model = new OperationSearch();
return $model->search($user_id, $params);
}
OperationSearch:
public function search($user_id, $params)
{
$query = Operation::find();
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => [
'pageSize' => 10,
],
// сортировка по умолчанию
'sort' => [
'defaultOrder' => [
'date_picked' => SORT_DESC
]
]
]);
return $dataProvider;
View:
....
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $filterModel,
.....
]); ?>
....
There is an Yii2 extension for complex GridViews: kartik-v/yii2-grid
It contains collapsible rows for showing details for a row.
You can view a demo here.
What you are looking for is the Expand Row Column Widget. It allows to expand a row by just loading the required data for this row details.
This extension has a good documentation is quite stable and tested and can be used straight forward.

Yii2 virtual attribute out of an attribute of a SqlDataProvider

My AddressController:
public function actionIndex() {
$searchModel = new AddressSearch;
$dataProvider = $searchModel->search($_GET);
return $this->render('index', [
'dataProvider' => $dataProvider,
'searchModel' => $searchModel,
]);
}
My model AddressSearch:
class AddressSearch extends Model {
public function search($params) {
$dataProvider = new SqlDataProvider([
'sql' => '
SELECT
name
FROM...
View:
GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
'name2',
I have an attribute called name in this dataProvider. I would like to create a new virtual attribute called name2 out of name by preg_replace()-ing a few parts of it.
The function itself is working. I have tried a lot of different things, but I still can't make it to fill the attribute name2 with data. name2 is always empty. Can you please point me to the right direction? Many thanks!
UPDATE: based on the brilliant ideas of #Imaginaroom and #rob006 I've done the following:
moved getName2() and the attributes I'm filling with SqlDataProvider from base model Address to AddressSearch
deleted the empty base model Address because I don't need it anyway. Less is more!
in search() I've added:
foreach ($dataProvider->getModels() as $row) {
$model = new AddressSearch;
$model->setAttributes($row, false);
$models[] = $model;
}
$dataProvider->setModels($models);
It works! Many thanks guys! It's fantastic that you are there and help!!!
The problem is that you're using SqlDataProvider which returns rows from the table as array instead of model instance. So that's why your getter (virtual-attribute) does not work in GridView - GridView does not work on Address model instance, but on raw array without name2 field.
You should change your SearchModel to use ActiveQuery:
$dataProvider = ActiveDataProvider([
'query' => Address::find()->select(['name']) //and whatever your query contains besides this
])
...
UPDATE: If you don't want to do it like this, you can add your logic directly in GridView like so:
GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
[
'header' => 'Name 2',
'value' => function($model) {
if (isset($this->_name2)) {
return $this->_name2;
}
return $this->_name2 = preg_replace([...
}
]
If you really need Address model instance and use SqlDataProvider at the same time, you may convert array to model instance manually:
$models = [];
foreach ($dataProvider->getModels() as $row) {
$model = Address::instantiate($row);
Address::populateRecord($model, $row);
$models[] = $model;
}
$dataProvider->setModels($models);
You can place this in AddressSearch::search() or create custom SqlDataProvider and override prepareModels().

Yii2 show active() records on a new index page of the same controller

I don't even know how to define my question, so I couldn't really search for an answer, so it would be appreciated if you could point me to the right direction.
I have a model a view and a controller. Normally if I'm calling index, I'm getting all records. That works fine. Now I would like to query from the DB only the active() records. So I would like to see on a new index page only the active records. Where do I have to define this? Of course, the function itself is already defined in ModelQuery. But in Controller, I have to create a new function, in order to be able to call a new view.
public function actionIndex2() {
$searchModel = new ModelSearch;
$dataProvider = $searchModel->search($_GET);
...
return $this->render('index2', [
'dataProvider' => $dataProvider,
'searchModel' => $searchModel,
]);
}
I have tried to add ->active() to all possible places here but no luck. I can't mess with ModelSearch, because then I would ruin basic index site functionality also. It's clear that I can use it when I'm doing so: Order::find()->active(), but now it's not the case. Do I have to create a new ModelSearch also? I hope it's not necessary. Many thanks!
Assuming you want filter by $id eg in your url /index2?id=10) you could try this way
public function actionIndex2($id) {
$searchModel = new ModelSearch;
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
$dataProvider->query->
andWhere(['id' => $id]);
...
return $this->render('index2', [
'dataProvider' => $dataProvider,
'searchModel' => $searchModel,
]);
}
So officially you have to add your ActiveQueries to the search method of your ModelSearch.
public function search($params) {
$query = Model::find()->activeQuery1()->activeQuery2();
$dataProvider = new ActiveDataProvider([
'query' => $query,
...
or
public function search($params) {
$query = Model::find();
$dataProvider = new ActiveDataProvider([
'query' => $query->activeQuery1()->activeQuery2(),
...
This means you have to create a new ModelSearch or a new search method in ModelSearch as many times as many combinations of ActiveQueries you wish to use.

Use closures in Yii2 ArrayDataProvider

In an ActiveDataProvider you can use closures as values, like:
$dataprovider = new ArrayDataProvider([
'allModels' => $array
]);
$gridColumns = [
'attrib_1',
[
'attribute' => 'attrib_2',
'label' => 'Label_2',
'value' => function($model) {
return Html::encode($model->value_2);
}
],
'attrib_3'
];
echo GridView::widget([
'dataProvider'=> $dataprovider,
'columns' => $gridColumns
]);
Is it possible to do the same or something like this, in an ArrayDataProvider?
Yes. Only difference is that $model is not an object but array so:
'value' => function($model) {
return Html::encode($model['value_2']);
}
For this purpose, I have created an extended version of ActiveDataProvider, that for each model got from provider I call a callback.
This is the custom ActiveDataProvider, put in common\components namespace in this case.
<?php
namespace common\components;
class CustomActiveDataProvider extends \yii\data\ActiveDataProvider
{
public $formatModelOutput = null;
public function getModels()
{
$inputModels = parent::getModels();
$outputModels = [];
if($this->formatModelOutput != null)
{
for($k=0;$k<count($inputModels);$k++)
{
$outputModels[] = call_user_func( $this->formatModelOutput, $k , $inputModels[$k]);
}
}
else
{
$outputModels = $inputModels;
}
return $outputModels;
}
}
This is the action in controller that uses it. For reusability, I call a model method instead calling a clousure, but you can call also a clousure.
public function actionIndex()
{
$query = Model::find();
$dataProvider = new \common\components\CustomActiveDataProvider([
'query' => $query,
'pagination' => ['pageSize' => null],
'formatModelOutput' => function($id, $model) {
return $model->dataModelPerActiveProvider;
}
]);
return $dataProvider;
}
At last, this is the method getDataModelPerActiveProvider in model:
public function getDataModelPerActiveProvider()
{
$this->id = 1;
// here you can customize other fields
// OR you can also return a custom array, for example:
// return ['field1' => 'test', 'field2' => 'foo', 'field3' => $this->id];
return $this;
}