├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── VERSION ├── autoload.php ├── composer.json ├── composer.lock ├── examples ├── execute_commands.php ├── execute_commands_connection.php ├── list_blocking_pop.php ├── monitor_loop.php ├── pubsub_loop.php ├── server_side_scripts.php └── transaction.php ├── phpiredis.ini ├── phpunit.xml.dist ├── src ├── Client.php ├── CommunicationException.php ├── Configuration │ ├── EventLoopOption.php │ ├── Options.php │ └── PhpiredisOption.php ├── Connection │ ├── AbstractConnection.php │ ├── Buffer │ │ └── StringBuffer.php │ ├── ConnectionException.php │ ├── ConnectionInterface.php │ ├── PhpiredisStreamConnection.php │ ├── State.php │ └── StreamConnection.php ├── Monitor │ └── Consumer.php ├── PubSub │ └── Consumer.php └── Transaction │ └── MultiExec.php ├── tests ├── PHPUnit │ └── PredisAsyncTestCase.php ├── Predis │ └── Async │ │ └── ClientTest.php ├── README.md └── bootstrap.php └── travisci-install.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .php-version 2 | phpunit.xml 3 | experiments/ 4 | vendor/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | - 7.1.0alpha1 8 | - hhvm 9 | services: 10 | - redis-server 11 | install: 12 | - ./travisci-install.sh 13 | before_script: 14 | - composer install 15 | matrix: 16 | allow_failures: 17 | - php: hhvm 18 | - php: 7.1.0alpha1 19 | fast_finish: true 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v0.3.0 (2014-xx-xx) 2 | =============================================================================== 3 | 4 | - Switched to PSR-4 for autoloading. 5 | 6 | - Switched to react/event-loop 0.4 and predis/predis 1.0. This change breaks the 7 | compatibility with previous versions of Predis\Async due to the changes needed 8 | to adapt to the new (and stable) API of Predis. Support for PHP 5.3 has been 9 | dropped since newer versions of React require PHP >= 5.4. 10 | 11 | - The phpiredis extension is now optional and by default the client uses a pure 12 | PHP protocol serializer / parser provided by the clue/redis-protocol library. 13 | This change also enables Predis\Async to be used with HHVM. 14 | 15 | - Callbacks are now required by Client::connect(), Client::executeCommand(), 16 | ConnectionInterface::connect() and ConnectionInterface::executeCommand() but 17 | they are still optional when using Client::__call(). 18 | 19 | 20 | v0.2.2 (2013-09-04) 21 | =============================================================================== 22 | 23 | - __FIX__: when connection refused exceptions are thrown the client gets stuck 24 | in a retry loop that keeps on raising exceptions until the specified server 25 | starts accepting new connections again. 26 | 27 | 28 | v0.2.1 (2013-08-27) 29 | =============================================================================== 30 | 31 | - __FIX__: properly release subscription to write events when the write buffer 32 | is empty (ISSUE #5). 33 | 34 | 35 | v0.2.0 (2013-07-27) 36 | =============================================================================== 37 | 38 | - The library now requires react/event-loop v0.3.x. 39 | 40 | 41 | v0.1.0 (2013-07-27) 42 | =============================================================================== 43 | 44 | - First versioned release of Predis\Async. This should be used if you still 45 | depend on react v0.2.x. 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2016 Daniele Alessandri 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Predis\Async # 2 | 3 | [![Software license][ico-license]](LICENSE) 4 | [![Latest stable][ico-version-stable]][link-packagist] 5 | [![Latest development][ico-version-dev]][link-packagist] 6 | [![Monthly installs][ico-downloads-monthly]][link-downloads] 7 | [![Build status][ico-travis]][link-travis] 8 | [![HHVM support][ico-hhvm]][link-hhvm] 9 | 10 | Asynchronous (non-blocking) version of [Predis](https://github.com/nrk/predis), the full-featured 11 | PHP client library for [Redis](http://redis.io), built on top of [React](http://reactphp.org/) to 12 | handle evented I/O. By default Predis\Async does not require any additional C extension to work, but 13 | it can be optionally paired with [phpiredis](https://github.com/nrk/phpiredis) to sensibly lower the 14 | overhead of serializing and parsing the Redis protocol. 15 | 16 | Predis\Async is currently under development but already works pretty well. The client foundation is 17 | being built on top of the event loop abstraction offered by [React](https://github.com/reactphp), an 18 | event-oriented framework for PHP that aims to provide everything needed to create reusable libraries 19 | and long-running applications using an evented approach powered by non-blocking I/O. This library is 20 | partially tested on [HHVM](http://www.hhvm.com), but support for this runtime should be considered 21 | experimental. 22 | 23 | Contributions are highly welcome and appreciated, feel free to open pull-requests with fixes or just 24 | [report issues](https://github.com/nrk/predis-async/issues) if you encounter weird behaviors and 25 | blatant bugs. 26 | 27 | ## Main features ## 28 | 29 | - Wide range of Redis versions supported (from __2.0__ to __3.0__ and __unstable__) using profiles. 30 | - Transparent key prefixing for all known Redis commands using a customizable prefixing strategy. 31 | - Abstraction for `MULTI` / `EXEC` transactions (Redis >= 2.0). 32 | - Abstraction for `PUBLISH` / `SUBSCRIBE` contexts (Redis >= 2.0). 33 | - Abstraction for `MONITOR` contexts (Redis >= 1.2). 34 | - Abstraction for Lua scripting (Redis >= 2.6). 35 | - Ability to connect to Redis using TCP/IP or UNIX domain sockets. 36 | - Redis connections can be established lazily, commands are queued while the client is connecting. 37 | - Flexible system for defining and registering custom sets of supported commands or profiles. 38 | 39 | ## Installing ## 40 | 41 | Predis\Async is available on [Packagist](http://packagist.org/packages/predis/predis-async). It is 42 | not required to have the [phpiredis](https://github.com/nrk/phpiredis) extension loaded as suggested 43 | since the client will work anyway using a pure-PHP protocol parser, but if the extension is detected 44 | at runtime then it will be automatically preferred over the slower default. It is possible to force 45 | the client to use the pure-PHP protocol parser even when the extension is detected simply by passing 46 | `['phpiredis' => false]` in the array of client options. 47 | 48 | ## Example ## 49 | 50 | ``` php 51 | connect(function ($client) use ($loop) { 58 | echo "Connected to Redis, now listening for incoming messages...\n"; 59 | 60 | $logger = new Predis\Async\Client('tcp://127.0.0.1:6379', $loop); 61 | 62 | $client->pubSubLoop('nrk:channel', function ($event) use ($logger) { 63 | $logger->rpush("store:{$event->channel}", $event->payload, function () use ($event) { 64 | echo "Stored message `{$event->payload}` from {$event->channel}.\n"; 65 | }); 66 | }); 67 | }); 68 | 69 | $loop->run(); 70 | ``` 71 | 72 | ## Differences with Predis ## 73 | 74 | Being an asynchronous client implementation, the underlying design of Predis\Async is different from 75 | the one of Predis which is a blocking implementation. Certain features have not been implemented yet 76 | (or cannot be implemented at all), just to name a few you will not find the usual abstractions for 77 | pipelining commands and creating cluster of nodes using client-side sharding. That said, they share 78 | a common style and a few basic classes so if you used Predis in the past you should feel at home. 79 | 80 | ## Contributing ## 81 | 82 | If you want to work on Predis\Async, it is highly recommended that you first run the test suite in 83 | order to check that everything is OK, and report strange behaviours or bugs. When modifying the code 84 | please make sure that no warnings or notices are emitted by PHP by running the interpreter in your 85 | development environment with the `error_reporting` variable set to `E_ALL | E_STRICT`. 86 | 87 | The recommended way to contribute to Predis\Async is to fork the project on GitHub, create new topic 88 | branches on your newly created repository to fix or add features (possibly with tests covering your 89 | modifications) and then open a new pull request with a description of the applied changes. Obviously 90 | you can use any other Git hosting provider of your preference. 91 | 92 | Please follow a few basic [commit guidelines](http://git-scm.com/book/ch5-2.html#Commit-Guidelines) 93 | before opening pull requests. 94 | 95 | ### Project ### 96 | - [Source code](https://github.com/nrk/predis-async/) 97 | - [Issue tracker](https://github.com/nrk/predis-async/issues) 98 | 99 | ## Author ## 100 | 101 | - [Daniele Alessandri](mailto:suppakilla@gmail.com) ([twitter](http://twitter.com/JoL1hAHN)) 102 | 103 | ## License ## 104 | 105 | The code for Predis\Async is distributed under the terms of the MIT license (see LICENSE). 106 | 107 | [ico-license]: https://img.shields.io/github/license/nrk/predis-async.svg?style=flat-square 108 | [ico-version-stable]: https://img.shields.io/packagist/v/predis/predis-async.svg?style=flat-square 109 | [ico-version-dev]: https://img.shields.io/packagist/vpre/predis/predis-async.svg?style=flat-square 110 | [ico-downloads-monthly]: https://img.shields.io/packagist/dm/predis/predis-async.svg?style=flat-square 111 | [ico-travis]: https://img.shields.io/travis/nrk/predis-async.svg?style=flat-square 112 | [ico-hhvm]: https://img.shields.io/hhvm/predis/predis-async.svg?style=flat-square 113 | 114 | [link-packagist]: https://packagist.org/packages/predis/predis-async 115 | [link-travis]: https://travis-ci.org/nrk/predis-async 116 | [link-downloads]: https://packagist.org/packages/predis/predis-async/stats 117 | [link-hhvm]: http://hhvm.h4cc.de/package/predis/predis-async 118 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.0-dev 2 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require __DIR__.'/vendor/autoload.php'; 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "predis/predis-async", 3 | "type": "library", 4 | "description": "Asynchronous version of Predis", 5 | "keywords": ["nosql", "redis", "predis"], 6 | "homepage": "http://github.com/nrk/predis-async", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Daniele Alessandri", 11 | "email": "suppakilla@gmail.com", 12 | "homepage": "http://clorophilla.net" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=5.4.0", 17 | "predis/predis": "^1.0", 18 | "react/event-loop": "~0.4", 19 | "clue/redis-protocol": "~0.3" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "~4.8" 23 | }, 24 | "suggest": { 25 | "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" 26 | }, 27 | "autoload": { 28 | "psr-4": {"Predis\\Async\\": "src/"} 29 | }, 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "0.3-dev" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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 | "hash": "71546d2a467c1472877b3bdd49ae520d", 8 | "content-hash": "61aca3d55984cf5ebca6de4384142590", 9 | "packages": [ 10 | { 11 | "name": "clue/redis-protocol", 12 | "version": "v0.3.0", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/clue/php-redis-protocol.git", 16 | "reference": "2ffcdfa281b1084f666340e13f05a1d26d35ed26" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/clue/php-redis-protocol/zipball/2ffcdfa281b1084f666340e13f05a1d26d35ed26", 21 | "reference": "2ffcdfa281b1084f666340e13f05a1d26d35ed26", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": ">=5.3" 26 | }, 27 | "type": "library", 28 | "autoload": { 29 | "psr-0": { 30 | "Clue\\Redis\\Protocol": "src" 31 | } 32 | }, 33 | "notification-url": "https://packagist.org/downloads/", 34 | "license": [ 35 | "MIT" 36 | ], 37 | "authors": [ 38 | { 39 | "name": "Christian Lück", 40 | "email": "christian@lueck.tv" 41 | } 42 | ], 43 | "description": "A streaming redis wire protocol parser and serializer implementation in PHP", 44 | "homepage": "https://github.com/clue/redis-protocol", 45 | "keywords": [ 46 | "parser", 47 | "protocol", 48 | "redis", 49 | "serializer", 50 | "streaming" 51 | ], 52 | "time": "2014-01-26 23:49:05" 53 | }, 54 | { 55 | "name": "predis/predis", 56 | "version": "v1.1.0", 57 | "source": { 58 | "type": "git", 59 | "url": "https://github.com/nrk/predis.git", 60 | "reference": "0e17edbefb50c6cbd1acc4a6f6ef06399deb1af2" 61 | }, 62 | "dist": { 63 | "type": "zip", 64 | "url": "https://api.github.com/repos/nrk/predis/zipball/0e17edbefb50c6cbd1acc4a6f6ef06399deb1af2", 65 | "reference": "0e17edbefb50c6cbd1acc4a6f6ef06399deb1af2", 66 | "shasum": "" 67 | }, 68 | "require": { 69 | "php": ">=5.3.9" 70 | }, 71 | "require-dev": { 72 | "phpunit/phpunit": "~4.8" 73 | }, 74 | "suggest": { 75 | "ext-curl": "Allows access to Webdis when paired with phpiredis", 76 | "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" 77 | }, 78 | "type": "library", 79 | "autoload": { 80 | "psr-4": { 81 | "Predis\\": "src/" 82 | } 83 | }, 84 | "notification-url": "https://packagist.org/downloads/", 85 | "license": [ 86 | "MIT" 87 | ], 88 | "authors": [ 89 | { 90 | "name": "Daniele Alessandri", 91 | "email": "suppakilla@gmail.com", 92 | "homepage": "http://clorophilla.net" 93 | } 94 | ], 95 | "description": "Flexible and feature-complete Redis client for PHP and HHVM", 96 | "homepage": "http://github.com/nrk/predis", 97 | "keywords": [ 98 | "nosql", 99 | "predis", 100 | "redis" 101 | ], 102 | "time": "2016-06-01 22:06:21" 103 | }, 104 | { 105 | "name": "react/event-loop", 106 | "version": "v0.4.2", 107 | "source": { 108 | "type": "git", 109 | "url": "https://github.com/reactphp/event-loop.git", 110 | "reference": "164799f73175e1c80bba92a220ea35df6ca371dd" 111 | }, 112 | "dist": { 113 | "type": "zip", 114 | "url": "https://api.github.com/repos/reactphp/event-loop/zipball/164799f73175e1c80bba92a220ea35df6ca371dd", 115 | "reference": "164799f73175e1c80bba92a220ea35df6ca371dd", 116 | "shasum": "" 117 | }, 118 | "require": { 119 | "php": ">=5.4.0" 120 | }, 121 | "suggest": { 122 | "ext-event": "~1.0", 123 | "ext-libev": "*", 124 | "ext-libevent": ">=0.1.0" 125 | }, 126 | "type": "library", 127 | "extra": { 128 | "branch-alias": { 129 | "dev-master": "0.5-dev" 130 | } 131 | }, 132 | "autoload": { 133 | "psr-4": { 134 | "React\\EventLoop\\": "src" 135 | } 136 | }, 137 | "notification-url": "https://packagist.org/downloads/", 138 | "license": [ 139 | "MIT" 140 | ], 141 | "description": "Event loop abstraction layer that libraries can use for evented I/O.", 142 | "keywords": [ 143 | "asynchronous", 144 | "event-loop" 145 | ], 146 | "time": "2016-03-08 02:09:32" 147 | } 148 | ], 149 | "packages-dev": [ 150 | { 151 | "name": "doctrine/instantiator", 152 | "version": "1.0.5", 153 | "source": { 154 | "type": "git", 155 | "url": "https://github.com/doctrine/instantiator.git", 156 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" 157 | }, 158 | "dist": { 159 | "type": "zip", 160 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", 161 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", 162 | "shasum": "" 163 | }, 164 | "require": { 165 | "php": ">=5.3,<8.0-DEV" 166 | }, 167 | "require-dev": { 168 | "athletic/athletic": "~0.1.8", 169 | "ext-pdo": "*", 170 | "ext-phar": "*", 171 | "phpunit/phpunit": "~4.0", 172 | "squizlabs/php_codesniffer": "~2.0" 173 | }, 174 | "type": "library", 175 | "extra": { 176 | "branch-alias": { 177 | "dev-master": "1.0.x-dev" 178 | } 179 | }, 180 | "autoload": { 181 | "psr-4": { 182 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 183 | } 184 | }, 185 | "notification-url": "https://packagist.org/downloads/", 186 | "license": [ 187 | "MIT" 188 | ], 189 | "authors": [ 190 | { 191 | "name": "Marco Pivetta", 192 | "email": "ocramius@gmail.com", 193 | "homepage": "http://ocramius.github.com/" 194 | } 195 | ], 196 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 197 | "homepage": "https://github.com/doctrine/instantiator", 198 | "keywords": [ 199 | "constructor", 200 | "instantiate" 201 | ], 202 | "time": "2015-06-14 21:17:01" 203 | }, 204 | { 205 | "name": "phpdocumentor/reflection-docblock", 206 | "version": "2.0.4", 207 | "source": { 208 | "type": "git", 209 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 210 | "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" 211 | }, 212 | "dist": { 213 | "type": "zip", 214 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", 215 | "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", 216 | "shasum": "" 217 | }, 218 | "require": { 219 | "php": ">=5.3.3" 220 | }, 221 | "require-dev": { 222 | "phpunit/phpunit": "~4.0" 223 | }, 224 | "suggest": { 225 | "dflydev/markdown": "~1.0", 226 | "erusev/parsedown": "~1.0" 227 | }, 228 | "type": "library", 229 | "extra": { 230 | "branch-alias": { 231 | "dev-master": "2.0.x-dev" 232 | } 233 | }, 234 | "autoload": { 235 | "psr-0": { 236 | "phpDocumentor": [ 237 | "src/" 238 | ] 239 | } 240 | }, 241 | "notification-url": "https://packagist.org/downloads/", 242 | "license": [ 243 | "MIT" 244 | ], 245 | "authors": [ 246 | { 247 | "name": "Mike van Riel", 248 | "email": "mike.vanriel@naenius.com" 249 | } 250 | ], 251 | "time": "2015-02-03 12:10:50" 252 | }, 253 | { 254 | "name": "phpspec/prophecy", 255 | "version": "v1.6.1", 256 | "source": { 257 | "type": "git", 258 | "url": "https://github.com/phpspec/prophecy.git", 259 | "reference": "58a8137754bc24b25740d4281399a4a3596058e0" 260 | }, 261 | "dist": { 262 | "type": "zip", 263 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/58a8137754bc24b25740d4281399a4a3596058e0", 264 | "reference": "58a8137754bc24b25740d4281399a4a3596058e0", 265 | "shasum": "" 266 | }, 267 | "require": { 268 | "doctrine/instantiator": "^1.0.2", 269 | "php": "^5.3|^7.0", 270 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", 271 | "sebastian/comparator": "^1.1", 272 | "sebastian/recursion-context": "^1.0" 273 | }, 274 | "require-dev": { 275 | "phpspec/phpspec": "^2.0" 276 | }, 277 | "type": "library", 278 | "extra": { 279 | "branch-alias": { 280 | "dev-master": "1.6.x-dev" 281 | } 282 | }, 283 | "autoload": { 284 | "psr-0": { 285 | "Prophecy\\": "src/" 286 | } 287 | }, 288 | "notification-url": "https://packagist.org/downloads/", 289 | "license": [ 290 | "MIT" 291 | ], 292 | "authors": [ 293 | { 294 | "name": "Konstantin Kudryashov", 295 | "email": "ever.zet@gmail.com", 296 | "homepage": "http://everzet.com" 297 | }, 298 | { 299 | "name": "Marcello Duarte", 300 | "email": "marcello.duarte@gmail.com" 301 | } 302 | ], 303 | "description": "Highly opinionated mocking framework for PHP 5.3+", 304 | "homepage": "https://github.com/phpspec/prophecy", 305 | "keywords": [ 306 | "Double", 307 | "Dummy", 308 | "fake", 309 | "mock", 310 | "spy", 311 | "stub" 312 | ], 313 | "time": "2016-06-07 08:13:47" 314 | }, 315 | { 316 | "name": "phpunit/php-code-coverage", 317 | "version": "2.2.4", 318 | "source": { 319 | "type": "git", 320 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 321 | "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" 322 | }, 323 | "dist": { 324 | "type": "zip", 325 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", 326 | "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", 327 | "shasum": "" 328 | }, 329 | "require": { 330 | "php": ">=5.3.3", 331 | "phpunit/php-file-iterator": "~1.3", 332 | "phpunit/php-text-template": "~1.2", 333 | "phpunit/php-token-stream": "~1.3", 334 | "sebastian/environment": "^1.3.2", 335 | "sebastian/version": "~1.0" 336 | }, 337 | "require-dev": { 338 | "ext-xdebug": ">=2.1.4", 339 | "phpunit/phpunit": "~4" 340 | }, 341 | "suggest": { 342 | "ext-dom": "*", 343 | "ext-xdebug": ">=2.2.1", 344 | "ext-xmlwriter": "*" 345 | }, 346 | "type": "library", 347 | "extra": { 348 | "branch-alias": { 349 | "dev-master": "2.2.x-dev" 350 | } 351 | }, 352 | "autoload": { 353 | "classmap": [ 354 | "src/" 355 | ] 356 | }, 357 | "notification-url": "https://packagist.org/downloads/", 358 | "license": [ 359 | "BSD-3-Clause" 360 | ], 361 | "authors": [ 362 | { 363 | "name": "Sebastian Bergmann", 364 | "email": "sb@sebastian-bergmann.de", 365 | "role": "lead" 366 | } 367 | ], 368 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 369 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 370 | "keywords": [ 371 | "coverage", 372 | "testing", 373 | "xunit" 374 | ], 375 | "time": "2015-10-06 15:47:00" 376 | }, 377 | { 378 | "name": "phpunit/php-file-iterator", 379 | "version": "1.4.1", 380 | "source": { 381 | "type": "git", 382 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 383 | "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" 384 | }, 385 | "dist": { 386 | "type": "zip", 387 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", 388 | "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", 389 | "shasum": "" 390 | }, 391 | "require": { 392 | "php": ">=5.3.3" 393 | }, 394 | "type": "library", 395 | "extra": { 396 | "branch-alias": { 397 | "dev-master": "1.4.x-dev" 398 | } 399 | }, 400 | "autoload": { 401 | "classmap": [ 402 | "src/" 403 | ] 404 | }, 405 | "notification-url": "https://packagist.org/downloads/", 406 | "license": [ 407 | "BSD-3-Clause" 408 | ], 409 | "authors": [ 410 | { 411 | "name": "Sebastian Bergmann", 412 | "email": "sb@sebastian-bergmann.de", 413 | "role": "lead" 414 | } 415 | ], 416 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 417 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 418 | "keywords": [ 419 | "filesystem", 420 | "iterator" 421 | ], 422 | "time": "2015-06-21 13:08:43" 423 | }, 424 | { 425 | "name": "phpunit/php-text-template", 426 | "version": "1.2.1", 427 | "source": { 428 | "type": "git", 429 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 430 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 431 | }, 432 | "dist": { 433 | "type": "zip", 434 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 435 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 436 | "shasum": "" 437 | }, 438 | "require": { 439 | "php": ">=5.3.3" 440 | }, 441 | "type": "library", 442 | "autoload": { 443 | "classmap": [ 444 | "src/" 445 | ] 446 | }, 447 | "notification-url": "https://packagist.org/downloads/", 448 | "license": [ 449 | "BSD-3-Clause" 450 | ], 451 | "authors": [ 452 | { 453 | "name": "Sebastian Bergmann", 454 | "email": "sebastian@phpunit.de", 455 | "role": "lead" 456 | } 457 | ], 458 | "description": "Simple template engine.", 459 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 460 | "keywords": [ 461 | "template" 462 | ], 463 | "time": "2015-06-21 13:50:34" 464 | }, 465 | { 466 | "name": "phpunit/php-timer", 467 | "version": "1.0.8", 468 | "source": { 469 | "type": "git", 470 | "url": "https://github.com/sebastianbergmann/php-timer.git", 471 | "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" 472 | }, 473 | "dist": { 474 | "type": "zip", 475 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", 476 | "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", 477 | "shasum": "" 478 | }, 479 | "require": { 480 | "php": ">=5.3.3" 481 | }, 482 | "require-dev": { 483 | "phpunit/phpunit": "~4|~5" 484 | }, 485 | "type": "library", 486 | "autoload": { 487 | "classmap": [ 488 | "src/" 489 | ] 490 | }, 491 | "notification-url": "https://packagist.org/downloads/", 492 | "license": [ 493 | "BSD-3-Clause" 494 | ], 495 | "authors": [ 496 | { 497 | "name": "Sebastian Bergmann", 498 | "email": "sb@sebastian-bergmann.de", 499 | "role": "lead" 500 | } 501 | ], 502 | "description": "Utility class for timing", 503 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 504 | "keywords": [ 505 | "timer" 506 | ], 507 | "time": "2016-05-12 18:03:57" 508 | }, 509 | { 510 | "name": "phpunit/php-token-stream", 511 | "version": "1.4.8", 512 | "source": { 513 | "type": "git", 514 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 515 | "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" 516 | }, 517 | "dist": { 518 | "type": "zip", 519 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", 520 | "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", 521 | "shasum": "" 522 | }, 523 | "require": { 524 | "ext-tokenizer": "*", 525 | "php": ">=5.3.3" 526 | }, 527 | "require-dev": { 528 | "phpunit/phpunit": "~4.2" 529 | }, 530 | "type": "library", 531 | "extra": { 532 | "branch-alias": { 533 | "dev-master": "1.4-dev" 534 | } 535 | }, 536 | "autoload": { 537 | "classmap": [ 538 | "src/" 539 | ] 540 | }, 541 | "notification-url": "https://packagist.org/downloads/", 542 | "license": [ 543 | "BSD-3-Clause" 544 | ], 545 | "authors": [ 546 | { 547 | "name": "Sebastian Bergmann", 548 | "email": "sebastian@phpunit.de" 549 | } 550 | ], 551 | "description": "Wrapper around PHP's tokenizer extension.", 552 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 553 | "keywords": [ 554 | "tokenizer" 555 | ], 556 | "time": "2015-09-15 10:49:45" 557 | }, 558 | { 559 | "name": "phpunit/phpunit", 560 | "version": "4.8.26", 561 | "source": { 562 | "type": "git", 563 | "url": "https://github.com/sebastianbergmann/phpunit.git", 564 | "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74" 565 | }, 566 | "dist": { 567 | "type": "zip", 568 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc1d8cd5b5de11625979125c5639347896ac2c74", 569 | "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74", 570 | "shasum": "" 571 | }, 572 | "require": { 573 | "ext-dom": "*", 574 | "ext-json": "*", 575 | "ext-pcre": "*", 576 | "ext-reflection": "*", 577 | "ext-spl": "*", 578 | "php": ">=5.3.3", 579 | "phpspec/prophecy": "^1.3.1", 580 | "phpunit/php-code-coverage": "~2.1", 581 | "phpunit/php-file-iterator": "~1.4", 582 | "phpunit/php-text-template": "~1.2", 583 | "phpunit/php-timer": "^1.0.6", 584 | "phpunit/phpunit-mock-objects": "~2.3", 585 | "sebastian/comparator": "~1.1", 586 | "sebastian/diff": "~1.2", 587 | "sebastian/environment": "~1.3", 588 | "sebastian/exporter": "~1.2", 589 | "sebastian/global-state": "~1.0", 590 | "sebastian/version": "~1.0", 591 | "symfony/yaml": "~2.1|~3.0" 592 | }, 593 | "suggest": { 594 | "phpunit/php-invoker": "~1.1" 595 | }, 596 | "bin": [ 597 | "phpunit" 598 | ], 599 | "type": "library", 600 | "extra": { 601 | "branch-alias": { 602 | "dev-master": "4.8.x-dev" 603 | } 604 | }, 605 | "autoload": { 606 | "classmap": [ 607 | "src/" 608 | ] 609 | }, 610 | "notification-url": "https://packagist.org/downloads/", 611 | "license": [ 612 | "BSD-3-Clause" 613 | ], 614 | "authors": [ 615 | { 616 | "name": "Sebastian Bergmann", 617 | "email": "sebastian@phpunit.de", 618 | "role": "lead" 619 | } 620 | ], 621 | "description": "The PHP Unit Testing framework.", 622 | "homepage": "https://phpunit.de/", 623 | "keywords": [ 624 | "phpunit", 625 | "testing", 626 | "xunit" 627 | ], 628 | "time": "2016-05-17 03:09:28" 629 | }, 630 | { 631 | "name": "phpunit/phpunit-mock-objects", 632 | "version": "2.3.8", 633 | "source": { 634 | "type": "git", 635 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 636 | "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" 637 | }, 638 | "dist": { 639 | "type": "zip", 640 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", 641 | "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", 642 | "shasum": "" 643 | }, 644 | "require": { 645 | "doctrine/instantiator": "^1.0.2", 646 | "php": ">=5.3.3", 647 | "phpunit/php-text-template": "~1.2", 648 | "sebastian/exporter": "~1.2" 649 | }, 650 | "require-dev": { 651 | "phpunit/phpunit": "~4.4" 652 | }, 653 | "suggest": { 654 | "ext-soap": "*" 655 | }, 656 | "type": "library", 657 | "extra": { 658 | "branch-alias": { 659 | "dev-master": "2.3.x-dev" 660 | } 661 | }, 662 | "autoload": { 663 | "classmap": [ 664 | "src/" 665 | ] 666 | }, 667 | "notification-url": "https://packagist.org/downloads/", 668 | "license": [ 669 | "BSD-3-Clause" 670 | ], 671 | "authors": [ 672 | { 673 | "name": "Sebastian Bergmann", 674 | "email": "sb@sebastian-bergmann.de", 675 | "role": "lead" 676 | } 677 | ], 678 | "description": "Mock Object library for PHPUnit", 679 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 680 | "keywords": [ 681 | "mock", 682 | "xunit" 683 | ], 684 | "time": "2015-10-02 06:51:40" 685 | }, 686 | { 687 | "name": "sebastian/comparator", 688 | "version": "1.2.0", 689 | "source": { 690 | "type": "git", 691 | "url": "https://github.com/sebastianbergmann/comparator.git", 692 | "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" 693 | }, 694 | "dist": { 695 | "type": "zip", 696 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", 697 | "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", 698 | "shasum": "" 699 | }, 700 | "require": { 701 | "php": ">=5.3.3", 702 | "sebastian/diff": "~1.2", 703 | "sebastian/exporter": "~1.2" 704 | }, 705 | "require-dev": { 706 | "phpunit/phpunit": "~4.4" 707 | }, 708 | "type": "library", 709 | "extra": { 710 | "branch-alias": { 711 | "dev-master": "1.2.x-dev" 712 | } 713 | }, 714 | "autoload": { 715 | "classmap": [ 716 | "src/" 717 | ] 718 | }, 719 | "notification-url": "https://packagist.org/downloads/", 720 | "license": [ 721 | "BSD-3-Clause" 722 | ], 723 | "authors": [ 724 | { 725 | "name": "Jeff Welch", 726 | "email": "whatthejeff@gmail.com" 727 | }, 728 | { 729 | "name": "Volker Dusch", 730 | "email": "github@wallbash.com" 731 | }, 732 | { 733 | "name": "Bernhard Schussek", 734 | "email": "bschussek@2bepublished.at" 735 | }, 736 | { 737 | "name": "Sebastian Bergmann", 738 | "email": "sebastian@phpunit.de" 739 | } 740 | ], 741 | "description": "Provides the functionality to compare PHP values for equality", 742 | "homepage": "http://www.github.com/sebastianbergmann/comparator", 743 | "keywords": [ 744 | "comparator", 745 | "compare", 746 | "equality" 747 | ], 748 | "time": "2015-07-26 15:48:44" 749 | }, 750 | { 751 | "name": "sebastian/diff", 752 | "version": "1.4.1", 753 | "source": { 754 | "type": "git", 755 | "url": "https://github.com/sebastianbergmann/diff.git", 756 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" 757 | }, 758 | "dist": { 759 | "type": "zip", 760 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", 761 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", 762 | "shasum": "" 763 | }, 764 | "require": { 765 | "php": ">=5.3.3" 766 | }, 767 | "require-dev": { 768 | "phpunit/phpunit": "~4.8" 769 | }, 770 | "type": "library", 771 | "extra": { 772 | "branch-alias": { 773 | "dev-master": "1.4-dev" 774 | } 775 | }, 776 | "autoload": { 777 | "classmap": [ 778 | "src/" 779 | ] 780 | }, 781 | "notification-url": "https://packagist.org/downloads/", 782 | "license": [ 783 | "BSD-3-Clause" 784 | ], 785 | "authors": [ 786 | { 787 | "name": "Kore Nordmann", 788 | "email": "mail@kore-nordmann.de" 789 | }, 790 | { 791 | "name": "Sebastian Bergmann", 792 | "email": "sebastian@phpunit.de" 793 | } 794 | ], 795 | "description": "Diff implementation", 796 | "homepage": "https://github.com/sebastianbergmann/diff", 797 | "keywords": [ 798 | "diff" 799 | ], 800 | "time": "2015-12-08 07:14:41" 801 | }, 802 | { 803 | "name": "sebastian/environment", 804 | "version": "1.3.7", 805 | "source": { 806 | "type": "git", 807 | "url": "https://github.com/sebastianbergmann/environment.git", 808 | "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716" 809 | }, 810 | "dist": { 811 | "type": "zip", 812 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/4e8f0da10ac5802913afc151413bc8c53b6c2716", 813 | "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716", 814 | "shasum": "" 815 | }, 816 | "require": { 817 | "php": ">=5.3.3" 818 | }, 819 | "require-dev": { 820 | "phpunit/phpunit": "~4.4" 821 | }, 822 | "type": "library", 823 | "extra": { 824 | "branch-alias": { 825 | "dev-master": "1.3.x-dev" 826 | } 827 | }, 828 | "autoload": { 829 | "classmap": [ 830 | "src/" 831 | ] 832 | }, 833 | "notification-url": "https://packagist.org/downloads/", 834 | "license": [ 835 | "BSD-3-Clause" 836 | ], 837 | "authors": [ 838 | { 839 | "name": "Sebastian Bergmann", 840 | "email": "sebastian@phpunit.de" 841 | } 842 | ], 843 | "description": "Provides functionality to handle HHVM/PHP environments", 844 | "homepage": "http://www.github.com/sebastianbergmann/environment", 845 | "keywords": [ 846 | "Xdebug", 847 | "environment", 848 | "hhvm" 849 | ], 850 | "time": "2016-05-17 03:18:57" 851 | }, 852 | { 853 | "name": "sebastian/exporter", 854 | "version": "1.2.1", 855 | "source": { 856 | "type": "git", 857 | "url": "https://github.com/sebastianbergmann/exporter.git", 858 | "reference": "7ae5513327cb536431847bcc0c10edba2701064e" 859 | }, 860 | "dist": { 861 | "type": "zip", 862 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e", 863 | "reference": "7ae5513327cb536431847bcc0c10edba2701064e", 864 | "shasum": "" 865 | }, 866 | "require": { 867 | "php": ">=5.3.3", 868 | "sebastian/recursion-context": "~1.0" 869 | }, 870 | "require-dev": { 871 | "phpunit/phpunit": "~4.4" 872 | }, 873 | "type": "library", 874 | "extra": { 875 | "branch-alias": { 876 | "dev-master": "1.2.x-dev" 877 | } 878 | }, 879 | "autoload": { 880 | "classmap": [ 881 | "src/" 882 | ] 883 | }, 884 | "notification-url": "https://packagist.org/downloads/", 885 | "license": [ 886 | "BSD-3-Clause" 887 | ], 888 | "authors": [ 889 | { 890 | "name": "Jeff Welch", 891 | "email": "whatthejeff@gmail.com" 892 | }, 893 | { 894 | "name": "Volker Dusch", 895 | "email": "github@wallbash.com" 896 | }, 897 | { 898 | "name": "Bernhard Schussek", 899 | "email": "bschussek@2bepublished.at" 900 | }, 901 | { 902 | "name": "Sebastian Bergmann", 903 | "email": "sebastian@phpunit.de" 904 | }, 905 | { 906 | "name": "Adam Harvey", 907 | "email": "aharvey@php.net" 908 | } 909 | ], 910 | "description": "Provides the functionality to export PHP variables for visualization", 911 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 912 | "keywords": [ 913 | "export", 914 | "exporter" 915 | ], 916 | "time": "2015-06-21 07:55:53" 917 | }, 918 | { 919 | "name": "sebastian/global-state", 920 | "version": "1.1.1", 921 | "source": { 922 | "type": "git", 923 | "url": "https://github.com/sebastianbergmann/global-state.git", 924 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" 925 | }, 926 | "dist": { 927 | "type": "zip", 928 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", 929 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", 930 | "shasum": "" 931 | }, 932 | "require": { 933 | "php": ">=5.3.3" 934 | }, 935 | "require-dev": { 936 | "phpunit/phpunit": "~4.2" 937 | }, 938 | "suggest": { 939 | "ext-uopz": "*" 940 | }, 941 | "type": "library", 942 | "extra": { 943 | "branch-alias": { 944 | "dev-master": "1.0-dev" 945 | } 946 | }, 947 | "autoload": { 948 | "classmap": [ 949 | "src/" 950 | ] 951 | }, 952 | "notification-url": "https://packagist.org/downloads/", 953 | "license": [ 954 | "BSD-3-Clause" 955 | ], 956 | "authors": [ 957 | { 958 | "name": "Sebastian Bergmann", 959 | "email": "sebastian@phpunit.de" 960 | } 961 | ], 962 | "description": "Snapshotting of global state", 963 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 964 | "keywords": [ 965 | "global state" 966 | ], 967 | "time": "2015-10-12 03:26:01" 968 | }, 969 | { 970 | "name": "sebastian/recursion-context", 971 | "version": "1.0.2", 972 | "source": { 973 | "type": "git", 974 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 975 | "reference": "913401df809e99e4f47b27cdd781f4a258d58791" 976 | }, 977 | "dist": { 978 | "type": "zip", 979 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", 980 | "reference": "913401df809e99e4f47b27cdd781f4a258d58791", 981 | "shasum": "" 982 | }, 983 | "require": { 984 | "php": ">=5.3.3" 985 | }, 986 | "require-dev": { 987 | "phpunit/phpunit": "~4.4" 988 | }, 989 | "type": "library", 990 | "extra": { 991 | "branch-alias": { 992 | "dev-master": "1.0.x-dev" 993 | } 994 | }, 995 | "autoload": { 996 | "classmap": [ 997 | "src/" 998 | ] 999 | }, 1000 | "notification-url": "https://packagist.org/downloads/", 1001 | "license": [ 1002 | "BSD-3-Clause" 1003 | ], 1004 | "authors": [ 1005 | { 1006 | "name": "Jeff Welch", 1007 | "email": "whatthejeff@gmail.com" 1008 | }, 1009 | { 1010 | "name": "Sebastian Bergmann", 1011 | "email": "sebastian@phpunit.de" 1012 | }, 1013 | { 1014 | "name": "Adam Harvey", 1015 | "email": "aharvey@php.net" 1016 | } 1017 | ], 1018 | "description": "Provides functionality to recursively process PHP variables", 1019 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1020 | "time": "2015-11-11 19:50:13" 1021 | }, 1022 | { 1023 | "name": "sebastian/version", 1024 | "version": "1.0.6", 1025 | "source": { 1026 | "type": "git", 1027 | "url": "https://github.com/sebastianbergmann/version.git", 1028 | "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" 1029 | }, 1030 | "dist": { 1031 | "type": "zip", 1032 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", 1033 | "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", 1034 | "shasum": "" 1035 | }, 1036 | "type": "library", 1037 | "autoload": { 1038 | "classmap": [ 1039 | "src/" 1040 | ] 1041 | }, 1042 | "notification-url": "https://packagist.org/downloads/", 1043 | "license": [ 1044 | "BSD-3-Clause" 1045 | ], 1046 | "authors": [ 1047 | { 1048 | "name": "Sebastian Bergmann", 1049 | "email": "sebastian@phpunit.de", 1050 | "role": "lead" 1051 | } 1052 | ], 1053 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1054 | "homepage": "https://github.com/sebastianbergmann/version", 1055 | "time": "2015-06-21 13:59:46" 1056 | }, 1057 | { 1058 | "name": "symfony/yaml", 1059 | "version": "v2.8.7", 1060 | "source": { 1061 | "type": "git", 1062 | "url": "https://github.com/symfony/yaml.git", 1063 | "reference": "815fabf3f48c7d1df345a69d1ad1a88f59757b34" 1064 | }, 1065 | "dist": { 1066 | "type": "zip", 1067 | "url": "https://api.github.com/repos/symfony/yaml/zipball/815fabf3f48c7d1df345a69d1ad1a88f59757b34", 1068 | "reference": "815fabf3f48c7d1df345a69d1ad1a88f59757b34", 1069 | "shasum": "" 1070 | }, 1071 | "require": { 1072 | "php": ">=5.3.9" 1073 | }, 1074 | "type": "library", 1075 | "extra": { 1076 | "branch-alias": { 1077 | "dev-master": "2.8-dev" 1078 | } 1079 | }, 1080 | "autoload": { 1081 | "psr-4": { 1082 | "Symfony\\Component\\Yaml\\": "" 1083 | }, 1084 | "exclude-from-classmap": [ 1085 | "/Tests/" 1086 | ] 1087 | }, 1088 | "notification-url": "https://packagist.org/downloads/", 1089 | "license": [ 1090 | "MIT" 1091 | ], 1092 | "authors": [ 1093 | { 1094 | "name": "Fabien Potencier", 1095 | "email": "fabien@symfony.com" 1096 | }, 1097 | { 1098 | "name": "Symfony Community", 1099 | "homepage": "https://symfony.com/contributors" 1100 | } 1101 | ], 1102 | "description": "Symfony Yaml Component", 1103 | "homepage": "https://symfony.com", 1104 | "time": "2016-06-06 11:11:27" 1105 | } 1106 | ], 1107 | "aliases": [], 1108 | "minimum-stability": "stable", 1109 | "stability-flags": [], 1110 | "prefer-stable": false, 1111 | "prefer-lowest": false, 1112 | "platform": { 1113 | "php": ">=5.4.0" 1114 | }, 1115 | "platform-dev": [] 1116 | } 1117 | -------------------------------------------------------------------------------- /examples/execute_commands.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require __DIR__.'/../autoload.php'; 13 | 14 | $client = new Predis\Async\Client('tcp://127.0.0.1:6379'); 15 | 16 | $client->connect(function ($client) { 17 | echo "Connected to Redis!\n"; 18 | 19 | $client->set('foo', 'bar', function ($response, $client) { 20 | echo "`foo` has been set to `bar`, let's check if it's true... "; 21 | 22 | $client->get('foo', function($foo, $client) { 23 | echo $foo === 'bar' ? 'YES! :-)' : 'NO :-(', "\n"; 24 | 25 | $client->disconnect(); 26 | }); 27 | }); 28 | }); 29 | 30 | $client->getEventLoop()->run(); 31 | -------------------------------------------------------------------------------- /examples/execute_commands_connection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require __DIR__.'/../autoload.php'; 13 | 14 | $loop = new React\EventLoop\StreamSelectLoop(); 15 | $parameters = Predis\Connection\Parameters::create('tcp://127.0.0.1:6379'); 16 | $profile = Predis\Profile\Factory::getDefault(); 17 | 18 | $connection = new Predis\Async\Connection\StreamConnection($loop, $parameters); 19 | 20 | $connection->connect(function ($connection) use ($profile) { 21 | $ping = $profile->createCommand('ping'); 22 | 23 | $connection->executeCommand($ping, function ($response, $connection) use ($profile) { 24 | $echo = $profile->createCommand('echo', [$response]); 25 | 26 | $connection->executeCommand($echo, function ($response, $connection) { 27 | var_dump($response); 28 | 29 | $connection->disconnect(); 30 | }); 31 | }); 32 | }); 33 | 34 | $loop->run(); 35 | -------------------------------------------------------------------------------- /examples/list_blocking_pop.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require __DIR__.'/../autoload.php'; 13 | 14 | $loop = new React\EventLoop\StreamSelectLoop(); 15 | 16 | $consumer = new Predis\Async\Client('tcp://127.0.0.1:6379', $loop); 17 | $producer = new Predis\Async\Client('tcp://127.0.0.1:6379', $loop); 18 | 19 | $consumer->connect(function ($consumer) use ($producer) { 20 | echo "Connected to Redis, will BLPOP for max 10 seconds on `nrk:queue` and produce an item in ~5 seconds.\n"; 21 | 22 | $start = microtime(true); 23 | 24 | $consumer->blpop('nrk:queue', 10, function ($response) use ($consumer, $producer, $start) { 25 | list($queue, $stop) = $response; 26 | 27 | $seconds = round((float) $stop - $start, 3); 28 | echo "Received item from `$queue` after $seconds seconds!\n"; 29 | 30 | $consumer->disconnect(); 31 | $producer->disconnect(); 32 | }); 33 | 34 | $consumer->getEventLoop()->addTimer(5, function () use ($producer) { 35 | $producer->lpush('nrk:queue', $microtime = microtime(true), function () use ($microtime) { 36 | echo "Just pushed $microtime to `nrk:queue`.\n"; 37 | }); 38 | }); 39 | }); 40 | 41 | $loop->run(); 42 | -------------------------------------------------------------------------------- /examples/monitor_loop.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require __DIR__.'/../autoload.php'; 13 | 14 | $client = new Predis\Async\Client('tcp://127.0.0.1:6379'); 15 | 16 | $client->connect(function ($client) { 17 | echo "Connected to Redis, now listening for incoming messages...\n"; 18 | 19 | $client->monitor(function ($event) { 20 | $message = "[T%d] Client %s sent `%s` on database #%d with the following arguments: %s.\n"; 21 | 22 | $feedback = sprintf($message, 23 | $event->timestamp, 24 | $event->client, 25 | $event->command, 26 | $event->database, 27 | $event->arguments 28 | ); 29 | 30 | echo $feedback; 31 | }); 32 | }); 33 | 34 | $client->getEventLoop()->run(); 35 | -------------------------------------------------------------------------------- /examples/pubsub_loop.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require __DIR__.'/../autoload.php'; 13 | 14 | $client = new Predis\Async\Client('tcp://127.0.0.1:6379'); 15 | 16 | $client->connect(function ($client) { 17 | echo "Connected to Redis, now listening for incoming messages...\n"; 18 | 19 | $client->pubSubLoop('nrk:channel', function ($event, $pubsub) { 20 | $message = "Received message `%s` from channel `%s` [type: %s].\n"; 21 | 22 | $feedback = sprintf($message, 23 | $event->payload, 24 | $event->channel, 25 | $event->kind 26 | ); 27 | 28 | echo $feedback; 29 | 30 | if ($event->payload === 'quit') { 31 | $pubsub->quit(); 32 | } 33 | }); 34 | }); 35 | 36 | $client->getEventLoop()->run(); 37 | -------------------------------------------------------------------------------- /examples/server_side_scripts.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require __DIR__.'/../autoload.php'; 13 | 14 | class ListPushRandomValue extends Predis\Command\ScriptCommand 15 | { 16 | const LUA = <<getProfile()->defineCommand('lpushrand', 'ListPushRandomValue'); 37 | 38 | $client->connect(function ($client) { 39 | echo "Connected to Redis!\n"; 40 | 41 | $client->script('load', ListPushRandomValue::LUA, function ($_, $client) { 42 | $client->lpushrand('random_values', $seed = mt_rand(), function ($value, $client) { 43 | var_dump($value); 44 | 45 | $client->disconnect(); 46 | }); 47 | }); 48 | }); 49 | 50 | $client->getEventLoop()->run(); 51 | -------------------------------------------------------------------------------- /examples/transaction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require __DIR__.'/../autoload.php'; 13 | 14 | $client = new Predis\Async\Client('tcp://127.0.0.1:6379'); 15 | 16 | $client->connect(function ($client) { 17 | echo "Connected to Redis!\n"; 18 | 19 | $tx = $client->transaction(); 20 | $tx->ping(); 21 | $tx->echo("FOO"); 22 | $tx->echo("BAR"); 23 | $tx->execute(function ($replies, $client) { 24 | var_dump($replies); 25 | 26 | $client->info('cpu', function ($cpuInfo, $client) { 27 | var_dump($cpuInfo); 28 | 29 | $client->disconnect(); 30 | }); 31 | }); 32 | }); 33 | 34 | $client->getEventLoop()->run(); 35 | -------------------------------------------------------------------------------- /phpiredis.ini: -------------------------------------------------------------------------------- 1 | extension=phpiredis.so 2 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | tests/Predis/Async 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | lib/Predis/Async 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async; 13 | 14 | use Predis\ClientException; 15 | use Predis\NotSupportedException; 16 | use Predis\Configuration\OptionsInterface; 17 | use Predis\Connection\Parameters; 18 | use Predis\Connection\ParametersInterface; 19 | use Predis\Command\CommandInterface; 20 | use Predis\Response\ResponseInterface; 21 | use Predis\Async\Connection\ConnectionInterface; 22 | use Predis\Async\Connection\PhpiredisStreamConnection; 23 | use Predis\Async\Connection\StreamConnection; 24 | use React\EventLoop\LoopInterface; 25 | 26 | /** 27 | * Client class used for connecting and executing commands on Redis. 28 | * 29 | * @author Daniele Alessandri 30 | */ 31 | class Client 32 | { 33 | const VERSION = '0.3.0-dev'; 34 | 35 | private $profile; 36 | protected $connection; 37 | 38 | /** 39 | * @param mixed $parameters Connection parameters. 40 | * @param mixed $options Options to configure some behaviours of the client. 41 | */ 42 | public function __construct($parameters = null, $options = null) 43 | { 44 | $this->options = $this->createOptions($options ?: []); 45 | $this->connection = $this->createConnection($parameters, $this->options); 46 | $this->profile = $this->options->profile; 47 | } 48 | 49 | /** 50 | * Creates an instance of Predis\Async\Configuration\Options from different 51 | * types of arguments or simply returns the passed argument if it is an 52 | * instance of Predis\Configuration\OptionsInterface. 53 | * 54 | * @param mixed $options Client options. 55 | * 56 | * @return OptionsInterface 57 | */ 58 | protected function createOptions($options) 59 | { 60 | if (is_array($options)) { 61 | return new Configuration\Options($options); 62 | } 63 | 64 | if ($options instanceof LoopInterface) { 65 | return new Configuration\Options(['eventloop' => $options]); 66 | } 67 | 68 | if ($options instanceof OptionsInterface) { 69 | return $options; 70 | } 71 | 72 | throw new \InvalidArgumentException('Invalid type for client options'); 73 | } 74 | 75 | /** 76 | * Creates an instance of connection parameters. 77 | * 78 | * @param mixed $parameters Connection parameters. 79 | * 80 | * @return ParametersInterface 81 | */ 82 | protected function createParameters($parameters) 83 | { 84 | if ($parameters instanceof ParametersInterface) { 85 | return $parameters; 86 | } 87 | 88 | return Parameters::create($parameters); 89 | } 90 | 91 | /** 92 | * Initializes a connection from various types of arguments or returns the 93 | * passed object if it implements Predis\Connection\ConnectionInterface. 94 | * 95 | * @param mixed $parameters Connection parameters or instance. 96 | * @param OptionsInterface $options Client options. 97 | * 98 | * @return ConnectionInterface 99 | */ 100 | protected function createConnection($parameters, OptionsInterface $options) 101 | { 102 | if ($parameters instanceof ConnectionInterface) { 103 | if ($parameters->getEventLoop() !== $this->options->eventloop) { 104 | throw new ClientException('Client and connection must share the same event loop.'); 105 | } 106 | 107 | return $parameters; 108 | } 109 | 110 | $eventloop = $this->options->eventloop; 111 | $parameters = $this->createParameters($parameters); 112 | 113 | if ($options->phpiredis) { 114 | $connection = new PhpiredisStreamConnection($eventloop, $parameters); 115 | } else { 116 | $connection = new StreamConnection($eventloop, $parameters); 117 | } 118 | 119 | if (isset($options->on_error)) { 120 | $this->setErrorCallback($connection, $options->on_error); 121 | } 122 | 123 | return $connection; 124 | } 125 | 126 | /** 127 | * Sets the callback used to notify the client about connection errors. 128 | * 129 | * @param ConnectionInterface $connection Connection instance. 130 | * @param callable $callback Callback for error event. 131 | */ 132 | protected function setErrorCallback(ConnectionInterface $connection, callable $callback) 133 | { 134 | $connection->setErrorCallback(function ($connection, $exception) use ($callback) { 135 | call_user_func($callback, $this, $exception, $connection); 136 | }); 137 | } 138 | 139 | /** 140 | * Returns the client options specified upon initialization. 141 | * 142 | * @return OptionsInterface 143 | */ 144 | public function getOptions() 145 | { 146 | return $this->options; 147 | } 148 | 149 | /** 150 | * Returns the server profile used by the client. 151 | * 152 | * @return Predis\Profile\ProfileInterface; 153 | */ 154 | public function getProfile() 155 | { 156 | return $this->profile; 157 | } 158 | 159 | /** 160 | * Returns the underlying event loop. 161 | * 162 | * @return LoopInterface 163 | */ 164 | public function getEventLoop() 165 | { 166 | return $this->options->eventloop; 167 | } 168 | 169 | /** 170 | * Opens the connection to the server. 171 | * 172 | * @param callable $callback Callback for connection event. 173 | */ 174 | public function connect(callable $callback) 175 | { 176 | $this->connection->connect(function ($connection) use ($callback) { 177 | call_user_func($callback, $this, $connection); 178 | }); 179 | } 180 | 181 | /** 182 | * Closes the underlying connection from the server. 183 | */ 184 | public function disconnect() 185 | { 186 | $this->connection->disconnect(); 187 | } 188 | 189 | /** 190 | * Returns the current state of the underlying connection. 191 | * 192 | * @return bool 193 | */ 194 | public function isConnected() 195 | { 196 | return $this->connection->isConnected(); 197 | } 198 | 199 | /** 200 | * Returns the underlying connection instance. 201 | * 202 | * @return ConnectionInterface 203 | */ 204 | public function getConnection() 205 | { 206 | return $this->connection; 207 | } 208 | 209 | /** 210 | * Creates a Redis command with the specified arguments and sends a request 211 | * to the server. 212 | * 213 | * @param string $method Command ID. 214 | * @param array $arguments Arguments for the command (optional callback as last argument). 215 | * 216 | * @return mixed 217 | */ 218 | public function __call($method, $arguments) 219 | { 220 | if (!is_callable($callback = array_pop($arguments))) { 221 | $arguments[] = $callback; 222 | $callback = function () { /* NOOP */ }; 223 | } 224 | 225 | $this->executeCommand($this->createCommand($method, $arguments), $callback); 226 | } 227 | 228 | /** 229 | * Creates a new instance of the specified Redis command. 230 | * 231 | * @param string $method Command ID. 232 | * @param array $arguments Arguments for the command. 233 | * 234 | * @return CommandInterface 235 | */ 236 | public function createCommand($method, $arguments = []) 237 | { 238 | return $this->profile->createCommand($method, $arguments); 239 | } 240 | 241 | /** 242 | * Executes the specified Redis command. 243 | * 244 | * @param CommandInterface $command Command instance. 245 | * @param callable $callback Response callback. 246 | */ 247 | public function executeCommand(CommandInterface $command, callable $callback) 248 | { 249 | $this->connection->executeCommand($command, $this->wrapCallback($callback)); 250 | } 251 | 252 | /** 253 | * Wraps a command callback used to parse the raw response by adding more 254 | * arguments that will be passed back to user code. 255 | * 256 | * @param callable $callback Response callback. 257 | */ 258 | protected function wrapCallback(callable $callback) 259 | { 260 | return function ($response, $connection, $command) use ($callback) { 261 | if ($command && !$response instanceof ResponseInterface) { 262 | $response = $command->parseResponse($response); 263 | } 264 | 265 | call_user_func($callback, $response, $this, $command); 266 | }; 267 | } 268 | 269 | /** 270 | * Creates a new transaction context. 271 | * 272 | * @return MultiExec 273 | */ 274 | public function transaction(/* arguments */) 275 | { 276 | return new Transaction\MultiExec($this); 277 | } 278 | 279 | /** 280 | * Creates a new monitor consumer. 281 | * 282 | * @param callable $callback Callback invoked on each payload message. 283 | * @param bool $autostart Flag indicating if the consumer should be auto-started. 284 | * 285 | * @return Monitor\Consumer 286 | */ 287 | public function monitor(callable $callback, $autostart = true) 288 | { 289 | $monitor = new Monitor\Consumer($this, $callback); 290 | 291 | if ($autostart) { 292 | $monitor->start(); 293 | } 294 | 295 | return $monitor; 296 | } 297 | 298 | /** 299 | * Creates a new pub/sub consumer. 300 | * 301 | * @param mixed $channels List of channels for subscription. 302 | * @param callable $callback Callback invoked on each payload message. 303 | * 304 | * @return PubSub\Consumer 305 | */ 306 | public function pubSubLoop($channels, callable $callback) 307 | { 308 | $pubsub = new PubSub\Consumer($this, $callback); 309 | 310 | if (is_string($channels)) { 311 | $channels = ['subscribe' => [$channels]]; 312 | } 313 | 314 | if (isset($channels['subscribe'])) { 315 | $pubsub->subscribe($channels['subscribe']); 316 | } 317 | 318 | if (isset($channels['psubscribe'])) { 319 | $pubsub->psubscribe($channels['psubscribe']); 320 | } 321 | 322 | return $pubsub; 323 | } 324 | 325 | /** 326 | * {@inheritdoc} 327 | */ 328 | public function pipeline(/* arguments */) 329 | { 330 | throw new NotSupportedException('Not yet implemented'); 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/CommunicationException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async; 13 | 14 | use Predis\PredisException; 15 | use Predis\Async\Connection\ConnectionInterface; 16 | 17 | /** 18 | * Base exception class for asynchronous network-related errors. 19 | * 20 | * @author Daniele Alessandri 21 | */ 22 | abstract class CommunicationException extends PredisException 23 | { 24 | private $connection; 25 | 26 | /** 27 | * @param ConnectionInterface $connection Connection that generated the exception. 28 | * @param string $message Error message. 29 | * @param int $code Error code. 30 | * @param \Exception $innerException Inner exception for wrapping the original error. 31 | */ 32 | public function __construct(ConnectionInterface $connection, 33 | $message = null, $code = null, \Exception $innerException = null) 34 | { 35 | parent::__construct($message, $code, $innerException); 36 | 37 | $this->connection = $connection; 38 | } 39 | 40 | /** 41 | * Gets the connection that generated the exception. 42 | * 43 | * @return ConnectionInterface 44 | */ 45 | public function getConnection() 46 | { 47 | return $this->connection; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Configuration/EventLoopOption.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\Configuration; 13 | 14 | use Predis\Configuration\OptionInterface; 15 | use Predis\Configuration\OptionsInterface; 16 | use React\EventLoop\LoopInterface; 17 | use React\EventLoop\StreamSelectLoop; 18 | 19 | /** 20 | * Injects the event loop instance used by the client. The default value when no 21 | * event loop is specified is to use a new instance of the stream_select()-based 22 | * loop provided by react/event-loop. 23 | * 24 | * @author Daniele Alessandri 25 | */ 26 | class EventLoopOption implements OptionInterface 27 | { 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function filter(OptionsInterface $options, $value) 32 | { 33 | if (!$value instanceof LoopInterface) { 34 | throw new \InvalidArgumentException('Invalid value for the eventloop option'); 35 | } 36 | 37 | return $value; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function getDefault(OptionsInterface $options) 44 | { 45 | return new StreamSelectLoop(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Configuration/Options.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\Configuration; 13 | 14 | use Predis\Configuration\Options as BaseOptions; 15 | use Predis\Configuration\PrefixOption; 16 | use Predis\Configuration\ProfileOption; 17 | 18 | /** 19 | * Manages Predis options with filtering, conversion and lazy initialization of 20 | * values using a mini-DI container approach. 21 | * 22 | * @property-read mixed eventloop Event loop instance. 23 | * @property-read bool phpiredis Use phpiredis (only when available). 24 | * @property-read mixed prefix Key prefixing strategy using the given prefix. 25 | * @property-read mixed profile Server profile. 26 | * 27 | * @author Daniele Alessandri 28 | */ 29 | class Options extends BaseOptions 30 | { 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function getHandlers() 35 | { 36 | return array( 37 | 'profile' => new ProfileOption(), 38 | 'prefix' => new PrefixOption(), 39 | 'eventloop' => new EventLoopOption(), 40 | 'phpiredis' => new PhpiredisOption(), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Configuration/PhpiredisOption.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\Configuration; 13 | 14 | use Predis\Configuration\OptionInterface; 15 | use Predis\Configuration\OptionsInterface; 16 | 17 | /** 18 | * Configures the client to use the phpiredis extension (when available) for 19 | * faster protocol serialization and parsing. The default value is to use the 20 | * extension when it is detected at runtime. 21 | * 22 | * @author Daniele Alessandri 23 | */ 24 | class PhpiredisOption implements OptionInterface 25 | { 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function filter(OptionsInterface $options, $value) 30 | { 31 | if (!$value) { 32 | return false; 33 | } 34 | 35 | if (!is_object($value) && $asbool = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) { 36 | return $asbool; 37 | } 38 | 39 | return false; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function getDefault(OptionsInterface $options) 46 | { 47 | return function_exists('phpiredis_reader_create'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Connection/AbstractConnection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\Connection; 13 | 14 | use SplQueue; 15 | use Predis\Command\CommandInterface; 16 | use Predis\Connection\ParametersInterface; 17 | use React\EventLoop\LoopInterface; 18 | 19 | /** 20 | * Base class providing the common logic used by to communicate asynchronously 21 | * with Redis. 22 | * 23 | * @author Daniele Alessandri 24 | */ 25 | abstract class AbstractConnection implements ConnectionInterface 26 | { 27 | protected $loop; 28 | protected $parameters; 29 | protected $stream; 30 | protected $buffer; 31 | protected $commands; 32 | protected $state; 33 | protected $timeout = null; 34 | protected $errorCallback = null; 35 | protected $readableCallback = null; 36 | protected $writableCallback = null; 37 | 38 | /** 39 | * @param LoopInterface $loop Event loop instance. 40 | * @param ParametersInterface $parameters Initialization parameters for the connection. 41 | */ 42 | public function __construct(LoopInterface $loop, ParametersInterface $parameters) 43 | { 44 | $this->loop = $loop; 45 | $this->parameters = $parameters; 46 | 47 | $this->buffer = new Buffer\StringBuffer(); 48 | $this->commands = new SplQueue(); 49 | $this->readableCallback = [$this, 'read']; 50 | $this->writableCallback = [$this, 'write']; 51 | 52 | $this->state = new State(); 53 | $this->state->setProcessCallback($this->getProcessCallback()); 54 | } 55 | 56 | /** 57 | * Disconnects from the server and destroys the underlying resource when 58 | * PHP's garbage collector kicks in. 59 | */ 60 | public function __destruct() 61 | { 62 | if ($this->isConnected()) { 63 | $this->disconnect(); 64 | } 65 | } 66 | 67 | /** 68 | * Returns the callback used to handle commands and firing the appropriate 69 | * callbacks depending on the state of the connection. 70 | * 71 | * @return mixed 72 | */ 73 | protected function getProcessCallback() 74 | { 75 | return function ($state, $response) { 76 | list($command, $callback) = $this->commands->dequeue(); 77 | 78 | switch ($command->getId()) { 79 | case 'SUBSCRIBE': 80 | case 'PSUBSCRIBE': 81 | $wrapper = $this->getStreamingWrapperCreator(); 82 | $callback = $wrapper($this, $callback); 83 | $state->setStreamingContext(State::PUBSUB, $callback); 84 | break; 85 | 86 | case 'MONITOR': 87 | $wrapper = $this->getStreamingWrapperCreator(); 88 | $callback = $wrapper($this, $callback); 89 | $state->setStreamingContext(State::MONITOR, $callback); 90 | break; 91 | 92 | case 'MULTI': 93 | $state->setState(State::MULTIEXEC); 94 | goto process; 95 | 96 | case 'EXEC': 97 | case 'DISCARD': 98 | $state->setState(State::CONNECTED); 99 | goto process; 100 | 101 | default: 102 | process: 103 | call_user_func($callback, $response, $this, $command); 104 | break; 105 | } 106 | }; 107 | } 108 | 109 | /** 110 | * Returns a wrapper to the user-provided callback used to handle response 111 | * chunks streamed by replies to commands such as MONITOR, SUBSCRIBE, etc. 112 | * 113 | * @return mixed 114 | */ 115 | protected function getStreamingWrapperCreator() 116 | { 117 | return function ($connection, $callback) { 118 | return function ($state, $response) use ($connection, $callback) { 119 | call_user_func($callback, $response, $connection, null); 120 | }; 121 | }; 122 | } 123 | 124 | /** 125 | * Creates the underlying resource used to communicate with Redis. 126 | * 127 | * @return mixed 128 | */ 129 | protected function createResource(callable $callback) 130 | { 131 | $parameters = $this->parameters; 132 | $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT; 133 | 134 | if ($parameters->scheme === 'unix') { 135 | $uri = "unix://$parameters->path"; 136 | } else { 137 | $uri = "$parameters->scheme://$parameters->host:$parameters->port"; 138 | } 139 | 140 | if (!$stream = @stream_socket_client($uri, $errno, $errstr, 0, $flags)) { 141 | $this->onError(new ConnectionException($this, trim($errstr), $errno)); 142 | 143 | return; 144 | } 145 | 146 | stream_set_blocking($stream, 0); 147 | 148 | $this->state->setState(State::CONNECTING); 149 | 150 | $this->loop->addWriteStream($stream, function ($stream) use ($callback) { 151 | if ($this->onConnect()) { 152 | call_user_func($callback, $this); 153 | $this->write(); 154 | } 155 | }); 156 | 157 | $this->timeout = $this->armTimeoutMonitor( 158 | $parameters->timeout ?: 5, $this->errorCallback ?: function () { } 159 | ); 160 | 161 | return $stream; 162 | } 163 | 164 | /** 165 | * Sets a timeout monitor to handle timeouts when connecting to Redis. 166 | * 167 | * @param float $timeout Timeout value in seconds 168 | * @param callable $callback Callback invoked upon timeout. 169 | */ 170 | protected function armTimeoutMonitor($timeout, callable $callback) 171 | { 172 | $timer = $this->loop->addTimer($timeout, function ($timer) { 173 | list($connection, $callback) = $timer->getData(); 174 | 175 | $connection->disconnect(); 176 | call_user_func($callback, $connection, new ConnectionException($connection, 'Connection timed out')); 177 | }); 178 | 179 | $timer->setData([$this, $callback]); 180 | 181 | return $timer; 182 | } 183 | 184 | /** 185 | * Stops the timeout monitor if initialized. 186 | */ 187 | protected function disarmTimeoutMonitor() 188 | { 189 | if (isset($this->timeout)) { 190 | $this->timeout->cancel(); 191 | $this->timeout = null; 192 | } 193 | } 194 | 195 | /** 196 | * {@inheritdoc} 197 | */ 198 | public function isConnected() 199 | { 200 | return isset($this->stream) && stream_socket_get_name($this->stream, true) !== false; 201 | } 202 | 203 | /** 204 | * {@inheritdoc} 205 | */ 206 | public function connect(callable $callback) 207 | { 208 | if (!$this->isConnected()) { 209 | $this->stream = $this->createResource($callback); 210 | } 211 | } 212 | 213 | /** 214 | * {@inheritdoc} 215 | */ 216 | public function disconnect() 217 | { 218 | $this->disarmTimeoutMonitor(); 219 | 220 | $this->loop->nextTick(function () { 221 | if (isset($this->stream)) { 222 | $this->loop->removeStream($this->stream); 223 | $this->state->setState(State::DISCONNECTED); 224 | $this->buffer->reset(); 225 | 226 | unset($this->stream); 227 | } 228 | }); 229 | } 230 | 231 | /** 232 | * {@inheritdoc} 233 | */ 234 | public function getResource() 235 | { 236 | if (isset($this->stream)) { 237 | return $this->stream; 238 | } 239 | 240 | $this->stream = $this->createResource(function () { /* NOOP */ }); 241 | 242 | return $this->stream; 243 | } 244 | 245 | /** 246 | * {@inheritdoc} 247 | */ 248 | public function setErrorCallback(callable $callback) 249 | { 250 | $this->errorCallback = $callback; 251 | } 252 | 253 | /** 254 | * {@inheritdoc} 255 | */ 256 | public function onConnect() 257 | { 258 | $stream = $this->getResource(); 259 | 260 | // The following code is a terrible hack but it seems to be the only way 261 | // to detect connection refused errors with PHP's stream sockets. You 262 | // should blame PHP for this, as usual. 263 | if (stream_socket_get_name($stream, true) === false) { 264 | return $this->onError(new ConnectionException($this, "Connection refused")); 265 | } 266 | 267 | $this->state->setState(State::CONNECTED); 268 | $this->disarmTimeoutMonitor(); 269 | 270 | if ($this->buffer->isEmpty()) { 271 | $this->loop->removeWriteStream($stream); 272 | $this->loop->addReadStream($stream, $this->readableCallback); 273 | } 274 | 275 | return true; 276 | } 277 | 278 | /** 279 | * {@inheritdoc} 280 | */ 281 | protected function onError(\Exception $exception) 282 | { 283 | $this->disconnect(); 284 | 285 | if (isset($this->errorCallback)) { 286 | call_user_func($this->errorCallback, $this, $exception); 287 | } 288 | 289 | return false; 290 | } 291 | 292 | /** 293 | * {@inheritdoc} 294 | */ 295 | public function getParameters() 296 | { 297 | return $this->parameters; 298 | } 299 | 300 | /** 301 | * {@inheritdoc} 302 | */ 303 | public function getEventLoop() 304 | { 305 | return $this->loop; 306 | } 307 | 308 | /** 309 | * Returns the identifier for the connection. 310 | * 311 | * @return string 312 | */ 313 | protected function getIdentifier() 314 | { 315 | if ($this->parameters->scheme === 'unix') { 316 | return $this->parameters->path; 317 | } 318 | 319 | return "{$this->parameters->host}:{$this->parameters->port}"; 320 | } 321 | 322 | /** 323 | * {@inheritdoc} 324 | */ 325 | public function write() 326 | { 327 | $stream = $this->getResource(); 328 | 329 | if ($this->buffer->isEmpty()) { 330 | $this->loop->removeWriteStream($stream); 331 | 332 | return; 333 | } 334 | 335 | $buffer = $this->buffer->read(4096); 336 | 337 | if (-1 === $ret = @stream_socket_sendto($stream, $buffer)) { 338 | return $this->onError(new ConnectionException($this, 'Error while writing bytes to the server')); 339 | } 340 | 341 | $this->buffer->discard(min($ret, strlen($buffer))); 342 | } 343 | 344 | /** 345 | * {@inheritdoc} 346 | */ 347 | public function read() 348 | { 349 | $buffer = stream_socket_recvfrom($this->getResource(), 4096); 350 | 351 | if ($buffer === false || $buffer === '') { 352 | return $this->onError(new ConnectionException($this, 'Error while reading bytes from the server')); 353 | } 354 | 355 | $this->parseResponseBuffer($buffer); 356 | } 357 | 358 | /** 359 | * Parses the incoming buffer and emits response objects when the buffer 360 | * contains one or more response payloads available for consumption. 361 | * 362 | * @param string $buffer Buffer read from the network stream. 363 | */ 364 | abstract public function parseResponseBuffer($buffer); 365 | 366 | /** 367 | * {@inheritdoc} 368 | */ 369 | abstract public function executeCommand(CommandInterface $command, callable $callback); 370 | 371 | /** 372 | * {@inheritdoc} 373 | */ 374 | public function __toString() 375 | { 376 | return $this->getIdentifier(); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/Connection/Buffer/StringBuffer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\Connection\Buffer; 13 | 14 | /** 15 | * Class providing a basic string buffer. 16 | * 17 | * @author Daniele Alessandri 18 | */ 19 | class StringBuffer 20 | { 21 | private $buffer; 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function __construct() 27 | { 28 | $this->buffer = ''; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function read($length) 35 | { 36 | if (false === $buffer = substr($this->buffer, 0, $length)) { 37 | return ''; 38 | } 39 | 40 | return $buffer; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function consume($length) 47 | { 48 | if ('' !== $buffer = $this->read($length)) { 49 | $this->buffer = substr($this->buffer, strlen($buffer)) ?: ''; 50 | } 51 | 52 | return $buffer; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function discard($length) 59 | { 60 | $this->buffer = substr($this->buffer, $length) ?: ''; 61 | 62 | return $length; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function append($buffer) 69 | { 70 | $this->buffer .= $buffer; 71 | 72 | return strlen($buffer); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function isEmpty() 79 | { 80 | return $this->buffer === ''; 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function length() 87 | { 88 | return strlen($this->buffer); 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function reset() 95 | { 96 | $this->buffer = ''; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Connection/ConnectionException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\Connection; 13 | 14 | use Predis\Async\CommunicationException; 15 | 16 | /** 17 | * Exception class that identifies connection-related errors. 18 | * 19 | * @author Daniele Alessandri 20 | */ 21 | class ConnectionException extends CommunicationException 22 | { 23 | } 24 | -------------------------------------------------------------------------------- /src/Connection/ConnectionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\Connection; 13 | 14 | use Predis\Command\CommandInterface; 15 | use Predis\Connection\ParametersInterface; 16 | use React\EventLoop\LoopInterface; 17 | 18 | /** 19 | * Defines a connection object used to communicate asynchronously with Redis. 20 | * 21 | * @author Daniele Alessandri 22 | */ 23 | interface ConnectionInterface 24 | { 25 | /** 26 | * Opens the connection to Redis. 27 | * 28 | * @param callable $callback Callable invoked when the connection is established. 29 | */ 30 | public function connect(callable $callback); 31 | 32 | /** 33 | * Closes the connection to Redis. 34 | */ 35 | public function disconnect(); 36 | 37 | /** 38 | * Checks if the connection to Redis is considered open. 39 | * 40 | * @return bool 41 | */ 42 | public function isConnected(); 43 | 44 | /** 45 | * Returns the underlying resource used to communicate with Redis. 46 | * 47 | * @return mixed 48 | */ 49 | public function getResource(); 50 | 51 | /** 52 | * Returns the parameters used to initialize the connection. 53 | * 54 | * @return ParametersInterface 55 | */ 56 | public function getParameters(); 57 | 58 | /** 59 | * Returns the underlying event loop. 60 | * 61 | * @return LoopInterface 62 | */ 63 | public function getEventLoop(); 64 | 65 | /** 66 | * Writes a request for the given command over the connection and reads back 67 | * the response returned by Redis firing the user-provided callback. 68 | * 69 | * @param CommandInterface $command Redis command. 70 | * @param callable $callback Callback. 71 | */ 72 | public function executeCommand(CommandInterface $command, callable $callback); 73 | 74 | /** 75 | * Writes the buffer to a writable network streams. 76 | * 77 | * @return mixed 78 | */ 79 | public function write(); 80 | 81 | /** 82 | * Reads responses from a readable network streams. 83 | * 84 | * @return mixed 85 | */ 86 | public function read(); 87 | 88 | /** 89 | * Returns a string representation of the connection. 90 | * 91 | * @return string 92 | */ 93 | public function __toString(); 94 | } 95 | -------------------------------------------------------------------------------- /src/Connection/PhpiredisStreamConnection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\Connection; 13 | 14 | use Predis\Command\CommandInterface; 15 | use Predis\Connection\ParametersInterface; 16 | use Predis\Response\Error as ErrorResponse; 17 | use Predis\Response\Status as StatusResponse; 18 | use React\EventLoop\LoopInterface; 19 | 20 | class PhpiredisStreamConnection extends AbstractConnection 21 | { 22 | protected $reader; 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function __construct(LoopInterface $loop, ParametersInterface $parameters) 28 | { 29 | parent::__construct($loop, $parameters); 30 | 31 | $this->initializeReader(); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function __destruct() 38 | { 39 | phpiredis_reader_destroy($this->reader); 40 | 41 | parent::__destruct(); 42 | } 43 | 44 | /** 45 | * Initializes the protocol reader resource. 46 | */ 47 | protected function initializeReader() 48 | { 49 | $this->reader = phpiredis_reader_create(); 50 | 51 | phpiredis_reader_set_status_handler($this->reader, $this->getStatusHandler()); 52 | phpiredis_reader_set_error_handler($this->reader, $this->getErrorHandler()); 53 | } 54 | 55 | /** 56 | * Returns the handler used by the protocol reader to handle status replies. 57 | * 58 | * @return \Closure 59 | */ 60 | protected function getStatusHandler() 61 | { 62 | return function ($payload) { 63 | return StatusResponse::get($payload); 64 | }; 65 | } 66 | 67 | /** 68 | * Returns the handler used by the protocol reader to handle Redis errors. 69 | * 70 | * @return \Closure 71 | */ 72 | protected function getErrorHandler() 73 | { 74 | return function ($errorMessage) { 75 | return new ErrorResponse($errorMessage); 76 | }; 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function parseResponseBuffer($buffer) 83 | { 84 | phpiredis_reader_feed($reader = $this->reader, $buffer); 85 | 86 | while (phpiredis_reader_get_state($reader) === PHPIREDIS_READER_STATE_COMPLETE) { 87 | $this->state->process(phpiredis_reader_get_reply($reader)); 88 | } 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function executeCommand(CommandInterface $command, callable $callback) 95 | { 96 | if ($this->buffer->isEmpty() && $stream = $this->getResource()) { 97 | $this->loop->addWriteStream($stream, $this->writableCallback); 98 | } 99 | 100 | $cmdargs = $command->getArguments(); 101 | array_unshift($cmdargs, $command->getId()); 102 | 103 | $this->buffer->append(phpiredis_format_command($cmdargs)); 104 | $this->commands->enqueue([$command, $callback]); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Connection/State.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\Connection; 13 | 14 | use InvalidArgumentException; 15 | use RuntimeException; 16 | 17 | /** 18 | * Class used to track connection states. 19 | * 20 | * @author Daniele Alessandri 21 | */ 22 | class State 23 | { 24 | const DISCONNECTED = 1; // 0b00000001 25 | const CONNECTING = 2; // 0b00000010 26 | const CONNECTED = 4; // 0b00000100 27 | const STREAM_CONTEXT = 8; // 0b00001000 28 | 29 | const MULTIEXEC = 20; // 0b00010100 30 | const MONITOR = 40; // 0b00101000 31 | const PUBSUB = 72; // 0b01001000 32 | 33 | protected $processCallback; 34 | protected $streamCallback; 35 | 36 | protected $state = self::DISCONNECTED; 37 | 38 | /** 39 | * Sets the current state value using the specified flags. 40 | * 41 | * @param int $flags Flags. 42 | */ 43 | protected function setFlags($flags) 44 | { 45 | $this->state = $flags; 46 | } 47 | 48 | /** 49 | * Returns the current state value. 50 | * 51 | * @return int 52 | */ 53 | protected function getFlags() 54 | { 55 | return $this->state; 56 | } 57 | 58 | /** 59 | * Checks if the specified flags are set in the current state value. 60 | * 61 | * @param int $flags Flags. 62 | * 63 | * @return bool 64 | */ 65 | protected function checkFlags($flags) 66 | { 67 | return ($this->state & $flags) === $flags; 68 | } 69 | 70 | /** 71 | * Sets the specified flags in the current state value. 72 | * 73 | * @param int $flags Flags. 74 | */ 75 | protected function flag($flags) 76 | { 77 | $this->state |= $flags; 78 | } 79 | 80 | /** 81 | * Unsets the specified flags from the current state value. 82 | * 83 | * @param int $flags Flags. 84 | */ 85 | protected function unflag($flags) 86 | { 87 | $this->state &= ~$flags; 88 | } 89 | 90 | /** 91 | * Switches the internal state to one of the supported states. 92 | * 93 | * @param int $context State flag. 94 | */ 95 | public function setState($state) 96 | { 97 | $state &= ~248; // 0b11111000 98 | 99 | if (($state & ($state - 1)) !== 0) { 100 | throw new InvalidArgumentException("State must be a valid state value"); 101 | } 102 | 103 | $this->setFlags($state); 104 | $this->streamCallback = null; 105 | } 106 | 107 | /** 108 | * Switches the internal state from a context to CONNECTED. 109 | */ 110 | public function clearStreamingContext() 111 | { 112 | $this->setFlags(self::CONNECTED); 113 | $this->streamCallback = null; 114 | } 115 | 116 | /** 117 | * Switches the internal state to one of the supported Redis contexts and 118 | * associates a callback to process streaming reply items. 119 | * 120 | * @param int $context Context flag. 121 | * @param callable $callback Callable object. 122 | */ 123 | public function setStreamingContext($context, callable $callback) 124 | { 125 | if (0 === $context &= ~7) { // 0b00000111 126 | throw new InvalidArgumentException("Context must be a valid context value"); 127 | } 128 | 129 | $this->setFlags($context); 130 | $this->streamCallback = $callback; 131 | } 132 | 133 | /** 134 | * Sets the callback used to handle responses in the CONNECTED state. 135 | * 136 | * @param callable $callback Callable object. 137 | */ 138 | public function setProcessCallback(callable $callback) 139 | { 140 | $this->processCallback = $callback; 141 | } 142 | 143 | /** 144 | * Processes a response depending on the current state. 145 | * 146 | * @param mixed $response Response returned from the server. 147 | * 148 | * @return mixed 149 | */ 150 | public function process($response) 151 | { 152 | if ($this->checkFlags(self::CONNECTED)) { 153 | return call_user_func($this->processCallback, $this, $response); 154 | } 155 | 156 | if ($this->checkFlags(self::STREAM_CONTEXT)) { 157 | return call_user_func($this->streamCallback, $this, $response); 158 | } 159 | 160 | // TODO: we should handle invalid states in a different manner. 161 | throw new RuntimeException("Invalid connection state: $this"); 162 | } 163 | 164 | /** 165 | * Returns a string with a mnemonic representation of the current state. 166 | * 167 | * @return string 168 | */ 169 | public function __toString() 170 | { 171 | if ($this->checkFlags(self::DISCONNECTED)) { 172 | return 'DISCONNECTED'; 173 | } 174 | 175 | if ($this->checkFlags(self::CONNECTING)) { 176 | return 'CONNECTING'; 177 | } 178 | 179 | if ($this->checkFlags(self::CONNECTED)) { 180 | return 'CONNECTED'; 181 | } 182 | 183 | if ($this->checkFlags(self::MULTIEXEC)) { 184 | return '[CONTEXT] MULTI/EXEC'; 185 | } 186 | 187 | if ($this->checkFlags(self::PUBSUB)) { 188 | return '[CONTEXT] PUB/SUB'; 189 | } 190 | 191 | if ($this->checkFlags(self::MONITOR)) { 192 | return '[CONTEXT] MONITOR'; 193 | } 194 | 195 | return 'UNKNOWN'; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/Connection/StreamConnection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\Connection; 13 | 14 | use Predis\Command\CommandInterface; 15 | use Predis\Connection\ParametersInterface; 16 | use Predis\Response\Error as ErrorResponse; 17 | use Predis\Response\Status as StatusResponse; 18 | use Clue\Redis\Protocol\Model\StatusReply; 19 | use Clue\Redis\Protocol\Model\ErrorReply; 20 | use Clue\Redis\Protocol\Parser\ResponseParser; 21 | use Clue\Redis\Protocol\Serializer\RecursiveSerializer; 22 | use React\EventLoop\LoopInterface; 23 | 24 | class StreamConnection extends AbstractConnection 25 | { 26 | protected $parser; 27 | protected $serializer; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function __construct(LoopInterface $loop, ParametersInterface $parameters) 33 | { 34 | parent::__construct($loop, $parameters); 35 | 36 | $this->initializeResponseParser(); 37 | $this->initializeRequestSerializer(); 38 | } 39 | 40 | /** 41 | * Initializes the response parser instance. 42 | */ 43 | protected function initializeResponseParser() 44 | { 45 | $this->parser = new ResponseParser(); 46 | } 47 | 48 | /** 49 | * Initializes the request serializer instance. 50 | */ 51 | protected function initializeRequestSerializer() 52 | { 53 | $this->serializer = new RecursiveSerializer(); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function parseResponseBuffer($buffer) 60 | { 61 | foreach ($this->parser->pushIncoming($buffer) as $response) { 62 | $value = $response->getValueNative(); 63 | 64 | if ($response instanceof StatusReply) { 65 | $response = StatusResponse::get($value); 66 | } elseif ($response instanceof ErrorReply) { 67 | $response = new ErrorResponse($value); 68 | } else { 69 | $response = $value; 70 | } 71 | 72 | $this->state->process($response); 73 | } 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function executeCommand(CommandInterface $command, callable $callback) 80 | { 81 | if ($this->buffer->isEmpty() && $stream = $this->getResource()) { 82 | $this->loop->addWriteStream($stream, $this->writableCallback); 83 | } 84 | 85 | $request = $this->serializer->getRequestMessage($command->getId(), $command->getArguments()); 86 | 87 | $this->buffer->append($request); 88 | $this->commands->enqueue([$command, $callback]); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Monitor/Consumer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\Monitor; 13 | 14 | use Predis\Async\Client; 15 | 16 | /** 17 | * Redis MONITOR consumer abstraction. 18 | * 19 | * @author Daniele Alessandri 20 | */ 21 | class Consumer 22 | { 23 | protected $client; 24 | protected $callback; 25 | 26 | /** 27 | * @param Client $client Client instance. 28 | * @param callable $callback Callback invoked on each received message. 29 | */ 30 | public function __construct(Client $client, callable $callback) 31 | { 32 | $this->client = $client; 33 | $this->callback = $callback; 34 | } 35 | 36 | /** 37 | * Parses the response string returned by the server into an object. 38 | * 39 | * @param string $payload Payload string. 40 | * 41 | * @return object 42 | */ 43 | protected function parsePayload($payload) 44 | { 45 | $database = 0; 46 | $client = null; 47 | 48 | $pregCallback = function ($matches) use (&$database, &$client) { 49 | if (2 === $count = count($matches)) { 50 | // Redis <= 2.4 51 | $database = (int) $matches[1]; 52 | } 53 | 54 | if (4 === $count) { 55 | // Redis >= 2.6 56 | $database = (int) $matches[2]; 57 | $client = $matches[3]; 58 | } 59 | 60 | return ' '; 61 | }; 62 | 63 | $event = preg_replace_callback('/ \(db (\d+)\) | \[(\d+) (.*?)\] /', $pregCallback, $payload, 1); 64 | @list($timestamp, $command, $arguments) = explode(' ', $event, 3); 65 | 66 | return (object) [ 67 | 'timestamp' => (float) $timestamp, 68 | 'database' => $database, 69 | 'client' => $client, 70 | 'command' => substr($command, 1, -1), 71 | 'arguments' => $arguments, 72 | ]; 73 | } 74 | 75 | /** 76 | * Wraps the user-provided callback to process payloads returned by the server. 77 | * 78 | * @param string $payload Payload returned by the server. 79 | * @param Client $client Associated client instance. 80 | * @param CommandInterface $command Command instance (always NULL in case of streaming contexts). 81 | */ 82 | public function __invoke($payload, $client, $command) 83 | { 84 | $parsedPayload = $this->parsePayload($payload); 85 | call_user_func($this->callback, $parsedPayload, $this); 86 | } 87 | 88 | /** 89 | * Initializes the consumer and sends the MONITOR command to the server. 90 | */ 91 | public function start() 92 | { 93 | $command = $this->client->createCommand('MONITOR'); 94 | $this->client->executeCommand($command, $this); 95 | } 96 | 97 | /** 98 | * Stops the consumer. Internally this is done by disconnecting from server 99 | * since there is no way to terminate the stream initialized by MONITOR. 100 | */ 101 | public function stop() 102 | { 103 | $this->client->disconnect(); 104 | } 105 | 106 | /** 107 | * Returns the underlying client instance. 108 | * 109 | * @return Client 110 | */ 111 | public function getClient() 112 | { 113 | return $this->client; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/PubSub/Consumer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\PubSub; 13 | 14 | use RuntimeException; 15 | use Predis\Command\Command; 16 | use Predis\Command\CommandInterface; 17 | use Predis\Response\ResponseInterface; 18 | use Predis\Async\Client; 19 | 20 | /** 21 | * Redis PUB/SUB consumer abstraction. 22 | * 23 | * @author Daniele Alessandri 24 | */ 25 | class Consumer 26 | { 27 | const SUBSCRIBE = 'subscribe'; 28 | const UNSUBSCRIBE = 'unsubscribe'; 29 | const PSUBSCRIBE = 'psubscribe'; 30 | const PUNSUBSCRIBE = 'punsubscribe'; 31 | const MESSAGE = 'message'; 32 | const PMESSAGE = 'pmessage'; 33 | const PONG = 'pong'; 34 | 35 | protected $client; 36 | protected $callback; 37 | protected $closing; 38 | 39 | /** 40 | * @param Client $client Client instance. 41 | * @param callable $callback Callback invoked on each received message. 42 | */ 43 | public function __construct(Client $client, callable $callback) 44 | { 45 | $this->client = $client; 46 | $this->callback = $callback; 47 | $this->closing = false; 48 | } 49 | 50 | /** 51 | * Parses the response array returned by the server into an object. 52 | * 53 | * @param array $response Message payload. 54 | * 55 | * @return object 56 | */ 57 | protected function parsePayload($response) 58 | { 59 | if ($response instanceof ResponseInterface) { 60 | return $response; 61 | } 62 | 63 | // TODO: I don't exactly like how we are handling this condition. 64 | if ($this->closing) { 65 | return null; 66 | } 67 | 68 | switch ($response[0]) { 69 | case self::SUBSCRIBE: 70 | case self::UNSUBSCRIBE: 71 | case self::PSUBSCRIBE: 72 | case self::PUNSUBSCRIBE: 73 | if ($response[2] === 0) { 74 | $this->closing = true; 75 | } 76 | 77 | return null; 78 | 79 | case self::MESSAGE: 80 | return (object) [ 81 | 'kind' => $response[0], 82 | 'channel' => $response[1], 83 | 'payload' => $response[2], 84 | ]; 85 | 86 | case self::PMESSAGE: 87 | return (object) [ 88 | 'kind' => $response[0], 89 | 'pattern' => $response[1], 90 | 'channel' => $response[2], 91 | 'payload' => $response[3], 92 | ]; 93 | 94 | case self::PONG: 95 | return (object) [ 96 | 'kind' => $response[0], 97 | 'payload' => $response[1], 98 | ]; 99 | 100 | default: 101 | throw new RuntimeException( 102 | "Received an unknown message type {$response[0]} inside of a pubsub context" 103 | ); 104 | } 105 | } 106 | 107 | /** 108 | * Closes the underlying connection to the server. 109 | */ 110 | public function quit() 111 | { 112 | $this->closing = true; 113 | $this->client->quit(); 114 | } 115 | 116 | /** 117 | * Writes a Redis command on the underlying connection. 118 | * 119 | * @param string $method Command ID. 120 | * @param array $arguments Arguments for the command. 121 | * @param callable $callback Optional callback. 122 | */ 123 | protected function writeRequest($method, $arguments, callable $callback = null) 124 | { 125 | $arguments = Command::normalizeArguments($arguments ?: []); 126 | $command = $this->client->createCommand($method, $arguments); 127 | 128 | $this->client->executeCommand($command, $callback); 129 | } 130 | 131 | /** 132 | * Subscribes to one or more channels. 133 | * 134 | * @param mixed ... List of channels. 135 | */ 136 | public function subscribe(/* channels */) 137 | { 138 | $this->writeRequest('subscribe', func_get_args(), $this); 139 | } 140 | 141 | /** 142 | * Subscribes to one or more channels by pattern. 143 | * 144 | * @param mixed ... List of pattenrs. 145 | */ 146 | public function psubscribe(/* channels */) 147 | { 148 | $this->writeRequest('psubscribe', func_get_args(), $this); 149 | } 150 | 151 | /** 152 | * Unsubscribes from one or more channels. 153 | * 154 | * @param mixed ... $channels List of channels. 155 | */ 156 | public function unsubscribe(/* channels */) 157 | { 158 | $this->writeRequest('unsubscribe', func_get_args()); 159 | } 160 | 161 | /** 162 | * Unsubscribes from one or more channels by pattern. 163 | * 164 | * @param mixed ... List of patterns.. 165 | */ 166 | public function punsubscribe(/* channels */) 167 | { 168 | $this->writeRequest('punsubscribe', func_get_args()); 169 | } 170 | 171 | /** 172 | * PING the server with an optional payload that will be echoed as a 173 | * PONG message in the pub/sub loop. 174 | * 175 | * @param string $payload Optional PING payload. 176 | */ 177 | public function ping($payload = null) 178 | { 179 | $this->writeRequest('ping', [$payload]); 180 | } 181 | 182 | /** 183 | * Wraps the user-provided callback to process payloads returned by the server. 184 | * 185 | * @param string $payload Payload returned by the server. 186 | * @param Client $client Associated client instance. 187 | * @param CommandInterface $command Command instance (always NULL in case of streaming contexts). 188 | */ 189 | public function __invoke($payload, $client, $command) 190 | { 191 | $parsedPayload = $this->parsePayload($payload); 192 | 193 | if ($this->closing) { 194 | $this->client->disconnect(); 195 | $this->closing = false; 196 | 197 | return; 198 | } 199 | 200 | if (isset($parsedPayload)) { 201 | call_user_func($this->callback, $parsedPayload, $this); 202 | } 203 | } 204 | 205 | /** 206 | * Returns the underlying client instance. 207 | * 208 | * @return Client 209 | */ 210 | public function getClient() 211 | { 212 | return $this->client; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/Transaction/MultiExec.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async\Transaction; 13 | 14 | use RuntimeException; 15 | use SplQueue; 16 | use Predis\Response\ResponseInterface; 17 | use Predis\Response\Status as StatusResponse; 18 | use Predis\Async\Client; 19 | 20 | /** 21 | * Client-side abstraction of a Redis transaction based on MULTI / EXEC. 22 | * 23 | * @author Daniele Alessandri 24 | */ 25 | class MultiExec 26 | { 27 | protected $client; 28 | 29 | /** 30 | * @param Client $client Client instance. 31 | */ 32 | public function __construct(Client $client) 33 | { 34 | $this->client = $client; 35 | $this->commands = new SplQueue(); 36 | 37 | $this->initialize(); 38 | } 39 | 40 | /** 41 | * Initializes the transaction context. 42 | */ 43 | protected function initialize() 44 | { 45 | $command = $this->client->createCommand('MULTI'); 46 | 47 | $this->client->executeCommand($command, function ($response) { 48 | if (false === $response) { 49 | throw new RuntimeException('Could not initialize a MULTI / EXEC transaction'); 50 | } 51 | }); 52 | } 53 | 54 | /** 55 | * Dynamically invokes a Redis command with the specified arguments. 56 | * 57 | * @param string $method Command ID. 58 | * @param array $arguments Arguments for the command. 59 | * 60 | * @return MultiExecContext 61 | */ 62 | public function __call($method, $arguments) 63 | { 64 | $commands = $this->commands; 65 | $command = $this->client->createCommand($method, $arguments); 66 | 67 | $this->client->executeCommand($command, function ($response, $_, $command) use ($commands) { 68 | if (!$response instanceof StatusResponse || $response != 'QUEUED') { 69 | throw new RuntimeException('Unexpected response in MULTI / EXEC [expected +QUEUED]'); 70 | } 71 | 72 | $commands->enqueue($command); 73 | }); 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Handles the actual execution of the whole transaction. 80 | * 81 | * @param callable $callback Callback invoked after execution. 82 | */ 83 | public function execute(callable $callback) 84 | { 85 | $commands = $this->commands; 86 | $command = $this->client->createCommand('EXEC'); 87 | 88 | $this->client->executeCommand($command, function ($responses, $client) use ($commands, $callback) { 89 | $size = count($responses); 90 | $processed = []; 91 | 92 | for ($i = 0; $i < $size; $i++) { 93 | $command = $commands->dequeue(); 94 | $response = $responses[$i]; 95 | 96 | unset($responses[$i]); 97 | 98 | if (!$response instanceof ResponseInterface) { 99 | $response = $command->parseResponse($response); 100 | } 101 | 102 | $processed[$i] = $response; 103 | } 104 | 105 | call_user_func($callback, $processed, $client); 106 | }); 107 | } 108 | 109 | /** 110 | * This method is an alias for execute(). 111 | * 112 | * @param callable $callback Callback invoked after execution. 113 | */ 114 | public function exec(callable $callback) 115 | { 116 | $this->execute($callback); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/PHPUnit/PredisAsyncTestCase.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async; 13 | 14 | use \PHPUnit_Framework_TestCase as StandardTestCase; 15 | 16 | use Predis\Connection\Parameters; 17 | use Predis\Profile\Factory as ProfileFactory; 18 | use React\EventLoop\StreamSelectLoop; 19 | 20 | use Predis\Async\Configuration\Options; 21 | 22 | /** 23 | * 24 | */ 25 | abstract class PredisAsyncTestCase extends StandardTestCase 26 | { 27 | /** 28 | * Returns a new instance of connection parameters. 29 | * 30 | * @param array $override Override default connection parameters. 31 | * 32 | * @return Predis\Connection\ParametersInterface 33 | */ 34 | protected function getParameters($override = null) 35 | { 36 | $parameters = new Parameters(array_merge([ 37 | 'scheme' => 'tcp', 38 | 'host' => REDIS_SERVER_HOST, 39 | 'port' => REDIS_SERVER_PORT, 40 | 'timeout' => 0.5, 41 | ]), $override ?: []); 42 | 43 | return $parameters; 44 | } 45 | 46 | /** 47 | * Returns a new instance of client options. 48 | * 49 | * @param array $override Override default options. 50 | * 51 | * @return Predis\Async\Configuration\OptionsInterface 52 | */ 53 | protected function getOptions($override = null) 54 | { 55 | $options = new Options(array_merge([ 56 | 'profile' => ProfileFactory::get(REDIS_SERVER_VERSION), 57 | 'eventloop' => $this->getEventLoop(), 58 | ]), $override ?: []); 59 | 60 | return $options; 61 | } 62 | 63 | /** 64 | * Returns a new instance of event loop. 65 | * 66 | * @return StreamSelectLoop 67 | */ 68 | protected function getEventLoop() 69 | { 70 | $loop = new StreamSelectLoop(); 71 | 72 | return $loop; 73 | } 74 | 75 | /** 76 | * Returns a new instance of client. 77 | * 78 | * @param array $parameters Override default parameters. 79 | * @param array $override Override default options. 80 | * 81 | * @return Client 82 | */ 83 | public function getClient($parameters = null, $options = null) 84 | { 85 | $parameters = $this->getParameters(); 86 | $options = $this->getOptions(); 87 | 88 | $client = new Client($parameters, $options); 89 | 90 | return $client; 91 | } 92 | 93 | /** 94 | * Executes the callback with a client connected to Redis. 95 | * 96 | * @param mixed $callback Callable object. 97 | * @param array $parameters Override default parameters. 98 | * @param array $override Override default options. 99 | */ 100 | public function withConnectedClient($callback, $parameters = null, $options = null) 101 | { 102 | $options = array_merge([ 103 | 'on_error' => function ($client, $exception) { 104 | throw $exception; 105 | }, 106 | ], $options ?: []); 107 | 108 | $client = $this->getClient($parameters, $options); 109 | $trigger = false; 110 | 111 | $client->connect(function ($client, $connection) use ($callback, &$trigger) { 112 | $trigger = true; 113 | 114 | $client->select(REDIS_SERVER_DBNUM, function ($_, $client) use ($callback, $connection) { 115 | call_user_func($callback, $this, $client, $connection); 116 | }); 117 | }); 118 | 119 | $loop = $client->getEventLoop(); 120 | 121 | $loop->addTimer(0.01, function () use (&$trigger) { 122 | $this->assertTrue($trigger, 'The client was unable to connect to Redis'); 123 | }); 124 | 125 | $loop->run(); 126 | } 127 | 128 | /** 129 | * Detects the presence of the phpiredis extension. 130 | * 131 | * @return bool 132 | */ 133 | public function isPhpiredisAvailable() 134 | { 135 | return function_exists('phpiredis_reader_create'); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/Predis/Async/ClientTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Predis\Async; 13 | 14 | use Predis\Connection\Parameters; 15 | use Predis\Profile\Factory as ProfileFactory; 16 | use Predis\Async\Connection\StreamConnection; 17 | 18 | /** 19 | * 20 | */ 21 | class ClientTest extends PredisAsyncTestCase 22 | { 23 | /** 24 | * @group disconnected 25 | */ 26 | public function testConstructorWithoutArguments() 27 | { 28 | $client = new Client(); 29 | 30 | $connection = $client->getConnection(); 31 | $this->assertInstanceOf('Predis\Async\Connection\ConnectionInterface', $connection); 32 | 33 | $parameters = $connection->getParameters(); 34 | $this->assertSame($parameters->host, '127.0.0.1'); 35 | $this->assertSame($parameters->port, 6379); 36 | 37 | $options = $client->getOptions(); 38 | $this->assertSame($options->profile->getVersion(), ProfileFactory::getDefault()->getVersion()); 39 | 40 | $this->assertFalse($client->isConnected()); 41 | } 42 | 43 | /** 44 | * @group disconnected 45 | */ 46 | public function testConstructorWithNullArgument() 47 | { 48 | $client = new Client(null); 49 | 50 | $connection = $client->getConnection(); 51 | $this->assertInstanceOf('Predis\Async\Connection\ConnectionInterface', $connection); 52 | 53 | $parameters = $connection->getParameters(); 54 | $this->assertSame($parameters->host, '127.0.0.1'); 55 | $this->assertSame($parameters->port, 6379); 56 | 57 | $options = $client->getOptions(); 58 | $this->assertSame($options->profile->getVersion(), ProfileFactory::getDefault()->getVersion()); 59 | 60 | $this->assertFalse($client->isConnected()); 61 | } 62 | 63 | /** 64 | * @group disconnected 65 | */ 66 | public function testConstructorWithNullAndNullArguments() 67 | { 68 | $client = new Client(null, null); 69 | 70 | $connection = $client->getConnection(); 71 | $this->assertInstanceOf('Predis\Async\Connection\ConnectionInterface', $connection); 72 | 73 | $parameters = $connection->getParameters(); 74 | $this->assertSame($parameters->host, '127.0.0.1'); 75 | $this->assertSame($parameters->port, 6379); 76 | 77 | $options = $client->getOptions(); 78 | $this->assertSame($options->profile->getVersion(), ProfileFactory::getDefault()->getVersion()); 79 | 80 | $this->assertFalse($client->isConnected()); 81 | } 82 | 83 | /** 84 | * @group disconnected 85 | */ 86 | public function testConstructorWithArrayArgument() 87 | { 88 | $client = new Client($arg1 = ['host' => 'localhost', 'port' => 7000]); 89 | 90 | $parameters = $client->getConnection()->getParameters(); 91 | $this->assertSame($parameters->host, $arg1['host']); 92 | $this->assertSame($parameters->port, $arg1['port']); 93 | } 94 | 95 | /** 96 | * @group disconnected 97 | */ 98 | public function testConstructorWithStringArgument() 99 | { 100 | $client = new Client('tcp://localhost:7000'); 101 | 102 | $parameters = $client->getConnection()->getParameters(); 103 | $this->assertSame($parameters->host, 'localhost'); 104 | $this->assertSame($parameters->port, 7000); 105 | } 106 | 107 | /** 108 | * @group disconnected 109 | */ 110 | public function testConstructorWithConnectionArgument() 111 | { 112 | $parameters = $this->getParameters(); 113 | $eventloop = $this->getEventLoop(); 114 | 115 | $connection = new StreamConnection($eventloop, $parameters); 116 | 117 | $client = new Client($connection, $eventloop); 118 | 119 | $this->assertInstanceOf('Predis\Async\Connection\ConnectionInterface', $client->getConnection()); 120 | $this->assertSame($connection, $client->getConnection()); 121 | $this->assertSame($connection->getEventLoop(), $client->getEventLoop()); 122 | 123 | $parameters = $client->getConnection()->getParameters(); 124 | $this->assertSame($parameters->host, REDIS_SERVER_HOST); 125 | $this->assertSame($parameters->port, REDIS_SERVER_PORT); 126 | } 127 | 128 | /** 129 | * @group disconnected 130 | * @expectedException Predis\ClientException 131 | * @expectedExceptionMessage Client and connection must share the same event loop. 132 | */ 133 | public function testConnectionAndClientMustShareSameEventLoop() 134 | { 135 | $parameters = $this->getParameters(); 136 | $eventloop = $this->getEventLoop(); 137 | 138 | $connection = new StreamConnection($eventloop, $parameters); 139 | $client = new Client($connection); 140 | } 141 | 142 | /** 143 | * @group disconnected 144 | * @todo How should we test for the error callback? 145 | */ 146 | public function testConstructorWithNullAndArrayArgument() 147 | { 148 | $options = [ 149 | 'profile' => '2.0', 150 | 'prefix' => 'prefix:', 151 | 'eventloop' => $loop = $this->getEventLoop(), 152 | 'on_error' => $callback = function ($client, $error) { }, 153 | ]; 154 | 155 | $client = new Client(null, $options); 156 | 157 | $profile = $client->getProfile(); 158 | $this->assertSame($profile->getVersion(), ProfileFactory::get('2.0')->getVersion()); 159 | $this->assertInstanceOf('Predis\Command\Processor\KeyPrefixProcessor', $profile->getProcessor()); 160 | $this->assertSame('prefix:', $profile->getProcessor()->getPrefix()); 161 | 162 | $this->assertSame($loop, $client->getEventLoop()); 163 | } 164 | 165 | /** 166 | * @group disconnected 167 | */ 168 | public function testConstructorWithNullAndEventLoopArgument() 169 | { 170 | $client = new Client(null, $loop = $this->getEventLoop()); 171 | $this->assertSame($loop, $client->getEventLoop()); 172 | } 173 | 174 | /** 175 | * @group disconnected 176 | */ 177 | public function testConnectAndDisconnect() 178 | { 179 | $loop = $this->getEventLoop(); 180 | $callback = function ($client, $connection) { }; 181 | 182 | $connection = $this->getMock('Predis\Async\Connection\ConnectionInterface'); 183 | $connection->expects($this->once())->method('getEventLoop')->will($this->returnValue($loop)); 184 | $connection->expects($this->once())->method('connect')->with($callback); 185 | $connection->expects($this->once())->method('disconnect'); 186 | 187 | $client = new Client($connection, $loop); 188 | $client->connect($callback); 189 | $client->disconnect(); 190 | } 191 | 192 | /** 193 | * @group disconnected 194 | */ 195 | public function testIsConnectedChecksConnectionState() 196 | { 197 | $loop = $this->getEventLoop(); 198 | $callback = function ($client, $connection) { }; 199 | 200 | $connection = $this->getMock('Predis\Async\Connection\ConnectionInterface'); 201 | $connection->expects($this->once())->method('getEventLoop')->will($this->returnValue($loop)); 202 | $connection->expects($this->once())->method('isConnected'); 203 | 204 | $client = new Client($connection, $loop); 205 | $client->isConnected(); 206 | } 207 | 208 | /** 209 | * @group disconnected 210 | */ 211 | public function testCreatesNewCommandUsingSpecifiedProfile() 212 | { 213 | $ping = ProfileFactory::getDefault()->createCommand('ping', []); 214 | 215 | $profile = $this->getMock('Predis\Profile\ProfileInterface'); 216 | $profile->expects($this->once()) 217 | ->method('createCommand') 218 | ->with('ping', []) 219 | ->will($this->returnValue($ping)); 220 | 221 | $client = new Client(null, ['profile' => $profile]); 222 | $this->assertSame($ping, $client->createCommand('ping', [])); 223 | } 224 | 225 | /** 226 | * @group disconnected 227 | */ 228 | public function testExecuteCommandReturnsParsedRepliesInCallback() 229 | { 230 | $this->markTestIncomplete('This test has not been implemented yet.'); 231 | } 232 | 233 | /** 234 | * @group disconnected 235 | */ 236 | public function testExecuteCommandReturnsErrorResponseInCallback() 237 | { 238 | $this->markTestIncomplete('This test has not been implemented yet.'); 239 | } 240 | 241 | /** 242 | * @group disconnected 243 | */ 244 | public function testExecuteCommandEvenWithoutCallback() 245 | { 246 | $this->markTestIncomplete('This test has not been implemented yet.'); 247 | } 248 | 249 | /** 250 | * @group disconnected 251 | * @expectedException Predis\ClientException 252 | * @expectedExceptionMessage Command 'INVALIDCOMMAND' is not a registered Redis command 253 | */ 254 | public function testThrowsExceptionOnNonRegisteredRedisCommand() 255 | { 256 | $this->getClient()->invalidCommand(); 257 | } 258 | 259 | /** 260 | * @group disconnected 261 | */ 262 | public function testPubSubLoopReturnsConsumer() 263 | { 264 | $client = $this->getClient(); 265 | $pubsub = $client->pubSubLoop([], function ($event, $pubsub) {}); 266 | 267 | $this->assertInstanceOf('Predis\Async\PubSub\Consumer', $pubsub); 268 | $this->assertFalse($client->isConnected()); 269 | } 270 | 271 | /** 272 | * @group disconnected 273 | * @todo The underlying transaction should be lazily initialized. 274 | */ 275 | public function testMonitorReturnsConsumer() 276 | { 277 | $client = $this->getClient(); 278 | $monitor = $client->monitor(function ($event, $monitor) {}, false); 279 | 280 | $this->assertInstanceOf('Predis\Async\Monitor\Consumer', $monitor); 281 | $this->assertFalse($client->isConnected()); 282 | } 283 | 284 | /** 285 | * @group connected 286 | * @todo The underlying transaction should be lazily initialized. 287 | */ 288 | public function testTransactionReturnsMultiExecInstance() 289 | { 290 | $client = $this->getClient(); 291 | $transaction = $client->transaction(); 292 | 293 | $this->assertInstanceOf('Predis\Async\Transaction\MultiExec', $transaction); 294 | $this->assertTrue($client->isConnected()); 295 | } 296 | 297 | /** 298 | * @group connected 299 | */ 300 | public function testCanConnectToRedis() 301 | { 302 | $trigger = false; 303 | 304 | $parameters = $this->getParameters(); 305 | $options = $this->getOptions(); 306 | $client = new Client($parameters, $options); 307 | 308 | $client->connect(function ($cbkClient, $cbkConnection) use ($client, &$trigger) { 309 | $trigger = true; 310 | 311 | $this->assertInstanceOf('Predis\Async\Client', $cbkClient); 312 | $this->assertInstanceOf('Predis\Async\Connection\ConnectionInterface', $cbkConnection); 313 | 314 | $this->assertSame($client, $cbkClient); 315 | 316 | $client->disconnect(); 317 | }); 318 | 319 | $loop = $client->getEventLoop(); 320 | 321 | $loop->addTimer(0.05, function () use (&$trigger) { 322 | $this->assertTrue($trigger, 'The client was unable to connect to Redis'); 323 | }); 324 | 325 | $loop->run(); 326 | } 327 | 328 | /** 329 | * @group connected 330 | */ 331 | public function testCanSendCommandsToRedis() 332 | { 333 | $this->withConnectedClient(function ($test, $client) { 334 | $client->echo('Predis\Async', function ($reply, $client, $command) use ($test) { 335 | $test->assertInstanceOf('Predis\Async\Client', $client); 336 | $test->assertInstanceOf('Predis\Command\CommandInterface', $command); 337 | $test->assertSame('Predis\Async', $reply); 338 | 339 | $client->disconnect(); 340 | }); 341 | }); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # About testing Predis\Async # 2 | 3 | __ATTENTION__: Do not ever run this test suite against instances of Redis running 4 | in production environments or containing data you are interested in! If you still 5 | want to test this software on a production server without hitting the database, 6 | please read ahead to undestand how to disable integration tests. 7 | 8 | Predis\Async ships with a comprehensive test suite that uses __PHPUnit__ to cover 9 | every aspect of the library. The suite is organized into several unit groups with 10 | the PHPUnit `@group` annotation which makes it possible to run only selected groups 11 | of tests. The main groups are: 12 | 13 | - __disconnected__: generic tests that verify the correct behaviour of the 14 | library without requiring an active connection to Redis. 15 | - __connected__: integration tests that require an active connection to Redis 16 | - __slow__: tests that might slow down the execution of the test suite, they 17 | can be either __connected__ or __disconnected__. 18 | 19 | A list of all the available groups in the suite can be obtained by running: 20 | 21 | ```bash 22 | $ phpunit --list-groups 23 | ``` 24 | Groups of tests can be disabled or enabled via the XML configuration file or the 25 | standard command-line test runner. Please note that due to a bug in PHPUnit, 26 | older versions ignore the `--group` option when the group is excluded in the XML 27 | configuration file. More details about this issue are available on [PHPUnit's bug 28 | tracker](http://github.com/sebastianbergmann/phpunit/issues/320). 29 | 30 | ### Combining groups for inclusion or exclusion with the command-line runner ### 31 | 32 | ```bash 33 | $ phpunit --group disconnected --exclude-group commands,slow 34 | ``` 35 | 36 | ### Integration tests ### 37 | 38 | The suite performs integration tests against a running instance of Redis (>= 2.4.0 39 | is required) to verify the correct behaviour of the implementation of each command 40 | and certain abstractions implemented in Predis\Async depending on them. These tests 41 | are identified by the __connected__ group. 42 | 43 | Integration tests for commands that are not defined in the specified server profile 44 | (see the value of the `REDIS_SERVER_VERSION` constant in `phpunit.xml`) are marked 45 | as __skipped__ automatically. 46 | 47 | By default, the test suite is configured to execute integration tests using the 48 | server profile for Redis v2.4 (which is the current stable version of Redis). You 49 | can optionally run the suite against a Redis instance built from the `unstable` 50 | branch with the development profile by changing the `REDIS_SERVER_VERSION` to `dev` 51 | in the `phpunit.xml` file. 52 | 53 | If you do not have a Redis instance up and running or available for testing, you 54 | can completely disable integration tests by excluding the __connected__ group: 55 | 56 | ```bash 57 | $ phpunit --exclude-group connected 58 | ``` 59 | 60 | ### Slow tests ### 61 | 62 | Certain tests can slow down the execution of the test suite. These tests can be disabled 63 | by excluding the __slow__ group: 64 | 65 | ```bash 66 | $ phpunit --exclude-group slow 67 | ``` 68 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | require __DIR__.'/../autoload.php'; 13 | 14 | require __DIR__.'/PHPUnit/PredisAsyncTestCase.php'; 15 | -------------------------------------------------------------------------------- /travisci-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ef -o pipefail 4 | 5 | if [ -z "$TRAVIS_PHP_VERSION" ]; then 6 | echo "This script is meant to be executed on Travis CI." 7 | exit 1 8 | fi 9 | 10 | if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then 11 | 12 | git clone https://github.com/redis/hiredis.git \ 13 | && pushd hiredis \ 14 | && git checkout v0.13.3 \ 15 | && make \ 16 | && sudo make install \ 17 | && popd 18 | 19 | git clone https://github.com/nrk/phpiredis.git \ 20 | && pushd phpiredis \ 21 | && git checkout php7 \ 22 | && phpize \ 23 | && ./configure --enable-phpiredis \ 24 | && make \ 25 | && make install \ 26 | && popd 27 | 28 | phpenv config-add phpiredis.ini 29 | fi 30 | 31 | composer self-update 32 | --------------------------------------------------------------------------------