├── .gitignore ├── ActiveRecord.php ├── QueryHelper.php ├── README.md ├── Serializer.php └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /ActiveRecord.php: -------------------------------------------------------------------------------- 1 | resolveFields($_fields, $expandKeys) as $field => $definition) { 29 | $data[$field] = is_string($definition) ? $this->$definition : call_user_func($definition, $this, $field); 30 | } 31 | if ($this instanceof Linkable) { 32 | $data['_links'] = Link::serialize($this->getLinks()); 33 | } 34 | 35 | foreach ($expandKeys as $key) { 36 | if (isset($data[$key])) { 37 | $rel = $data[$key]; 38 | $nextFields = isset($fields[$key]) ? $fields[$key] : []; 39 | if (is_array($rel)) { 40 | foreach ($rel as $k => $v) { 41 | $data[$key][$k] = $v->toArray($nextFields, $expand[$key]); 42 | } 43 | } else if (is_object($rel)) { 44 | $data[$key] = $rel->toArray($nextFields, $expand[$key]); 45 | } else { 46 | $data[$key] = ArrayHelper::toArray($rel); 47 | } 48 | } 49 | } 50 | 51 | return $data; 52 | } 53 | } -------------------------------------------------------------------------------- /QueryHelper.php: -------------------------------------------------------------------------------- 1 | ['eq','!eq'], // 仅支持等于和不等于查询 21 | * 'name'=>'*', //支持所有查询类型 22 | * 'store.id'=>['eq'] // 关联表并查询id 23 | * ] 24 | */ 25 | public $ruleWhere = []; 26 | // 支持的排序的字段,支持关联表,例如['id','create_time','store.id'] 27 | public $ruleSort = []; 28 | 29 | // 分页下每页支持的最大数据条目数量,支持关联表。第一个元素是主表的值 例如:[20,'store'=>10,'store.storePost'=>10] 30 | public $rulePerpage = []; 31 | // 预处理查询数据集合 32 | public $prepareData = []; 33 | 34 | /* 支持的查询条件 */ 35 | private $conditions = [ 36 | 'eq', // name = 'jack' 37 | '!eq', // name != 'jack' 38 | 'like', // name LIKE '%jack%' 39 | 'llike', // name LIKE '%jack' 40 | 'rlike', // name LIKE 'jack%' 41 | 'null', // name IS NULL 42 | '!null', // name IS NOT NULL 43 | 'less_than', // money < 10 44 | 'more_than', // money > 10 45 | 'less_than_eq', // money <= 10 46 | 'more_than_eq', // money >= 10 47 | 'in', // id IN ('1','2') 48 | '!in', // id NOT IN ('1','2') 49 | ]; 50 | 51 | // 查询参数(不包含扩展参数) 52 | private $_where = []; 53 | // 扩展查询 54 | private $_extend = [ 55 | '_sort' => null, // 排序,支持多表:_sort=id,subtable.id.desc 56 | '_fields' => null, // 结果集仅包含哪些字段,支持多表:_fields=id,name,subtable.id 57 | '_expand' => null, // 欲关联查询:_expand=subtable,subtable.other 58 | ]; 59 | 60 | 61 | /* @var ActiveQuery $query */ 62 | private $_mainQuery = null; 63 | /* @var ActiveQuery $query */ 64 | private $_query = null; 65 | 66 | 67 | function __construct($modelClass) 68 | { 69 | /* @var $modelClass \yii\db\BaseActiveRecord */ 70 | $this->_mainQuery = $modelClass::find(); 71 | $this->_query = clone $this->_mainQuery; 72 | } 73 | 74 | /** 75 | * 批量设置接口开放的规则 76 | * @param array $rule 77 | */ 78 | private function setRules(array $rule) 79 | { 80 | if (isset($rule['sort'])) { 81 | $this->ruleSort = $rule['sort']; 82 | } 83 | if (isset($rule['where'])) { 84 | $this->ruleWhere = $rule['where']; 85 | } 86 | } 87 | 88 | 89 | /** 90 | * 初始化url查询参数 91 | */ 92 | private function parseParams() 93 | { 94 | $paramString = Yii::$app->request->queryString; 95 | $params = []; 96 | foreach (explode('&', $paramString) as $pair) { 97 | $tmp = explode('=', $pair, 2); 98 | if (count($tmp) === 2) { 99 | $params[$tmp[0]] = urldecode($tmp[1]); 100 | } 101 | } 102 | // 提取扩展查询参数 103 | foreach ($this->_extend as $k => $v) { 104 | if (isset($params[$k])) { 105 | $this->_extend[$k] = $params[$k]; 106 | unset($params[$k]); 107 | } 108 | } 109 | $this->_where = $params; 110 | } 111 | 112 | public function build(array $rules) 113 | { 114 | // 设置可用的查询规则 115 | $this->setRules($rules); 116 | // 解析请求参数 117 | $this->parseParams(); 118 | // 预处理关联查询 119 | $this->prepareRelated(); 120 | // 预处理搜索条件 121 | $this->prepareWhere(); 122 | // 预处理排序条件 123 | $this->prepareOrderBy(); 124 | // 应用查询参数 125 | $this->applyData(); 126 | return $this->_query; 127 | } 128 | 129 | /** 130 | * 获取请求参数,并匹配所设置的查询规则,返回格式化后的查询数据 131 | * @return array 132 | */ 133 | private function formatWhereParams() 134 | { 135 | $format = []; 136 | foreach ($this->ruleWhere as $rKey => $rVal) { 137 | foreach ($this->_where as $pKey => $pVal) { 138 | // 匹配允许查询的字段 139 | if ($pKey != $rKey) 140 | continue; 141 | 142 | // 拆解参数值 143 | $value = explode(':', $pVal, 2); 144 | if (count($value) === 2) { 145 | // 格式为:id=like:10 146 | if (in_array($value[0], $this->conditions)) { 147 | $_rule = $value[0]; 148 | $_val = $value[1]; 149 | } else { 150 | // 若查询条件不存在,默认为eq查询 151 | $_rule = 'eq'; 152 | $_val = $pVal; 153 | } 154 | } else { 155 | // 格式为:id=100 156 | if ($pVal == 'null' || $pVal == '!null') { 157 | // id = null || id = !null 158 | $_rule = $pVal; 159 | $_val = null; 160 | } else { 161 | // id = 10 162 | $_rule = 'eq'; 163 | $_val = $pVal; 164 | } 165 | } 166 | 167 | // 检测查询参数是否在ruleWhere内设置 168 | if (is_array($rVal)) { 169 | if (!in_array($_rule, $rVal)) 170 | continue; 171 | } elseif ($rVal != '*') { 172 | continue; 173 | } 174 | 175 | $format[] = [ 176 | 'rule' => $_rule, 177 | 'field' => $rKey, 178 | 'value' => $_val, 179 | ]; 180 | } 181 | } 182 | 183 | return $format; 184 | } 185 | 186 | /** 187 | * 预处理查询条件 188 | */ 189 | private function prepareWhere() 190 | { 191 | $formats = $this->formatWhereParams(); 192 | // 归类查询条件 193 | foreach ($formats as $format) { 194 | $pairs = explode('.', $format['field']); 195 | if (count($pairs) === 1) { 196 | // 主表查询条件 197 | $this->prepareData = ArrayHelper::merge($this->prepareData, [ 198 | '_' => [ 199 | 'where' => [$this->ruleConverter($format['rule'], $format['field'], $format['value'])] 200 | ] 201 | ]); 202 | } else { 203 | // 关联表 204 | $fieldName = array_pop($pairs); 205 | $relation = implode('.', $pairs); 206 | $this->prepareData = ArrayHelper::merge($this->prepareData, [ 207 | $relation => ['where' => [$this->ruleConverter($format['rule'], $fieldName, $format['value'])]] 208 | ]); 209 | } 210 | } 211 | } 212 | 213 | /** 214 | * url规则转换为Yii query格式 215 | * @param string $rule 216 | * @param string $field 217 | * @param string $value 218 | * @return array 219 | */ 220 | private function ruleConverter($rule, $field, $value) 221 | { 222 | $rules = [ 223 | 'eq' => [$field => $value], 224 | '!eq' => ['NOT', [$field => $value]], 225 | 'like' => ['LIKE', $field, $value], 226 | 'llike' => ['LIKE', $field, '%'.$value, false], 227 | 'rlike' => ['LIKE', $field, $value.'%', false], 228 | 'null' => [$field => null], 229 | '!null' => ['NOT', [$field => null]], 230 | 'less_than' => ['<', $field, $value], 231 | 'more_than' => ['>', $field, $value], 232 | 'less_than_eq' => ['<=', $field, $value], 233 | 'more_than_eq' => ['>=', $field, $value], 234 | 'in' => ['IN', $field, explode(',', $value)], 235 | '!in' => ['NOT IN', $field, explode(',', $value)], 236 | ]; 237 | 238 | return isset($rules[$rule]) ? $rules[$rule] : []; 239 | } 240 | 241 | /** 242 | * 格式化排序规则,过滤可用排序 243 | * @return array 244 | */ 245 | private function formatSortParams() 246 | { 247 | if (!$this->ruleSort || !$this->_extend['_sort']) { 248 | return []; 249 | } 250 | 251 | $sortArray = explode(',', $this->_extend['_sort']); 252 | $params = []; 253 | foreach ($sortArray as $item) { 254 | $pairs = $this->fetchSort($item); 255 | if (in_array($pairs['field'], $this->ruleSort)) { 256 | $params[$pairs['field']] = $pairs['sort']; 257 | } 258 | } 259 | return $params; 260 | } 261 | 262 | 263 | /** 264 | * 预处理排序条件,url参数格式:http://localhost?_sort=id.desc,name.asc 265 | */ 266 | private function prepareOrderBy() 267 | { 268 | $params = $this->formatSortParams(); 269 | foreach ($params as $k => $v) { 270 | $pairs = explode('.', $k); 271 | if (count($pairs) === 1) { 272 | $this->prepareData = ArrayHelper::merge($this->prepareData, [ 273 | '_' => ['orderBy' => [[$k => $v]]] 274 | ]); 275 | } else { 276 | // 关联表查询 277 | $fieldName = array_pop($pairs); 278 | $way = implode('.', $pairs); 279 | $this->prepareData = ArrayHelper::merge($this->prepareData, [ 280 | $way => ['orderBy' => [[$fieldName => $v]]] 281 | ]); 282 | } 283 | } 284 | } 285 | 286 | /** 287 | * 提取排序规则 288 | * @param string $str 289 | * @return array 290 | */ 291 | private function fetchSort($str) 292 | { 293 | $pairs = explode('.', $str); 294 | if (in_array(strtoupper(end($pairs)), ['ASC', 'DESC'])) { 295 | $sortWay = strtoupper(array_pop($pairs)); 296 | } else { 297 | $sortWay = 'ASC'; 298 | } 299 | 300 | $refs = [ 301 | 'ASC' => SORT_ASC, 302 | 'DESC' => SORT_DESC, 303 | ]; 304 | 305 | return [ 306 | 'field' => implode('.', $pairs), 307 | 'sort' => $refs[$sortWay] 308 | ]; 309 | } 310 | 311 | /** 312 | * 预处理关联表信息 313 | */ 314 | private function prepareRelated() 315 | { 316 | if (!$this->_extend['_expand']) { 317 | return; 318 | } 319 | $withs = explode(',', $this->_extend['_expand']); 320 | foreach ($withs as $item) { 321 | $this->prepareData = ArrayHelper::merge($this->prepareData, [ 322 | $item => [] 323 | ]); 324 | } 325 | } 326 | 327 | /** 328 | * 应用查询数据 329 | */ 330 | private function applyData() 331 | { 332 | if (isset($this->prepareData['_'])) { 333 | foreach ($this->prepareData['_'] as $k => $v) { 334 | if ($k == 'where') { 335 | foreach ($v as $item) { 336 | $this->_query->andWhere($item); 337 | } 338 | } elseif ($k == 'orderBy') { 339 | foreach ($v as $item) { 340 | $this->_query->addOrderBy($item); 341 | } 342 | } 343 | } 344 | } 345 | $with = []; 346 | foreach ($this->prepareData as $k => $params) { 347 | if ($k == '_') { 348 | continue; 349 | } 350 | if (empty($params)) { 351 | $with[] = $k; 352 | } else { 353 | $with[$k] = function(ActiveQuery $query) use ($params){ 354 | foreach ($params as $pkey => $pVal) { 355 | if ($pkey == 'where') { 356 | foreach ($pVal as $item) 357 | $query->andWhere($item); 358 | } elseif ($pkey == 'orderBy') { 359 | foreach ($pVal as $item) 360 | $query->addOrderBy($item); 361 | } 362 | } 363 | }; 364 | } 365 | } 366 | $this->_query->with($with); 367 | } 368 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 概述 2 | ------ 3 | 该扩展强化了Yii Restful查询。**支持无限级关联** 4 | 5 | 在关联查询数据时不是每次都回表查询,而是使用了with加载关联数据,由yii进行数据关系匹配,减少对数据库的访问。 6 | 7 | ``` 8 | 例如查询所有user下的所有books,默认yii ar模型需要循环每个user,再调用user->books进行查询,要多次访问数据库。 9 | 而使用with急切加载,只需要查询2遍即可获得所有数据。 10 | ``` 11 | URL 参数支持 12 | --- 13 | - 支持无限级关联搜索 14 | 15 | url 关键字 | 解释 | 例子 16 | ---|---|--- 17 | eq | 等于查询 | api.com?id=eq:1 等价于 api.com?id=1 18 | !eq | 不等于 | api.com?id=!eq:1,sql:id != 1 19 | like | 模糊查询| api.com?name=like:数据 ,sql:%数据% 20 | llike | 左模糊查询| api.com?name=llike:数据 ,sql:%数据 21 | rlike | 右模糊查询| api.com?name=rlike:数据 ,sql:数据% 22 | null | NULL 查询 | api.com?name=null,sql: name IS NULL 23 | !null | NOT NULL 查询 | api.com?name=!null,sql: name IS NOT NULL 24 | less_than | 小于查询 | api.com?age=less_than:10,sql: age < 10 25 | more_than | 大于查询 | api.com?age=more_than:10,sql: age > 10 26 | less_than_eq | 小于等于查询 | api.com?age=less_than_eq:10,sql: age <= 10 27 | more_than_eq | 大于等于查询 | api.com?age=more_than_eq:10,sql: age >= 10 28 | in | 范围查询 | api.com?id=in:1,2,3,sql: id IN (1,2,3) 29 | !in | 排除范围查询 | api.com?id=!in:1,2,3,sql: id NOT IN (1,2,3) 30 | 31 | demo 32 | ``` 33 | // 查询id为1的用户,以及关联的模糊查询books.name 34 | api.com/users?id=1,books.name=like:自然 35 | ``` 36 | 37 | - 支持无限级关联排序 38 | 39 | url关键字 `_sort` 40 | ``` 41 | // user表id使用asc排序(不穿排序方式默认asc),books.name使用倒序,books.author.age倒序 42 | api.com/users?id=1,books.name=like:自然&_sort=id,books.name.desc,books.author.age.desc 43 | ``` 44 | 45 | - 支持无限级字段过滤 46 | 47 | url关键字 `_fields` 48 | ``` 49 | api.com/users?_fields=id,name,books.name,books.author.name 50 | ``` 51 | 52 | - 指定返回关联数据 53 | 54 | url关键字 `_expand` 55 | 56 | 对应activeRecord内的relations。**以上所有关联查询必须先在此参数中设定,否则无法查询到关联数据** 57 | 58 | 例如需查询:books.name=like:自然,则必须先通过_expand关联books 59 | 60 | ``` 61 | // 将返回关联的books,以及books内关联的author数据 62 | api.com/users?_expand=books,books.author 63 | ``` 64 | 65 | 控制层限制 66 | ---- 67 | 为了防止预期外的查询,可设置相应的规则进行查询限制。 68 | 69 | 场景: 70 | 71 | 某些字段不允许like查询,或者只允许某个字段排序(其他字段排序可能造成性能问题) 72 | 73 | 可通过设置规则,仅对符合规则的url参数进行查询。 74 | 75 | 76 | ```php 77 | 78 | public function actionIndex() 79 | { 80 | ... 81 | $rules = [ 82 | 'where' => [ 83 | 'id' => '*', // 允许任意查询条件 84 | 'name' => ['eq'], // 只允许等于查询 85 | 'books.name'=>['like','in'] // 允许like查询和in查询 86 | ], 87 | // 只允许以下字段排序 88 | 'sort' => [ 89 | 'id', 90 | 'books.id' 91 | ] 92 | ]; 93 | ... 94 | } 95 | ``` 96 | 97 | 安装 98 | ----- 99 | 建议通过 [composer](http://getcomposer.org/download/)安装 100 | ``` 101 | composer require sndwow/yii2-rest-query-helper 102 | ``` 103 | 也可手动安装到:/vendor/sndwow/yii2-rest-query-helper 104 | 105 | 需要修改vendor/yiisoft/extensions.php,配置中追加 106 | ```php 107 | 'sndwow/yii2-rest-query-helper' => 108 | array ( 109 | 'name' => 'sndwow/yii2-rest-query-helper', 110 | 'version' => '1.0.1.0', 111 | 'alias' => 112 | array ( 113 | '@sndwow/rest' => $vendorDir . '/sndwow/yii2-rest-query-helper', 114 | ), 115 | ), 116 | ``` 117 | 118 | 使用 119 | ----- 120 | - 所有关联 AR Model 需继承 sndwow\rest\ActiveRecord 使其强化关联能力 121 | 122 | - 在控制器中指定 serializer 为 sndwow\rest\Serializer 123 | ```php 124 | class UserController extends \yii\rest\Controller 125 | { 126 | public $serializer = 'sndwow\rest\Serializer'; 127 | public function actionIndex() 128 | { 129 | // 仅在此方法指定 130 | // $this->serializer = 'sndwow\rest\Serializer'; 131 | 132 | // 使用User作为主表,也可以使用类名:new QueryHelper('app\models\User') 133 | $helper = new QueryHelper(User::className()) 134 | 135 | // 设置查询、排序规则 136 | $rules = [ 137 | 'where' => [ 138 | 'id' => '*', // 允许任意查询条件 139 | 'name' => ['eq'], // 只允许等于查询 140 | 'books.name'=>['like','in'] // 允许like查询和in查询 141 | ], 142 | // 只允许以下字段排序 143 | 'sort' => [ 144 | 'id', 145 | 'books.id' 146 | ] 147 | ]; 148 | 149 | // 应用规则,并返回query实例 150 | // 实例与普通的query一样,只不过关联了相关数据,例如可以 $query->asArray()->all() 151 | $query = $helper->build($rules); 152 | 153 | // 使用此方式返回可以被sndwow\rest\Serializer进行处理 154 | // 同时也支持yii model 里的 fields,可自定义返回字段及数据 155 | return new ActiveDataProvider([ 156 | 'query' => $query 157 | ]); 158 | } 159 | } 160 | ``` 161 | 例子: 162 | 163 | 继承 yii\rest\ActiveController 用法 164 | ```php 165 | namespace app\modules\v1\controllers; 166 | 167 | use app\modules\v1\models\Category; 168 | use app\modules; 169 | use sndwow\rest\QueryHelper; 170 | use yii\data\ActiveDataProvider; 171 | use yii\rest\ActiveController; 172 | 173 | class CategoryController extends ActiveController 174 | { 175 | public $modelClass = 'app\modules\v1\models\Category'; 176 | 177 | public function actions() 178 | { 179 | $actions = parent::actions(); 180 | unset($actions['index']); 181 | return $actions; 182 | } 183 | public function actionIndex() 184 | { 185 | $this->serializer = 'sndwow\rest\Serializer'; 186 | $rules = [ 187 | 'sort' => [ 188 | 'id', 189 | 'create_time', 190 | 'items.markets.update_time' 191 | ], 192 | 'where' => [ 193 | 'id' => '*', 194 | 'name' => ['like', 'eq'], 195 | 'items.name' => ['like'], 196 | ] 197 | ]; 198 | $helper = new QueryHelper(Category::className()); 199 | $query = $helper->build($rules); 200 | return new ActiveDataProvider([ 201 | 'query' => $query 202 | ]); 203 | } 204 | } 205 | ``` 206 | 207 | 继承 yii\rest\Controller 用法 208 | ```php 209 | namespace app\modules\v1\controllers; 210 | 211 | use app\modules\v1\models\Category; 212 | use app\modules; 213 | use sndwow\rest\QueryHelper; 214 | use yii\data\ActiveDataProvider; 215 | use yii\rest\Controller; 216 | 217 | class CategoryController extends Controller 218 | { 219 | public $modelClass = 'app\modules\v1\models\Category'; 220 | 221 | public function actionIndex() 222 | { 223 | $this->serializer = 'sndwow\rest\Serializer'; 224 | $rules = [ 225 | 'sort' => [ 226 | 'id', 227 | 'create_time', 228 | 'items.markets.update_time' 229 | ], 230 | 'where' => [ 231 | 'id' => '*', 232 | 'name' => ['like', 'eq'], 233 | 'items.name' => ['like'], 234 | ] 235 | ]; 236 | $helper = new QueryHelper(Category::className()); 237 | $query = $helper->build($rules); 238 | return new ActiveDataProvider([ 239 | 'query' => $query 240 | ]); 241 | } 242 | } 243 | ``` -------------------------------------------------------------------------------- /Serializer.php: -------------------------------------------------------------------------------- 1 | formatExpand(); 22 | $fields = $this->formatFields(); 23 | 24 | return [$fields, $expand]; 25 | } 26 | 27 | private function formatFields() 28 | { 29 | $param = $this->request->get($this->fieldsParam); 30 | $arr = is_string($param) ? preg_split('/\s*,\s*/', $param, -1, PREG_SPLIT_NO_EMPTY) : []; 31 | $fields = []; 32 | 33 | // 提取主表信息 34 | foreach ($arr as $item) { 35 | $pairs = explode('.', $item); 36 | if (count($pairs) === 1) { 37 | $fields[] = $item; 38 | } else { 39 | $fields = ArrayHelper::merge($fields, $this->inlineFields($pairs)); 40 | } 41 | } 42 | \Yii::warning($fields); 43 | return $fields; 44 | } 45 | 46 | private function inlineFields($arr) 47 | { 48 | $data = []; 49 | $val = array_shift($arr); 50 | if (!is_null($val)) { 51 | if (count($arr) === 1) { 52 | $data[$val][] = $arr[0]; 53 | } else { 54 | $data[$val] = $this->inlineFields($arr); 55 | } 56 | } 57 | return $data; 58 | } 59 | 60 | 61 | private function formatExpand() 62 | { 63 | $param = $this->request->get($this->expandParam); 64 | $arr = is_string($param) ? preg_split('/\s*,\s*/', $param, -1, PREG_SPLIT_NO_EMPTY) : []; 65 | $expand = []; 66 | foreach ($arr as $item) { 67 | $pairs = explode('.', $item); 68 | $expand = ArrayHelper::merge($expand, $this->inlineExpand($pairs)); 69 | } 70 | return $expand; 71 | } 72 | 73 | private function inlineExpand($arr) 74 | { 75 | $data = []; 76 | $val = array_shift($arr); 77 | if (!is_null($val)) { 78 | $data[$val] = $this->inlineExpand($arr); 79 | } 80 | return $data; 81 | } 82 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sndwow/yii2-rest-query-helper", 3 | "description": "扩展yii2的rest功能,使其支持更多的参数", 4 | "type": "yii2-extension", 5 | "keywords": ["yii2","extension"], 6 | "license": "BSD-4-Clause", 7 | "authors": [ 8 | { 9 | "name": "sndwow", 10 | "email": "thmod@qq.com" 11 | } 12 | ], 13 | "require": { 14 | "yiisoft/yii2": "*" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "sndwow\\rest\\": "" 19 | } 20 | } 21 | } 22 | --------------------------------------------------------------------------------