I need help with search model for ArrayDataProvider. Let's say i have an array:
$cities = [
['city' => "Chicago", 'year' => 1984],
['city' => "Washington", 'year' => 2001],
['city' => Manchester", 'year' => 1997],
//and so on...
];
I create an ArrayDataProvider:
$provider = new \yii\data\ArrayDataProvider([
'allModels' => $catalog,
'sort' => [
'attributes' => ['city', 'year'],
],
]);
Then I create a GridView:
echo \yii\grid\GridView::widget([
'dataProvider' => $provider,
'filterModel' => (new LibrarySearchModel()),
'columns' => $columns,
'showHeader' => true,
'summary' => false,
]);
All works fine, but i need a filtering in GridView. There is no option to use ActiveDataProvider and I cant find any tutorial how to filter a data in ArrayDataProvider.
Can someone help me with code for filter model or recomend the docs for my case?
This is example of how to use ArrayDataProvider with filters in the GridView.
Let's create simple action.
public function actionExample()
{
$data = new \app\models\Data();
$provider = $data->search(Yii::$app->request->get());
return $this->render('example', [
'provider' => $provider,
'filter' => $data,
]);
}
This is classic Yii 2 approach to the GridView so I will not explain it (you can find details in the Guide linked above).
Now the view.
<?php
echo \yii\grid\GridView::widget([
'dataProvider' => $provider,
'filterModel' => $filter,
'columns' => [
'name',
'code',
],
]);
Again, nothing different from the ActiveDataProvider approach. As you can see here we are expecting two columns: name and code - these will be defined below.
Data model.
Prepare the model that will handle the data source. Explanation is given in the comments.
<?php
namespace app\models;
use yii\base\Model;
/**
* Our data model extends yii\base\Model class so we can get easy to use and yet
* powerful Yii 2 validation mechanism.
*/
class Data extends Model
{
/**
* We plan to get two columns in our grid that can be filtered.
* Add more if required. You don't have to add all of them.
*/
public $name;
public $code;
/**
* Here we can define validation rules for each filtered column.
* See http://www.yiiframework.com/doc-2.0/guide-input-validation.html
* for more information about validation.
*/
public function rules()
{
return [
[['name', 'code'], 'string'],
// our columns are just simple string, nothing fancy
];
}
/**
* In this example we keep this special property to know if columns should be
* filtered or not. See search() method below.
*/
private $_filtered = false;
/**
* This method returns ArrayDataProvider.
* Filtered and sorted if required.
*/
public function search($params)
{
/**
* $params is the array of GET parameters passed in the actionExample().
* These are being loaded and validated.
* If validation is successful _filtered property is set to true to prepare
* data source. If not - data source is displayed without any filtering.
*/
if ($this->load($params) && $this->validate()) {
$this->_filtered = true;
}
return new \yii\data\ArrayDataProvider([
// ArrayDataProvider here takes the actual data source
'allModels' => $this->getData(),
'sort' => [
// we want our columns to be sortable:
'attributes' => ['name', 'code'],
],
]);
}
/**
* Here we are preparing the data source and applying the filters
* if _filtered property is set to true.
*/
protected function getData()
{
$data = [
['name' => 'Paul', 'code' => 'abc'],
['name' => 'John', 'code' => 'ade'],
['name' => 'Rick', 'code' => 'dbn'],
];
if ($this->_filtered) {
$data = array_filter($data, function ($value) {
$conditions = [true];
if (!empty($this->name)) {
$conditions[] = strpos($value['name'], $this->name) !== false;
}
if (!empty($this->code)) {
$conditions[] = strpos($value['code'], $this->code) !== false;
}
return array_product($conditions);
});
}
return $data;
}
}
The filtering in this example is handled by the array_filter function. Both columns are filtered "database LIKE"-style - if column value contains the searched string the data array row is not removed from the source.
To make it work like and conditions in ActiveDataProvider we put boolean result of every column check in the $conditions array and return product of that array in array_filter.
array_product($conditions) is equivalent of writing $conditions[0] && $conditions[1] && $conditions[2] && ...
This all results in the filterable and sortable GridView widget with two columns.
Related
I have two gridview tables currently, one shows all the data and the other i want it to show only data where id = 2. Is it possible to filter only one table without affecting the other? I know I can filter from the search model but that will affect all tables and i want it to affect only one.
Can i have two dataprovider?
This is the code in my search model:
class JobPlanningSearch extends JobPlanning
{
/**
* #inheritdoc
*/
public function rules()
{
return [
[['id', 'priority', 'employer_id', 'client_id', 'status', 'activity'], 'integer'],
[['job_description', 'impediment', 'date', 'estimated_time', 'due_date'], 'safe'],
];
}
/**
* #inheritdoc
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* #param array $params
*
* #return ActiveDataProvider
*/
public function search($params)
{
$query = JobPlanning::find();
// add conditions that should always apply here
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
// grid filtering conditions
$query->andFilterWhere([
'id' => $this->id,
'priority' => $this->priority,
'client_id' => $this->client_id,
'employer_id' => $this->employer_id,
'estimated_time' => $this->estimated_time,
'status' => $this->status,
'activity' => $this->activity,
//'actual' => $this->actual,
//'actual' => 1,
]);
$query->andFilterWhere(['like', 'job_description', $this->job_description]);
$query->andFilterWhere(['like', 'activity', $this->activity]);
return $dataProvider;
}
You need two dataproviders, like this:
$searchModelOne = new JobPlanningSearch();
$dataProviderOne = $searchModelOne->search(Yii::$app->request->queryParams);
$dataProviderOne->pagination->pageParam = 'dp-one-page'; //set page param for first dataprovider
$searchModelTwo = new JobPlanningSearch();
searchModelTwo->id = 2; // set id = 2 in second dataprovider
$dataProviderTwo = $searchModelTwo->search(Yii::$app->request->queryParams);
$dataProviderTwo->pagination->pageParam = 'dp-two-page'; //set page param for second dataprovider
You need to use two searchModels, one for each GridView and of course two data providers.
Also you need to change the formName attribute of one of the searchModels to avoid filtering to affects both grids.
And the in the controller, when passing parameters to the search() method of the modified searchModel, pass the params in the new name you assigned to formName in this searchModel.
You will have to manipulate the DataProvider in the controller.
$searchModel = new JobPlanningSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
$dataProvider->query->andWhere(['=','id',2]);
remove it from the search function in JobPlanningSearch.
$query->andFilterWhere([
/* 'id' => $this->id, */ // you must remove it from here
'priority' => $this->priority,
'client_id' => $this->client_id,
'employer_id' => $this->employer_id,
'estimated_time' => $this->estimated_time,
...
]);
In this case, the ID will always be 2.
But if you want to have a default value (the first time) and allow the user to change it, you should skip step 2. And add a condition in the controller:
...
if(count(Yii::$app->request->queryParams) == 0){
$dataProvider->query->andWhere(['=','id',2]);
}
This is just an example, you should check that the ID field has not been sent in the queryparams.
For the following factory definition, the column order needs to be sequential. There is already a column id that is auto-incremented. The first row's order should start at 1 and each additional row's order should be the next number (1,2,3, etc.)
$factory->define(App\AliasCommand::class, function (Faker\Generator $faker) {
return [
'user_id' => App\User::inRandomOrder()->first()->id,
'command' => $faker->word,
'content' => $faker->sentence,
'order' => (App\AliasCommand::count()) ?
App\AliasCommand::orderBy('order', 'desc')->first()->order + 1 : 1
];
});
It should be setting the order column to be 1 more than the previous row, however, it results in all rows being assigned 1.
Here's something that might work.
$factory->define(App\AliasCommand::class, function (Faker\Generator $faker) {
static $order = 1;
return [
'user_id' => App\User::inRandomOrder()->first()->id,
'command' => $faker->word,
'content' => $faker->sentence,
'order' => $order++
];
});
It just keeps a counter internal to that function.
Update:
Laravel 8 introduced new factory classes so this request becomes:
class AliasCommandFactory extends Factory {
private static $order = 1;
protected $model = AliasCommand::class;
public function definition() {
$faker = $this->faker;
return [
'user_id' => User::inRandomOrder()->first()->id,
'command' => $faker->word,
'content' => $faker->sentence,
'order' => self::$order++
];
}
}
The answer by #apokryfos is a good solution if you're sure the factory model generations will only be run in sequential order and you're not concerned with pre-existing data.
However, this can result in incorrect order values if, for example, you want to generate models to be inserted into your test database, where some records already exist.
Using a closure for the column value, we can better automate the sequential order.
$factory->define(App\AliasCommand::class, function (Faker\Generator $faker) {
return [
'user_id' => App\User::inRandomOrder()->first()->id,
'command' => $faker->word,
'content' => $faker->sentence,
'order' => function() {
$max = App\AliasCommand::max('order'); // returns 0 if no records exist.
return $max+1;
}
];
});
You almost had it right in your example, the problem is that you were running the order value execution at the time of defining the factory rather than the above code, which executes at the time the individual model is generated.
By the same principle, you should also enclose the user_id code in a closure, otherwise all of your factory generated models will have the same user ID.
To achieve true autoIncrement rather use this approach:
$__count = App\AliasCommand::count();
$__lastid = $__count ? App\AliasCommand::orderBy('order', 'desc')->first()->id : 0 ;
$factory->define(App\AliasCommand::class,
function(Faker\Generator $faker) use($__lastid){
return [
'user_id' => App\User::inRandomOrder()->first()->id,
'command' => $faker->word,
'content' => $faker->sentence,
'order' => $faker->unique()->numberBetween($min=$__lastid+1, $max=$__lastid+25),
/* +25 (for example here) is the number of records you want to insert
per run.
You can set this value in a config file and get it from there
for both Seeder and Factory ( i.e here ).
*/
];
});
In Laravel 9 (and possibly some earlier versions?), there's a pretty clean way to make this happen when you're creating models (from the docs):
$users = User::factory()
->count(10)
->sequence(fn ($sequence) => ['order' => $sequence->index])
->create();
If you'd like to start with 1 instead of 0:
$users = User::factory()
->count(10)
->sequence(fn ($sequence) => ['order' => $sequence->index + 1])
->create();
The solution also solves already data on table conditions:
class UserFactory extends Factory
{
/**
* #var string
*/
protected $model = User::class;
/**
* #var int
*/
protected static int $id = 0;
/**
* #return array
*/
public function definition()
{
if ( self::$id == 0 ) {
self::$id = User::query()->max("id") ?? 0;
// Initialize the id from database if exists.
// If conditions is necessary otherwise it would return same max id.
}
self::$id++;
return [
"id" => self::$id,
"email" => $this->faker->email,
];
}
}
I add another table field in my view , but the search button disappeared , How can I retrieve this form button ?
SaleItems = And a table mysql database.
<?php
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'formatter' => ['class' => 'yii\i18n\Formatter','nullDisplay' => ''],
'columns' => [
['class' => 'yii\grid\SerialColumn'],
**[
'attribute' => 'Data',
'content' => function(SaleItems $model, $key, $index, $column) {
return date('d/m/Y', strtotime($model->sale->date));
},
'header' => 'DATA'
],**
]); ?>
</div>
The search field disappears because in not setted properly in searchModel ..
for this you must extend the related modelSeacrh adding the relation with the related model and the filter for the field you need ..
for this you must set in the base mode, the relation
public function getSale()
{
return $this->hasOne(Sale::className(), ['id' => 'sale_id']);
}
/* Getter for sale data */
public function getSaleData() {
return $this->sale->data;
}
then setting up the search model declaring
/* your calculated attribute */
public $date;
/* setup rules */
public function rules() {
return [
/* your other rules */
[['data'], 'safe']
];
}
and extending
public function search($params) {
adding proper sort, for data
$dataProvider->setSort([
'attributes' => [
.....
'data' => [
'asc' => ['tbl_sale.data' => SORT_ASC],
'desc' => ['tbl_sale.data' => SORT_DESC],
'label' => 'Data'
]
]
]);
proper relation
if (!($this->load($params) && $this->validate())) {
/**
* The following line will allow eager loading with country data
* to enable sorting by country on initial loading of the grid.
*/
$query->joinWith(['sale']);
return $dataProvider;
}
and proper filter condition
// filter by sale data
$query->joinWith(['sale' => function ($q) {
$q->where('sale.data = ' . $this->data . ' ');
}]);
Once you do al this the searchModel contain the information for filter and the search field is showed in gridview
What in this answer is just a list of suggestion
you can find detailed tutorial in this doc (see the scenario 2 and adapt to your need)
http://www.yiiframework.com/wiki/621/filter-sort-by-calculated-related-fields-in-gridview-yii-2-0/
i am need to sort some fields (asc,desc) in GridView, but same fields are calculated. Look at code below:
SearchModel:
class ObjectSearch extends Object {
use SearchModelTrait;
public function rules()
{
return [
['id', 'integer', 'min' => 1],
];
}
public function search($params)
{
$this->company_id = \Yii::$app->user->identity->companyId;
$query = Object::find()->where(['company_id' => $this->company_id]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => false,
]);
$dataProvider->setSort([
'attributes' => [
'id',
'name',
'lastReportResult' => [
'asc' => ['lastReportResult' =>SORT_ASC ],
'desc' => ['lastReportResult' => SORT_DESC],
'default' => SORT_ASC
],
'reportPercentDiff'
]
]);
if (!($this->load($params,'ObjectSearch') && $this->validate())) {
return $dataProvider;
}
$this->addCondition($query, 'id');
return $dataProvider;
}
Methods in Object model:
public function getLastReportResult()
{
$lastReport = $this->getLastReport();
$message = 0;
if (!empty($lastReport)) {
$statistic = new ReportStatistic($lastReport);
$message = $statistic->getPercent();
}
return $message;
}
/**
* #return int
*/
public function getReportPercentDiff()
{
$lastReport = $this->getLastReport();
$message = 0;
if (!empty($lastReport)) {
$statistic = $lastReport->getReportDiff();
if (!empty($statistic['diff'])) {
$message = $statistic['diff']['right_answers_percent_diff'];
} elseif (!empty($statistic['message'])) {
$message = $statistic['message'];
}
}
return $message;
}
So, by this methods, i am calculating a values of two fields, which are need's sorting. This way doesn't working, i have a Database Exception, because object table hasn't this fields. exception
How to do sorting of this fields ?
Update: I am the author of this answer and this answer is not accurate. Preferred way is to use database view
Add two public properties to ObjectSearch.php and mark it as safe
class ObjectSearch extends Object {
use SearchModelTrait;
public $lastReportResult, $reportPercentDiff;
public function rules()
{
return [
['id', 'integer', 'min' => 1],
[['lastReportResult', 'reportPercentDiff'], 'safe']
];
}
public function search($params)
{
$this->company_id = \Yii::$app->user->identity->companyId;
$query = Object::find()->where(['company_id' => $this->company_id]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => false,
]);
$dataProvider->setSort([
'attributes' => [
'id',
'name',
'lastReportResult' => [
'asc' => ['lastReportResult' =>SORT_ASC ],
'desc' => ['lastReportResult' => SORT_DESC],
'default' => SORT_ASC
],
'reportPercentDiff' => [
'asc' => ['reportPercentDiff' =>SORT_ASC ],
'desc' => ['reportPercentDiff' => SORT_DESC],
'default' => SORT_ASC
],
]
]);
if (!($this->load($params,'ObjectSearch') && $this->validate())) {
return $dataProvider;
}
$this->addCondition($query, 'id');
return $dataProvider;
}
Then in index.php (view file in which you are having grid view) add lastReportResult and reportPercentDiff in array of all attributes (list of all attributes ob Object model)
...
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
// your other attribute here
'lastReportResult',
'reportPercentDiff',
['class' => 'yii\grid\ActionColumn'],
],
]); ?>
...
For more info you can visit Kartik's blog at Yii
Though this is an old thread, stumbled upon this and tried to find other method to achieve sorting of purely calculated field to no avail... and this post unfortunately is not an answer as well... It just that I feel the need to post it here to give a heads up to those that still looking for the solution so as not to scratch their heads when trying the solution given and still fail.
The given example from documentation or referred links as far as I have tested only works if you have a column within the database schema (whether in the main table or the related tables). It will not work if the virtual attribute/calculated field you create is based on calculating (as an example multiplication of 2 column on the table)
e.g:
table purchase: | purchase_id | product_id | quantity |
table product: | product_id | unit_price |
then, if we use a virtual attribute 'purchase_total' for model 'purchase' which is the multiplication of quantity and unit_price (from the join table of purchase and product on product_id), eventually you will hit an error saying 'purchase_total' column can not be found when you tried to sort them using the method discussed so far.
I am trying to setup the filter for related model in Yii2's GridView widget, but I am keep getting the error like the filter value must be an integer.
I have followed this question. Now, I have a two models Services.php and ServiceCharge.php.
In ServiceCharge.php the relation is setup like:
public function getServiceName()
{
return $this->hasOne(Services::className(),['id'=>'service_name']);
}
In the ServiceChargeSearch.php the code is like this:
<?php
namespace app\models;
use Yii;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use app\models\ServiceCharges;
/**
* ServiceChargesSearch represents the model behind the search form about `app\models\ServiceCharges`.
*/
class ServiceChargesSearch extends ServiceCharges
{
/**
* #inheritdoc
*/
public function attributes()
{
// add related fields to searchable attributes
return array_merge(parent::attributes(), ['serviceName.services']);
}
public function rules()
{
return [
[['id'], 'integer'],
[['charges_cash', 'charges_cashless'], 'number'],
[['id', 'serviceName.services', 'room_category'], 'safe'],
];
}
/**
* #inheritdoc
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* #param array $params
*
* #return ActiveDataProvider
*/
public function search($params)
{
$query = ServiceCharges::find();
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$dataProvider->sort->attributes['serviceName.services'] = [
'asc' => ['serviceName.services' => SORT_ASC],
'desc' => ['serviceName.services' => SORT_DESC],
];
$query->joinWith(['serviceName']);
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
$query->andFilterWhere([
'id' => $this->id,
// 'service_name' => $this->service_name,
'room_category' => $this->room_category,
'charges_cash' => $this->charges_cash,
'charges_cashless' => $this->charges_cashless,
])
->andFilterWhere(['LIKE', 'serviceName.services', $this->getAttribute('serviceName.services')]);
return $dataProvider;
}
}
and in my Gridview it is setup like this:
[
'attribute'=>'service_name',
'value'=>'serviceName.services',
],
Which is showing the services name from the related model correctly.
I am not able to see what I am doing wrong, but the filter field for the attribute for service is not showing at all.
Actually it is much simpler than it seems.
add the column_name to safe attribute.
Note: this should be relation Name
add the join with query - like - $query->joinWith(['serviceName','roomCategory']);
add the filter condition like:
->andFilterWhere(['like', 'services.services', $this->service_name])
->andFilterWhere(['like', 'room_category.room_category', $this->room_category]);
if like to add sorting add the code like:
$dataProvider->sort->attributes['service_name'] = [
'asc' => ['services.services' => SORT_ASC],
'desc' => ['services.services' => SORT_DESC],
];
$dataProvider->sort->attributes['room_category'] = [
'asc' => ['room_category.room_category' => SORT_ASC],
'desc' => ['room_category.room_category' => SORT_DESC],
];
5 you should also set the relation name say public $roomCategory
That's it. Both sorting and filtering for related table works perfectly.
Note: Remove default validation like integer for related column and default filtering generated by gii otherwise it will generate an error.
Update on Latest version:
Adding Public $attribute is not needed.
Adding safe attribute for relation is also not needed.
but the attribute in your current model, which you want filter is
to added to safe attribute that is a must.
and most importantly in your gridview, the related attribute has to
be in closure format.
that is example
[
'attribute=>'attribute_name',
'value=function($data){
return $data->relationname->related_table_attribute_name
}
],
remember it you are using relation_name.related_table_attribute_name filter somehow doesn't work for me.
There is a fairly comprehensive set of instructions on the Yii Framework website. The only thing to note is that the search model complains about the following lines, but everything appears to work as intended without them:
$this->addCondition(...);
For a model, PaymentEvent (table: subs_payment_event), which has a currency_id field linked to model Currency, this is the complete set of additional code (using the Basic template):
In the main model, PaymentEvent.php:
public function getCurrencyName()
{
return $this->currency->name;
}
In the search model, PaymentEventSearch.php:
public $currencyName;
In its rules:
[['currencyName'], 'safe'],
In the attributes of its setSort statement, include:
'currencyName' => [
'asc' => ['subs_currency.name' => SORT_ASC],
'desc' => ['subs_currency.name' => SORT_DESC],
'label' => 'Currency'
],
Before the grid filtering conditions:
$query->joinWith(['currency' => function ($q) {
$q->where('subs_currency.name LIKE "%' . $this->currencyName . '%"');
}]);
Finally, in the GridView columns array in the view (including my usual link across to the related model records):
[
'attribute' => 'currencyName',
'label' => 'Currency',
'format' => 'raw',
'value' => function ($data) {
return Html::a($data->currency->name, ['/currency/' . $data->currency_id]);
},
],