├── .gitignore ├── .travis.yml ├── LICENSE ├── README.cn.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Commands │ ├── Command.php │ ├── CountCommand.php │ ├── DeleteCommand.php │ ├── Factory.php │ ├── FactoryInterface.php │ ├── GetCommand.php │ ├── GetsetHashCommand.php │ ├── GetsetListCommand.php │ ├── GetsetSetCommand.php │ ├── GetsetStringCommand.php │ ├── GetsetZsetCommand.php │ ├── HgetallCommand.php │ ├── HmsetCommand.php │ ├── KeysCommand.php │ ├── LpushCommand.php │ ├── LrangeCommand.php │ ├── RpushCommand.php │ ├── SaddCommand.php │ ├── SetCommand.php │ ├── SmembersCommand.php │ ├── Traits │ │ └── Existence.php │ ├── ZaddCommand.php │ └── ZrangeCommand.php ├── Examples │ ├── BaseModel.php │ ├── HashModel.php │ ├── ListModel.php │ ├── SetModel.php │ ├── StringModel.php │ └── ZsetModel.php ├── Model.php └── QueryBuilder.php └── tests ├── CommandTest.php ├── CountTest.php ├── CreationTest.php ├── DeleteTest.php ├── GetsetTest.php ├── ModelTest.php ├── QueryBuilderTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | composer.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | 4 | php: 5 | - 5.6 6 | - 7.1 7 | - 7.2 8 | 9 | services: 10 | - redis-server 11 | 12 | before_script: 13 | - composer self-update 14 | - composer install --no-interaction 15 | - redis-server --version 16 | 17 | script: 18 | - vendor/bin/phpunit 19 | 20 | matrix: 21 | fast_finish: true 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 LI Mengxiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.cn.md: -------------------------------------------------------------------------------- 1 | # 让redis操作更简单,为不同数据类型封装统一的命令 2 | 3 | [![Build Status](https://travis-ci.org/limen/redisun.svg?branch=master)](https://travis-ci.org/limen/redisun) 4 | [![Packagist](https://img.shields.io/packagist/l/limen/redisun.svg?maxAge=2592000)](https://packagist.org/packages/limen/redisun) 5 | 6 | [English](https://github.com/limen/redisun/blob/master/README.cn.md) 7 | [Wiki](https://github.com/limen/redisun/wiki) 8 | 9 | ## 特性 10 | 11 | + 为不同的数据类型封装了统一的命令,支持常用的5种数据类型:string, hash, list, set, zset 12 | + 支持类似SQL的查询方法,如where、where in等 13 | + 使用eval降低在网络通信上的时间消耗 14 | + "set"类命令均支持修改key的ttl或保留当前ttl 15 | 16 | ## 已封装的命令 17 | + create: 创建key 18 | + createNotExists: 当key不存在时创建 19 | + createExists: 当key存在时创建 20 | + insert: 类似create,支持披批量创建 21 | + insertNotExists: 批量创建,当所有key不存在时才创建 22 | + insertExists: 批量创建,当所有key存在时才创建 23 | + get: 批量获取key 24 | + getAndSet: 获取key并设置新值 25 | + find: 获取单个key 26 | + findBatch: 批量获取 27 | + update: 批量更新 28 | + destroy: 删除key 29 | + destroyBatch: 批量删除 30 | + delete: 批量删除 31 | 32 | ## 安装 33 | 34 | 推荐使用[composer](https://getcomposer.org/ "")安装 35 | 36 | ```bash 37 | composer require "limen/redisun" 38 | ``` 39 | 40 | ## 使用 41 | 42 | ``` 43 | use Limen\Redisun\Examples\HashModel; 44 | use Limen\Redisun\Examples\StringModel; 45 | 46 | $person = [ 47 | 'name' => 'martin', 48 | 'age' => '22', 49 | 'height' => '175', 50 | 'nation' => 'China', 51 | ]; 52 | $hashModel = new HashModel(); 53 | $hashModel->create(1, $person); 54 | $hashModel->find(1); // 返回 $person 55 | $hashModel->where('id',1)->first(); // 返回 $person 56 | $hashModel->where('id',1)->get(); // 返回 ['redisun:1:hash' => $person] 57 | $hashModel->where('id',1)->delete(); // 从redis数据库删除"redisun:1:hash" 58 | 59 | $nick = 'martin-walk'; 60 | 61 | $stringModel = new StringModel(); 62 | $stringModel->insert([ 63 | 'id' => 1, 64 | 'name' => 'martin' 65 | ], $nick); 66 | $stringModel->where('id',1)->first(); // 返回 $nick 67 | $stringModel->where('id',1)->get(); // 返回 ['redisun:1:string:martin' => $nick] 68 | ``` 69 | 70 | ## 概念 71 | 72 | #### _Key表征_ 73 | 74 | 每一个Model都有自己的key表征。查询时依据key表征来构造key。例如 75 | 76 | ``` 77 | school:{schoolId}:class:{classId}:members 78 | ``` 79 | 80 | 对于拥有这个key表征的Model,我们可以使用where和where in来查询redis 81 | 82 | ``` 83 | $model->where('schoolId',1)->whereIn('classId',[1,2])->get(); 84 | ``` 85 | 86 | 最终构造出的key如下。这也是将要向redis查询的key 87 | 88 | ``` 89 | school:1:class:1:members 90 | school:1:class:2:members 91 | ``` 92 | 93 | #### _Key域_ 94 | 95 | Key域是key表征中的动态部分。 96 | 97 | 以上面的key表征为例,它有两个域 98 | 99 | + schoolId 100 | + classId 101 | 102 | #### _完全key_ 103 | 104 | 一个构造出的key不包含未绑定的域时,这个key被认为是完全的。例如 105 | 106 | ``` 107 | school:1:class:2:members 108 | ``` 109 | 110 | 相反,一个不完全的key类似于 111 | 112 | ``` 113 | school:1:class:{classId}:members 114 | ``` 115 | 116 | ## 返回的数据集 117 | 118 | 批量查询时,返回的数据集是由key索引的关联数组,索引对应的值为key对应的值。 119 | 120 | 当上面构造出的两个key都存在时,返回的数据集如下 121 | 122 | ``` 123 | [ 124 | 'school:1:class:1:members' => , 125 | 'school:1:class:2:members' => , 126 | ] 127 | ``` 128 | 129 | 如果某个key不存在,数据集的索引将没有该key 130 | 131 | 数据集中元素的值的类型,根据不同的redis数据类型,可以是 132 | 133 | + string: 字符串 134 | + hash: 关联数组 135 | + list: 数组 136 | + set: 数组 137 | + zset: 数组 138 | 139 | 140 | ## 命令手册 141 | 142 | ### create 143 | 144 | 当一个model的key表征只有一个域时,可以使用该方法 145 | 146 | ttl参数可选。 147 | 148 | key表征如下的Hash类型的Model 149 | ``` 150 | user:{id}:info 151 | ``` 152 | 153 | ``` 154 | $model->create(1, [ 155 | 'name' => 'maria', 156 | 'age' => 22, 157 | ], 10); // key "user:1:info" 将在10s后过期 158 | ``` 159 | 160 | key表征如下的Zset类型的Model 161 | ``` 162 | shop:{id}:customers 163 | ``` 164 | 165 | ``` 166 | // key -> 成员, value -> 分值 167 | $model->create(1, [ 168 | 'maria' => 1, 169 | 'martin' => 2, 170 | ]); // "shop:1:customers"将不会过期 171 | ``` 172 | 173 | ### createNotExists 174 | 175 | 类似setnx,支持多种数据类型 176 | 177 | ### createExists 178 | 179 | 类似setxx,支持多种数据类型 180 | 181 | ### insert 182 | 183 | 可选参数用于检查key是否存在,使insert的行为类似setnx或setxx. 184 | 185 | key表征如下的Zset类型的Model 186 | 187 | ``` 188 | user:{id}:code 189 | ``` 190 | 191 | ``` 192 | $model->insert([ 193 | 'id' => 1, 194 | ], 10010, 20); 195 | ``` 196 | 197 | ### insertNotExists 198 | 199 | 类似createNotExists 200 | 201 | ### insertExists 202 | 203 | 类似createExists 204 | 205 | ### find 206 | 使用条件同create 207 | 208 | ``` 209 | $model->find(1); 210 | ``` 211 | 212 | ### findBatch 213 | 214 | 类型于find,返回的数据集由"id"索引 215 | 216 | ``` 217 | $model->findBatch([1,2,3]); 218 | // [ 219 | // 1 => , 220 | // 2 => , 221 | // 3 => , 222 | // ] 223 | ``` 224 | 225 | ### updateBatch 226 | 227 | 类似于findBatch. 228 | 229 | 不存在的key将被创建。 230 | 231 | 如果不传入ttl参数,key的ttl将不被改变。 232 | 233 | ``` 234 | $model->updateBatch([1,2,3], $value); 235 | ``` 236 | 237 | ### all 238 | 239 | key表征如下的model 240 | ``` 241 | user:{id}:code 242 | ``` 243 | 244 | ``` 245 | $model->all(); // 返回匹配模式user:*:code(keys user:*:code)的所有key的值 246 | ``` 247 | 248 | 249 | ### where 250 | 251 | 绑定一个key域 252 | 253 | ``` 254 | $model->where('id', 1)->where('name', 'maria'); 255 | ``` 256 | 257 | ### whereIn 258 | 259 | 类似于where,为一个key域绑定多个值 260 | 261 | ``` 262 | $model->whereIn('id', [1,2,3]); 263 | ``` 264 | 265 | ### first 266 | 267 | 获取构造出的key中第一个存在的key,如果所有构造的key都不存在,返回null 268 | 269 | ``` 270 | $model->whereIn('id', [1,2,3])->first(); // return string|array|null 271 | ``` 272 | 273 | ### update 274 | 275 | 如果key不存在,将被创建 276 | 277 | 如果不传入ttl参数,key的ttl将不被改变。 278 | 279 | ``` 280 | $model->where('id',1)->update($value); 281 | ``` 282 | 283 | ### delete 284 | 285 | 删除构造的key 286 | 287 | ``` 288 | $model->where('id',1)->delete(); 289 | ``` 290 | 291 | ### orderBy, sort 292 | 293 | 用户对返回的数据集进行排序。 294 | key表征如下的string类型的Model 295 | 296 | ``` 297 | user:{id}:code 298 | ``` 299 | 300 | ``` 301 | $model->insert([ 302 | 'id' => 1, 303 | ], 10010); 304 | $model->insert([ 305 | 'id' => 2, 306 | ], 10011); 307 | 308 | $model->whereIn('id', [1,2])->orderBy('id')->get(); 309 | // returned data set 310 | // [ 311 | // 'user:1:code' => 10010, 312 | // 'user:2:code' => 10011, 313 | // ] 314 | ``` 315 | 316 | ``` 317 | $model->newQuery()->whereIn('id', [1,2])->orderBy('id', 'desc')->get(); 318 | // returned data set 319 | // [ 320 | // 'user:2:code' => 10011, 321 | // 'user:1:code' => 10010, 322 | // ] 323 | ``` 324 | 325 | ``` 326 | $model->newQuery()->whereIn('id', [1,2])->sort(); 327 | // returned data set 328 | // [ 329 | // 'user:1:code' => 10010, 330 | // 'user:2:code' => 10011, 331 | // ] 332 | ``` 333 | 334 | ### count 335 | 336 | 返回存在的key的数量。 337 | 338 | ``` 339 | $model->where('id', 1)->count(); // 返回整数 340 | ``` 341 | 342 | ### max 343 | 344 | 返回数据集中的最大值 345 | 346 | ``` 347 | $model->where('id', 1)->max(); 348 | ``` 349 | 350 | ### min 351 | 352 | 返回数据集中的最小值 353 | 354 | ``` 355 | $model->where('id', 1)->min(); 356 | ``` 357 | 358 | ### sum 359 | 360 | 返回查询到的数据集的和 361 | 362 | ``` 363 | $model->where('id', 1)->sum(); 364 | ``` 365 | 366 | ## Predis的原生方法 367 | 368 | 当一个查询只构造出一个完整的key时,可以使用Predis的原生方法,例如 369 | 370 | // string model 371 | $model->where('id', 1)->set('maria'); 372 | 373 | // hash model 374 | $model->where('id', 1)->update([ 375 | 'name' => 'Maria', 376 | 'age' => '22', 377 | ]); 378 | // 等同于 379 | $model->where('id', 1)->hmset([ 380 | 'name' => 'Maria', 381 | 'age' => '22', 382 | ]); 383 | 384 | ## 查询构造器 385 | 386 | 负责为model构造待查询的key 387 | 388 | key表征 389 | 390 | ``` 391 | user:{id}:{name} 392 | ``` 393 | 394 | ```php 395 | $queryBuilder->whereIn('id', [1,2])->whereIn('name', ['maria', 'cat']); 396 | // 构造出的key 397 | // user:1:maria 398 | // user:1:cat 399 | // user:2:maria 400 | // user:2:cat 401 | 402 | $queryBuilder->refresh()->whereIn('id', [1,2]); 403 | // 构造出的key 404 | // user:1:{name} 405 | // user:2:{name} 406 | ``` 407 | 408 | ## 开发 409 | 410 | ### 测试 411 | 412 | ```bash 413 | $ phpunit --bootstrap tests/bootstrap.php tests/ 414 | ``` 415 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Make redis manipulations easy. Unify commands for all data types. 2 | 3 | [![Build Status](https://travis-ci.org/limen/redisun.svg?branch=master)](https://travis-ci.org/limen/redisun) 4 | [![Packagist](https://img.shields.io/packagist/l/limen/redisun.svg?maxAge=2592000)](https://packagist.org/packages/limen/redisun) 5 | 6 | [中文](https://github.com/limen/redisun/blob/master/README.cn.md) 7 | 8 | [Wiki](https://github.com/limen/redisun/wiki) 9 | 10 | [Python version](https://github.com/limen/redisun-py) 11 | 12 | ## Features 13 | 14 | + Unified commands for all data types: string, list, hash, set and zset. 15 | + support SQL like query 16 | + use "eval" to save time consumption on network. 17 | + "set" like commands all support to set new ttl or keep current ttl 18 | 19 | ## Unified commands 20 | 21 | + create: create key 22 | + createNotExists: create key when which not exists 23 | + createExists: create key when which exists 24 | + insert: similar to create except supporting multiple keys 25 | + insertNotExists: similar to createNotExists 26 | + insertExists: similar to createExists 27 | + get: get key to replace get, lrange, hgetall, smembers and zrange 28 | + getAndSet: get key and set new value 29 | + find: similar to get 30 | + findBatch: find batch 31 | + update: update keys 32 | + destroy: remove one key 33 | + destroyBatch: remove keys 34 | + delete: remove keys 35 | 36 | ## Installation 37 | 38 | Recommend to install via [composer](https://getcomposer.org/ ""). 39 | 40 | ```bash 41 | composer require "limen/redisun" 42 | ``` 43 | 44 | ## Usage 45 | 46 | ``` 47 | use Limen\Redisun\Examples\HashModel; 48 | use Limen\Redisun\Examples\StringModel; 49 | 50 | $person = [ 51 | 'name' => 'martin', 52 | 'age' => '22', 53 | 'height' => '175', 54 | 'nation' => 'China', 55 | ]; 56 | $hashModel = new HashModel(); 57 | $hashModel->create(1, $person); 58 | $hashModel->find(1); // return $person 59 | $hashModel->where('id',1)->first(); // return $person 60 | $hashModel->where('id',1)->get(); // return ['redisun:1:hash' => $person] 61 | $hashModel->where('id',1)->delete(); // remove key "redisun:1:hash" from database 62 | 63 | $nick = 'martin-walk'; 64 | 65 | $stringModel = new StringModel(); 66 | $stringModel->insert([ 67 | 'id' => 1, 68 | 'name' => 'martin' 69 | ], $nick); 70 | $stringModel->where('id',1)->first(); // return $nick 71 | $stringModel->where('id',1)->get(); // return ['redisun:1:string:martin' => $nick] 72 | ``` 73 | 74 | ## Concepts 75 | 76 | #### _Key representation_ 77 | 78 | Every model has its own key representation which tells how to build query keys. For example 79 | 80 | ``` 81 | school:{schoolId}:class:{classId}:members 82 | ``` 83 | 84 | We can use where clauses to query the Redis. 85 | 86 | ``` 87 | $model->where('schoolId',1)->whereIn('classId',[1,2])->get(); 88 | ``` 89 | 90 | The keys to query are 91 | 92 | ``` 93 | school:1:class:1:members 94 | school:1:class:2:members 95 | ``` 96 | 97 | #### _Key field_ 98 | 99 | Key field is a dynamic part of the key representation. 100 | 101 | Take the key representation above, it has two fields 102 | 103 | + schoolId 104 | + classId 105 | 106 | #### _Complete key_ 107 | 108 | When a key has no unbound field, we treat it as complete. For example 109 | 110 | ``` 111 | school:1:class:2:members 112 | ``` 113 | 114 | On the contrary, an incomplete key is similar to 115 | 116 | ``` 117 | school:1:class:{classId}:members 118 | ``` 119 | 120 | ## Returned data set 121 | 122 | The returned data set would be an associated array whose indices are the query keys. 123 | 124 | When both keys exist on Redis database, the returned data set would be 125 | 126 | ``` 127 | [ 128 | 'school:1:class:1:members' => , 129 | 'school:1:class:2:members' => , 130 | ] 131 | ``` 132 | 133 | If a key not exist, the equivalent index would be not set. 134 | 135 | The returned item's data type depends on the model's type which could be string, hash, list, set or zset. 136 | 137 | + string: string 138 | + hash: associated array 139 | + list: array 140 | + set: array 141 | + zset: array 142 | 143 | 144 | ## Methods 145 | 146 | ### create 147 | 148 | Can use when a model's key representation has only one dynamic field as its primary field. 149 | 150 | The item's ttl is optional. 151 | 152 | Hash type with key representation 153 | ``` 154 | user:{id}:info 155 | ``` 156 | 157 | ``` 158 | $model->create(1, [ 159 | 'name' => 'maria', 160 | 'age' => 22, 161 | ], 10); // the item "user:1:info" would expire after 10 seconds 162 | ``` 163 | 164 | zset type with key representation 165 | ``` 166 | shop:{id}:customers 167 | ``` 168 | 169 | ``` 170 | // key -> member, value -> score 171 | $model->create(1, [ 172 | 'maria' => 1, 173 | 'martin' => 2, 174 | ]); // the item "shop:1:customers" would not expire 175 | ``` 176 | 177 | ### createExists 178 | Similar to "setxx" but supports more data types: string, hash, set, zset and list. 179 | 180 | ### createNotExists 181 | Similar to "setnx" but supports more data types. 182 | 183 | ### insert 184 | 185 | An optional parameter make it possible to insert like "setnx" and "setxx". 186 | String type with key representation. 187 | 188 | ``` 189 | user:{id}:code 190 | ``` 191 | 192 | ``` 193 | $model->insert([ 194 | 'id' => 1, 195 | ], 10010, 20); // the item "user:1:code" would expire after 20 seconds 196 | ``` 197 | 198 | ### insertExists 199 | 200 | Similar to createExists 201 | 202 | ### insertNotExists 203 | 204 | Similar to createNotExists 205 | 206 | ### find 207 | Can use when a model's key representation has only one dynamic field as its primary field. 208 | 209 | ``` 210 | $model->find(1); 211 | ``` 212 | 213 | ### findBatch 214 | 215 | Similar to find. The returned data set are indexed by ids. 216 | ``` 217 | $model->findBatch([1,2,3]); 218 | // [ 219 | // 1 => , 220 | // 2 => , 221 | // 3 => , 222 | // ] 223 | ``` 224 | 225 | ### updateBatch 226 | 227 | Similar to findBatch. 228 | 229 | The key would be created if not exist. The key's ttl would not be modified if the ttl parameter not set. 230 | 231 | ``` 232 | $model->updateBatch([1,2,3], $value); 233 | ``` 234 | 235 | ### all 236 | 237 | key representation 238 | 239 | ``` 240 | user:{id}:code 241 | ``` 242 | 243 | ``` 244 | $model->all(); // return all keys which match pattern "user:*:code" 245 | ``` 246 | 247 | 248 | ### where 249 | 250 | Similar to SQL 251 | 252 | ``` 253 | $model->where('id', 1)->where('name', 'maria'); 254 | ``` 255 | 256 | ### whereIn 257 | 258 | Similar to where 259 | 260 | ``` 261 | $model->whereIn('id', [1,2,3]); 262 | ``` 263 | 264 | ### first 265 | 266 | Get first exist item from query keys. Return null when all query keys not exist. 267 | 268 | ``` 269 | $model->whereIn('id', [1,2,3])->first(); // return string|array|null 270 | ``` 271 | 272 | ### update 273 | 274 | The key would be created if not exist. The key's ttl would not be modified if the ttl parameter not set. 275 | 276 | ``` 277 | $model->where('id',1)->update($value); 278 | ``` 279 | 280 | ### delete 281 | 282 | Delete query keys. 283 | 284 | ``` 285 | $model->where('id',1)->delete(); 286 | ``` 287 | 288 | ### orderBy, sort 289 | 290 | string type with key representation 291 | 292 | ``` 293 | user:{id}:code 294 | ``` 295 | 296 | ``` 297 | $model->insert([ 298 | 'id' => 1, 299 | ], 10010); 300 | $model->insert([ 301 | 'id' => 2, 302 | ], 10011); 303 | 304 | $model->whereIn('id', [1,2])->orderBy('id')->get(); 305 | // returned data set 306 | // [ 307 | // 'user:1:code' => 10010, 308 | // 'user:2:code' => 10011, 309 | // ] 310 | ``` 311 | 312 | ``` 313 | $model->newQuery()->whereIn('id', [1,2])->orderBy('id', 'desc')->get(); 314 | // returned data set 315 | // [ 316 | // 'user:2:code' => 10011, 317 | // 'user:1:code' => 10010, 318 | // ] 319 | ``` 320 | 321 | ``` 322 | $model->newQuery()->whereIn('id', [1,2])->sort(); 323 | // returned data set 324 | // [ 325 | // 'user:1:code' => 10010, 326 | // 'user:2:code' => 10011, 327 | // ] 328 | ``` 329 | 330 | ### count 331 | 332 | Count the exist query keys. 333 | 334 | ``` 335 | $model->where('id', 1)->count(); // return an integer 336 | ``` 337 | 338 | ### max 339 | 340 | Get the maximum item in the returned data set. 341 | 342 | ``` 343 | $model->where('id', 1)->max(); 344 | ``` 345 | 346 | ### min 347 | 348 | Get the minimum item in the returned data set. 349 | 350 | ``` 351 | $model->where('id', 1)->min(); 352 | ``` 353 | 354 | ### sum 355 | 356 | Get the sum of the returned data set. 357 | 358 | ``` 359 | $model->where('id', 1)->sum(); 360 | ``` 361 | 362 | ## Predis native methods 363 | 364 | Predis native methods such as "sadd", "hset" can use when the query contains only one complete query key. 365 | 366 | // string model 367 | $model->where('id', 1)->set('maria'); 368 | 369 | // hash model 370 | $model->where('id', 1)->update([ 371 | 'name' => 'Maria', 372 | 'age' => '22', 373 | ]); 374 | // equals to 375 | $model->where('id', 1)->hmset([ 376 | 'name' => 'Maria', 377 | 'age' => '22', 378 | ]); 379 | 380 | ## Query builder 381 | 382 | Taking the job to build query keys for model. 383 | 384 | key representation 385 | 386 | ``` 387 | user:{id}:{name} 388 | ``` 389 | 390 | ```php 391 | $queryBuilder->whereIn('id', [1,2])->whereIn('name', ['maria', 'cat']); 392 | // built keys 393 | // user:1:maria 394 | // user:1:cat 395 | // user:2:maria 396 | // user:2:cat 397 | 398 | $queryBuilder->refresh()->whereIn('id', [1,2]); 399 | // built keys 400 | // user:1:{name} 401 | // user:2:{name} 402 | ``` 403 | 404 | ## Development 405 | 406 | ### Test 407 | 408 | ```bash 409 | $ phpunit --bootstrap tests/bootstrap.php tests/ 410 | ``` 411 | 412 | 413 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "limen/redisun", 3 | "description": "Make redis manipulations easy. Unify commands for all data types.", 4 | "type": "library", 5 | "keywords": [ 6 | "redis", 7 | "lua", 8 | "eval", 9 | "sql", 10 | "orm" 11 | ], 12 | "homepage": "https://github.com/limen/redisun", 13 | "authors": [ 14 | { 15 | "name": "LI Mengxiang", 16 | "email": "limengxiang876@gmail.com", 17 | "homepage": "https://github.com/limen" 18 | } 19 | ], 20 | "license": "MIT", 21 | "require": { 22 | "php": ">=5.5", 23 | "predis/predis": "^1.1" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "~4.8" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Limen\\Redisun\\": "src/" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Commands/Command.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Limen\Redisun\Commands; 12 | use \Exception; 13 | use Limen\Redisun\Commands\Traits\Existence; 14 | use Limen\Redisun\Model; 15 | use Predis\Command\ScriptCommand; 16 | 17 | /** 18 | * Lua script command 19 | * 20 | * Class Command 21 | * @package Limen\Redisun\Commands 22 | */ 23 | abstract class Command extends ScriptCommand 24 | { 25 | use Existence; 26 | 27 | /** 28 | * Keys to manipulate 29 | * @var array 30 | */ 31 | protected $keys; 32 | 33 | /** 34 | * Additional arguments 35 | * @var array 36 | */ 37 | protected $arguments; 38 | 39 | /** 40 | * Lua script 41 | * @var string 42 | */ 43 | protected $script; 44 | 45 | /** 46 | * Keys ttl in second 47 | * @var 48 | */ 49 | protected $ttl; 50 | 51 | /** 52 | * Command constructor. 53 | * @param array $keys 54 | * @param array $args 55 | */ 56 | public function __construct($keys = [], $args = []) 57 | { 58 | $this->keys = $keys; 59 | $this->arguments = $args; 60 | } 61 | 62 | public function getArguments() 63 | { 64 | return array_merge($this->keys, $this->arguments); 65 | } 66 | 67 | public function getKeysCount() 68 | { 69 | return count($this->keys); 70 | } 71 | 72 | /** 73 | * Set keys ttl 74 | * @param int $seconds 75 | * @return $this 76 | */ 77 | public function setTtl($seconds) 78 | { 79 | $this->ttl = $seconds; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * @return mixed 86 | */ 87 | public function getTtl() 88 | { 89 | return $this->ttl; 90 | } 91 | 92 | /** 93 | * Resolve data returned from "eval" 94 | * 95 | * @param $data 96 | * @return mixed 97 | * @throws Exception 98 | */ 99 | public function parseResponse($data) 100 | { 101 | if (empty($data)) { 102 | return []; 103 | } 104 | 105 | if (isset($data[0]) && count($data[0]) === $this->getKeysCount()) { 106 | $items = array_combine($data[0], $data[1]); 107 | 108 | return array_filter($items, [$this, 'notNil']); 109 | } 110 | 111 | throw new Exception('Error when evaluate lua script. Response is: ' . json_encode($data)); 112 | } 113 | 114 | /** 115 | * @param $item 116 | * @return bool 117 | */ 118 | protected function notNil($item) 119 | { 120 | return $item !== [] && $item !== null; 121 | } 122 | 123 | /** 124 | * @return string 125 | */ 126 | protected function joinArguments() 127 | { 128 | $joined = ''; 129 | 130 | for ($i = 1; $i <= count($this->arguments); $i++) { 131 | $joined .= "ARGV[$i],"; 132 | } 133 | 134 | return rtrim($joined, ','); 135 | } 136 | 137 | protected function getTmpKey() 138 | { 139 | return uniqid('__limen__redisun__' . time() . '__' . rand(1, 1000) . '__'); 140 | } 141 | 142 | /** 143 | * TODO: Pass ttl as an argument to reduce script memory usage on the server side. 144 | * 145 | * @param $ttl 146 | * 147 | * @return string 148 | */ 149 | protected function luaSetTtl($ttl) 150 | { 151 | if (!$ttl) { 152 | $script = ''; 153 | } elseif ($ttl == Model::TTL_PERSIST) { 154 | $script = << 14 | * 15 | * @package Limen\Redisun\Commands 16 | */ 17 | class CountCommand extends Command 18 | { 19 | /** 20 | * Gets the body of a Lua script. 21 | * 22 | * @return string 23 | */ 24 | public function getScript() 25 | { 26 | $script = << 16 | * 17 | * @package Limen\Redisun\Commands 18 | */ 19 | class DeleteCommand extends Command 20 | { 21 | /** 22 | * Gets the body of a Lua script. 23 | * 24 | * @return string 25 | */ 26 | public function getScript() 27 | { 28 | $script = << 0 then 34 | cnt = cnt + redis.call('del', unpack(ks)); 35 | end 36 | else 37 | cnt = cnt + redis.call('del', v); 38 | end 39 | end 40 | return cnt; 41 | LUA; 42 | return $script; 43 | } 44 | 45 | public function parseResponse($data) 46 | { 47 | return (int)$data; 48 | } 49 | } -------------------------------------------------------------------------------- /src/Commands/Factory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Limen\Redisun\Commands; 12 | 13 | class Factory implements FactoryInterface 14 | { 15 | /** 16 | * @param string $command redis command in lower case 17 | * @param array $keys KEYS for redis "eval" command 18 | * @param array $args ARGV for redis "eval" command 19 | * @return Command 20 | * @throws \Exception 21 | */ 22 | public function getCommand($command, $keys = [], $args = []) 23 | { 24 | $instance = null; 25 | 26 | $className = __NAMESPACE__ . '\\' . ucfirst($command) . 'Command'; 27 | 28 | if (class_exists($className)) { 29 | $instance = new $className($keys, $args); 30 | 31 | if (! $instance instanceof Command) { 32 | throw new \Exception("$className is not subclass of " . __NAMESPACE__ . '\\Command'); 33 | } 34 | } else { 35 | throw new \Exception("$className not exists"); 36 | } 37 | 38 | return $instance; 39 | } 40 | } -------------------------------------------------------------------------------- /src/Commands/FactoryInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Limen\Redisun\Commands; 12 | 13 | class GetCommand extends Command 14 | { 15 | public function getScript() 16 | { 17 | $script = <<luaSetTtl($this->getTtl()); 9 | $setTtl = $luaSetTtl ? 1 : 0; 10 | 11 | $script = <<= 0 then 26 | redis.call('expire',v,ttl) 27 | end 28 | end 29 | return {KEYS,values}; 30 | LUA; 31 | return $script; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Commands/GetsetListCommand.php: -------------------------------------------------------------------------------- 1 | luaSetTtl($this->getTtl()); 9 | $setTtl = $luaSetTtl ? 1 : 0; 10 | 11 | $script = <<= 0 then 24 | redis.call('expire',v,ttl) 25 | end 26 | end 27 | return {KEYS,values}; 28 | LUA; 29 | return $script; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Commands/GetsetSetCommand.php: -------------------------------------------------------------------------------- 1 | luaSetTtl($this->getTtl()); 9 | $setTtl = $luaSetTtl ? 1 : 0; 10 | 11 | $script = <<= 0 then 24 | redis.call('expire',v,ttl) 25 | end 26 | end 27 | return {KEYS,values}; 28 | LUA; 29 | return $script; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Commands/GetsetStringCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Limen\Redisun\Commands; 12 | 13 | class GetsetStringCommand extends Command 14 | { 15 | public function getScript() 16 | { 17 | $luaSetTtl = $this->luaSetTtl($this->getTtl()); 18 | $setTtl = $luaSetTtl ? 1 : 0; 19 | 20 | $script = <<= 0 then 29 | redis.call('expire',v,ttl) 30 | end 31 | end 32 | return {KEYS,values}; 33 | LUA; 34 | return $script; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Commands/GetsetZsetCommand.php: -------------------------------------------------------------------------------- 1 | luaSetTtl($this->getTtl()); 9 | $setTtl = $luaSetTtl ? 1 : 0; 10 | 11 | $script = <<= 0 then 26 | redis.call('expire',v,ttl) 27 | end 28 | end 29 | return {KEYS,values}; 30 | LUA; 31 | return $script; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Commands/HgetallCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Limen\Redisun\Commands; 12 | 13 | /** 14 | * Command for "hgetall" 15 | * Class HgetallCommand 16 | * @package Limen\Redisun\Commands 17 | */ 18 | class HgetallCommand extends Command 19 | { 20 | public function getScript() 21 | { 22 | $script = << 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Limen\Redisun\Commands; 12 | 13 | class HmsetCommand extends Command 14 | { 15 | public function getScript() 16 | { 17 | $luaSetTtl = $this->luaSetTtl($this->getTtl()); 18 | $setTtl = $luaSetTtl ? 1 : 0; 19 | $checkExist = $this->existenceScript; 20 | $delScript = $this->deleteScript; 21 | 22 | $script = << 0 then 37 | redis.call('expire', v, ttl); 38 | end 39 | end 40 | return {KEYS,values}; 41 | LUA; 42 | return $script; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Commands/KeysCommand.php: -------------------------------------------------------------------------------- 1 | luaSetTtl($this->getTtl()); 9 | $setTtl = $luaSetTtl ? 1 : 0; 10 | $checkScript = $this->existenceScript; 11 | $delScript = $this->deleteScript; 12 | 13 | $script = << 0 then 27 | redis.call('expire', v, ttl) 28 | end 29 | values[#values+1]=rs; 30 | end 31 | return {KEYS,values}; 32 | LUA; 33 | return $script; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Commands/LrangeCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Limen\Redisun\Commands; 12 | 13 | class RpushCommand extends Command 14 | { 15 | public function getScript() 16 | { 17 | $luaSetTtl = $this->luaSetTtl($this->getTtl()); 18 | $setTtl = $luaSetTtl ? 1 : 0; 19 | $checkScript = $this->existenceScript; 20 | $delScript = $this->deleteScript; 21 | 22 | $script = << 0 then 36 | redis.call('expire', v, ttl) 37 | end 38 | values[#values+1]=rs; 39 | end 40 | return {KEYS,values}; 41 | LUA; 42 | return $script; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Commands/SaddCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Limen\Redisun\Commands; 12 | 13 | class SaddCommand extends Command 14 | { 15 | public function getScript() 16 | { 17 | $luaSetTtl = $this->luaSetTtl($this->getTtl()); 18 | $setTtl = $luaSetTtl ? 1 : 0; 19 | $checkScript = $this->existenceScript; 20 | $delScript = $this->deleteScript; 21 | 22 | $script = << 0 then 37 | redis.call('expire',v,ttl) 38 | end 39 | values[#values+1]=rs1; 40 | else 41 | values[#values+1]=nil; 42 | end 43 | end 44 | return {KEYS,values}; 45 | LUA; 46 | return $script; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Commands/SetCommand.php: -------------------------------------------------------------------------------- 1 | luaSetTtl($this->getTtl()); 9 | $setTtl = $luaSetTtl ? '1' : '0'; 10 | $checkScript = $this->existenceScript; 11 | 12 | $script = <<= 0 then 22 | redis.call('expire', v, ttl); 23 | end 24 | end 25 | return {KEYS,values}; 26 | LUA; 27 | return $script; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Commands/SmembersCommand.php: -------------------------------------------------------------------------------- 1 | existenceScript = <<existenceScript = <<deleteScript = <<luaSetTtl($this->getTtl()); 9 | $setTtl = $luaSetTtl ? 1 : 0; 10 | $checkScript = $this->existenceScript; 11 | $delScript = $this->deleteScript; 12 | 13 | $script = << 0 then 30 | redis.call('expire', v, ttl) 31 | end 32 | values[#values+1] = rs1; 33 | else 34 | values[#values+1] = nil; 35 | end 36 | end 37 | return {KEYS,values}; 38 | LUA; 39 | return $script; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/Commands/ZrangeCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Limen\Redisun\Examples; 12 | 13 | use Limen\Redisun\Model; 14 | 15 | /** 16 | * Class BaseModel 17 | * @package Limen\Redisun\Examples 18 | * 19 | * @author LI Mengxiang 20 | */ 21 | class BaseModel extends Model 22 | { 23 | protected function initRedisClient($parameters, $options) 24 | { 25 | if (!isset($parameters['host'])) { 26 | $parameters['host'] = 'localhost'; 27 | } 28 | 29 | if (!isset($parameters['port'])) { 30 | $parameters['port'] = 6379; 31 | } 32 | 33 | if (!isset($parameters['database'])) { 34 | $parameters['database'] = 0; 35 | } 36 | 37 | parent::initRedisClient($parameters, $options); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Examples/HashModel.php: -------------------------------------------------------------------------------- 1 | $b) { 15 | return 1; 16 | } elseif ($a < $b) { 17 | return -1; 18 | } else { 19 | return 0; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Examples/ZsetModel.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Limen\Redisun; 12 | 13 | use Exception; 14 | use Limen\Redisun\Commands\Command; 15 | use Limen\Redisun\Commands\CountCommand; 16 | use Limen\Redisun\Commands\DeleteCommand; 17 | use Limen\Redisun\Commands\Factory; 18 | use Limen\Redisun\Commands\FactoryInterface; 19 | use Predis\Client as RedisClient; 20 | 21 | /** 22 | * CRUD model for redis 23 | * Class Model 24 | * 25 | * @package Limen\Redisun 26 | * 27 | * @author LI Mengxiang 28 | */ 29 | abstract class Model 30 | { 31 | const TYPE_STRING = 'string'; 32 | const TYPE_SET = 'set'; 33 | const TYPE_SORTED_SET = 'zset'; 34 | const TYPE_LIST = 'list'; 35 | const TYPE_HASH = 'hash'; 36 | 37 | // not expired ttl 38 | const TTL_PERSIST = '-1'; 39 | 40 | /** 41 | * Redis data type 42 | * @var string 43 | * Could be string, list, set, zset, hash 44 | */ 45 | protected $type; 46 | 47 | /** 48 | * Redis key representation. 49 | * users:{id}:phone e.g. 50 | * @var string 51 | */ 52 | protected $key; 53 | 54 | /** 55 | * @var string 56 | */ 57 | protected $delimiter = ':'; 58 | 59 | /** 60 | * Primary key name like database 61 | * @var string 62 | */ 63 | protected $primaryFieldName = 'id'; 64 | 65 | /** 66 | * @var string 67 | */ 68 | protected $fieldWrapper = '{}'; 69 | 70 | /** 71 | * @var QueryBuilder 72 | */ 73 | protected $queryBuilder; 74 | 75 | /** 76 | * @var FactoryInterface 77 | */ 78 | protected $commandFactory; 79 | 80 | /** 81 | * @var array 82 | */ 83 | protected $orderBys = []; 84 | 85 | /** 86 | * offset for pagination 87 | * @var int 88 | */ 89 | protected $offset; 90 | 91 | /** 92 | * limit for pagination 93 | * @var int 94 | */ 95 | protected $limit; 96 | 97 | /** 98 | * Push method for list type 99 | * @var string 100 | */ 101 | protected $listPushMethod = 'rpush'; 102 | 103 | /** 104 | * @var RedisClient 105 | */ 106 | protected $redClient; 107 | 108 | /** 109 | * if set to true, the subclass must override method compare() 110 | * @var bool 111 | */ 112 | protected $sortable = false; 113 | 114 | /** 115 | * @var array 116 | */ 117 | private $orderByFieldIndices = []; 118 | 119 | public function __construct($parameters = null, $options = null) 120 | { 121 | $this->initRedisClient($parameters, $options); 122 | $this->newQuery(); 123 | $this->setCommandFactory(); 124 | } 125 | 126 | /** 127 | * Refresh query builder 128 | * @return $this 129 | */ 130 | public function newQuery() 131 | { 132 | $this->orderBys = []; 133 | $this->limit = null; 134 | $this->offset = null; 135 | 136 | return $this->freshQueryBuilder(); 137 | } 138 | 139 | /** 140 | * @param $factory FactoryInterface 141 | */ 142 | public function setCommandFactory($factory = null) 143 | { 144 | $this->commandFactory = $factory ?: new Factory(); 145 | } 146 | 147 | /** 148 | * @return FactoryInterface 149 | */ 150 | public function getCommandFactory() 151 | { 152 | return $this->commandFactory; 153 | } 154 | 155 | /** 156 | * @return QueryBuilder 157 | */ 158 | public function getQueryBuilder() 159 | { 160 | return $this->queryBuilder; 161 | } 162 | 163 | /** 164 | * @return string 165 | */ 166 | public function getPrimaryFieldName() 167 | { 168 | return $this->primaryFieldName; 169 | } 170 | 171 | /** 172 | * Query like database 173 | * The {$bindingKey} part in the key representation would be replace by $value 174 | * @param $field string 175 | * @param $value string 176 | * @return $this 177 | */ 178 | public function where($field, $value) 179 | { 180 | $this->queryBuilder->whereEqual($field, $value); 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * @param $field 187 | * @param array $values 188 | * @return $this 189 | */ 190 | public function whereIn($field, array $values) 191 | { 192 | $this->queryBuilder->whereIn($field, $values); 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * @param $field 199 | * @param string $order 200 | * @return $this 201 | */ 202 | public function orderBy($field, $order = 'asc') 203 | { 204 | $this->orderBys[$field] = $order; 205 | 206 | return $this; 207 | } 208 | 209 | /** 210 | * @param int $offset 211 | * @return $this 212 | */ 213 | public function skip($offset) 214 | { 215 | $this->offset = $offset; 216 | 217 | return $this; 218 | } 219 | 220 | /** 221 | * @param int $limit 222 | * @return $this 223 | */ 224 | public function take($limit) 225 | { 226 | $this->limit = $limit; 227 | 228 | return $this; 229 | } 230 | 231 | /** 232 | * Get all items 233 | * @return array 234 | */ 235 | public function all() 236 | { 237 | $this->newQuery(); 238 | 239 | return $this->get(); 240 | } 241 | 242 | /** 243 | * Retrieve items 244 | * @return array 245 | */ 246 | public function get() 247 | { 248 | $data = $this->getProxy(); 249 | 250 | return $data; 251 | } 252 | 253 | /** 254 | * @see CountCommand 255 | * @return int 256 | */ 257 | public function count() 258 | { 259 | $keys = $this->prepareFuzzyKeys(); 260 | $command = $this->commandFactory->getCommand('count', $keys); 261 | 262 | return $this->executeCommand($command); 263 | } 264 | 265 | /** 266 | * @param string $order asc|desc 267 | * @return array 268 | * @throws Exception 269 | */ 270 | public function sort($order = 'asc') 271 | { 272 | $this->checkSortable(); 273 | 274 | $values = $this->get(); 275 | 276 | if (!$values) { 277 | return []; 278 | } 279 | 280 | if ($order == 'asc') { 281 | usort($values, [$this, 'compare']); 282 | } else { 283 | usort($values, [$this, 'revcompare']); 284 | } 285 | 286 | return $values; 287 | } 288 | 289 | /** 290 | * @return mixed 291 | * @throws Exception 292 | */ 293 | public function max() 294 | { 295 | $this->checkSortable(); 296 | 297 | $values = $this->get(); 298 | 299 | if (!$values) { 300 | return null; 301 | } 302 | 303 | $max = array_pop($values); 304 | 305 | foreach ($values as $v) { 306 | if ($this->compare($v, $max) === 1) { 307 | $max = $v; 308 | } 309 | } 310 | 311 | return $max; 312 | } 313 | 314 | /** 315 | * @return mixed 316 | * @throws Exception 317 | */ 318 | public function min() 319 | { 320 | $this->checkSortable(); 321 | 322 | $values = $this->get(); 323 | 324 | if (!$values) { 325 | return null; 326 | } 327 | 328 | $min = array_pop($values); 329 | 330 | foreach ($values as $v) { 331 | if ($this->compare($v, $min) === -1) { 332 | $min = $v; 333 | } 334 | } 335 | 336 | return $min; 337 | } 338 | 339 | /** 340 | * @return array 341 | */ 342 | public function getKeys() 343 | { 344 | return $this->prepareKeys(); 345 | } 346 | 347 | /** 348 | * @return array 349 | */ 350 | public function getCompleteKeys() 351 | { 352 | return $this->prepareCompleteKeys(); 353 | } 354 | 355 | /** 356 | * @return number 357 | */ 358 | public function sum() 359 | { 360 | $values = $this->get(); 361 | 362 | return array_sum($values); 363 | } 364 | 365 | /** 366 | * Retrieve first item 367 | * @return mixed|null 368 | */ 369 | public function first() 370 | { 371 | $items = $this->take(1)->get(); 372 | 373 | return $items ? array_shift($items) : null; 374 | } 375 | 376 | /** 377 | * Create an item 378 | * @param $id int|string Primary key 379 | * @param $value mixed 380 | * @param int $ttl 381 | * @param bool|null check key exists or not before create, not check if null 382 | * @return bool 383 | */ 384 | public function create($id, $value, $ttl = null, $exists = null) 385 | { 386 | $this->newQuery(); 387 | $queryKey = $this->queryBuilder->whereEqual($this->primaryFieldName, $id)->firstQueryKey(); 388 | if (!$this->isCompleteKey($queryKey)) { 389 | return false; 390 | } 391 | 392 | return $this->insertProxy($queryKey, $value, $ttl, $exists); 393 | } 394 | 395 | /** 396 | * Similar to setnx 397 | * @param $id 398 | * @param $value 399 | * @param null $ttl 400 | * @return bool 401 | */ 402 | public function createNotExists($id, $value, $ttl = null) 403 | { 404 | return $this->create($id, $value, $ttl, false); 405 | } 406 | 407 | /** 408 | * Similar to setxx 409 | * @param $id 410 | * @param $value 411 | * @param null $ttl 412 | * @return bool 413 | */ 414 | public function createExists($id, $value, $ttl = null) 415 | { 416 | return $this->create($id, $value, $ttl, true); 417 | } 418 | 419 | /** 420 | * @param array $bindings 421 | * @param $value 422 | * @param int $ttl 423 | * @param bool $exists 424 | * @return mixed 425 | */ 426 | public function insert(array $bindings, $value, $ttl = null, $exists = null) 427 | { 428 | $this->newQuery(); 429 | 430 | foreach ($bindings as $k => $v) { 431 | $this->queryBuilder->whereEqual($k, $v); 432 | } 433 | 434 | $queryKey = $this->queryBuilder->firstQueryKey(); 435 | 436 | if (!$this->isCompleteKey($queryKey)) { 437 | return false; 438 | } 439 | 440 | return $this->insertProxy($queryKey, $value, $ttl, $exists); 441 | } 442 | 443 | /** 444 | * Insert when key exists 445 | * 446 | * @param array $bindings 447 | * @param $value 448 | * @param null $ttl 449 | * @return mixed 450 | */ 451 | public function insertExists(array $bindings, $value, $ttl = null) 452 | { 453 | return $this->insert($bindings, $value, $ttl, true); 454 | } 455 | 456 | /** 457 | * Insert when key not exists 458 | * 459 | * @param array $bindings 460 | * @param $value 461 | * @param null $ttl 462 | * @return mixed 463 | */ 464 | public function insertNotExists(array $bindings, $value, $ttl = null) 465 | { 466 | return $this->insert($bindings, $value, $ttl, false); 467 | } 468 | 469 | /** 470 | * find an item 471 | * @param $id int|string Primary key 472 | * @return mixed 473 | */ 474 | public function find($id) 475 | { 476 | $this->newQuery(); 477 | 478 | $this->queryBuilder->whereEqual($this->primaryFieldName, $id); 479 | 480 | $queryKey = $this->queryBuilder->firstQueryKey(); 481 | 482 | if (!$this->isCompleteKey($queryKey)) { 483 | return null; 484 | } 485 | 486 | list($method, $parameters) = $this->getFindMethodAndParameters(); 487 | 488 | array_unshift($parameters, $queryKey); 489 | $value = call_user_func_array([$this->redClient, $method], $parameters); 490 | 491 | return $value; 492 | } 493 | 494 | /** 495 | * Update items, need to use where() first 496 | * @param $value 497 | * @param int $ttl ttl in second 498 | * @return mixed 499 | */ 500 | public function update($value, $ttl = null) 501 | { 502 | $queryKeys = $this->prepareKeys(false); 503 | 504 | if (count($queryKeys)) { 505 | return $this->updateBatchProxy($queryKeys, $value, $ttl); 506 | } 507 | 508 | return false; 509 | } 510 | 511 | /** 512 | * Delete items 513 | * 514 | * @see DeleteCommand 515 | * @return bool|int 516 | */ 517 | public function delete() 518 | { 519 | $queryKeys = $this->prepareFuzzyKeys(); 520 | if (empty($queryKeys)) { 521 | return false; 522 | } 523 | 524 | $command = $this->commandFactory->getCommand('delete', $queryKeys); 525 | return $this->executeCommand($command); 526 | } 527 | 528 | /** 529 | * Destroy item 530 | * @param string $id primary key 531 | * @return bool 532 | * @throws Exception 533 | */ 534 | public function destroy($id) 535 | { 536 | $this->newQuery(); 537 | 538 | $queryKey = $this->queryBuilder->whereEqual($this->primaryFieldName, $id)->firstQueryKey(); 539 | 540 | if (!$this->isCompleteKey($queryKey)) { 541 | return false; 542 | } 543 | 544 | return (bool)$this->redClient->del([$queryKey]); 545 | } 546 | 547 | /** 548 | * @param array $ids primary keys 549 | * @return array 550 | * @throws Exception 551 | */ 552 | public function findBatch(array $ids) 553 | { 554 | $primaryKeys = []; 555 | 556 | foreach ($ids as $id) { 557 | $primaryKeys[$id] = $this->getPrimaryKey($id); 558 | } 559 | 560 | $this->newQuery()->whereIn($this->getPrimaryFieldName(), $ids); 561 | 562 | $queryKeys = $this->prepareCompleteKeys(); 563 | 564 | if (!$queryKeys) { 565 | return []; 566 | } 567 | 568 | $data = $this->getProxy($queryKeys); 569 | 570 | $list = []; 571 | 572 | foreach ($data as $k => $v) { 573 | $id = array_search($k, $primaryKeys); 574 | 575 | if ($id) { 576 | $list[$id] = $v; 577 | } 578 | } 579 | 580 | return $list; 581 | } 582 | 583 | /** 584 | * @param array $ids primary keys 585 | * @return int 586 | */ 587 | public function destroyBatch(array $ids) 588 | { 589 | $this->newQuery()->whereIn($this->getPrimaryFieldName(), $ids); 590 | 591 | $queryKeys = $this->prepareCompleteKeys(); 592 | 593 | if (!$queryKeys) { 594 | return false; 595 | } 596 | 597 | return $this->redClient->del($queryKeys); 598 | } 599 | 600 | /** 601 | * @param array $ids primary keys 602 | * @param $value 603 | * @param int|null $ttl 604 | * @return mixed 605 | */ 606 | public function updateBatch(array $ids, $value, $ttl = null) 607 | { 608 | $this->newQuery()->whereIn($this->getPrimaryFieldName(), $ids); 609 | $queryKeys = $this->prepareCompleteKeys(); 610 | if (!$queryKeys) { 611 | return false; 612 | } 613 | 614 | return $this->updateBatchProxy($queryKeys, $value, $ttl); 615 | } 616 | 617 | /** 618 | * Get key and set new value 619 | * 620 | * @param string|array $value 621 | * @param null $ttl 622 | * @return mixed 623 | * @throws Exception 624 | */ 625 | public function getAndSet($value, $ttl = null) 626 | { 627 | $keys = $this->queryBuilder->getQueryKeys(); 628 | if (count($keys) > 1) { 629 | throw new Exception('GetAndSet doesnt support multiple keys'); 630 | } elseif (count($keys) == 0) { 631 | throw new Exception('No query keys'); 632 | } 633 | $key = $keys[0]; 634 | if (!$this->isCompleteKey($key)) { 635 | throw new Exception('Not complete key'); 636 | } 637 | 638 | $value = $this->castValueForUpdate($value); 639 | $commandName = 'getset' . ucfirst($this->type); 640 | $command = $this->commandFactory->getCommand($commandName, [$key], $value); 641 | if (!is_null($ttl)) { 642 | $command->setTtl($ttl); 643 | } 644 | $result = $this->executeCommand($command); 645 | $data = isset($result[$key]) ? $result[$key] : null; 646 | if ($data && $this->type == static::TYPE_HASH) { 647 | $data = $this->resolveHash($data); 648 | } 649 | 650 | return $data; 651 | 652 | } 653 | 654 | /** 655 | * Call Predis function 656 | * @param $method 657 | * @param array $parameters 658 | * @return mixed 659 | * @throws \Exception 660 | */ 661 | public function __call($method, $parameters = []) 662 | { 663 | $keys = $this->queryBuilder->getQueryKeys(); 664 | 665 | if (count($keys) > 1) { 666 | throw new Exception('More than one key had been built and redis built-in method "' . $method . '" dont support batch operation.'); 667 | } elseif (count($keys) === 0) { 668 | throw new Exception('No query keys had been built, need to use where() first.'); 669 | } 670 | 671 | array_unshift($parameters, $keys[0]); 672 | return call_user_func_array([$this->redClient, $method], $parameters); 673 | } 674 | 675 | /** 676 | * Compare items to sort 677 | * @param $a 678 | * @param $b 679 | * @return int 1.a>b 0.a=b -1.a $b ? 1 : ($a == $b ? 0 : -1); 684 | } 685 | 686 | /** 687 | * @param $a 688 | * @param $b 689 | * @return int 690 | */ 691 | protected function revcompare($a, $b) 692 | { 693 | return -$this->compare($a, $b); 694 | } 695 | 696 | /** 697 | * Initialize redis client 698 | * 699 | * @param $parameters 700 | * @param $options 701 | */ 702 | protected function initRedisClient($parameters, $options) 703 | { 704 | $this->redClient = new RedisClient($parameters, $options); 705 | } 706 | 707 | /** 708 | * Prepare query keys 709 | * @param bool $forGet 710 | * @return array 711 | */ 712 | protected function prepareKeys($forGet = true) 713 | { 714 | $queryKeys = $this->prepareFuzzyKeys(); 715 | $existKeys = $this->getExistKeys($queryKeys); 716 | 717 | if ($forGet) { 718 | $this->setOrderByFieldIndices(); 719 | 720 | if ($this->orderByFieldIndices) { 721 | uasort($existKeys, [$this, 'sortByFields']); 722 | } 723 | 724 | if ($this->offset || $this->limit) { 725 | $existKeys = array_slice($existKeys, (int)$this->offset, $this->limit); 726 | } 727 | } 728 | 729 | return $existKeys; 730 | } 731 | 732 | /** 733 | * @return array 734 | */ 735 | protected function prepareFuzzyKeys() 736 | { 737 | $queryKeys = $this->queryBuilder->getQueryKeys(); 738 | 739 | // Caution! Would get all items. 740 | if (!$queryKeys) { 741 | $queryKeys = [$this->key]; 742 | } 743 | 744 | return $this->markUnboundFields($queryKeys); 745 | } 746 | 747 | /** 748 | * @return array 749 | */ 750 | protected function prepareCompleteKeys() 751 | { 752 | $keys = $this->queryBuilder->getQueryKeys(); 753 | 754 | if (!$keys) { 755 | return []; 756 | } 757 | 758 | return array_filter($keys, [$this, 'isCompleteKey']); 759 | } 760 | 761 | /** 762 | * @param $key 763 | * @return bool 764 | */ 765 | protected function isCompleteKey($key) 766 | { 767 | return !$this->hasUnboundField($key); 768 | } 769 | 770 | /** 771 | * @param $key 772 | * @param $value 773 | * @param null $ttl 774 | * @param null|bool $exists 775 | * @return bool 776 | */ 777 | protected function insertProxy($key, $value, $ttl = null, $exists = null) 778 | { 779 | $method = $this->getUpdateMethod(); 780 | if (!$method) { 781 | return false; 782 | } 783 | 784 | $value = $this->castValueForUpdate($value); 785 | $command = $this->commandFactory->getCommand($method, [$key], $value); 786 | if ($ttl) { 787 | $command->setTtl($ttl); 788 | } 789 | 790 | if ($exists === false) { 791 | $command->pleaseNotExists(); 792 | } elseif ($exists === true) { 793 | $command->pleaseExists(); 794 | } 795 | $command->pleaseDeleteIfExists(); 796 | $response = $this->executeCommand($command); 797 | 798 | return isset($response[$key]) && $response[$key]; 799 | } 800 | 801 | /** 802 | * @param $keys 803 | * @param $value 804 | * @param int $ttl ttl in second 805 | * @return bool 806 | */ 807 | protected function updateBatchProxy($keys, $value, $ttl = null) 808 | { 809 | $method = $this->getUpdateMethod(); 810 | if (empty($method)) { 811 | return false; 812 | } 813 | 814 | $value = $this->castValueForUpdate($value); 815 | $command = $this->commandFactory->getCommand($method, $keys, $value); 816 | $command->pleaseDeleteIfExists(); 817 | if ($ttl) { 818 | $command->setTtl($ttl); 819 | } 820 | 821 | return $this->executeCommand($command); 822 | } 823 | 824 | /** 825 | * @param $queryKeys 826 | * @return array 827 | */ 828 | protected function getProxy($queryKeys = null) 829 | { 830 | if ($queryKeys === null) { 831 | $queryKeys = $this->prepareKeys(); 832 | } 833 | 834 | $data = []; 835 | if ($queryKeys) { 836 | list($method, $params) = $this->getFindMethodAndParameters(); 837 | $command = $this->commandFactory->getCommand($method, $queryKeys); 838 | $data = $this->executeCommand($command); 839 | } 840 | if ($data && $this->type == static::TYPE_HASH) { 841 | $data = $this->resolveHashes($data); 842 | } 843 | 844 | return $data; 845 | } 846 | 847 | /** 848 | * @return $this 849 | */ 850 | protected function freshQueryBuilder() 851 | { 852 | $this->queryBuilder = new QueryBuilder($this->key); 853 | 854 | $keyParts = $this->explodeKey($this->key); 855 | 856 | foreach ($keyParts as $part) { 857 | if ($this->isUnboundField($part)) { 858 | $this->queryBuilder->setFieldNeedle($this->trimWrapper($part), $part); 859 | } 860 | } 861 | 862 | return $this; 863 | } 864 | 865 | /** 866 | * Get update method according to redis data type 867 | * 868 | * @return string 869 | */ 870 | protected function getUpdateMethod() 871 | { 872 | $method = ''; 873 | switch ($this->type) { 874 | case 'string': 875 | $method = 'set'; 876 | break; 877 | case 'list': 878 | $method = $this->listPushMethod; 879 | break; 880 | case 'set': 881 | $method = 'sadd'; 882 | break; 883 | case 'zset': 884 | $method = 'zadd'; 885 | break; 886 | case 'hash': 887 | $method = 'hmset'; 888 | break; 889 | default: 890 | break; 891 | } 892 | 893 | return $method; 894 | } 895 | 896 | /** 897 | * Cast value data type for update according to redis data type 898 | * 899 | * @param $value 900 | * @return array 901 | */ 902 | protected function castValueForUpdate($value) 903 | { 904 | switch ($this->type) { 905 | case 'string': 906 | $value = [(string)$value]; 907 | break; 908 | case 'list': 909 | case 'set': 910 | $value = (array)$value; 911 | break; 912 | case 'zset': 913 | $casted = []; 914 | foreach ($value as $k => $v) { 915 | $casted[] = $v; 916 | $casted[] = $k; 917 | } 918 | $value = $casted; 919 | break; 920 | case 'hash': 921 | $casted = []; 922 | foreach ($value as $k => $v) { 923 | $casted[] = $k; 924 | $casted[] = $v; 925 | } 926 | $value = $casted; 927 | break; 928 | default: 929 | break; 930 | } 931 | 932 | return $value; 933 | } 934 | 935 | /** 936 | * Get find method and default parameters according to redis data type. 937 | * @return array 938 | */ 939 | protected function getFindMethodAndParameters() 940 | { 941 | $method = ''; 942 | $parameters = []; 943 | 944 | switch ($this->type) { 945 | case 'string': 946 | $method = 'get'; 947 | break; 948 | case 'list': 949 | $method = 'lrange'; 950 | $parameters = [0, -1]; 951 | break; 952 | case 'set': 953 | $method = 'smembers'; 954 | break; 955 | case 'zset': 956 | $method = 'zrange'; 957 | $parameters = [0, -1]; 958 | break; 959 | case 'hash': 960 | $method = 'hgetall'; 961 | break; 962 | default: 963 | break; 964 | } 965 | 966 | return [$method, $parameters]; 967 | } 968 | 969 | /** 970 | * Get existed keys in redis database 971 | * 972 | * @param $queryKeys 973 | * @return array|mixed 974 | */ 975 | protected function getExistKeys($queryKeys) 976 | { 977 | $exist = []; 978 | 979 | if ($queryKeys) { 980 | $command = $this->commandFactory->getCommand('keys', $queryKeys); 981 | 982 | $exist = $this->executeCommand($command); 983 | 984 | $exist = array_unique($exist); 985 | } 986 | 987 | return $exist; 988 | } 989 | 990 | /** 991 | * @param Command $command 992 | * @return mixed 993 | */ 994 | protected function executeCommand($command) 995 | { 996 | $evalArgs = $command->getArguments(); 997 | array_unshift($evalArgs, $command->getKeysCount()); 998 | 999 | try { 1000 | array_unshift($evalArgs, sha1($command->getScript())); 1001 | $data = call_user_func_array([$this->redClient, 'evalsha'], $evalArgs); 1002 | } catch (\Exception $e) { 1003 | if (strpos($e->getMessage(), 'NOSCRIPT') !== false) { 1004 | $evalArgs[0] = $command->getScript(); 1005 | $data = call_user_func_array([$this->redClient, 'eval'], $evalArgs); 1006 | } else { 1007 | throw $e; 1008 | } 1009 | } 1010 | 1011 | $data = $command->parseResponse($data); 1012 | 1013 | return $data; 1014 | } 1015 | 1016 | /** 1017 | * Check a key whether has unbound field 1018 | * 1019 | * @param $key 1020 | * @return bool 1021 | */ 1022 | protected function hasUnboundField($key) 1023 | { 1024 | $parts = $this->explodeKey($key); 1025 | 1026 | foreach ($parts as $part) { 1027 | if ($this->isUnboundField($part)) { 1028 | return true; 1029 | } 1030 | } 1031 | 1032 | return false; 1033 | } 1034 | 1035 | /** 1036 | * @param string $part key particle 1037 | * @return bool|string 1038 | */ 1039 | protected function getFieldName($part) 1040 | { 1041 | if ($this->isUnboundField($part)) { 1042 | return substr($part, 1, -1); 1043 | } 1044 | 1045 | return false; 1046 | } 1047 | 1048 | /** 1049 | * Mark unbound field with * 1050 | * 1051 | * @param $keys 1052 | * @return array 1053 | */ 1054 | protected function markUnboundFields($keys) 1055 | { 1056 | $marked = []; 1057 | 1058 | foreach ($keys as $key) { 1059 | $parts = $this->explodeKey($key); 1060 | 1061 | foreach ($parts as &$part) { 1062 | if ($this->isUnboundField($part)) { 1063 | $part = '*'; 1064 | } 1065 | } 1066 | 1067 | $marked[] = $this->joinToKey($parts); 1068 | } 1069 | 1070 | return $marked; 1071 | } 1072 | 1073 | /** 1074 | * Compare two keys by key field(s) 1075 | * 1076 | * @param $key1 1077 | * @param $key2 1078 | * @return int 1079 | */ 1080 | protected function sortByFields($key1, $key2) 1081 | { 1082 | $key1Parts = $this->explodeKey($key1); 1083 | $key2Parts = $this->explodeKey($key2); 1084 | 1085 | $flag = 0; 1086 | 1087 | foreach ($this->orderByFieldIndices as $index => $order) { 1088 | if ($flag !== 0) { 1089 | break; 1090 | } 1091 | 1092 | if ($key1Parts[$index] > $key2Parts[$index]) { 1093 | $flag = $order == 'asc' ? 1 : -1; 1094 | } elseif ($key1Parts[$index] < $key2Parts[$index]) { 1095 | $flag = $order == 'asc' ? -1 : 1; 1096 | } else { 1097 | $flag = 0; 1098 | } 1099 | } 1100 | 1101 | return $flag; 1102 | } 1103 | 1104 | /** 1105 | * @param string $field 1106 | * @return string 1107 | */ 1108 | protected function getFieldNeedle($field) 1109 | { 1110 | return $this->fieldWrapper[0] . $field . $this->fieldWrapper[1]; 1111 | } 1112 | 1113 | /** 1114 | * @param $id 1115 | * @return mixed 1116 | */ 1117 | protected function getPrimaryKey($id) 1118 | { 1119 | return str_replace($this->getFieldNeedle($this->getPrimaryFieldName()), $id, $this->key); 1120 | } 1121 | 1122 | /** 1123 | * @throws Exception 1124 | */ 1125 | private function checkSortable() 1126 | { 1127 | if (!$this->sortable) { 1128 | throw new Exception(get_class($this) . ' is not sortable.'); 1129 | } 1130 | } 1131 | 1132 | /** 1133 | * Set order by field and order 1134 | */ 1135 | private function setOrderByFieldIndices() 1136 | { 1137 | $keyParts = $this->explodeKey($this->key); 1138 | 1139 | foreach ($this->orderBys as $field => $order) { 1140 | $needle = $this->fieldWrapper[0] . $field . $this->fieldWrapper[1]; 1141 | $this->orderByFieldIndices[array_search($needle, $keyParts)] = $order; 1142 | } 1143 | } 1144 | 1145 | /** 1146 | * @param $key 1147 | * @return array 1148 | */ 1149 | private function explodeKey($key) 1150 | { 1151 | return explode($this->delimiter, $key); 1152 | } 1153 | 1154 | /** 1155 | * @param $parts 1156 | * @return string 1157 | */ 1158 | private function joinToKey($parts) 1159 | { 1160 | return join($this->delimiter, $parts); 1161 | } 1162 | 1163 | /** 1164 | * @param $part 1165 | * @return bool 1166 | */ 1167 | private function isUnboundField($part) 1168 | { 1169 | return $this->fieldWrapper[0] === $part[0] 1170 | && $this->fieldWrapper[1] === $part[strlen($part) - 1]; 1171 | } 1172 | 1173 | /** 1174 | * @param $part 1175 | * @return bool|string 1176 | */ 1177 | private function trimWrapper($part) 1178 | { 1179 | return substr($part, 1, -1); 1180 | } 1181 | 1182 | /** 1183 | * raw hash data to associate array 1184 | * @param array $hashes 1185 | * @return array 1186 | */ 1187 | private function resolveHashes(array $hashes) 1188 | { 1189 | $assoc = []; 1190 | foreach ($hashes as $k => $hash) { 1191 | $item = $this->resolveHash($hash); 1192 | if ($item) { 1193 | $assoc[$k] = $item; 1194 | } 1195 | } 1196 | 1197 | return $assoc; 1198 | } 1199 | 1200 | /** 1201 | * @param $hash 1202 | * @return array 1203 | */ 1204 | private function resolveHash($hash) 1205 | { 1206 | $array = []; 1207 | for ($i = 0; $i < count($hash); $i = $i + 2) { 1208 | $array[$hash[$i]] = $hash[$i + 1]; 1209 | } 1210 | 1211 | return $array; 1212 | } 1213 | } 1214 | -------------------------------------------------------------------------------- /src/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Limen\Redisun; 12 | use \Exception; 13 | 14 | /** 15 | * Build redis keys for model 16 | * Class QueryBuilder 17 | * @package Limen\Redisun 18 | * 19 | * @author LI Mengxiang 20 | */ 21 | class QueryBuilder 22 | { 23 | /** 24 | * Key representation 25 | * @var string 26 | */ 27 | protected $key; 28 | 29 | /** 30 | * Built keys 31 | * @var array 32 | */ 33 | protected $queryKeys = []; 34 | 35 | /** 36 | * Where in pairs 37 | * @var array 38 | */ 39 | protected $whereIns = []; 40 | 41 | /** 42 | * Where between pairs 43 | * @var array 44 | */ 45 | protected $whereBetweens = []; 46 | 47 | /** 48 | * @var array 49 | */ 50 | protected $fieldNeedles = []; 51 | 52 | /** 53 | * QueryBuilder constructor. 54 | * @param string $key e.g. user:{id}:name 55 | */ 56 | public function __construct($key) 57 | { 58 | $this->key = $key; 59 | } 60 | 61 | /** 62 | * @param string $field e.g. id 63 | * @param string $needle e.g. {id} 64 | * @return $this 65 | */ 66 | public function setFieldNeedle($field, $needle) 67 | { 68 | $this->fieldNeedles[$field] = $needle; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * Get valid keys have been built 75 | * @return array 76 | */ 77 | public function getQueryKeys() 78 | { 79 | $this->getValidQueryKeys(); 80 | 81 | return $this->queryKeys; 82 | } 83 | 84 | /** 85 | * Get first key 86 | * @return string|null 87 | */ 88 | public function firstQueryKey() 89 | { 90 | $this->getValidQueryKeys(); 91 | 92 | return $this->queryKeys ? $this->queryKeys[0] : null; 93 | } 94 | 95 | /** 96 | * @param string $bindingKey 97 | * @param string $value 98 | * @return QueryBuilder 99 | */ 100 | public function whereEqual($bindingKey, $value) 101 | { 102 | return $this->whereIn($bindingKey, [$value]); 103 | } 104 | 105 | /** 106 | * @param $field string 107 | * @param string[] $values 108 | * @return $this 109 | */ 110 | public function whereIn($field, array $values) 111 | { 112 | $this->whereIns[$field] = isset($this->whereIns[$field]) ? 113 | array_merge($this->whereIns[$field], $values) : $values; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * @param string $key 120 | * @param array $range [min,max] 121 | * @return $this 122 | * @throws Exception 123 | */ 124 | public function whereBetween($key, $range) 125 | { 126 | if (!is_int($range[0]) || !is_int($range[1])) { 127 | throw new Exception('whereBetween parameters must be integer'); 128 | } 129 | 130 | if ($range[1] < $range[0]) { 131 | throw new Exception('whereBetween up bound must be greater than or equal to low bound'); 132 | } 133 | 134 | $this->whereBetweens[$key] = $range; 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * Refresh query builder to reuse it 141 | * 142 | * @return $this 143 | */ 144 | public function refresh() 145 | { 146 | $this->queryKeys = []; 147 | $this->whereIns = []; 148 | $this->whereBetweens = []; 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * Get valid query keys 155 | * @return $this 156 | */ 157 | protected function getValidQueryKeys() 158 | { 159 | $whereIns = []; 160 | 161 | foreach ($this->whereBetweens as $field => $range) { 162 | $whereIns[$field] = range($range[0], $range[1]); 163 | } 164 | 165 | foreach ($whereIns as $key => $value) { 166 | $this->whereIn($key, $value); 167 | } 168 | 169 | $this->queryKeys = []; 170 | 171 | foreach ($this->whereIns as $field => $range) { 172 | if ($this->queryKeys === []) { 173 | foreach ($range as $value) { 174 | $this->queryKeys[] = $this->bindValue($this->key, $field, $value); 175 | } 176 | } else { 177 | $queryKeys = $this->queryKeys; 178 | $this->queryKeys = []; 179 | foreach ($queryKeys as $item) { 180 | foreach ($range as $value) { 181 | $this->queryKeys[] = $this->bindValue($item, $field, $value); 182 | } 183 | } 184 | } 185 | } 186 | 187 | $this->queryKeys = array_unique(array_values($this->queryKeys)); 188 | 189 | return $this; 190 | } 191 | 192 | /** 193 | * @param string $queryKey 194 | * @param string $field 195 | * @param string $value 196 | * @return string 197 | */ 198 | protected function bindValue($queryKey, $field, $value) 199 | { 200 | return str_replace($this->fieldNeedles[$field], $value, $queryKey); 201 | } 202 | } -------------------------------------------------------------------------------- /tests/CommandTest.php: -------------------------------------------------------------------------------- 1 | getCommand('rpush', [$arguments[0], $arguments[1]], array_slice($arguments, 2)); 17 | 18 | $this->assertEquals(2, $command->getKeysCount()); 19 | 20 | $this->assertEquals($arguments, $command->getArguments()); 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /tests/CountTest.php: -------------------------------------------------------------------------------- 1 | insert(['id' => 1, 'name' => 'hello'], 'world1', 60); 12 | $model->insert(['id' => 2, 'name' => 'hello'], 'world2', 70); 13 | $model->insert(['id' => 3, 'name' => 'hello'], 'world3', 80); 14 | $cnt1 = $model->whereIn('id', ['1','2','3'])->count(); 15 | $cnt2 = $model->where('name', 'hello')->count(); 16 | $this->assertEquals($cnt1, 3); 17 | $this->assertEquals($cnt2, 3); 18 | } 19 | } -------------------------------------------------------------------------------- /tests/CreationTest.php: -------------------------------------------------------------------------------- 1 | 'maria', 15 | 'age' => 18, 16 | ]; 17 | $hash->create(1, $girl, 100); 18 | $this->assertFalse($hash->createNotExists(1, $girl, 100)); 19 | $this->assertTrue($hash->createExists(1, $girl, 100)); 20 | } catch (Exception $e) { 21 | throw $e; 22 | } finally { 23 | $hash->destroy(1); 24 | } 25 | 26 | try { 27 | $set = new SetModel(); 28 | $girls = [ 29 | 'maria', 30 | 'lily', 31 | ]; 32 | $set->create(1, $girls, 100); 33 | $this->assertTrue($set->createExists(1, $girl, 100)); 34 | $this->assertFalse($set->createNotExists(1, $girl, 100)); 35 | } catch (Exception $e) { 36 | throw $e; 37 | } finally { 38 | $set->destroy(1); 39 | } 40 | } 41 | 42 | public function testInsert() 43 | { 44 | try { 45 | $string = new \Limen\Redisun\Examples\StringModel(); 46 | $bindings = [ 47 | 'id' => 1, 48 | 'name' => 'demo', 49 | ]; 50 | $string->newQuery()->insert($bindings, 'hello world!', 120); 51 | $this->assertFalse($string->newQuery()->insert($bindings, 'hello world!', 120, false)); 52 | $this->assertTrue($string->newQuery()->insert($bindings, 'hello world!', 120, true)); 53 | } catch (Exception $e) { 54 | throw $e; 55 | } finally { 56 | $string->newQuery()->where('id', 1)->where('name', 'demo')->delete(); 57 | } 58 | 59 | try { 60 | $list = new \Limen\Redisun\Examples\ListModel(); 61 | $list->newQuery()->insert(['id' => 1], [1,2,3], 120); 62 | $this->assertFalse($list->newQuery()->insertNotExists(['id' => 1], [1,2,3], 120)); 63 | $this->assertTrue($list->newQuery()->insertExists(['id' => 1], [1,2,3], 120)); 64 | } catch (Exception $e) { 65 | throw $e; 66 | } finally { 67 | $list->destroy(1); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /tests/DeleteTest.php: -------------------------------------------------------------------------------- 1 | insert(['id' => 1, 'name' => 'hello'], 'world1', 60); 11 | $model->insert(['id' => 2, 'name' => 'hello'], 'world2', 70); 12 | $model->insert(['id' => 3, 'name' => 'hello'], 'world3', 80); 13 | $cnt1 = $model->newQuery()->where('name', 'hello')->count(); 14 | $cnt2 = $model->newQuery()->where('name', 'hello')->delete(); 15 | $this->assertEquals($cnt1, 3); 16 | $this->assertEquals($cnt2, 3); 17 | } 18 | } -------------------------------------------------------------------------------- /tests/GetsetTest.php: -------------------------------------------------------------------------------- 1 | create(1, ['google' => 18]); 17 | $oldValue = $model->where('id', 1)->getAndSet(['ms' => 19]); 18 | $this->assertEquals($oldValue, ['google']); 19 | $value = $model->where('id', 1)->first(); 20 | $this->assertEquals($value, ['ms']); 21 | } catch (Exception $e) { 22 | throw $e; 23 | } finally { 24 | $model->destroy(1); 25 | } 26 | } 27 | 28 | public function testSet() 29 | { 30 | try { 31 | $model = new \Limen\Redisun\Examples\SetModel(); 32 | $model->create(1, [1,2,3]); 33 | $oldValue = $model->where('id', 1)->getAndSet([1,2,3,4]); 34 | $this->assertEquals($oldValue, [1,2,3]); 35 | $value = $model->where('id', 1)->first(); 36 | $this->assertEquals($value, [1,2,3,4]); 37 | } catch (Exception $e) { 38 | throw $e; 39 | } finally { 40 | $model->destroy(1); 41 | } 42 | } 43 | 44 | public function testList() 45 | { 46 | try { 47 | $model = new \Limen\Redisun\Examples\ListModel(); 48 | $model->create(1, [1,2,3]); 49 | $oldValue = $model->where('id', 1)->getAndSet([1,2,3,4]); 50 | $this->assertEquals($oldValue, [1,2,3]); 51 | $value = $model->where('id', 1)->first(); 52 | $this->assertEquals($value, [1,2,3,4]); 53 | } catch (Exception $e) { 54 | throw $e; 55 | } finally { 56 | $model->destroy(1); 57 | } 58 | } 59 | 60 | public function testHash() 61 | { 62 | try { 63 | $person = [ 64 | 'name' => 'maria', 65 | 'age' => 22, 66 | ]; 67 | $model = new \Limen\Redisun\Examples\HashModel(); 68 | $model->create(1, $person); 69 | $oldValue = $model->where('id', 1)->getAndSet(['name' => 'maria']); 70 | $this->assertEquals($oldValue, $person); 71 | $value = $model->find(1); 72 | $this->assertEquals($value, ['name' => 'maria']); 73 | } catch (Exception $e) { 74 | throw $e; 75 | } finally { 76 | $model->destroy(1); 77 | } 78 | } 79 | 80 | public function testString() 81 | { 82 | try { 83 | $model = new StringModel(); 84 | $model->insert( 85 | [ 86 | 'id' => 1, 87 | 'name' => 'maria', 88 | ], 89 | 'mymaria', 90 | 120 91 | ); 92 | $oldValue = $model->newQuery()->where('id', 1) 93 | ->where('name', 'maria') 94 | ->getAndSet('mymaria1', 130); 95 | $this->assertEquals($oldValue, 'mymaria'); 96 | $ttl = $model->newQuery()->where('id', 1) 97 | ->where('name', 'maria') 98 | ->ttl(); 99 | $this->assertEquals($ttl, 130); 100 | $value = $model->newQuery()->where('id', 1) 101 | ->where('name', 'maria') 102 | ->first(); 103 | $this->assertEquals($value, 'mymaria1'); 104 | } catch (Exception $e) { 105 | throw $e; 106 | } finally { 107 | $model->newQuery()->where('id', 1) 108 | ->where('name', 'maria') 109 | ->delete(); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /tests/ModelTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ModelTest extends TestCase 16 | { 17 | public function testQueryKeys() 18 | { 19 | try { 20 | $model = new StringModel(); 21 | $range = range(1,20); 22 | $keys = []; 23 | foreach ($range as $i) { 24 | $model->insert([ 25 | 'id' => $i, 26 | 'name' => 'martin', 27 | ],'22'); 28 | $keys[] = "redisun:$i:string:martin"; 29 | } 30 | $value = $model->newQuery()->where('id', 1)->getKeys(); 31 | $this->assertEquals([ 32 | "redisun:1:string:martin", 33 | ], $value); 34 | $value = $model->newQuery()->whereIn('id', [1,2])->getKeys(); 35 | $this->assertEquals([ 36 | "redisun:1:string:martin", 37 | "redisun:2:string:martin", 38 | ], $value); 39 | $value = $model->newQuery() 40 | ->whereIn('id', [1,2,3,4,5,6]) 41 | ->orderBy('id') 42 | ->take(5) 43 | ->getKeys(); 44 | $this->assertEquals([ 45 | "redisun:1:string:martin", 46 | "redisun:2:string:martin", 47 | "redisun:3:string:martin", 48 | "redisun:4:string:martin", 49 | "redisun:5:string:martin", 50 | ], $value); 51 | $model->newQuery()->whereIn('id', $range)->delete(); 52 | $this->assertEquals([], $model->all()); 53 | } catch (Exception $e) { 54 | throw $e; 55 | } finally { 56 | $model->newQuery()->whereIn('id', $range)->delete(); 57 | } 58 | } 59 | 60 | public function testHashModel() 61 | { 62 | $a = [ 63 | 'name' => 'martin', 64 | 'age' => '22', 65 | 'height' => '175', 66 | 'nation' => 'China', 67 | ]; 68 | $b = [ 69 | 'name' => 'nathan', 70 | 'age' => '23', 71 | 'height' => '176', 72 | 'nation' => 'China', 73 | ]; 74 | $model = new HashModel(); 75 | $model->create(1, $a); 76 | $model->create(2, $b); 77 | 78 | $value = $model->newQuery()->where('id', 1)->first(); 79 | $this->assertEquals($a, $value); 80 | 81 | $this->assertEquals($model->find(1), $a); 82 | 83 | $values = $model->findBatch([1,2]); 84 | $this->assertEquals(2, count($values)); 85 | $this->assertEquals($a, $values[1]); 86 | $this->assertEquals($b, $values[2]); 87 | 88 | $values = $model->all(); 89 | $this->assertTrue(in_array($a, $values)); 90 | $this->assertTrue(in_array($b, $values)); 91 | 92 | $data = $model->newQuery()->whereIn('id', [1,2])->orderBy('id', 'desc')->get(); 93 | $this->assertEquals([$b, $a], array_values($data)); 94 | 95 | $data = $model->newQuery()->whereIn('id', [1,2])->orderBy('id', 'desc')->take(1)->get(); 96 | $this->assertEquals(1, count($data)); 97 | $this->assertEquals($b, $data['redisun:2:hash']); 98 | 99 | $updated = []; 100 | $updated['age'] = '24'; 101 | $model->newQuery()->where('id', 1)->update($updated); 102 | $value = $model->newQuery()->where('id', 1)->first(); 103 | $this->assertEquals($updated, $value); 104 | 105 | $model->destroy(1); 106 | $this->assertEquals($model->find(1), []); 107 | 108 | $model->updateBatch([1,2], $a); 109 | $this->assertEquals($model->find(1), $a); 110 | $this->assertEquals($model->find(2), $a); 111 | 112 | $model->destroyBatch([1,2]); 113 | $this->assertEquals($model->find(2), []); 114 | 115 | $model->newQuery()->whereIn('id', [1,2])->delete(); 116 | 117 | $this->assertEquals($model->all(), []); 118 | } 119 | 120 | public function testListModel() 121 | { 122 | $list = [1,2,3]; 123 | 124 | $model = new ListModel(); 125 | $model->create(1, [1,2,3]); 126 | $this->assertEquals($model->find(1), $list); 127 | 128 | $list[] = 4; 129 | $model->where('id', 1)->update($list); 130 | $this->assertEquals($model->find(1), $list); 131 | 132 | $model->where('id', 1)->delete(); 133 | $this->assertEquals($model->find(1), []); 134 | 135 | $this->assertEquals($model->all(), []); 136 | } 137 | 138 | public function testStringModel() 139 | { 140 | $value = 'martin-walk'; 141 | $model = new StringModel(); 142 | $model->insert([ 143 | 'id' => 1, 144 | 'name' => 'martin' 145 | ], $value); 146 | $this->assertEquals($value, $model->where('id', 1)->where('name', 'martin')->first()); 147 | 148 | $this->assertEquals($value, $model->where('id', 1)->first()); 149 | 150 | $this->assertEquals($value, $model->where('name', 'martin')->first()); 151 | 152 | $value = ucfirst($value); 153 | $model->where('id', 1)->update($value); 154 | $this->assertEquals($value, $model->where('id', 1)->first(1)); 155 | 156 | $model->where('id', 1)->delete(); 157 | $this->assertEquals($model->where('id', 1)->first(), null); 158 | 159 | $this->assertEquals($model->all(), []); 160 | } 161 | 162 | public function testZsetModel() 163 | { 164 | $zset = [ 165 | 'google' => 10000, 166 | 'amazon' => 8000, 167 | 'apple' => 20000, 168 | 'alibaba' => 2000, 169 | ]; 170 | $model = new ZsetModel(); 171 | $model->create(1, $zset); 172 | asort($zset); 173 | $this->assertEquals($model->find(1), array_keys($zset)); 174 | 175 | unset($zset['alibaba']); 176 | $model->where('id', 1)->update($zset); 177 | $this->assertEquals($model->find(1), array_keys($zset)); 178 | 179 | $model->destroy(1); 180 | $this->assertEquals($model->find(1), []); 181 | 182 | $this->assertEquals($model->all(), []); 183 | } 184 | 185 | public function testSetModel() 186 | { 187 | $set = [ 188 | 'alibaba', 189 | 'google', 190 | 'amazon', 191 | 'apple', 192 | ]; 193 | sort($set); 194 | $model = new SetModel(); 195 | $model->create(1, $set); 196 | $value = $model->find(1); 197 | sort($value); 198 | $this->assertEquals($value, $set); 199 | 200 | array_pop($set); 201 | $model->where('id', 1)->update($set); 202 | $value = $model->find(1); 203 | sort($value); 204 | $this->assertEquals($value, $set); 205 | $model->destroy(1); 206 | $this->assertEquals($model->find(1), []); 207 | $this->assertEquals($model->all(), []); 208 | } 209 | 210 | public function testAggregation() 211 | { 212 | $model = new StringModel(); 213 | $model->insert([ 214 | 'id' => 1, 215 | 'name' => 'martin', 216 | ],10); 217 | $model->insert([ 218 | 'id' => 2, 219 | 'name' => 'martin', 220 | ],20); 221 | $model->insert([ 222 | 'id' => 3, 223 | 'name' => 'martin', 224 | ],30); 225 | 226 | $this->assertEquals(60, $model->newQuery()->sum()); 227 | $this->assertEquals(10, $model->newQuery()->min()); 228 | $this->assertEquals(30, $model->newQuery()->max()); 229 | $this->assertEquals(3, $model->newQuery()->count()); 230 | $this->assertEquals(1, $model->newQuery()->where('id',1)->count()); 231 | $this->assertEquals(3, $model->newQuery()->where('name', 'martin')->count()); 232 | $this->assertEquals(0, $model->newQuery()->where('name', 'maria')->count()); 233 | $model->newQuery()->whereIn('id', [1,2,3])->where('name', 'martin')->delete(); 234 | $this->assertEquals($model->all(), []); 235 | } 236 | 237 | public function testSort() 238 | { 239 | $model = new StringModel(); 240 | 241 | $array = [ 242 | '10','20','30', 243 | ]; 244 | 245 | $model->insert([ 246 | 'id' => 1, 247 | 'name' => 'maria', 248 | ],$array[0]); 249 | $model->insert([ 250 | 'id' => 2, 251 | 'name' => 'maria', 252 | ],$array[1]); 253 | $model->insert([ 254 | 'id' => 3, 255 | 'name' => 'maria', 256 | ],$array[2]); 257 | 258 | $this->assertEquals($array, $model->newQuery()->sort('asc')); 259 | 260 | $this->assertEquals(array_reverse($array), $model->newQuery()->sort('desc')); 261 | 262 | $model->newQuery()->whereIn('id', [1,2,3])->where('name', 'maria')->delete(); 263 | 264 | $this->assertEquals($model->all(), []); 265 | } 266 | 267 | public function testTtl() 268 | { 269 | $ttl = 2; 270 | 271 | // StringModel 272 | $model = new StringModel(); 273 | 274 | $model->insert([ 275 | 'id' => 1, 276 | 'name' => 'maria', 277 | ], 'maria', $ttl); 278 | 279 | $this->assertEquals($ttl, $model->newQuery()->where('id',1)->where('name','maria')->ttl()); 280 | 281 | $model->newQuery()->where('id',1)->where('name','maria')->update('mary'); 282 | $this->assertGreaterThanOrEqual(0, $model->newQuery()->where('id',1)->where('name','maria')->ttl()); 283 | $this->assertLessThanOrEqual($ttl, $model->newQuery()->where('id',1)->where('name','maria')->ttl()); 284 | 285 | sleep($ttl + 1); 286 | $this->assertEquals([], $model->newQuery()->where('id',1)->where('name','maria')->get()); 287 | $this->assertNull($model->newQuery()->where('id',1)->where('name','maria')->first()); 288 | 289 | // HashModel 290 | $model = new HashModel(); 291 | $model->create(1, [ 292 | 'name' => 'maria', 293 | 'age' => 25, 294 | ], $ttl); 295 | $model->create(2, [ 296 | 'name' => 'maria', 297 | 'age' => 25, 298 | ], $ttl + 1); 299 | 300 | $this->assertEquals($ttl, $model->newQuery()->where('id',1)->ttl()); 301 | $model->where('id', 1)->update([ 302 | 'age' => 26, 303 | ]); 304 | $this->assertEquals($ttl, $model->newQuery()->where('id',1)->ttl()); 305 | 306 | $model->updateBatch([1,2], [ 307 | 'age' => 27 308 | ]); 309 | $this->assertEquals($ttl, $model->newQuery()->where('id',1)->ttl()); 310 | $this->assertEquals($ttl + 1, $model->newQuery()->where('id',2)->ttl()); 311 | 312 | sleep($ttl + 1); 313 | $this->assertEquals([], $model->newQuery()->where('id',1)->get()); 314 | 315 | // SetModel 316 | $model = new SetModel(); 317 | $model->create(1, [ 318 | 'martin', 319 | 'maria' 320 | ], $ttl); 321 | 322 | $this->assertEquals($ttl, $model->newQuery()->where('id',1)->ttl()); 323 | $model->where('id', 1)->update([ 324 | 'martin', 325 | 'maria', 326 | 'cathrine', 327 | ]); 328 | $this->assertGreaterThanOrEqual(0, $model->newQuery()->where('id',1)->ttl()); 329 | $this->assertLessThanOrEqual($ttl, $model->newQuery()->where('id',1)->ttl()); 330 | 331 | sleep($ttl + 1); 332 | $this->assertEquals([], $model->newQuery()->where('id',1)->get()); 333 | $this->assertEquals([], $model->find(1)); 334 | 335 | // ZsetModel 336 | $model = new ZsetModel(); 337 | $model->create(1, [ 338 | 'martin' => 1, 339 | 'maria' => 2, 340 | ], $ttl); 341 | 342 | $this->assertequals($ttl, $model->newQuery()->where('id',1)->ttl()); 343 | $model->where('id', 1)->update([ 344 | 'martin' => 2, 345 | 'maria' => 3, 346 | 'cathrine' => 1, 347 | ]); 348 | $this->assertGreaterThanOrEqual(0, $model->newQuery()->where('id',1)->ttl()); 349 | $this->assertLessThanOrEqual($ttl, $model->newQuery()->where('id',1)->ttl()); 350 | 351 | sleep($ttl + 1); 352 | $this->assertEquals([], $model->newQuery()->where('id',1)->get()); 353 | 354 | // ListModel 355 | $model = new ListModel(); 356 | $model->create(1, [ 357 | 'martin', 358 | 'maria', 359 | ], $ttl); 360 | 361 | $this->assertequals($ttl, $model->newQuery()->where('id',1)->ttl()); 362 | $model->where('id', 1)->update([ 363 | 'martin', 364 | 'maria', 365 | 'cathrine', 366 | ]); 367 | $this->assertGreaterThanOrEqual(0, $model->newQuery()->where('id',1)->ttl()); 368 | $this->assertLessThanOrEqual($ttl, $model->newQuery()->where('id',1)->ttl()); 369 | 370 | sleep($ttl + 1); 371 | $this->assertEquals([], $model->newQuery()->where('id',1)->get()); 372 | 373 | $model->create(1, [ 374 | 'martin', 375 | 'maria', 376 | ], $ttl); 377 | $ttl++; 378 | $model->newQuery()->where('id', 1)->update(['maria', 'martin'], $ttl); 379 | $this->assertequals($ttl, $model->newQuery()->where('id',1)->ttl()); 380 | 381 | $model->newQuery()->where('id',1)->delete(); 382 | $this->assertEquals([], $model->all()); 383 | } 384 | 385 | public function testNativeMethods() 386 | { 387 | $numbers = [1,2,3,4,5,6,100,200,300]; 388 | $listModel = new ListModel(); 389 | $listModel->newQuery()->where('id', 1)->rpush($numbers); 390 | $this->assertEquals($listModel->newQuery()->where('id', 1)->first(), $numbers); 391 | 392 | $listModel->newQuery()->where('id', 2)->lpush($numbers); 393 | $this->assertEquals($listModel->newQuery()->where('id', 2)->first(), array_reverse($numbers)); 394 | 395 | // clean up 396 | $listModel->newQuery()->whereIn('id', [1,2])->delete(); 397 | 398 | $this->assertEquals($listModel->newQuery()->whereIn('id', [1,2])->count(), 0); 399 | 400 | $set = ['alibaba', 'google', 'amazon', 'apple',]; 401 | $model = new SetModel(); 402 | $model->newQuery()->where('id', 1)->sadd($set); 403 | $value = $model->find(1); 404 | $this->assertTrue($this->compareSet($value, $set)); 405 | 406 | $model->newQuery()->where('id', 1)->srem($set); 407 | $this->assertFalse((bool)$model->find(1)); 408 | } 409 | 410 | protected function compareSet($a, $b) 411 | { 412 | if (count($a) !== count($b)) { 413 | return false; 414 | } 415 | 416 | foreach ($a as $v) { 417 | if (!in_array($v, $b)) { 418 | return false; 419 | } 420 | } 421 | 422 | return true; 423 | } 424 | } -------------------------------------------------------------------------------- /tests/QueryBuilderTest.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class QueryBuilderTest extends TestCase 12 | { 13 | public function testBuilder() 14 | { 15 | $key = 'school:{schoolId}:class:{classId}:students'; 16 | 17 | $builder = new QueryBuilder($key); 18 | $builder->setFieldNeedle('schoolId', '{schoolId}'); 19 | $builder->setFieldNeedle('classId', '{classId}'); 20 | 21 | $keys = $builder->whereEqual('schoolId', 1)->whereEqual('classId', 2)->getQueryKeys(); 22 | $this->assertEquals($keys, [ 23 | 'school:1:class:2:students', 24 | ]); 25 | 26 | $key = $builder->refresh()->whereEqual('schoolId', 1)->whereEqual('classId', 2)->firstQueryKey(); 27 | $this->assertEquals($key, 'school:1:class:2:students'); 28 | 29 | $keys = $builder->refresh()->whereIn('schoolId', [1,2])->getQueryKeys(); 30 | $this->assertEquals($keys, [ 31 | 'school:1:class:{classId}:students', 32 | 'school:2:class:{classId}:students', 33 | ]); 34 | 35 | $keys = $builder->refresh()->whereBetween('schoolId', [1,5])->getQueryKeys(); 36 | $this->assertEquals($keys, [ 37 | 'school:1:class:{classId}:students', 38 | 'school:2:class:{classId}:students', 39 | 'school:3:class:{classId}:students', 40 | 'school:4:class:{classId}:students', 41 | 'school:5:class:{classId}:students', 42 | ]); 43 | 44 | $keys = $builder->refresh()->whereIn('schoolId', [1,2])->whereIn('classId', [2,3])->getQueryKeys(); 45 | $this->assertEquals($keys, [ 46 | 'school:1:class:2:students', 47 | 'school:1:class:3:students', 48 | 'school:2:class:2:students', 49 | 'school:2:class:3:students', 50 | ]); 51 | 52 | $keys = $builder->refresh()->getQueryKeys(); 53 | $this->assertEquals($keys, []); 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |