使用 SearchModel 类实现你的搜索功能

技巧库 · forecho · 于 1年前 发布 · 1790 次阅读

Yii 默认的搜索实现

在 Yii1 时代用 Gii 生成生成 Model 的时候,Model 里面就有一个 search 方法。

在 Yii2 时代用 Gii 生成生成 Model 的时候,Model 里面是没有 search 方法的,也就是说默认没给你生成搜索方法。 要想实现搜索怎么办?

Gii 点击 CRUD Generator 生成 CRUD 的时候填写 Search Model Class input 框就会生成一个针对某个 Model 的 search 文件,这个值是可选的,如果 不填写,也就不会生成这个文件。

个人觉得每个 Model 再生成一个单独的 ModelSearch 文件就感觉文件有点太多了。一直想找一个通用省事的解决方案。

这个方案再我读 DevGroup-ru/dotplant2 项目源码的时候找到答案了。 非常感谢他们的开源代码,这个项目可以借鉴的不只有这一点。

使用这个方法主要解决了:当我们想实现表搜索的时候不再需要逐个生成 ModelSearch 文件了一个 SearchModel 文件搞定所有搜索,下面我们来谈谈如何使用。

使用 SearchModel 类实现你的搜索功能

先添加这个类文件 或者使用我优化过的 SearchModel 文件:

<?php
/**
 * author     : forecho <caizhenghai@gmail.com>
 * createTime : 2015/12/29 15:33
 * description:
 */
namespace common\components;
use yii\base\InvalidParamException;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use yii\data\Pagination;
use yii\db\ActiveQuery;
/**
 * // 示例一
 *
 * ```php
 * $searchModel = new SearchModel(
 * [
 *      'model' => Topic::className(),
 *      'scenario' => 'default',
 * ]
 * );
 *
 * $dataProvider = $searchModel->search(['SearchModel' => Yii::$app->request->queryParams]);
 *
 * return $this->render('index', [
 *      'dataProvider' => $dataProvider,
 * ]);
 * ```
 *
 * // 示例二
 *
 *```php
 * $searchModel = new SearchModel(
 * [
 *      'defaultOrder' => ['id' => SORT_DESC],
 *      'model' => Topic::className(),
 *      'scenario' => 'default',
 *      'relations' => ['comment' => []], // 关联表(可以是Model里面的关联)
 *      'partialMatchAttributes' => ['title'], // 模糊查询
 *      'pageSize' => 15
 * ]
 * );
 *
 * $dataProvider = $searchModel->search(['SearchModel' => Yii::$app->request->queryParams]);
 * $dataProvider->query->andWhere([Topic::tableName() . '.user_id' => 23, Comment::tableName() . '.status' => 1]);
 *
 * return $this->render('index', [
 *      'dataProvider' => $dataProvider,
 * ]);
 * ```
 *
 * Class SearchModel
 * @package common\components
 */
