yii2-gii
yii2-gii copied to clipboard
Updates for Search-Form Model.
Now search model will not inherit Active Record model.
This allows you to get rid of the duct tape method scenarios().
And teaches the right way of using models and active records.
Generated example for common\models\User.
<?php
namespace backend\models;
use Yii;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use common\models\User;
/**
* UserSearch represents the model behind the search form about `common\models\User`.
*/
class UserSearch extends Model
{
/**
* @var integer
*/
public $id;
/**
* @var string
*/
public $username;
/**
* @var string
*/
public $auth_key;
/**
* @var string
*/
public $password_hash;
/**
* @var string
*/
public $password_reset_token;
/**
* @var string
*/
public $email;
/**
* @var integer
*/
public $status;
/**
* @var integer
*/
public $created_at;
/**
* @var integer
*/
public $updated_at;
/**
* @var string
*/
public $description;
/**
* @var integer
*/
public $signin_at;
/**
* @var integer
*/
public $activity_at;
/**
* @inheritdoc
*/
public function rules()
{
return [
[['id', 'status', 'created_at', 'updated_at', 'signin_at', 'activity_at'], 'integer'],
[['username', 'auth_key', 'password_hash', 'password_reset_token', 'email', 'description'], 'safe'],
];
}
/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = User::find();
$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;
}
$query->andFilterWhere([
'id' => $this->id,
'status' => $this->status,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'signin_at' => $this->signin_at,
'activity_at' => $this->activity_at,
]);
$query->andFilterWhere(['like', 'username', $this->username])
->andFilterWhere(['like', 'auth_key', $this->auth_key])
->andFilterWhere(['like', 'password_hash', $this->password_hash])
->andFilterWhere(['like', 'password_reset_token', $this->password_reset_token])
->andFilterWhere(['like', 'email', $this->email])
->andFilterWhere(['like', 'description', $this->description])
return $dataProvider;
}
}
I'm all for merging it. @yiisoft/core-developers opinions?
in general I support this change but I am not sure if this counts as a BC break. An application may use the old structure and may need to add new search models while adding more functionality after gii update. This causes confusion/inconsistency in the app code.
@cebe fixed lost space. Thank.
About BC I agree, but better late than never. Since the current implementation hit me.
maybe we can keep this open for 2.1 and recommend using custom template for now?
I also just noticed that this change conflicts with the description of searching for related data in the guide: http://www.yiiframework.com/doc-2.0/guide-output-data-widgets.html#working-with-model-relations you can not easily define attributes that contain a "." in plain Model.
@githubjeka :+1:
Right. So there are cons and these are significant.
In this case there is need to use the validator string instead of safe
and filter created_at and updated_at need as interval
@cebe, not in plain Model, an instance of the class ActiveDataProvider I just checked and I have everything working perfectly
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
'username',
'profile.email',
[ // Display RBAC role description
'label' => 'Роль',
'attribute' => 'role',
'value' => 'itemNames.0.description',
'filter' => User::getRoleLest(),
],
],
]); ?>
@LAV45 how did you manage to filter by profile.email using plain Model?
@cebe

class UserSearch extends Model
{
// ...
public $email;
public function rules()
{
return [
// ...
[['email'], 'string'],
];
}
public function search($params)
{
$query = User::find()
->joinWith(['itemNames', 'profile'])
->where(['status' => User::STATUS_ACTIVE]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$dataProvider->sort->attributes['email'] = [
'asc' => ['profile.email' => SORT_ASC],
'desc' => ['profile.email' => SORT_DESC],
];
if (!($this->load($params) && $this->validate())) {
return $dataProvider;
}
$query
->andFilterWhere(['like', 'email', $this->email])
->andFilterWhere(['like', 'username', $this->username])
->andFilterWhere(['item_name' => $this->role]);
return $dataProvider;
}
}
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
'username',
//'profile.email',
[
'attribute' => 'email',
'value' => 'profile.email',
],
[ // Display RBAC role description
'label' => 'Роль',
'attribute' => 'role',
'value' => 'itemNames.0.description',
'filter' => User::getRoleLest(),
],
],
]); ?>
great, thanks for sharing this.
i also paste my variant
<?php
namespace common\base;
use Yii;
use yii\base\Model;
/**
* Class SearchModel
* @package common\base
*/
abstract class SearchModel extends Model
{
/**
* @var bool|array|\yii\data\Sort
*/
public $sort;
/**
* @var bool|array|\yii\data\Pagination
*/
public $pagination;
/**
* @var \yii\db\ActiveQuery
*/
public $query;
/**
* @inheritdoc
*/
public function __construct(\yii\db\ActiveQueryInterface $query = null, $config = [])
{
$this->query = $query;
parent::__construct($config);
}
/**
* @inheritdoc
*/
public function init()
{
parent::init();
if (!$this->query) {
$this->query = $this->defaultQuery();
}
}
/**
* @return \yii\db\ActiveQuery
*/
abstract public function defaultQuery();
/**
* Creates data provider instance with search query applied
* @param array $params
* @param null|string $formName
* @return \yii\data\ActiveDataProvider
*/
public function search($params = [], $formName = null)
{
$query = $this->createQuery();
if ($this instanceof \yii\base\Model && $this->load($params) && $this->validate()) {
/** @var self $this*/
$query->filterWhere($this->filters());
}
return $this->createDataProvider($query);
}
/**
* @return \yii\db\ActiveQuery
*/
protected function createQuery()
{
return clone $this->query;
}
/**
* @param \yii\db\ActiveQuery $query
* @return \yii\data\ActiveDataProvider
*/
protected function createDataProvider($query)
{
$config = ['class' => 'yii\data\ActiveDataProvider', 'query' => $query];
if ($this->sort !== null) {
$config['sort'] = $this->sort;
}
if ($this->pagination !== null) {
$config['pagination'] = $this->pagination;
}
return Yii::createObject($config);
}
/**
* @return array
*/
public function filters()
{
return [];
}
}
and example of search model
<?php
namespace backend\models;
use Yii;
use common\base\SearchModel;
use common\models\Ticket;
/**
* TicketSearch represents the model behind the search form about `common\models\Ticket`.
*/
class TicketSearch extends SearchModel
{
public $sort = false;
public $text;
public $department;
public $status;
/**
* @inheritdoc
*/
public function defaultQuery()
{
return Ticket::find();
}
/**
* @inheritdoc
*/
public function rules()
{
return [
['department', 'in', 'range' => array_keys($this->departmentLabels())],
['status', 'in', 'range' => array_keys($this->statusLabels())],
['text', 'safe'],
];
}
/**
* @inheritdoc
*/
public function filters()
{
return ['and',
[
'status' => $this->status,
'department' => $this->department,
],
['or',
['alt' => $this->text],
['contact_jabber' => $this->text],
['contact_email' => $this->text],
['contact_icq' => $this->text],
['token' => $this->text],
['like', 'subject', $this->text],
// ['like', 'message', $this->text],
]
];
}
/**
* Labels for select tag of form
* @return array
*/
public static function departmentLabels()
{
return Ticket::departmentLabels();
}
/**
* Labels for select tag of form
* @return array
*/
public static function statusLabels()
{
return Ticket::statusLabels();
}
/**
* @inheritdoc
*/
protected function createQuery()
{
$query = parent::createQuery()
->joinWith(['lastReply' => function (\yii\db\ActiveQuery $query) {
$query->orWhere(['lastReply.created_at' => null]);
}])
->orderBy([
'ticket.status' => SORT_DESC,
'IF(lastReply.created_at IS NULL, ticket.created_at, lastReply.created_at)' => SORT_DESC,
// 'lastReply.created_at' => SORT_DESC,
]);
return $query;
}
}
query in constructor for oop style. If we can use relation as query we can use it as
$search = new UserSearch ($adminRole->getRoles()); where getRoles return ActiveRelationTrait
@lynicidn I think you better create a new issue
@LAV45 it not issue it my variant solve
Great suggestion, vote to merge this!
question. why can't we have the search as an scenario of the original model?
as far as i see it it would just be adding an 'except' => 'search' for the required rules. Am I missing anything?
@Faryshta, Scenarios only need to edit data, and search nothing should be written, that it is proposed to inherit the Search model from the parent class, and Model. Thus the class Search is the ordinary form which receives and validates the data and nothing or where no records.
@LAV45 i use scenarion in my search model.
- find all book collections (only shared)
- find my collections (shared and privat) and scenarios deny mass assign attributes as user_id in search 2 or shared in search 1 =)
@LAV45 i disagree with your statement that scenarios are meant for data edition.
The most common example for using scenarios is user login and you don't need to edit the user when it logs in, actually thats the entire point of using an scenario in that case.
@Faryshta, I understand you about this login form is now trying to tell, but there are no scenarios.
@LAV45 i was talking about this http://www.yiiframework.com/doc-2.0/guide-structure-models.html#scenarios
@Faryshta SCENARIO is a good tool for small tasks. But we are talking about Composite pattern