├── doc └── pic1.jpg ├── composer.json ├── .gitignore ├── redis-cli ├── redis-cli.bat ├── config └── style.php ├── LICENSE ├── README.md ├── src ├── CustomStyle.php └── RedisCommand.php └── composer.lock /doc/pic1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizarot/redis-cli/HEAD/doc/pic1.jpg -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "symfony/console": "^3.4" 4 | }, 5 | "autoload": { 6 | "psr-4": { 7 | "Console\\": "src" 8 | }, 9 | "classmap": ["src"] 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | 4 | # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control 5 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 6 | # composer.lock 7 | 8 | .idea/* 9 | config/customStyle.php -------------------------------------------------------------------------------- /redis-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add($command); 12 | $app->setDefaultCommand($command->getName()); 13 | $app->run(); -------------------------------------------------------------------------------- /redis-cli.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem -------------------------------------------------------------- 4 | rem 命令行 for Windows. 如果不对,请自行修改php.exe路径到实际php文件位置 5 | rem -------------------------------------------------------------- 6 | 7 | @setlocal 8 | 9 | set PWD_PATH=%~dp0 10 | 11 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe 12 | 13 | "%PHP_COMMAND%" "%PWD_PATH%redis-cli" %* 14 | 15 | @endlocal -------------------------------------------------------------------------------- /config/style.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'fg' => 'default', 12 | 'bg' => 'default', 13 | ], 14 | 'success' => [ 15 | 'fg' => 'black', 16 | 'bg' => 'green', 17 | ], 18 | 'error' => [ 19 | 'fg' => 'white', 20 | 'bg' => 'red', 21 | ], 22 | 'warning' => [ 23 | 'fg' => 'white', 24 | 'bg' => 'red', 25 | ], 26 | 'note' => [ 27 | 'fg' => 'yellow', 28 | 'bg' => 'default', 29 | ], 30 | 'caution' => [ 31 | 'fg' => 'white', 32 | 'bg' => 'red', 33 | ], 34 | ]; 35 | 36 | // 可以复制数组到customStyle.php文件中,自定义即可 37 | @include __DIR__.'/customStyle.php'; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Will Zhang (张志宇) 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP 写的简单 Redis 客户端 2 | 3 | ## 是什么 4 | 5 | ![image](doc/pic1.jpg) 6 | 7 | - 简单操作redis的命令行工具 8 | - 简单不用每次查文档 9 | - 查看更直观,之前打算用Electron开发桌面版,但不熟. 所以还是先做一个命令行版本拿来用 10 | 11 | ## 为什么 12 | 13 | - 原因1: 每次做点简单的Redis操作都要翻手册,嫌麻烦. 所以把基本功能做了下简化 14 | - 原因2: Medis客户端收费了,同事想用发现找个简单操作的没有. 吹牛说自己写一个,于是填坑. 15 | 16 | ## 安装方法 17 | 1. clone项目代码: git clone https://github.com/wizarot/redis-cli.git 18 | 2. 进入文件件: cd redis-cli 19 | 3. 切换到当前最新版本tag: git checkout v1.3.2 20 | 4. 使用composer安装依赖包: composer update 21 | 22 | ## 使用方法 23 | - 启动: ./redis-cli 24 | - windows启动: redis-cli.bat (可能需要自行编辑一下,手动修改php.exe文件的路径) 25 | - 输入redis连接: host port (可以启用auth输入密码,如果有需要自己去src/RedisCommand.php -> connRedis() 修改 ) 26 | - help 或者 随意输入别的,显示帮助列表 27 | - exit 或者 ctrl+D 退出 28 | - 需要php安装redis扩展. 如果实在没有那考虑自己引入pRedis bundle然后改一下 connRedis()函数就能用了,不会提issue.我抽空处理 29 | - 可以自定义样式,复制config/style.php 到 config/customStyle.php 自己修改相应颜色 30 | - 可用颜色: [default, black, red, green, yellow, blue, magenta, cyan, white] 31 | 32 | ## 特性 (如果有需求或者其他想法可以提issue) 33 | - (v1.2新特性)输入模仿Linux可以记录命令历史和自动帮助,上下箭头查看历史记录. (小功能但是方便了很多!) 34 | - 用ls列出当前数据key和对应数据类型 35 | - select 切换数据库,默认在 0 库 36 | - ls ?/* 支持通配符搜索key 37 | - ttl key 查看生存时间 38 | - ttl key second 设定生存时间 39 | - persist key 设定生存时间为永久 40 | - mv name new_name 将key改名 41 | - rm key 删除key 42 | - config 获取redis 配置信息 43 | - get key 获取值和对应信息(主要功能) 44 | - set key 设置值/新增也可以. 操作流程有待优化,但是已经可用 45 | 46 | ## 其他 47 | 48 | 啰嗦下,这东西毕竟是花了些心思弄出来的.如果大家有什么想法和bug可以提issue. 我会抽时间处理. 49 | 使用上有什么问题也可以随时问. 50 | 51 | ## TODO 52 | 53 | - [x] 用scan代替get * 防止出现查死数据库的情况 54 | - [x] 完善自动填写功能,如果没有考虑自己加一个 55 | - [x] 完善上下箭头访问命令历史功能 56 | - [x] 完善windows命令行可执行文件使用 (有待测试和调整) 57 | - [x] 把显示颜色样式从项目中抽象出来放到配置文件中,方便用户自己微调 58 | -------------------------------------------------------------------------------- /src/CustomStyle.php: -------------------------------------------------------------------------------- 1 | 4 | * @link http://wizarot.me/ 5 | * 6 | * Date: 2019/6/20 7 | * Time: 10:45 AM 8 | */ 9 | 10 | namespace Console; 11 | 12 | 13 | use Symfony\Component\Console\Input\InputInterface; 14 | use Symfony\Component\Console\Output\BufferedOutput; 15 | use Symfony\Component\Console\Output\OutputInterface; 16 | use Symfony\Component\Console\Style\SymfonyStyle; 17 | use Symfony\Component\Console\Terminal; 18 | 19 | class CustomStyle extends SymfonyStyle 20 | { 21 | const MAX_LINE_LENGTH = 120; 22 | 23 | private $input; 24 | private $lineLength; 25 | private $bufferedOutput; 26 | // 定义样式颜色 27 | private $style; 28 | 29 | public function __construct(InputInterface $input, OutputInterface $output) 30 | { 31 | $this->input = $input; 32 | $this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter()); 33 | // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not. 34 | $width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH; 35 | $this->lineLength = min($width - (int)(\DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH); 36 | $style = []; 37 | // 导入一下显示颜色的配置文件 38 | include __DIR__.'/../config/style.php'; 39 | $this->style = $style; 40 | parent::__construct($input, $output); 41 | } 42 | 43 | 44 | /** 45 | * Formats a command comment. 46 | * 47 | * @param string|array $message 48 | */ 49 | public function comment($message) 50 | { 51 | $this->block($message, null, null, "style['comment']['fg']};bg={$this->style['comment']['bg']}> // ", false, false); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function success($message) 58 | { 59 | $this->block($message, 'OK', "fg={$this->style['success']['fg']};bg={$this->style['success']['bg']}", ' ', true); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function error($message) 66 | { 67 | $this->block($message, 'ERROR', "fg={$this->style['error']['fg']};bg={$this->style['error']['bg']}", ' ', true); 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function warning($message) 74 | { 75 | $this->block($message, 'WARNING', "fg={$this->style['warning']['fg']};bg={$this->style['warning']['bg']}", ' ', true); 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function note($message) 82 | { 83 | $this->block($message, 'NOTE', "fg={$this->style['note']['fg']};bg={$this->style['note']['bg']}", ' ! '); 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function caution($message) 90 | { 91 | $this->block($message, 'CAUTION', "fg={$this->style['caution']['fg']};bg={$this->style['caution']['bg']}", ' ! ', true); 92 | } 93 | 94 | 95 | } -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "b51bdee27c06e2a044e6131a31a764de", 8 | "packages": [ 9 | { 10 | "name": "psr/log", 11 | "version": "1.1.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/php-fig/log.git", 15 | "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", 20 | "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.3.0" 25 | }, 26 | "type": "library", 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "1.0.x-dev" 30 | } 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Psr\\Log\\": "Psr/Log/" 35 | } 36 | }, 37 | "notification-url": "https://packagist.org/downloads/", 38 | "license": [ 39 | "MIT" 40 | ], 41 | "authors": [ 42 | { 43 | "name": "PHP-FIG", 44 | "homepage": "http://www.php-fig.org/" 45 | } 46 | ], 47 | "description": "Common interface for logging libraries", 48 | "homepage": "https://github.com/php-fig/log", 49 | "keywords": [ 50 | "log", 51 | "psr", 52 | "psr-3" 53 | ], 54 | "time": "2018-11-20T15:27:04+00:00" 55 | }, 56 | { 57 | "name": "symfony/console", 58 | "version": "v3.4.27", 59 | "source": { 60 | "type": "git", 61 | "url": "https://github.com/symfony/console.git", 62 | "reference": "15a9104356436cb26e08adab97706654799d31d8" 63 | }, 64 | "dist": { 65 | "type": "zip", 66 | "url": "https://api.github.com/repos/symfony/console/zipball/15a9104356436cb26e08adab97706654799d31d8", 67 | "reference": "15a9104356436cb26e08adab97706654799d31d8", 68 | "shasum": "" 69 | }, 70 | "require": { 71 | "php": "^5.5.9|>=7.0.8", 72 | "symfony/debug": "~2.8|~3.0|~4.0", 73 | "symfony/polyfill-mbstring": "~1.0" 74 | }, 75 | "conflict": { 76 | "symfony/dependency-injection": "<3.4", 77 | "symfony/process": "<3.3" 78 | }, 79 | "provide": { 80 | "psr/log-implementation": "1.0" 81 | }, 82 | "require-dev": { 83 | "psr/log": "~1.0", 84 | "symfony/config": "~3.3|~4.0", 85 | "symfony/dependency-injection": "~3.4|~4.0", 86 | "symfony/event-dispatcher": "~2.8|~3.0|~4.0", 87 | "symfony/lock": "~3.4|~4.0", 88 | "symfony/process": "~3.3|~4.0" 89 | }, 90 | "suggest": { 91 | "psr/log": "For using the console logger", 92 | "symfony/event-dispatcher": "", 93 | "symfony/lock": "", 94 | "symfony/process": "" 95 | }, 96 | "type": "library", 97 | "extra": { 98 | "branch-alias": { 99 | "dev-master": "3.4-dev" 100 | } 101 | }, 102 | "autoload": { 103 | "psr-4": { 104 | "Symfony\\Component\\Console\\": "" 105 | }, 106 | "exclude-from-classmap": [ 107 | "/Tests/" 108 | ] 109 | }, 110 | "notification-url": "https://packagist.org/downloads/", 111 | "license": [ 112 | "MIT" 113 | ], 114 | "authors": [ 115 | { 116 | "name": "Fabien Potencier", 117 | "email": "fabien@symfony.com" 118 | }, 119 | { 120 | "name": "Symfony Community", 121 | "homepage": "https://symfony.com/contributors" 122 | } 123 | ], 124 | "description": "Symfony Console Component", 125 | "homepage": "https://symfony.com", 126 | "time": "2019-04-08T09:29:13+00:00" 127 | }, 128 | { 129 | "name": "symfony/debug", 130 | "version": "v3.4.27", 131 | "source": { 132 | "type": "git", 133 | "url": "https://github.com/symfony/debug.git", 134 | "reference": "681afbb26488903c5ac15e63734f1d8ac430c9b9" 135 | }, 136 | "dist": { 137 | "type": "zip", 138 | "url": "https://api.github.com/repos/symfony/debug/zipball/681afbb26488903c5ac15e63734f1d8ac430c9b9", 139 | "reference": "681afbb26488903c5ac15e63734f1d8ac430c9b9", 140 | "shasum": "" 141 | }, 142 | "require": { 143 | "php": "^5.5.9|>=7.0.8", 144 | "psr/log": "~1.0" 145 | }, 146 | "conflict": { 147 | "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" 148 | }, 149 | "require-dev": { 150 | "symfony/http-kernel": "~2.8|~3.0|~4.0" 151 | }, 152 | "type": "library", 153 | "extra": { 154 | "branch-alias": { 155 | "dev-master": "3.4-dev" 156 | } 157 | }, 158 | "autoload": { 159 | "psr-4": { 160 | "Symfony\\Component\\Debug\\": "" 161 | }, 162 | "exclude-from-classmap": [ 163 | "/Tests/" 164 | ] 165 | }, 166 | "notification-url": "https://packagist.org/downloads/", 167 | "license": [ 168 | "MIT" 169 | ], 170 | "authors": [ 171 | { 172 | "name": "Fabien Potencier", 173 | "email": "fabien@symfony.com" 174 | }, 175 | { 176 | "name": "Symfony Community", 177 | "homepage": "https://symfony.com/contributors" 178 | } 179 | ], 180 | "description": "Symfony Debug Component", 181 | "homepage": "https://symfony.com", 182 | "time": "2019-04-11T09:48:14+00:00" 183 | }, 184 | { 185 | "name": "symfony/polyfill-mbstring", 186 | "version": "v1.11.0", 187 | "source": { 188 | "type": "git", 189 | "url": "https://github.com/symfony/polyfill-mbstring.git", 190 | "reference": "fe5e94c604826c35a32fa832f35bd036b6799609" 191 | }, 192 | "dist": { 193 | "type": "zip", 194 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609", 195 | "reference": "fe5e94c604826c35a32fa832f35bd036b6799609", 196 | "shasum": "" 197 | }, 198 | "require": { 199 | "php": ">=5.3.3" 200 | }, 201 | "suggest": { 202 | "ext-mbstring": "For best performance" 203 | }, 204 | "type": "library", 205 | "extra": { 206 | "branch-alias": { 207 | "dev-master": "1.11-dev" 208 | } 209 | }, 210 | "autoload": { 211 | "psr-4": { 212 | "Symfony\\Polyfill\\Mbstring\\": "" 213 | }, 214 | "files": [ 215 | "bootstrap.php" 216 | ] 217 | }, 218 | "notification-url": "https://packagist.org/downloads/", 219 | "license": [ 220 | "MIT" 221 | ], 222 | "authors": [ 223 | { 224 | "name": "Nicolas Grekas", 225 | "email": "p@tchwork.com" 226 | }, 227 | { 228 | "name": "Symfony Community", 229 | "homepage": "https://symfony.com/contributors" 230 | } 231 | ], 232 | "description": "Symfony polyfill for the Mbstring extension", 233 | "homepage": "https://symfony.com", 234 | "keywords": [ 235 | "compatibility", 236 | "mbstring", 237 | "polyfill", 238 | "portable", 239 | "shim" 240 | ], 241 | "time": "2019-02-06T07:57:58+00:00" 242 | } 243 | ], 244 | "packages-dev": [], 245 | "aliases": [], 246 | "minimum-stability": "stable", 247 | "stability-flags": [], 248 | "prefer-stable": false, 249 | "prefer-lowest": false, 250 | "platform": [], 251 | "platform-dev": [] 252 | } 253 | -------------------------------------------------------------------------------- /src/RedisCommand.php: -------------------------------------------------------------------------------- 1 | setName('redis-cli') 48 | ->setDescription('PHP的redis命令行客户端'); 49 | } 50 | 51 | public function execute(InputInterface $input, OutputInterface $output) 52 | { 53 | $this->input = $input; 54 | $this->output = $output; 55 | // $io = new SymfonyStyle($input, $output); 56 | $io = new CustomStyle($input, $output); 57 | $this->io = $io; 58 | 59 | $this->connRedis(); 60 | $host = $this->host; 61 | $port = $this->port; 62 | 63 | 64 | do { 65 | // $command = trim($io->ask("{$host}:{$port}", $this->db)); 66 | $command = trim($this->autoAsk("{$host}:{$port}", $this->db)); 67 | 68 | // 每次执行前,查看重连数据库 69 | try { 70 | $this->redis->ping(); 71 | } catch (\Exception $exception) { 72 | $this->redis->connect($host, $port); 73 | } 74 | 75 | // 处理命令行逻辑 76 | switch (true) { 77 | case stripos($command, 'config') === 0 : 78 | // 只弄一个方便阅读配置文件,不打算支持修改 79 | $parameter = trim(substr($command, 7)); 80 | $this->getConfig($parameter); 81 | break; 82 | case stripos($command, 'info') === 0 : 83 | // 只弄一个方便阅读配置文件,不打算支持修改 84 | $parameter = trim(substr($command, 4)); 85 | $this->getInfo($parameter); 86 | break; 87 | case stripos($command, 'select ') === 0 : 88 | // 切换数据库 89 | $parameter = trim(substr($command, 7)); 90 | $result = $this->redis->select($parameter); 91 | if ($result === true) { 92 | $this->db = $parameter; 93 | $this->io->success('数据库切换成功!'); 94 | } else { 95 | $this->io->error('数据库切换失败!'); 96 | } 97 | break; 98 | case stripos($command, 'ls') === 0 : 99 | $parameter = trim(substr($command, 2)); 100 | // 先写这里,回头再抽象 101 | $this->listTable($parameter); 102 | break; 103 | case stripos($command, 'ttl') === 0 : 104 | $parameter = trim(substr($command, 3)); 105 | $parameter = explode(' ', $parameter, 2); 106 | if (count($parameter) == 1) { 107 | $this->getTtl($parameter); 108 | } else { 109 | $this->setTtl($parameter); 110 | } 111 | break; 112 | case stripos($command, 'persist ') === 0 : 113 | $parameter = trim(substr($command, 8)); 114 | $this->persist($parameter); 115 | break; 116 | case stripos($command, 'mv ') === 0 : 117 | $parameter = trim(substr($command, 3)); 118 | $parameter = explode(' ', $parameter, 2); 119 | $this->rename($parameter); 120 | break; 121 | case stripos($command, 'rm ') === 0 : 122 | $parameter = trim(substr($command, 3)); 123 | $parameter = explode(' ', $parameter, 2); 124 | $this->rm($parameter); 125 | break; 126 | case stripos($command, 'set ') === 0 : 127 | $parameter = trim(substr($command, 4)); 128 | $parameter = explode(' ', $parameter, 2); 129 | $this->set($parameter); 130 | break; 131 | case stripos($command, 'get ') === 0 : 132 | $parameter = trim(substr($command, 4)); 133 | $parameter = explode(' ', $parameter, 2); 134 | $this->get($parameter); 135 | break; 136 | case stripos($command, 'exit') === 0 : 137 | // 退出 138 | $io->success('Bye!'); 139 | 140 | return true; 141 | break; 142 | case stripos($command, 'help') === 0 : 143 | default: 144 | // 帮助列表 145 | $io->title('Help List 命令列表'); 146 | $io->listing([ 147 | 'help : 显示可用命令', 148 | 'select < 0 > : 切换数据库,默认0 ', 149 | 'ls : 列出所有keys', 150 | 'ls h?llo : 列出匹配keys,?通配1个字符,*通配任意长度字符,[aei]通配选线,特殊符号用\隔开', 151 | 'ttl key [ttl second] : 获取/设定生存时间,传第二个参数会设置生存时间', 152 | 'persist key : 移除给定key的生存时间', 153 | 'mv key new_key : key改名,如果新名字存在则会报错', 154 | 'rm key : 刪除key,支持通配符匹配', 155 | 'get key : 获取值', 156 | 'set key : 设置值', 157 | 'config [dir]: 获取配置,可选参数[配置名称(支持通配符)]', 158 | 'info: 获取当前Redis服务器信息', 159 | 160 | ] 161 | ); 162 | 163 | break; 164 | 165 | } 166 | 167 | } while (strtolower($command) != 'exit'); 168 | 169 | $io->success('Bye!'); 170 | 171 | return true; 172 | } 173 | 174 | // 新特性,自动填写答案,上下可以给出自动帮助 175 | protected function autoAsk($question, $default, $history = []) 176 | { 177 | if (empty($history)) { 178 | $history = $this->history; 179 | $keys = $this->keys; 180 | foreach ($keys as $i => $key) { 181 | $keys[] = 'get ' . $key; 182 | $keys[] = 'set ' . $key; 183 | } 184 | $history = array_merge($history, $keys); 185 | $history = array_unique($history); 186 | } 187 | $questionObj = new Question($question, $default); 188 | $questionObj->setAutocompleterValues($history); 189 | 190 | $command = $this->io->askQuestion($questionObj); 191 | if (!in_array($command, $this->history)) { 192 | $this->history[] = $command; 193 | // 控制下操作记录仅包含1000条 194 | if (count($this->history) > 1000) { 195 | array_shift($this->history); 196 | } 197 | } 198 | 199 | return $command; 200 | } 201 | 202 | protected function connRedis() 203 | { 204 | $this->redis = new \Redis(); 205 | 206 | do { 207 | // 输入服务器和port 208 | $this->host = $host = $this->autoAsk('Redis服务器host', '127.0.0.1', ['127.0.0.1']); 209 | $this->port = $port = $this->autoAsk('Redis服务器port', '6379', ['6379']); 210 | // TODO: 输入密码.. 211 | // $this->redis->auth(); 212 | 213 | $connResult = @$this->redis->connect($host, $port); 214 | if (!$connResult) { 215 | $this->io->error("连接服务器 {$host}:{$port} 失败!"); 216 | } 217 | 218 | } while ($connResult != true); 219 | 220 | 221 | // 连接服务器 222 | $this->io->success("连接服务器 {$host}:{$port} 成功!"); 223 | // 默认使用 0 号数据库 224 | $this->db = 0; 225 | $info = $this->redis->info(); 226 | if (key_exists('redis_version', $info)) { 227 | $this->version = $info['redis_version']; 228 | } 229 | 230 | return true; 231 | } 232 | 233 | protected function persist($key) 234 | { 235 | $this->redis->persist($key); 236 | $this->io->success("{$key} 设置过期时间为永久."); 237 | 238 | return true; 239 | } 240 | 241 | // 列出配置信息 242 | protected function getConfig($parameter) 243 | { 244 | if (empty($parameter)) { 245 | $parameter = '*'; 246 | } 247 | $config = $this->redis->config('GET', $parameter); 248 | $data = []; 249 | foreach ($config as $k => $item) { 250 | $data[] = [ 251 | $k, $item, 252 | ]; 253 | } 254 | $this->io->section("CONFIG:"); 255 | $this->io->table(['ITEM', 'VALUE'], $data); 256 | } 257 | 258 | protected function getInfo($parameter) 259 | { 260 | $info = $this->redis->info(); 261 | 262 | $data = []; 263 | foreach ($info as $k => $item) { 264 | $data[] = [ 265 | $k, $item, 266 | ]; 267 | } 268 | $this->io->section("INFO:"); 269 | $this->io->table(['ITEM', 'VALUE'], $data); 270 | } 271 | 272 | // 获取列表和key对应的类型,并返回表格 273 | protected function listTable($search = '') 274 | { 275 | if (empty($search)) { 276 | $search = '*'; 277 | } 278 | // 换个方式,大于2.8.0那么使用Scan搜索 279 | if (version_compare($this->version, '2.8.0')) { 280 | $iterator = null; 281 | while ($keys = $this->redis->scan($iterator, $search, $this->pageNumber)) { 282 | $data = []; 283 | foreach ($keys as $key) { 284 | $type = $this->redis->type($key); 285 | if ($type == 0) { 286 | continue;//不存在那么就直接跳过 287 | } 288 | // 根据类型显示颜色 289 | $type = $this->transType($type); 290 | $data[$key] = [$type, $key]; 291 | $this->keys[] = $key; 292 | } 293 | $this->io->table( 294 | ['TYPE', 'KEY'], 295 | $data 296 | ); 297 | $this->keys = array_unique($this->keys); 298 | // 最后一页不用了. 299 | if (count($data) >= $this->pageNumber) { 300 | $isBreak = $this->io->confirm('回车继续...'); 301 | if (!$isBreak) { 302 | return true; 303 | } 304 | } 305 | $this->keys = array_unique($this->keys); 306 | 307 | } 308 | } else { 309 | $keys = $this->redis->keys($search); 310 | sort($keys); 311 | $data = []; 312 | // 加一个分页功能 313 | foreach ($keys as $row => $key) { 314 | $type = $this->redis->type($key); 315 | if ($type == 0) { 316 | continue;//不存在那么就直接跳过 317 | } 318 | // 根据类型显示颜色 319 | $type = $this->transType($type); 320 | $data[$key] = [$type, $key]; 321 | $this->keys[] = $key; 322 | if ($row != 0 && $row % $this->pageNumber == 0) { 323 | $this->io->table( 324 | ['TYPE', 'KEY'], 325 | $data 326 | ); 327 | $isBreak = $this->io->confirm('回车继续...'); 328 | if (!$isBreak) { 329 | $this->keys = array_unique($this->keys); 330 | 331 | return true; 332 | } 333 | $data = []; 334 | } 335 | } 336 | 337 | $this->io->table( 338 | ['TYPE', 'KEY'], 339 | $data 340 | ); 341 | $this->keys = array_unique($this->keys); 342 | } 343 | 344 | } 345 | 346 | // 从 0,1,2..这种类型,转换成显示类型 347 | protected function transType($type) 348 | { 349 | switch ($type) { 350 | case 1: 351 | $type = 'STRING'; 352 | break; 353 | case 2: 354 | $type = 'SET '; 355 | break; 356 | case 3: 357 | $type = 'LIST '; 358 | break; 359 | case 4: 360 | $type = 'ZSET '; 361 | break; 362 | case 5: 363 | $type = 'HASH '; 364 | break; 365 | } 366 | 367 | return $type; 368 | } 369 | 370 | // 转换为方便输出的ttl格式 371 | protected function transTtl($ttl) 372 | { 373 | switch ($ttl) { 374 | case -2: 375 | $ttl = 'KEY不存在'; 376 | break; 377 | case -1: 378 | $ttl = '永久'; 379 | break; 380 | default: 381 | } 382 | 383 | return $ttl; 384 | } 385 | 386 | // 尝试将string类型数据,转换为数组或反序列化,如果成功那么就返回正常显示 387 | protected function convertString($string) 388 | { 389 | $data = json_decode($string, true); 390 | if (is_array($data)) { 391 | return $data; 392 | } 393 | 394 | $data = @unserialize($string); 395 | if ($data !== false) { 396 | return $data; 397 | } 398 | 399 | return false; 400 | } 401 | 402 | // 获取并显示数据的ttl生存时间 403 | protected function getTtl($parameters) 404 | { 405 | try { 406 | $ttl = $this->redis->ttl($parameters[0]); 407 | // 格式化显示 408 | $ttl = $this->transTtl($ttl); 409 | $this->io->table(['KEY', 'TTL (秒s)'], [ 410 | [$parameters[0], $ttl], 411 | ] 412 | ); 413 | } catch (\Exception $e) { 414 | $this->io->error($e->getMessage()); 415 | } 416 | 417 | } 418 | 419 | // 设置过期时间 420 | protected function setTtl($parameters) 421 | { 422 | try { 423 | $result = $this->redis->EXPIRE($parameters[0], (integer)$parameters[1]); 424 | // 格式化显示 425 | $result = $result == 1 ? ('生存时间设置为: ' . (integer)$parameters[1] . ' (秒)') : '失败'; 426 | $this->io->table(['KEY', 'TTL (秒s)'], [ 427 | [$parameters[0], $result], 428 | ] 429 | ); 430 | } catch (\Exception $e) { 431 | $this->io->error($e->getMessage()); 432 | } 433 | 434 | } 435 | 436 | // 重命名key 437 | protected function rename($parameters) 438 | { 439 | try { 440 | if (!key_exists('1', $parameters)) { 441 | throw new \Exception("缺少第2个参数"); 442 | } 443 | $result = $this->redis->renameNx($parameters[0], $parameters[1]); 444 | // 格式化显示 445 | if ($result == 1) { 446 | // 成功 447 | $this->io->success("修改成功"); 448 | } else { 449 | // 失败 450 | $this->io->error("修改失败"); 451 | } 452 | } catch (\Exception $e) { 453 | $this->io->error($e->getMessage()); 454 | } 455 | } 456 | 457 | // 删除key 458 | protected function rm($parameters) 459 | { 460 | try { 461 | // 支持 patten批量删除 462 | $removeKeys = []; 463 | while ($keys = $this->redis->scan($iterator, $parameters[0], $this->pageNumber)) { 464 | $removeKeys = array_merge($removeKeys, $keys); 465 | } 466 | 467 | if (empty($removeKeys)) { 468 | throw new \Exception("KEY: {$parameters[0]} 不存在"); 469 | } 470 | $count = count($removeKeys); 471 | $confirm = $this->io->confirm("确定要删除 {$parameters[0]} 共{$count}条记录 ?", false); 472 | if ($confirm) { 473 | $this->redis->del($removeKeys); 474 | $this->io->success("删除成功"); 475 | } 476 | 477 | } catch (\Exception $e) { 478 | $this->io->error($e->getMessage()); 479 | } 480 | } 481 | 482 | // 设置key 483 | protected function set($parameters) 484 | { 485 | try { 486 | $key = $parameters[0]; 487 | if ($this->redis->exists($key)) { 488 | $confirm = $this->io->confirm("KEY: {$key} 已存在,确定覆盖?", true); 489 | if (!$confirm) { 490 | return true; 491 | } 492 | // 这里显示下之前旧值,方便修改 493 | $this->get($parameters); 494 | $type = $this->redis->type($key); 495 | // 从整型值,转换为下面兼容的类型. 496 | switch ($type) { 497 | case 1: 498 | $type = 'String'; 499 | break; 500 | case 2: 501 | $type = 'Set'; 502 | break; 503 | case 3: 504 | $type = 'List'; 505 | break; 506 | case 4: 507 | $type = 'ZSet'; 508 | break; 509 | case 5: 510 | $type = 'Hash'; 511 | break; 512 | } 513 | } else { 514 | // 优化下这个逻辑,虽然修改已有数据类型是允许的,但是为了方便使用,我这里设置成不能改 515 | $type = $this->io->choice('请选择数据类型', ['String', 'Hash', 'List', 'Set', 'ZSet']); 516 | $type = strip_tags($type); 517 | } 518 | 519 | 520 | // 处理不同类型数据 521 | switch ($type) { 522 | case 'String': 523 | $this->setString($key); 524 | break; 525 | case 'Hash': 526 | $this->setHash($key); 527 | break; 528 | case 'List': 529 | $this->setList($key); 530 | break; 531 | case 'Set': 532 | $this->setSet($key); 533 | break; 534 | case 'ZSet': 535 | $this->setZSet($key); 536 | break; 537 | } 538 | 539 | } catch (\Exception $e) { 540 | $this->io->error($e->getMessage()); 541 | } 542 | } 543 | 544 | protected function setString($key) 545 | { 546 | $value = $this->io->ask('请输入值', null, function($value) { 547 | if (empty($value)) { 548 | throw new \RuntimeException('不能为空'); 549 | } 550 | 551 | return $value; 552 | } 553 | ); 554 | $this->redis->set($key, $value); 555 | $this->io->success("设置成功!"); 556 | $this->get([$key]); 557 | } 558 | 559 | protected function setZSet($key) 560 | { 561 | do { 562 | $exit = false; 563 | $item_key = $this->io->ask("编辑: help查看功能,exit编辑完成退出", "ZSet: " . $key); 564 | $item_key = trim($item_key); 565 | 566 | switch (true) { 567 | case stripos($item_key, 'exit') === 0 : 568 | $exit = true; 569 | $this->get([$key]); 570 | break; 571 | // TODO: rm 和 add 操作似乎不统一? 还没想好用哪种,各有优缺点. 572 | case stripos($item_key, 'rm ') === 0 : 573 | $parameter = trim(substr($item_key, 3)); 574 | $this->redis->zRem($key, $parameter); 575 | $this->io->success("删除成功!"); 576 | $this->get([$key]); 577 | break; 578 | case stripos($item_key, 'add') === 0 : 579 | $item_key = $this->io->ask('请输入排序权重值:Score', null, function($value) { 580 | if (empty($value)) { 581 | throw new \RuntimeException('不能为空'); 582 | } 583 | if (!is_numeric($value)) { 584 | throw new \RuntimeException('必须为数字'); 585 | } 586 | 587 | return $value; 588 | } 589 | ); 590 | $item_value = $this->io->ask('请输入Member值', null, function($value) { 591 | if (empty($value)) { 592 | throw new \RuntimeException('不能为空'); 593 | } 594 | 595 | return $value; 596 | } 597 | ); 598 | $this->redis->zAdd($key, (int)$item_key, $item_value); 599 | 600 | $this->io->success("修改成功!"); 601 | $this->get([$key]); 602 | break; 603 | case stripos($item_key, 'help ') === 0 : 604 | default: 605 | $this->io->title('Hash 命令列表'); 606 | $this->io->listing([ 607 | 'help : 显示可用命令', 608 | 'add : 增加记录', 609 | 'rm : 移除 value', 610 | 'exit : 退出编辑', 611 | ] 612 | ); 613 | 614 | } 615 | 616 | } while ($exit != true); 617 | 618 | return true; 619 | } 620 | 621 | protected function setSet($key) 622 | { 623 | do { 624 | $exit = false; 625 | $item_key = $this->io->ask("编辑: help查看功能,exit编辑完成退出", "Set: " . $key); 626 | $item_key = trim($item_key); 627 | 628 | switch (true) { 629 | case stripos($item_key, 'exit') === 0 : 630 | $exit = true; 631 | $this->get([$key]); 632 | break; 633 | // TODO: rm 和 add 操作似乎不统一? 还没想好用哪种,各有优缺点. 634 | case stripos($item_key, 'rm ') === 0 : 635 | $parameter = trim(substr($item_key, 3)); 636 | $this->redis->sRem($key, $parameter); 637 | $this->io->success("删除成功!"); 638 | $this->get([$key]); 639 | break; 640 | case stripos($item_key, 'add') === 0 : 641 | $item_value = $this->io->ask('请输入value', null, function($value) { 642 | if (empty($value)) { 643 | throw new \RuntimeException('不能为空'); 644 | } 645 | 646 | return $value; 647 | } 648 | ); 649 | $this->redis->sAdd($key, $item_value); 650 | 651 | $this->io->success("修改成功!"); 652 | $this->get([$key]); 653 | break; 654 | case stripos($item_key, 'help ') === 0 : 655 | default: 656 | $this->io->title('Hash 命令列表'); 657 | $this->io->listing([ 658 | 'help : 显示可用命令', 659 | 'add : 增加记录', 660 | 'rm : 移除 value', 661 | 'exit : 退出编辑', 662 | ] 663 | ); 664 | 665 | } 666 | 667 | } while ($exit != true); 668 | 669 | return true; 670 | } 671 | 672 | protected function setList($key) 673 | { 674 | do { 675 | $exit = false; 676 | $item_key = $this->io->ask("编辑: help查看功能,exit编辑完成退出", "List: " . $key); 677 | $item_key = trim($item_key); 678 | 679 | switch (true) { 680 | case stripos($item_key, 'exit') === 0 : 681 | $exit = true; 682 | $this->get([$key]); 683 | break; 684 | case stripos($item_key, 'lpop') === 0 : 685 | $v = $this->redis->lPop($key); 686 | $this->io->success("值 [$v] 出队"); 687 | $this->get([$key]); 688 | break; 689 | case stripos($item_key, 'rpop') === 0 : 690 | $v = $this->redis->rPop($key); 691 | $this->io->success("值 [$v] 出队"); 692 | $this->get([$key]); 693 | break; 694 | case stripos($item_key, 'lpush') === 0 : 695 | $item_value = $this->io->ask('请输入value', null, function($value) { 696 | if (is_null($value)) { 697 | throw new \RuntimeException('不能为空'); 698 | } 699 | 700 | return $value; 701 | } 702 | ); 703 | $item_value = trim($item_value); 704 | $this->redis->lPush($key, $item_value); 705 | 706 | $this->io->success("值 [$item_value] 左入队"); 707 | $this->get([$key]); 708 | break; 709 | case stripos($item_key, 'rpush') === 0 : 710 | $item_value = $this->io->ask('请输入value', null, function($value) { 711 | if (is_null($value)) { 712 | throw new \RuntimeException('不能为空'); 713 | } 714 | 715 | return $value; 716 | } 717 | ); 718 | $item_value = trim($item_value); 719 | $this->redis->rPush($key, $item_value); 720 | 721 | $this->io->success("值 [$item_value] 右入队"); 722 | $this->get([$key]); 723 | break; 724 | case stripos($item_key, 'help ') === 0 : 725 | default: 726 | $this->io->title('Hash 命令列表'); 727 | $this->io->listing([ 728 | 'help : 显示可用命令', 729 | 'lpush : 左入队', 730 | 'rpush : 右入队', 731 | 'lpop : 左出队', 732 | 'rpop : 右出队', 733 | 'exit : 退出编辑', 734 | ] 735 | ); 736 | 737 | } 738 | 739 | } while ($exit != true); 740 | 741 | return true; 742 | } 743 | 744 | // 设置 hash数据 745 | protected function setHash($key) 746 | { 747 | do { 748 | $exit = false; 749 | $item_key = $this->io->ask("编辑: help查看功能,exit编辑完成退出", "Hash: " . $key); 750 | $item_key = trim($item_key); 751 | 752 | switch (true) { 753 | case stripos($item_key, 'exit') === 0 : 754 | $exit = true; 755 | $this->get([$key]); 756 | break; 757 | case stripos($item_key, 'rm ') === 0 : 758 | $parameter = trim(substr($item_key, 3)); 759 | $this->redis->hDel($key, $parameter); 760 | $this->io->success("删除成功!"); 761 | $this->get([$key]); 762 | break; 763 | case stripos($item_key, 'mv ') === 0 : 764 | $parameter = trim(substr($item_key, 3)); 765 | $parameter = explode(' ', $parameter, 2); 766 | if (count($parameter) != 2) { 767 | throw new \RuntimeException('缺少参数'); 768 | } 769 | $v = $this->redis->hGet($key, $parameter[0]); 770 | // 改名操作 - 是否用事务? 771 | $this->redis->multi(); 772 | $this->redis->hSet($key, $parameter[1], $v); 773 | $this->redis->hDel($key, $parameter[0]); 774 | $this->redis->exec(); 775 | 776 | $this->io->success("修改成功!"); 777 | $this->get([$key]); 778 | break; 779 | case stripos($item_key, 'set') === 0 : 780 | $item_key = $this->io->ask('请输入key', null, function($value) { 781 | if (empty($value)) { 782 | throw new \RuntimeException('不能为空'); 783 | } 784 | 785 | return $value; 786 | } 787 | ); 788 | $item_value = $this->io->ask('请输入value', null, function($value) { 789 | if (empty($value)) { 790 | throw new \RuntimeException('不能为空'); 791 | } 792 | 793 | return $value; 794 | } 795 | ); 796 | $this->redis->hSet($key, $item_key, $item_value); 797 | 798 | $this->io->success("修改成功!"); 799 | $this->get([$key]); 800 | break; 801 | case stripos($item_key, 'help ') === 0 : 802 | default: 803 | $this->io->title('Hash 命令列表'); 804 | $this->io->listing([ 805 | 'help : 显示可用命令', 806 | 'set : 增加记录', 807 | 'rm : 移除key', 808 | 'mv : key改名', 809 | ] 810 | ); 811 | 812 | } 813 | 814 | } while ($exit != true); 815 | 816 | return true; 817 | } 818 | 819 | // 获取key数据详细内容 820 | protected function get($parameters) 821 | { 822 | try { 823 | if (!$this->redis->exists($parameters[0])) { 824 | throw new \Exception("KEY: {$parameters[0]} 不存在"); 825 | } 826 | $key = $parameters[0]; 827 | // 获取类型 828 | $type = $this->redis->type($key); 829 | $typeStr = $this->transType($type); 830 | // 获取ttl 831 | $ttl = $this->redis->ttl($key); 832 | $ttlStr = $this->transTtl($ttl); 833 | // 输出 834 | $this->io->section('STATUS:'); 835 | $this->io->table( 836 | ['TYPE', 'KEY', 'TTL'], 837 | [ 838 | [$typeStr, $key, $ttlStr], 839 | ] 840 | ); 841 | 842 | // 根据类型显示值 843 | // none(key不存在) int(0) 844 | // string(字符串) int(1) 845 | // list(列表) int(3) 846 | // set(集合) int(2) 847 | // zset(有序集) int(4) 848 | // hash(哈希表) int(5) 849 | switch ($type) { 850 | case 0: 851 | throw new \Exception("KEY: {$key} 不存在"); 852 | break; 853 | case 1: 854 | $this->io->section('VALUE:'); 855 | $content = (string)$this->redis->get($key); 856 | $this->io->text($content); 857 | // 尝试转换json或者反序列化,如果成功那么就再显示下. 858 | $data = $this->convertString($content); 859 | if ($data !== false) { 860 | $this->io->section('CONVERSION:'); 861 | print_r($data); 862 | } 863 | 864 | // 清理下数据 865 | unset($content); 866 | unset($data); 867 | break; 868 | case 2: 869 | // 集合set 870 | $this->io->section('VALUE:'); 871 | $value = $this->redis->sMembers($key); 872 | print_r($value); 873 | break; 874 | case 3: 875 | // 列表List 876 | $this->io->section('VALUE:'); 877 | $value = $this->redis->lRange($key, 0, -1); 878 | print_r($value); 879 | break; 880 | case 4: 881 | // 有续集Zset 882 | $this->io->section('VALUE:'); 883 | $value = $this->redis->zRevRange($key, 0, -1);//按照score倒序拍 884 | $data = []; 885 | foreach ($value as $id => $item) { 886 | $data[] = [ 887 | $id, 888 | $this->redis->zScore($key, $item), 889 | $item, 890 | ]; 891 | } 892 | $this->io->table( 893 | ['ID', 'SCORE', 'MEMBER'], 894 | $data 895 | ); 896 | break; 897 | case 5: 898 | // 哈希表 899 | $this->io->section('VALUE:'); 900 | $value = (array)$this->redis->hGetAll($key); 901 | $data = []; 902 | foreach ($value as $key => $item) { 903 | $data[] = [ 904 | $key, $item, 905 | ]; 906 | } 907 | $this->io->table( 908 | ['KEY', 'MEMBER'], 909 | $data 910 | ); 911 | break; 912 | 913 | } 914 | 915 | } catch (\Exception $e) { 916 | $this->io->error($e->getMessage()); 917 | } 918 | } 919 | 920 | } --------------------------------------------------------------------------------