class SearchModel extends Model
{
    private $attributes;
    private $attributeLabels;
    private $internalRelations;
    private $model;
    private $modelClassName;
    private $relationAttributes = [];
    private $rules;
    private $scenarios;
    public $defaultOrder;
    public $groupBy;
    public $pageSize = 20;
    public $partialMatchAttributes = []; // 模糊查询
    public $relations = [];
    /**
     * @param ActiveQuery $query
     * @param string $attribute
     * @param bool $partialMath
     */
    private function addCondition($query, $attribute, $partialMath = false)
    {
        if (isset($this->relationAttributes[$attribute])) {
            $attributeName = $this->relationAttributes[$attribute];
        } else {
            $attributeName = call_user_func([$this->modelClassName, 'tableName']) . '.' . $attribute;
        }
        $value = $this->$attribute;
        if ($value === '') {
            return;
        }
        if ($partialMath) {
            $query->andWhere(['like', $attributeName, trim($value)]);
        } else {
            $query->andWhere($this->conditionTrans($attributeName, $value));
        }
    }
    /**
     * 可以查询大于小于和IN
     * @param $attributeName
     * @param $value
     * @return array
     */
    private function conditionTrans($attributeName, $value)
    {
        switch (true) {
            case is_array($value):
                return [$attributeName => $value];
                break;
            case stripos($value, '>=') !== false:
                return ['>=', $attributeName, substr($value, 2)];
                break;
            case stripos($value, '<=') !== false:
                return ['<=', $attributeName, substr($value, 2)];
                break;
            case stripos($value, '<') !== false:
                return ['<', $attributeName, substr($value, 1)];
                break;
            case stripos($value, '>') !== false:
                return ['>', $attributeName, substr($value, 1)];
                break;
            case stripos($value, ',') !== false:
                return [$attributeName => explode(',', $value)];
                break;
            default:
                return [$attributeName => $value];
                break;
        }
    }
    /**
     * @param array $params
     * @throws \yii\base\InvalidParamException
     */
    public function __construct($params)
    {
        $this->scenario = 'search';
        parent::__construct($params);
        if ($this->model === null) {
            throw new InvalidParamException('Param "model" cannot be empty');
        }
        $this->rules = $this->model->rules();
        $this->scenarios = $this->model->scenarios();
        $this->attributeLabels = $this->model->attributeLabels();
        foreach ($this->safeAttributes() as $attribute) {
            $this->attributes[$attribute] = '';
        }
    }
    /**
     * @param string $name
     * @return mixed
     */
    public function __get($name)
    {
        if (isset($this->attributes[$name])) {
            return $this->attributes[$name];
        }
        return parent::__get($name);
    }
    /**
     * @param string $name
     * @param mixed $value
     */
    public function __set($name, $value)
    {
        if (isset($this->attributes[$name])) {
            $this->attributes[$name] = $value;
        } else {
            parent::__set($name, $value);
        }
    }
    /**
     * @return Model
     */
    public function getModel()
    {
        return $this->model;
    }
    /**
     * @param mixed $value
     */
    public function setModel($value)
    {
        if ($value instanceof Model) {
            $this->model = $value;
            $this->scenario = $this->model->scenario;
            $this->modelClassName = get_class($value);
        } else {
            $this->model = new $value;
            $this->modelClassName = $value;
        }
    }
    /**
     * @return array
     */
    public function rules()
    {
        return $this->rules;
    }
    /**
     * @return array
     */
    public function attributeLabels()
    {
        return $this->attributeLabels;
    }
    /**
     * @return array
     */
    public function scenarios()
    {
        return $this->scenarios;
    }
    /**
     * @param array $params
     * @return ActiveDataProvider
     */
    public function search($params)
    {
        $query = call_user_func([$this->modelClassName, 'find']);
        $dataProvider = new ActiveDataProvider(
            [
                'query' => $query,
                'pagination' => new Pagination(
                    [
                        'forcePageParam' => false,
                        'pageSize' => $this->pageSize,
                    ]
                ),
            ]
        );
        if (is_array($this->relations)) {
            foreach ($this->relations as $relation => $attributes) {
                $pieces = explode('.', $relation);
                $path = '';
                $parentPath = '';
                foreach ($pieces as $i => $piece) {
                    if ($i == 0) {
                        $path = $piece;
                    } else {
                        $parentPath = $path;
                        $path .= '.' . $piece;
                    }
                    if (!isset($this->internalRelations[$path])) {
                        if ($i == 0) {
                            $relationClass = call_user_func([$this->model, 'get' . $piece]);
                        } else {
                            $className = $this->internalRelations[$parentPath]['className'];
                            $relationClass = call_user_func([new $className, 'get' . $piece]);
                        }
                        $this->internalRelations[$path] = [
                            'className' => $relationClass->modelClass,
                            'tableName' => call_user_func([$relationClass->modelClass, 'tableName']),
                        ];
                    }
                }
                foreach ((array)$attributes as $attribute) {
                    $attributeName = str_replace('.', '_', $relation) . '_' . $attribute;
                    $tableAttribute = $this->internalRelations[$relation]['tableName'] . '.' . $attribute;
                    $this->rules[] = [$attributeName, 'safe'];
                    $this->scenarios[$this->scenario][] = $attributeName;
                    $this->attributes[$attributeName] = '';
                    $this->relationAttributes[$attributeName] = $tableAttribute;
                    $dataProvider->sort->attributes[$attributeName] = [
                        'asc' => [$tableAttribute => SORT_ASC],
                        'desc' => [$tableAttribute => SORT_DESC],
                    ];
                }
            }
            $query->joinWith(array_keys($this->relations));
        }
        if (is_array($this->defaultOrder)) {
            $dataProvider->sort->defaultOrder = $this->defaultOrder;
        }
        if (is_array($this->groupBy)) {
            $query->addGroupBy($this->groupBy);
        }
        $this->load($params);
        foreach ($this->attributes as $name => $value) {
            $this->addCondition($query, $name, in_array($name, $this->partialMatchAttributes));
        }
        return $dataProvider;
    }
}

到你的代码库中,我喜欢放在 namespace common\components; 命名空间下。

最基本的使用

public function actionSearch()
{
    $searchModel = new SearchModel(
        [
            'model' => Topic::className(),
            'scenario' => 'default',
        ]
    );
    
    $dataProvider = $searchModel->search(['SearchModel' => Yii::$app->request->queryParams]);
    
    return $this->render('index', [
        'dataProvider' => $dataProvider,
    ]);
}

Yii::$app->request->queryParams 必须是一维数组,对应的数据库中字段。

高级的用法

public function actionSearch()
{    
    $searchModel = new SearchModel(
        [
            'defaultOrder' => ['id' => SORT_DESC],
            'model' => Topic::className(),
            'scenario' => 'default',
            'relations' => ['comment' => []], // 关联表(可以是Model里面的关联)
            'partialMatchAttributes' => ['title'], // 模糊查询
            'pageSize' => 15
        ]
    );

    $dataProvider = $searchModel->search(['SearchModel' => Yii::$app->request->queryParams]);
    $dataProvider->query->andWhere([Topic::tableName() . '.user_id' => 23, Comment::tableName() . '.status' => 1 ]);
    
    return $this->render('index', [
        'dataProvider' => $dataProvider,
    ]);
}

本文由 forecho 创作,采用 知识共享署名 3.0 中国大陆许可协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。


如果这篇文章对您有帮助,不妨微信小额赞助我一下,让我有动力继续写出高质量的教程。

本帖已被设为精华帖!
共收到 5 条回复 Yii2高级用法
andrew#111个月前 0 个赞

@forecho 想请教下,如果我的数据集是通过存储过程查询得来的,是否也可以实现searchmodal

qq2508481#27个月前 0 个赞

'model' => Topic::className(), 菜鸟求指导这需要怎么配置

forecho#37个月前 0 个赞

@qq2508481 #2楼 看示例,Topic 是指你要搜索的 Model,示例是要搜索话题 Model,对应的是 topic 表。

如果你要搜索 user 表,对应的应该是 'model' => User::className()

我这样讲,你能理解吗?

qq2508481#47个月前 0 个赞

@forecho #3楼 非常棒!能理解。

yiier#54个月前 0 个赞

非常棒

添加回复 (需要登录)
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册