yii2-gii icon indicating copy to clipboard operation
yii2-gii copied to clipboard

Updates for Search-Form Model.

Open githubjeka opened this issue 10 years ago • 24 comments
trafficstars

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; 
    } 
} 

githubjeka avatar Jun 24 '15 08:06 githubjeka

I'm all for merging it. @yiisoft/core-developers opinions?

samdark avatar Jun 24 '15 10:06 samdark

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 avatar Jun 24 '15 12:06 cebe

@cebe fixed lost space. Thank.

About BC I agree, but better late than never. Since the current implementation hit me.

githubjeka avatar Jun 24 '15 12:06 githubjeka

maybe we can keep this open for 2.1 and recommend using custom template for now?

cebe avatar Jun 24 '15 12:06 cebe

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.

cebe avatar Jun 24 '15 21:06 cebe

@githubjeka :+1:

lav45 avatar Jun 25 '15 05:06 lav45

Right. So there are cons and these are significant.

samdark avatar Jun 25 '15 08:06 samdark

In this case there is need to use the validator string instead of safe

lav45 avatar Jun 25 '15 13:06 lav45

and filter created_at and updated_at need as interval

lynicidn avatar Jun 25 '15 14:06 lynicidn

@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 avatar Jun 25 '15 14:06 lav45

@LAV45 how did you manage to filter by profile.email using plain Model?

cebe avatar Jun 25 '15 21:06 cebe

@cebe image

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

lav45 avatar Jun 26 '15 06:06 lav45

great, thanks for sharing this.

cebe avatar Jun 26 '15 12:06 cebe

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 avatar Jun 26 '15 13:06 lynicidn

@lynicidn I think you better create a new issue

lav45 avatar Jun 26 '15 13:06 lav45

@LAV45 it not issue it my variant solve

lynicidn avatar Jun 26 '15 13:06 lynicidn

Great suggestion, vote to merge this!

creocoder avatar Jun 28 '15 14:06 creocoder

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 avatar Jul 15 '15 18:07 Faryshta

@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 avatar Jul 15 '15 22:07 lav45

@LAV45 i use scenarion in my search model.

  1. find all book collections (only shared)
  2. find my collections (shared and privat) and scenarios deny mass assign attributes as user_id in search 2 or shared in search 1 =)

lynicidn avatar Jul 16 '15 06:07 lynicidn

@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 avatar Jul 16 '15 13:07 Faryshta

@Faryshta, I understand you about this login form is now trying to tell, but there are no scenarios.

lav45 avatar Jul 16 '15 16:07 lav45

@LAV45 i was talking about this http://www.yiiframework.com/doc-2.0/guide-structure-models.html#scenarios

Faryshta avatar Jul 16 '15 18:07 Faryshta

@Faryshta SCENARIO is a good tool for small tasks. But we are talking about Composite pattern

lav45 avatar Aug 28 '17 07:08 lav45