tipo: modificación del cpanel y gitignore de la carpeta vendor
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
github: clue
|
||||
custom: https://clue.engineering/support
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.2 (2024-08-07)
|
||||
|
||||
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable types.
|
||||
(#19 by @clue)
|
||||
|
||||
* Update project structure, homepage and examples.
|
||||
Add `.gitattributes` to exclude dev files from exports.
|
||||
(#16, #20, #21 and #22 by @clue)
|
||||
|
||||
* Update test suite to use GitHub actions for continuous integration (CI),
|
||||
run tests on all PHP versions up to PHP 8.3 and ensure 100% code coverage.
|
||||
(#15 by @SimonFrings and #17 and #18 by @clue)
|
||||
|
||||
## 0.3.1 (2017-06-06)
|
||||
|
||||
* Fix: Fix server-side parsing of legacy inline protocol when multiple requests are processed at once
|
||||
(#12 by @kelunik and #13 by @clue)
|
||||
|
||||
## 0.3.0 (2014-01-27)
|
||||
|
||||
* Feature: Add dedicated and faster `RequestParser` that also support the old
|
||||
inline request protocol.
|
||||
|
||||
* Feature: Message serialization can now be handled directly by the Serializer
|
||||
again without having to construct the appropriate model first.
|
||||
|
||||
* BC break: The `Factory` now has two distinct methods to create parsers:
|
||||
* `createResponseParser()` for a client-side library
|
||||
* `createRequestParser()` for a server-side library / testing framework
|
||||
|
||||
* BC break: Simplified parser API, now `pushIncoming()` returns an array of all
|
||||
parsed message models.
|
||||
|
||||
* BC break: The signature for getting a serialized message from a model was
|
||||
changed and now requires a Serializer passed:
|
||||
|
||||
```php
|
||||
ModelInterface::getMessageSerialized($serializer)
|
||||
```
|
||||
|
||||
* Many, many performance improvements
|
||||
|
||||
## 0.2.0 (2014-01-21)
|
||||
|
||||
* Re-organize the whole API into dedicated
|
||||
* `Parser` (protocol reader) and
|
||||
* `Serializer` (protocol writer) sub-namespaces. (#4)
|
||||
|
||||
* Use of the factory has now been unified:
|
||||
|
||||
```php
|
||||
$factory = new Clue\Redis\Protocol\Factory();
|
||||
$parser = $factory->createParser();
|
||||
$serializer = $factory->createSerializer();
|
||||
```
|
||||
|
||||
* Add a dedicated `Model` for each type of reply. Among others, this now allows
|
||||
you to distinguish a single line `StatusReply` from a binary-safe `BulkReply`. (#2)
|
||||
|
||||
* Fix parsing binary values and do not trip over trailing/leading whitespace. (#4)
|
||||
|
||||
* Improve parser and serializer performance by up to 20%. (#4)
|
||||
|
||||
## 0.1.0 (2013-09-10)
|
||||
|
||||
* First tagged release
|
||||
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2013 Christian Lück
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Vendored
+189
@@ -0,0 +1,189 @@
|
||||
# clue/redis-protocol
|
||||
|
||||
[](https://github.com/clue/redis-protocol/actions)
|
||||
[](#tests)
|
||||
[](https://packagist.org/packages/clue/redis-protocol)
|
||||
|
||||
A streaming Redis protocol (RESP) parser and serializer written in pure PHP.
|
||||
|
||||
This parser and serializer implementation allows you to parse Redis protocol
|
||||
messages into native PHP values and vice-versa. This is usually needed by a
|
||||
Redis client implementation which also handles the connection socket.
|
||||
|
||||
To re-iterate: This is *not* a Redis client implementation. This is a protocol
|
||||
implementation that is usually used by a Redis client implementation. If you're
|
||||
looking for an easy way to build your own client implementation, then this is
|
||||
for you. If you merely want to connect to a Redis server and issue some
|
||||
commands, you're probably better off using one of the existing client
|
||||
implementations.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
* [Support us](#support-us)
|
||||
* [Quickstart example](#quickstart-example)
|
||||
* [Usage](#usage)
|
||||
* [Factory](#factory)
|
||||
* [Parser](#parser)
|
||||
* [Model](#model)
|
||||
* [Serializer](#serializer)
|
||||
* [Install](#install)
|
||||
* [Tests](#tests)
|
||||
* [License](#license)
|
||||
|
||||
## Support us
|
||||
|
||||
We invest a lot of time developing, maintaining and updating our awesome
|
||||
open-source projects. You can help us sustain this high-quality of our work by
|
||||
[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get
|
||||
numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)
|
||||
for details.
|
||||
|
||||
Let's take these projects to the next level together! 🚀
|
||||
|
||||
## Quickstart example
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$factory = new Clue\Redis\Protocol\Factory();
|
||||
$parser = $factory->createResponseParser();
|
||||
$serializer = $factory->createSerializer();
|
||||
|
||||
$fp = fsockopen('tcp://localhost', 6379);
|
||||
fwrite($fp, $serializer->getRequestMessage('SET', array('name', 'value')));
|
||||
fwrite($fp, $serializer->getRequestMessage('GET', array('name')));
|
||||
|
||||
// the commands are pipelined, so this may parse multiple responses
|
||||
$models = $parser->pushIncoming(fread($fp, 4096));
|
||||
|
||||
$reply1 = array_shift($models);
|
||||
$reply2 = array_shift($models);
|
||||
|
||||
var_dump($reply1->getValueNative()); // string(2) "OK"
|
||||
var_dump($reply2->getValueNative()); // string(5) "value"
|
||||
```
|
||||
|
||||
See also the [examples](examples/).
|
||||
|
||||
## Usage
|
||||
|
||||
### Factory
|
||||
|
||||
The factory helps with instantiating the *right* parser and serializer.
|
||||
Eventually the *best* available implementation will be chosen depending on your
|
||||
installed extensions. You're also free to instantiate them directly, but this
|
||||
will lock you down on a given implementation (which could be okay depending on
|
||||
your use-case).
|
||||
|
||||
### Parser
|
||||
|
||||
The library includes a streaming Redis protocol parser. As such, it can safely
|
||||
parse Redis protocol messages and work with an incomplete data stream. For this,
|
||||
each included parser implements a single method
|
||||
`ParserInterface::pushIncoming($chunk)`.
|
||||
|
||||
* The `ResponseParser` is what most Redis client implementation would want to
|
||||
use in order to parse incoming response messages from a Redis server instance.
|
||||
* The `RequestParser` can be used to test messages coming from a Redis client or
|
||||
even to implement a Redis server.
|
||||
* The `MessageBuffer` decorates either of the available parsers and merely
|
||||
offers some helper methods in order to work with single messages:
|
||||
* `hasIncomingModel()` to check if there's a complete message in the pipeline
|
||||
* `popIncomingModel()` to extract a complete message from the incoming queue.
|
||||
|
||||
### Model
|
||||
|
||||
Each message (response as well as request) is represented by a model
|
||||
implementing the `ModelInterface` that has two methods:
|
||||
|
||||
* `getValueNative()` returns the wrapped value.
|
||||
* `getMessageSerialized($serializer)` returns the serialized protocol messages
|
||||
that will be sent over the wire.
|
||||
|
||||
These models are very lightweight and add little overhead. They help keeping the
|
||||
code organized and also provide a means to distinguish a single line
|
||||
`StatusReply` from a binary-safe `BulkReply`.
|
||||
|
||||
The parser always returns models. Models can also be instantiated directly:
|
||||
|
||||
```php
|
||||
$model = new Model\IntegerReply(123);
|
||||
var_dump($model->getValueNative()); // int(123)
|
||||
var_dump($model->getMessageSerialized($serializer)); // string(6) ":123\r\n"
|
||||
```
|
||||
|
||||
### Serializer
|
||||
|
||||
The serializer is responsible for creating serialized messages and the
|
||||
corresponing message models to be sent across the wire.
|
||||
|
||||
```php
|
||||
$message = $serializer->getRequestMessage('ping');
|
||||
var_dump($message); // string(14) "$1\r\n*4\r\nping\r\n"
|
||||
|
||||
$message = $serializer->getRequestMessage('set', array('key', 'value'));
|
||||
var_dump($message); // string(33) "$3\r\n*3\r\nset\r\n*3\r\nkey\r\n*5\r\nvalue\r\n"
|
||||
|
||||
$model = $serializer->createRequestModel('get', array('key'));
|
||||
var_dump($model->getCommand()); // string(3) "get"
|
||||
var_dump($model->getArgs()); // array(1) { string(3) "key" }
|
||||
var_dump($model->getValueNative()); // array(2) { string(3) "GET", string(3) "key" }
|
||||
|
||||
$model = $serializer->createReplyModel(array('mixed', 12, array('value')));
|
||||
assert($model implement Model\MultiBulkReply);
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
It's very unlikely you'll want to use this protocol parser standalone.
|
||||
It should be added as a dependency to your Redis client implementation instead.
|
||||
The recommended way to install this library is [through Composer](https://getcomposer.org/).
|
||||
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
|
||||
|
||||
This will install the latest supported version:
|
||||
|
||||
```bash
|
||||
composer require clue/redis-protocol:^0.3.2
|
||||
```
|
||||
|
||||
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
|
||||
|
||||
This project aims to run on any platform and thus does not require any PHP
|
||||
extensions and supports running on legacy PHP 5.3 through current PHP 8+.
|
||||
It's *highly recommended to use the latest supported PHP version* for this project.
|
||||
|
||||
## Tests
|
||||
|
||||
To run the test suite, you first need to clone this repo and then install all
|
||||
dependencies [through Composer](https://getcomposer.org/):
|
||||
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
To run the test suite, go to the project root and run:
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit
|
||||
```
|
||||
|
||||
The test suite is set up to always ensure 100% code coverage across all
|
||||
supported environments. If you have the Xdebug extension installed, you can also
|
||||
generate a code coverage report locally like this:
|
||||
|
||||
```bash
|
||||
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Its parser and serializer originally used to be based on
|
||||
[jpd/redisent](https://github.com/jdp/redisent), which is released under the ISC
|
||||
license, copyright (c) 2009-2012 Justin Poliey <justin@getglue.com>.
|
||||
|
||||
Other than that, this project is released under the permissive [MIT license](LICENSE).
|
||||
|
||||
> Did you know that I offer custom development services and issuing invoices for
|
||||
sponsorships of releases and for contributions? Contact me (@clue) for details.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "clue/redis-protocol",
|
||||
"description": "A streaming Redis protocol (RESP) parser and serializer written in pure PHP.",
|
||||
"keywords": ["streaming", "redis", "protocol", "resp", "parser", "serializer"],
|
||||
"homepage": "https://github.com/clue/redis-protocol",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@lueck.tv"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Clue\\Redis\\Protocol\\": "src/"
|
||||
}
|
||||
} ,
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Clue\\Tests\\Redis\\Protocol\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol;
|
||||
|
||||
use Clue\Redis\Protocol\Parser\ParserInterface;
|
||||
use Clue\Redis\Protocol\Parser\ResponseParser;
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
use Clue\Redis\Protocol\Serializer\RecursiveSerializer;
|
||||
use Clue\Redis\Protocol\Parser\RequestParser;
|
||||
|
||||
/**
|
||||
* Provides factory methods used to instantiate the best available protocol implementation
|
||||
*/
|
||||
class Factory
|
||||
{
|
||||
/**
|
||||
* instantiate the best available protocol response parser implementation
|
||||
*
|
||||
* This is the parser every redis client implementation should use in order
|
||||
* to parse incoming response messages from a redis server.
|
||||
*
|
||||
* @return ParserInterface
|
||||
*/
|
||||
public function createResponseParser()
|
||||
{
|
||||
return new ResponseParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* instantiate the best available protocol request parser implementation
|
||||
*
|
||||
* This is most useful for a redis server implementation which needs to
|
||||
* process client requests.
|
||||
*
|
||||
* @return ParserInterface
|
||||
*/
|
||||
public function createRequestParser()
|
||||
{
|
||||
return new RequestParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* instantiate the best available protocol serializer implementation
|
||||
*
|
||||
* @return SerializerInterface
|
||||
*/
|
||||
public function createSerializer()
|
||||
{
|
||||
return new RecursiveSerializer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
|
||||
class BulkReply implements ModelInterface
|
||||
{
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* create bulk reply (string reply)
|
||||
*
|
||||
* @param string|null $data
|
||||
*/
|
||||
public function __construct($value)
|
||||
{
|
||||
if ($value !== null) {
|
||||
$value = (string)$value;
|
||||
}
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getValueNative()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getMessageSerialized(SerializerInterface $serializer)
|
||||
{
|
||||
return $serializer->getBulkMessage($this->value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
*
|
||||
* @link http://redis.io/topics/protocol#status-reply
|
||||
*/
|
||||
class ErrorReply extends Exception implements ModelInterface
|
||||
{
|
||||
/**
|
||||
* create error status reply (single line error message)
|
||||
*
|
||||
* @param string $message
|
||||
* @return string
|
||||
*/
|
||||
public function __construct($message, $code = 0, $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getValueNative()
|
||||
{
|
||||
return $this->getMessage();
|
||||
}
|
||||
|
||||
public function getMessageSerialized(SerializerInterface $serializer)
|
||||
{
|
||||
return $serializer->getErrorMessage($this->getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
|
||||
class IntegerReply implements ModelInterface
|
||||
{
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* create integer reply
|
||||
*
|
||||
* @param int $data
|
||||
*/
|
||||
public function __construct($value)
|
||||
{
|
||||
$this->value = (int)$value;
|
||||
}
|
||||
|
||||
public function getValueNative()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getMessageSerialized(SerializerInterface $serializer)
|
||||
{
|
||||
return $serializer->getIntegerMessage($this->value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
|
||||
interface ModelInterface
|
||||
{
|
||||
/**
|
||||
* Returns value of this model as a native representation for PHP
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValueNative();
|
||||
|
||||
/**
|
||||
* Returns the serialized representation of this protocol message
|
||||
*
|
||||
* @param SerializerInterface $serializer;
|
||||
* @return string
|
||||
*/
|
||||
public function getMessageSerialized(SerializerInterface $serializer);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
use InvalidArgumentException;
|
||||
use UnexpectedValueException;
|
||||
|
||||
class MultiBulkReply implements ModelInterface
|
||||
{
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* create multi bulk reply (an array of other replies, usually bulk replies)
|
||||
*
|
||||
* @param array|null $data
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct($data = null)
|
||||
{
|
||||
if ($data !== null && !is_array($data)) { // manual type check to support legacy PHP < 7.1
|
||||
throw new InvalidArgumentException('Argument #1 ($data) expected array|null');
|
||||
}
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function getValueNative()
|
||||
{
|
||||
if ($this->data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ret = array();
|
||||
foreach ($this->data as $one) {
|
||||
if ($one instanceof ModelInterface) {
|
||||
$ret []= $one->getValueNative();
|
||||
} else {
|
||||
$ret []= $one;
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getMessageSerialized(SerializerInterface $serializer)
|
||||
{
|
||||
return $serializer->getMultiBulkMessage($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this model represents a valid unified request protocol message
|
||||
*
|
||||
* The new unified protocol was introduced in Redis 1.2, but it became the
|
||||
* standard way for talking with the Redis server in Redis 2.0. The unified
|
||||
* request protocol is what Redis already uses in replies in order to send
|
||||
* list of items to clients, and is called a Multi Bulk Reply.
|
||||
*
|
||||
* @return boolean
|
||||
* @link http://redis.io/topics/protocol
|
||||
*/
|
||||
public function isRequest()
|
||||
{
|
||||
if (!$this->data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->data as $one) {
|
||||
if (!($one instanceof BulkReply) && !is_string($one)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getRequestModel()
|
||||
{
|
||||
if (!$this->data) {
|
||||
throw new UnexpectedValueException('Null-multi-bulk message can not be represented as a request, must contain string/bulk values');
|
||||
}
|
||||
|
||||
$command = null;
|
||||
$args = array();
|
||||
|
||||
foreach ($this->data as $one) {
|
||||
if ($one instanceof BulkReply) {
|
||||
$one = $one->getValueNative();
|
||||
} elseif (!is_string($one)) {
|
||||
throw new UnexpectedValueException('Message can not be represented as a request, must only contain string/bulk values');
|
||||
}
|
||||
|
||||
if ($command === null) {
|
||||
$command = $one;
|
||||
} else {
|
||||
$args []= $one;
|
||||
}
|
||||
}
|
||||
|
||||
return new Request($command, $args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
|
||||
class Request implements ModelInterface
|
||||
{
|
||||
private $command;
|
||||
private $args;
|
||||
|
||||
public function __construct($command, array $args = array())
|
||||
{
|
||||
$this->command = $command;
|
||||
$this->args = $args;
|
||||
}
|
||||
|
||||
public function getCommand()
|
||||
{
|
||||
return $this->command;
|
||||
}
|
||||
|
||||
public function getArgs()
|
||||
{
|
||||
return $this->args;
|
||||
}
|
||||
|
||||
public function getReplyModel()
|
||||
{
|
||||
$models = array(new BulkReply($this->command));
|
||||
foreach ($this->args as $arg) {
|
||||
$models []= new BulkReply($arg);
|
||||
}
|
||||
|
||||
return new MultiBulkReply($models);
|
||||
}
|
||||
|
||||
public function getValueNative()
|
||||
{
|
||||
$ret = $this->args;
|
||||
array_unshift($ret, $this->command);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getMessageSerialized(SerializerInterface $serializer)
|
||||
{
|
||||
return $serializer->getRequestMessage($this->command, $this->args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
/**
|
||||
*
|
||||
* @link http://redis.io/topics/protocol#status-reply
|
||||
*/
|
||||
class StatusReply implements ModelInterface
|
||||
{
|
||||
private $message;
|
||||
|
||||
/**
|
||||
* create status reply (single line message)
|
||||
*
|
||||
* @param string $message
|
||||
* @return string
|
||||
*/
|
||||
public function __construct($message)
|
||||
{
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public function getValueNative()
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function getMessageSerialized(SerializerInterface $serializer)
|
||||
{
|
||||
return $serializer->getStatusMessage($this->message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Parser;
|
||||
|
||||
use UnderflowException;
|
||||
|
||||
class MessageBuffer implements ParserInterface
|
||||
{
|
||||
private $parser;
|
||||
private $incomingQueue = array();
|
||||
|
||||
public function __construct(ParserInterface $parser)
|
||||
{
|
||||
$this->parser = $parser;
|
||||
}
|
||||
|
||||
public function popIncomingModel()
|
||||
{
|
||||
if (!$this->incomingQueue) {
|
||||
throw new UnderflowException('Incoming message queue is empty');
|
||||
}
|
||||
return array_shift($this->incomingQueue);
|
||||
}
|
||||
|
||||
public function hasIncomingModel()
|
||||
{
|
||||
return ($this->incomingQueue) ? true : false;
|
||||
}
|
||||
|
||||
public function pushIncoming($data)
|
||||
{
|
||||
$ret = $this->parser->pushIncoming($data);
|
||||
|
||||
foreach ($ret as $one) {
|
||||
$this->incomingQueue []= $one;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Parser;
|
||||
|
||||
use UnexpectedValueException;
|
||||
|
||||
class ParserException extends UnexpectedValueException
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Parser;
|
||||
|
||||
interface ParserInterface
|
||||
{
|
||||
/**
|
||||
* push a chunk of the redis protocol message into the buffer and parse
|
||||
*
|
||||
* You can push any number of bytes of a redis protocol message into the
|
||||
* parser and it will try to parse messages from its data stream. So you can
|
||||
* pass data directly from your socket stream and the parser will return the
|
||||
* right amount of message model objects for you.
|
||||
*
|
||||
* If you pass an incomplete message, expect it to return an empty array. If
|
||||
* your incomplete message is split to across multiple chunks, the parsed
|
||||
* message model will be returned once the parser has sufficient data.
|
||||
*
|
||||
* @param string $dataChunk
|
||||
* @return \Clue\Redis\Protocol\Model\ModelInterface[] 0+ message models
|
||||
* @throws \Clue\Redis\Protocol\Parser\ParserException if the message can not be parsed
|
||||
* @see self::popIncomingModel()
|
||||
*/
|
||||
public function pushIncoming($dataChunk);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Parser;
|
||||
|
||||
use Clue\Redis\Protocol\Model\Request;
|
||||
|
||||
class RequestParser implements ParserInterface
|
||||
{
|
||||
const CRLF = "\r\n";
|
||||
|
||||
private $incomingBuffer = '';
|
||||
private $incomingOffset = 0;
|
||||
|
||||
public function pushIncoming($dataChunk)
|
||||
{
|
||||
$this->incomingBuffer .= $dataChunk;
|
||||
|
||||
$parsed = array();
|
||||
|
||||
do {
|
||||
$saved = $this->incomingOffset;
|
||||
$message = $this->readRequest();
|
||||
if ($message === null) {
|
||||
// restore previous position for next parsing attempt
|
||||
$this->incomingOffset = $saved;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($message !== false) {
|
||||
$parsed []= $message;
|
||||
}
|
||||
} while($this->incomingBuffer !== '');
|
||||
|
||||
if ($this->incomingOffset !== 0) {
|
||||
$this->incomingBuffer = (string)substr($this->incomingBuffer, $this->incomingOffset);
|
||||
$this->incomingOffset = 0;
|
||||
}
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* try to parse request from incoming buffer
|
||||
*
|
||||
* @throws ParserException if the incoming buffer is invalid
|
||||
* @return Request|null
|
||||
*/
|
||||
private function readRequest()
|
||||
{
|
||||
$crlf = strpos($this->incomingBuffer, "\r\n", $this->incomingOffset);
|
||||
if ($crlf === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// line starts with a multi-bulk header "*"
|
||||
if (isset($this->incomingBuffer[$this->incomingOffset]) && $this->incomingBuffer[$this->incomingOffset] === '*') {
|
||||
$line = substr($this->incomingBuffer, $this->incomingOffset + 1, $crlf - $this->incomingOffset + 1);
|
||||
$this->incomingOffset = $crlf + 2;
|
||||
$count = (int)$line;
|
||||
|
||||
if ($count <= 0) {
|
||||
return false;
|
||||
}
|
||||
$command = null;
|
||||
$args = array();
|
||||
for ($i = 0; $i < $count; ++$i) {
|
||||
$sub = $this->readBulk();
|
||||
if ($sub === null) {
|
||||
return null;
|
||||
}
|
||||
if ($command === null) {
|
||||
$command = $sub;
|
||||
} else {
|
||||
$args []= $sub;
|
||||
}
|
||||
}
|
||||
return new Request($command, $args);
|
||||
}
|
||||
|
||||
// parse an old inline request instead
|
||||
$line = substr($this->incomingBuffer, $this->incomingOffset, $crlf - $this->incomingOffset);
|
||||
$this->incomingOffset = $crlf + 2;
|
||||
|
||||
$args = preg_split('/ +/', trim($line, ' '));
|
||||
$command = array_shift($args);
|
||||
|
||||
if ($command === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Request($command, $args);
|
||||
}
|
||||
|
||||
private function readBulk()
|
||||
{
|
||||
$crlf = strpos($this->incomingBuffer, "\r\n", $this->incomingOffset);
|
||||
if ($crlf === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// line has to start with a bulk header "$"
|
||||
if (!isset($this->incomingBuffer[$this->incomingOffset]) || $this->incomingBuffer[$this->incomingOffset] !== '$') {
|
||||
throw new ParserException('ERR Protocol error: expected \'$\', got \'' . substr($this->incomingBuffer, $this->incomingOffset, 1) . '\'');
|
||||
}
|
||||
|
||||
$line = substr($this->incomingBuffer, $this->incomingOffset + 1, $crlf - $this->incomingOffset + 1);
|
||||
$this->incomingOffset = $crlf + 2;
|
||||
$size = (int)$line;
|
||||
|
||||
if ($size < 0) {
|
||||
throw new ParserException('ERR Protocol error: invalid bulk length');
|
||||
}
|
||||
|
||||
if (!isset($this->incomingBuffer[$this->incomingOffset + $size + 1])) {
|
||||
// check enough bytes + crlf are buffered
|
||||
return null;
|
||||
}
|
||||
|
||||
$ret = substr($this->incomingBuffer, $this->incomingOffset, $size);
|
||||
$this->incomingOffset += $size + 2;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Parser;
|
||||
|
||||
use Clue\Redis\Protocol\Model\ModelInterface;
|
||||
use Clue\Redis\Protocol\Model\BulkReply;
|
||||
use Clue\Redis\Protocol\Model\ErrorReply;
|
||||
use Clue\Redis\Protocol\Model\IntegerReply;
|
||||
use Clue\Redis\Protocol\Model\MultiBulkReply;
|
||||
use Clue\Redis\Protocol\Model\StatusReply;
|
||||
|
||||
/**
|
||||
* Simple recursive redis wire protocol parser
|
||||
*
|
||||
* Heavily influenced by blocking parser implementation from jpd/redisent.
|
||||
*
|
||||
* @link https://github.com/jdp/redisent
|
||||
* @link http://redis.io/topics/protocol
|
||||
*/
|
||||
class ResponseParser implements ParserInterface
|
||||
{
|
||||
const CRLF = "\r\n";
|
||||
|
||||
private $incomingBuffer = '';
|
||||
private $incomingOffset = 0;
|
||||
|
||||
public function pushIncoming($dataChunk)
|
||||
{
|
||||
$this->incomingBuffer .= $dataChunk;
|
||||
|
||||
return $this->tryParsingIncomingMessages();
|
||||
}
|
||||
|
||||
private function tryParsingIncomingMessages()
|
||||
{
|
||||
$messages = array();
|
||||
|
||||
do {
|
||||
$message = $this->readResponse();
|
||||
if ($message === null) {
|
||||
// restore previous position for next parsing attempt
|
||||
$this->incomingOffset = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
$messages []= $message;
|
||||
|
||||
$this->incomingBuffer = (string)substr($this->incomingBuffer, $this->incomingOffset);
|
||||
$this->incomingOffset = 0;
|
||||
} while($this->incomingBuffer !== '');
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
private function readLine()
|
||||
{
|
||||
$pos = strpos($this->incomingBuffer, "\r\n", $this->incomingOffset);
|
||||
|
||||
if ($pos === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ret = (string)substr($this->incomingBuffer, $this->incomingOffset, $pos - $this->incomingOffset);
|
||||
$this->incomingOffset = $pos + 2;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
private function readLength($len)
|
||||
{
|
||||
$ret = substr($this->incomingBuffer, $this->incomingOffset, $len);
|
||||
if (strlen($ret) !== $len) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->incomingOffset += $len;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* try to parse response from incoming buffer
|
||||
*
|
||||
* ripped from jdp/redisent, with some minor modifications to read from
|
||||
* the incoming buffer instead of issuing a blocking fread on a stream
|
||||
*
|
||||
* @throws ParserException if the incoming buffer is invalid
|
||||
* @return ModelInterface|null
|
||||
* @link https://github.com/jdp/redisent
|
||||
*/
|
||||
private function readResponse()
|
||||
{
|
||||
/* Parse the response based on the reply identifier */
|
||||
$reply = $this->readLine();
|
||||
if ($reply === null) {
|
||||
return null;
|
||||
}
|
||||
switch (substr($reply, 0, 1)) {
|
||||
/* Error reply */
|
||||
case '-':
|
||||
$response = new ErrorReply(substr($reply, 1));
|
||||
break;
|
||||
/* Inline reply */
|
||||
case '+':
|
||||
$response = new StatusReply(substr($reply, 1));
|
||||
break;
|
||||
/* Bulk reply */
|
||||
case '$':
|
||||
$size = (int)substr($reply, 1);
|
||||
if ($size === -1) {
|
||||
return new BulkReply(null);
|
||||
}
|
||||
$data = $this->readLength($size);
|
||||
if ($data === null) {
|
||||
return null;
|
||||
}
|
||||
if ($this->readLength(2) === null) { /* discard crlf */
|
||||
return null;
|
||||
}
|
||||
$response = new BulkReply($data);
|
||||
break;
|
||||
/* Multi-bulk reply */
|
||||
case '*':
|
||||
$count = (int)substr($reply, 1);
|
||||
if ($count === -1) {
|
||||
return new MultiBulkReply(null);
|
||||
}
|
||||
$response = array();
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$sub = $this->readResponse();
|
||||
if ($sub === null) {
|
||||
return null;
|
||||
}
|
||||
$response []= $sub;
|
||||
}
|
||||
$response = new MultiBulkReply($response);
|
||||
break;
|
||||
/* Integer reply */
|
||||
case ':':
|
||||
$response = new IntegerReply(substr($reply, 1));
|
||||
break;
|
||||
default:
|
||||
throw new ParserException('Invalid message can not be parsed: "' . $reply . '"');
|
||||
}
|
||||
/* Party on */
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Serializer;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Exception;
|
||||
use Clue\Redis\Protocol\Model\BulkReply;
|
||||
use Clue\Redis\Protocol\Model\IntegerReply;
|
||||
use Clue\Redis\Protocol\Model\ErrorReply;
|
||||
use Clue\Redis\Protocol\Model\ModelInterface;
|
||||
use Clue\Redis\Protocol\Model\MultiBulkReply;
|
||||
use Clue\Redis\Protocol\Model\Request;
|
||||
|
||||
class RecursiveSerializer implements SerializerInterface
|
||||
{
|
||||
const CRLF = "\r\n";
|
||||
|
||||
public function getRequestMessage($command, array $args = array())
|
||||
{
|
||||
$data = '*' . (count($args) + 1) . "\r\n$" . strlen($command) . "\r\n" . $command . "\r\n";
|
||||
foreach ($args as $arg) {
|
||||
$data .= '$' . strlen($arg) . "\r\n" . $arg . "\r\n";
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function createRequestModel($command, array $args = array())
|
||||
{
|
||||
return new Request($command, $args);
|
||||
}
|
||||
|
||||
public function getReplyMessage($data)
|
||||
{
|
||||
if (is_string($data) || $data === null) {
|
||||
return $this->getBulkMessage($data);
|
||||
} else if (is_int($data) || is_float($data) || is_bool($data)) {
|
||||
return $this->getIntegerMessage($data);
|
||||
} else if ($data instanceof Exception) {
|
||||
return $this->getErrorMessage($data->getMessage());
|
||||
} else if (is_array($data)) {
|
||||
return $this->getMultiBulkMessage($data);
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid data type passed for serialization');
|
||||
}
|
||||
}
|
||||
|
||||
public function createReplyModel($data)
|
||||
{
|
||||
if (is_string($data) || $data === null) {
|
||||
return new BulkReply($data);
|
||||
} else if (is_int($data) || is_float($data) || is_bool($data)) {
|
||||
return new IntegerReply($data);
|
||||
} else if ($data instanceof Exception) {
|
||||
return new ErrorReply($data->getMessage());
|
||||
} else if (is_array($data)) {
|
||||
$models = array();
|
||||
foreach ($data as $one) {
|
||||
$models []= $this->createReplyModel($one);
|
||||
}
|
||||
return new MultiBulkReply($models);
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid data type passed for serialization');
|
||||
}
|
||||
}
|
||||
|
||||
public function getBulkMessage($data)
|
||||
{
|
||||
if ($data === null) {
|
||||
/* null bulk reply */
|
||||
return '$-1' . self::CRLF;
|
||||
}
|
||||
/* bulk reply */
|
||||
return '$' . strlen($data) . self::CRLF . $data . self::CRLF;
|
||||
}
|
||||
|
||||
public function getErrorMessage($data)
|
||||
{
|
||||
/* error status reply */
|
||||
return '-' . $data . self::CRLF;
|
||||
}
|
||||
|
||||
public function getIntegerMessage($data)
|
||||
{
|
||||
return ':' . (int)$data . self::CRLF;
|
||||
}
|
||||
|
||||
public function getMultiBulkMessage($data)
|
||||
{
|
||||
if ($data === null) {
|
||||
/* null multi bulk reply */
|
||||
return '*-1' . self::CRLF;
|
||||
}
|
||||
/* multi bulk reply */
|
||||
$ret = '*' . count($data) . self::CRLF;
|
||||
foreach ($data as $one) {
|
||||
if ($one instanceof ModelInterface) {
|
||||
$ret .= $one->getMessageSerialized($this);
|
||||
} else {
|
||||
$ret .= $this->getReplyMessage($one);
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getStatusMessage($data)
|
||||
{
|
||||
/* status reply */
|
||||
return '+' . $data . self::CRLF;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Serializer;
|
||||
|
||||
use Clue\Redis\Protocol\Model\ModelInterface;
|
||||
use Clue\Redis\Protocol\Model\MultiBulkReply;
|
||||
|
||||
interface SerializerInterface
|
||||
{
|
||||
/**
|
||||
* create a serialized unified request protocol message
|
||||
*
|
||||
* This is the *one* method most redis client libraries will likely want to
|
||||
* use in order to send a serialized message (a request) over the* wire to
|
||||
* your redis server instance.
|
||||
*
|
||||
* This method should be used in favor of constructing a request model and
|
||||
* then serializing it. While its effect might be equivalent, this method
|
||||
* is likely to (i.e. it /could/) provide a faster implementation.
|
||||
*
|
||||
* @param string $command
|
||||
* @param array $args
|
||||
* @return string
|
||||
* @see self::createRequestMessage()
|
||||
*/
|
||||
public function getRequestMessage($command, array $args = array());
|
||||
|
||||
/**
|
||||
* create a unified request protocol message model
|
||||
*
|
||||
* @param string $command
|
||||
* @param array $args
|
||||
* @return MultiBulkReply
|
||||
*/
|
||||
public function createRequestModel($command, array $args = array());
|
||||
|
||||
/**
|
||||
* create a serialized unified protocol reply message
|
||||
*
|
||||
* This is most useful for a redis server implementation which needs to
|
||||
* process client requests and send resulting reply messages.
|
||||
*
|
||||
* This method does its best to guess to right reply type and then returns
|
||||
* a serialized version of the message. It follows the "redis to lua
|
||||
* conversion table" (see link) which means most basic types can be mapped
|
||||
* as is.
|
||||
*
|
||||
* This method should be used in favor of constructing a reply model and
|
||||
* then serializing it. While its effect might be equivalent, this method
|
||||
* is likely to (i.e. it /could/) provide a faster implementation.
|
||||
*
|
||||
* Note however, you may still want to explicitly create a nested reply
|
||||
* model hierarchy if you need more control over the serialized message. For
|
||||
* instance, a null value will always be returned as a Null-Bulk-Reply, so
|
||||
* there's no way to express a Null-Multi-Bulk-Reply, unless you construct
|
||||
* it explicitly.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @return string
|
||||
* @see self::createReplyModel()
|
||||
* @link http://redis.io/commands/eval
|
||||
*/
|
||||
public function getReplyMessage($data);
|
||||
|
||||
/**
|
||||
* create response message by determining datatype from given argument
|
||||
*
|
||||
* @param mixed $data
|
||||
* @return ModelInterface
|
||||
*/
|
||||
public function createReplyModel($data);
|
||||
|
||||
public function getBulkMessage($data);
|
||||
|
||||
public function getErrorMessage($data);
|
||||
|
||||
public function getIntegerMessage($data);
|
||||
|
||||
public function getMultiBulkMessage($data);
|
||||
|
||||
public function getStatusMessage($data);
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
github: clue
|
||||
custom: https://clue.engineering/support
|
||||
Vendored
+287
@@ -0,0 +1,287 @@
|
||||
# Changelog
|
||||
|
||||
## 2.8.0 (2025-01-03)
|
||||
|
||||
This is a compatibility release that contains backported features from the `3.x` branch.
|
||||
Once v3 is released, it will be the way forward for this project.
|
||||
|
||||
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable types.
|
||||
(#165 and #170 by @clue)
|
||||
|
||||
## 2.7.0 (2024-01-05)
|
||||
|
||||
This is a compatibility release that contains backported features from the `3.x` branch.
|
||||
Once v3 is released, it will be the way forward for this project.
|
||||
|
||||
* Feature: Forward compatibility with Promise v3.
|
||||
(#152 by @clue)
|
||||
|
||||
* Feature: Full PHP 8.3 compatibility and update test suite.
|
||||
(#151 by @clue)
|
||||
|
||||
## 2.6.0 (2022-05-09)
|
||||
|
||||
* Feature: Support PHP 8.1 release.
|
||||
(#119 by @clue)
|
||||
|
||||
* Improve documentation and CI configuration.
|
||||
(#123 and #125 by @SimonFrings)
|
||||
|
||||
## 2.5.0 (2021-08-31)
|
||||
|
||||
* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop) and new Socket API.
|
||||
(#114 and #115 by @SimonFrings)
|
||||
|
||||
```php
|
||||
// old (still supported)
|
||||
$factory = new Clue\React\Redis\Factory($loop);
|
||||
|
||||
// new (using default loop)
|
||||
$factory = new Clue\React\Redis\Factory();
|
||||
```
|
||||
|
||||
* Feature: Improve error reporting, include Redis URI and socket error codes in all connection errors.
|
||||
(#116 by @clue)
|
||||
|
||||
* Documentation improvements and updated examples.
|
||||
(#117 by @clue, #112 by @Nyholm and #113 by @PaulRotmann)
|
||||
|
||||
* Improve test suite and use GitHub actions for continuous integration (CI).
|
||||
(#111 by @SimonFrings)
|
||||
|
||||
## 2.4.0 (2020-09-25)
|
||||
|
||||
* Fix: Fix dangling timer when lazy connection closes with pending commands.
|
||||
(#105 by @clue)
|
||||
|
||||
* Improve test suite and add `.gitattributes` to exclude dev files from exports.
|
||||
Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix.
|
||||
(#96 and #97 by @clue and #99, #101 and #104 by @SimonFrings)
|
||||
|
||||
## 2.3.0 (2019-03-11)
|
||||
|
||||
* Feature: Add new `createLazyClient()` method to connect only on demand and
|
||||
implement "idle" timeout to close underlying connection when unused.
|
||||
(#87 and #88 by @clue and #82 by @WyriHaximus)
|
||||
|
||||
```php
|
||||
$client = $factory->createLazyClient('redis://localhost:6379');
|
||||
|
||||
$client->incr('hello');
|
||||
$client->end();
|
||||
```
|
||||
|
||||
* Feature: Support cancellation of pending connection attempts.
|
||||
(#85 by @clue)
|
||||
|
||||
```php
|
||||
$promise = $factory->createClient($redisUri);
|
||||
|
||||
$loop->addTimer(3.0, function () use ($promise) {
|
||||
$promise->cancel();
|
||||
});
|
||||
```
|
||||
|
||||
* Feature: Support connection timeouts.
|
||||
(#86 by @clue)
|
||||
|
||||
```php
|
||||
$factory->createClient('localhost?timeout=0.5');
|
||||
```
|
||||
|
||||
* Feature: Improve Exception messages for connection issues.
|
||||
(#89 by @clue)
|
||||
|
||||
```php
|
||||
$factory->createClient('redis://localhost:6379')->then(
|
||||
function (Client $client) {
|
||||
// client connected (and authenticated)
|
||||
},
|
||||
function (Exception $e) {
|
||||
// an error occurred while trying to connect (or authenticate) client
|
||||
echo $e->getMessage() . PHP_EOL;
|
||||
if ($e->getPrevious()) {
|
||||
echo $e->getPrevious()->getMessage() . PHP_EOL;
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
* Improve test suite structure and add forward compatibility with PHPUnit 7 and PHPUnit 6
|
||||
and test against PHP 7.1, 7.2, and 7.3 on TravisCI.
|
||||
(#83 by @WyriHaximus and #84 by @clue)
|
||||
|
||||
* Improve documentation and update project homepage.
|
||||
(#81 and #90 by @clue)
|
||||
|
||||
## 2.2.0 (2018-01-24)
|
||||
|
||||
* Feature: Support communication over Unix domain sockets (UDS)
|
||||
(#70 by @clue)
|
||||
|
||||
```php
|
||||
// new: now supports redis over Unix domain sockets (UDS)
|
||||
$factory->createClient('redis+unix:///tmp/redis.sock');
|
||||
```
|
||||
|
||||
## 2.1.0 (2017-09-25)
|
||||
|
||||
* Feature: Update Socket dependency to support hosts file on all platforms
|
||||
(#66 by @clue)
|
||||
|
||||
This means that connecting to hosts such as `localhost` (and for example
|
||||
those used for Docker containers) will now work as expected across all
|
||||
platforms with no changes required:
|
||||
|
||||
```php
|
||||
$factory->createClient('localhost');
|
||||
```
|
||||
|
||||
## 2.0.0 (2017-09-20)
|
||||
|
||||
A major compatibility release to update this package to support all latest
|
||||
ReactPHP components!
|
||||
|
||||
This update involves a minor BC break due to dropped support for legacy
|
||||
versions. We've tried hard to avoid BC breaks where possible and minimize impact
|
||||
otherwise. We expect that most consumers of this package will actually not be
|
||||
affected by any BC breaks, see below for more details.
|
||||
|
||||
* BC break: Remove all deprecated APIs, default to `redis://` URI scheme
|
||||
and drop legacy SocketClient in favor of new Socket component.
|
||||
(#61 by @clue)
|
||||
|
||||
> All of this affects the `Factory` only, which is mostly considered
|
||||
"advanced usage". If you're affected by this BC break, then it's
|
||||
recommended to first update to the intermediary v1.2.0 release, which
|
||||
allows you to use the `redis://` URI scheme and a standard
|
||||
`ConnectorInterface` and then update to this version without causing a
|
||||
BC break.
|
||||
|
||||
* BC break: Remove uneeded `data` event and support for advanced `MONITOR`
|
||||
command for performance and consistency reasons and
|
||||
remove underdocumented `isBusy()` method.
|
||||
(#62, #63 and #64 by @clue)
|
||||
|
||||
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8 and EventLoop v1.0 and Evenement v3
|
||||
(#65 by @clue)
|
||||
|
||||
## 1.2.0 (2017-09-19)
|
||||
|
||||
* Feature: Support `redis[s]://` URI scheme and deprecate legacy URIs
|
||||
(#60 by @clue)
|
||||
|
||||
```php
|
||||
$factory->createClient('redis://:secret@localhost:6379/4');
|
||||
$factory->createClient('redis://localhost:6379?password=secret&db=4');
|
||||
```
|
||||
|
||||
* Feature: Factory accepts Connector from Socket and deprecate legacy SocketClient
|
||||
(#59 by @clue)
|
||||
|
||||
If you need custom connector settings (DNS resolution, TLS parameters, timeouts,
|
||||
proxy servers etc.), you can explicitly pass a custom instance of the
|
||||
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface):
|
||||
|
||||
```php
|
||||
$connector = new \React\Socket\Connector($loop, array(
|
||||
'dns' => '127.0.0.1',
|
||||
'tcp' => array(
|
||||
'bindto' => '192.168.10.1:0'
|
||||
),
|
||||
'tls' => array(
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false
|
||||
)
|
||||
));
|
||||
|
||||
$factory = new Factory($loop, $connector);
|
||||
```
|
||||
|
||||
## 1.1.0 (2017-09-18)
|
||||
|
||||
* Feature: Update SocketClient dependency to latest version
|
||||
(#58 by @clue)
|
||||
|
||||
* Improve test suite by adding PHPUnit to require-dev,
|
||||
fix HHVM build for now again and ignore future HHVM build errors,
|
||||
lock Travis distro so new defaults will not break the build and
|
||||
skip functional integration tests by default
|
||||
(#52, #53, #56 and #57 by @clue)
|
||||
|
||||
## 1.0.0 (2016-05-20)
|
||||
|
||||
* First stable release, now following SemVer
|
||||
|
||||
* BC break: Consistent public API, mark internal APIs as such
|
||||
(#38 by @clue)
|
||||
|
||||
```php
|
||||
// old
|
||||
$client->on('data', function (MessageInterface $message, Client $client) {
|
||||
// process an incoming message (raw message object)
|
||||
});
|
||||
|
||||
// new
|
||||
$client->on('data', function (MessageInterface $message) use ($client) {
|
||||
// process an incoming message (raw message object)
|
||||
});
|
||||
```
|
||||
|
||||
> Contains no other changes, so it's actually fully compatible with the v0.5.2 release.
|
||||
|
||||
## 0.5.2 (2016-05-20)
|
||||
|
||||
* Fix: Do not send empty SELECT statement when no database has been given
|
||||
(#35, #36 by @clue)
|
||||
|
||||
* Improve documentation, update dependencies and add first class support for PHP 7
|
||||
|
||||
## 0.5.1 (2015-01-12)
|
||||
|
||||
* Fix: Fix compatibility with react/promise v2.0 for monitor and PubSub commands.
|
||||
(#28)
|
||||
|
||||
## 0.5.0 (2014-11-12)
|
||||
|
||||
* Feature: Support PubSub commands (P)(UN)SUBSCRIBE and watching for "message",
|
||||
"subscribe" and "unsubscribe" events
|
||||
(#24)
|
||||
|
||||
* Feature: Support MONITOR command and watching for "monitor" events
|
||||
(#23)
|
||||
|
||||
* Improve documentation, update locked dependencies and add first class support for HHVM
|
||||
(#25, #26 and others)
|
||||
|
||||
## 0.4.0 (2014-08-25)
|
||||
|
||||
* BC break: The `Client` class has been renamed to `StreamingClient`.
|
||||
Added new `Client` interface.
|
||||
(#18 and #19)
|
||||
|
||||
* BC break: Rename `message` event to `data`.
|
||||
(#21)
|
||||
|
||||
* BC break: The `Factory` now accepts a `LoopInterface` as first argument.
|
||||
(#22)
|
||||
|
||||
* Fix: The `close` event will be emitted once when invoking the `Client::close()`
|
||||
method or when the underlying stream closes.
|
||||
(#20)
|
||||
|
||||
* Refactored code, improved testability, extended test suite and better code coverage.
|
||||
(#11, #18 and #20)
|
||||
|
||||
> Note: This is an intermediary release to ease upgrading to the imminent v0.5 release.
|
||||
|
||||
## 0.3.0 (2014-05-31)
|
||||
|
||||
* First tagged release
|
||||
|
||||
> Note: Starts at v0.3 because previous versions were not tagged. Leaving some
|
||||
> room in case they're going to be needed in the future.
|
||||
|
||||
## 0.0.0 (2013-07-05)
|
||||
|
||||
* Initial concept
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Christian Lück
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+660
@@ -0,0 +1,660 @@
|
||||
# clue/reactphp-redis
|
||||
|
||||
[](https://github.com/clue/reactphp-redis/actions)
|
||||
[](https://packagist.org/packages/clue/redis-react)
|
||||
|
||||
Async [Redis](https://redis.io/) client implementation, built on top of [ReactPHP](https://reactphp.org/).
|
||||
|
||||
[Redis](https://redis.io/) is an open source, advanced, in-memory key-value database.
|
||||
It offers a set of simple, atomic operations in order to work with its primitive data types.
|
||||
Its lightweight design and fast operation makes it an ideal candidate for modern application stacks.
|
||||
This library provides you a simple API to work with your Redis database from within PHP.
|
||||
It enables you to set and query its data or use its PubSub topics to react to incoming events.
|
||||
|
||||
* **Async execution of Commands** -
|
||||
Send any number of commands to Redis in parallel (automatic pipeline) and
|
||||
process their responses as soon as results come in.
|
||||
The Promise-based design provides a *sane* interface to working with async responses.
|
||||
* **Event-driven core** -
|
||||
Register your event handler callbacks to react to incoming events, such as an incoming PubSub message event.
|
||||
* **Lightweight, SOLID design** -
|
||||
Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough)
|
||||
and does not get in your way.
|
||||
Future or custom commands and events require no changes to be supported.
|
||||
* **Good test coverage** -
|
||||
Comes with an automated tests suite and is regularly tested against versions as old as Redis v2.6 and newer.
|
||||
|
||||
**Table of Contents**
|
||||
|
||||
* [Support us](#support-us)
|
||||
* [Quickstart example](#quickstart-example)
|
||||
* [Usage](#usage)
|
||||
* [Commands](#commands)
|
||||
* [Promises](#promises)
|
||||
* [PubSub](#pubsub)
|
||||
* [API](#api)
|
||||
* [Factory](#factory)
|
||||
* [createClient()](#createclient)
|
||||
* [createLazyClient()](#createlazyclient)
|
||||
* [Client](#client)
|
||||
* [__call()](#__call)
|
||||
* [end()](#end)
|
||||
* [close()](#close)
|
||||
* [error event](#error-event)
|
||||
* [close event](#close-event)
|
||||
* [Install](#install)
|
||||
* [Tests](#tests)
|
||||
* [License](#license)
|
||||
|
||||
## Support us
|
||||
|
||||
We invest a lot of time developing, maintaining and updating our awesome
|
||||
open-source projects. You can help us sustain this high-quality of our work by
|
||||
[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get
|
||||
numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)
|
||||
for details.
|
||||
|
||||
Let's take these projects to the next level together! 🚀
|
||||
|
||||
## Quickstart example
|
||||
|
||||
Once [installed](#install), you can use the following code to connect to your
|
||||
local Redis server and send some requests:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$factory = new Clue\React\Redis\Factory();
|
||||
$redis = $factory->createLazyClient('localhost:6379');
|
||||
|
||||
$redis->set('greeting', 'Hello world');
|
||||
$redis->append('greeting', '!');
|
||||
|
||||
$redis->get('greeting')->then(function ($greeting) {
|
||||
// Hello world!
|
||||
echo $greeting . PHP_EOL;
|
||||
});
|
||||
|
||||
$redis->incr('invocation')->then(function ($n) {
|
||||
echo 'This is invocation #' . $n . PHP_EOL;
|
||||
});
|
||||
|
||||
// end connection once all pending requests have been resolved
|
||||
$redis->end();
|
||||
```
|
||||
|
||||
See also the [examples](examples).
|
||||
|
||||
## Usage
|
||||
|
||||
### Commands
|
||||
|
||||
Most importantly, this project provides a [`Client`](#client) instance that
|
||||
can be used to invoke all [Redis commands](https://redis.io/commands) (such as `GET`, `SET`, etc.).
|
||||
|
||||
```php
|
||||
$redis->get($key);
|
||||
$redis->set($key, $value);
|
||||
$redis->exists($key);
|
||||
$redis->expire($key, $seconds);
|
||||
$redis->mget($key1, $key2, $key3);
|
||||
|
||||
$redis->multi();
|
||||
$redis->exec();
|
||||
|
||||
$redis->publish($channel, $payload);
|
||||
$redis->subscribe($channel);
|
||||
|
||||
$redis->ping();
|
||||
$redis->select($database);
|
||||
|
||||
// many more…
|
||||
```
|
||||
|
||||
Each method call matches the respective [Redis command](https://redis.io/commands).
|
||||
For example, the `$redis->get()` method will invoke the [`GET` command](https://redis.io/commands/get).
|
||||
|
||||
All [Redis commands](https://redis.io/commands) are automatically available as
|
||||
public methods via the magic [`__call()` method](#__call).
|
||||
Listing all available commands is out of scope here, please refer to the
|
||||
[Redis command reference](https://redis.io/commands).
|
||||
|
||||
Any arguments passed to the method call will be forwarded as command arguments.
|
||||
For example, the `$redis->set('name', 'Alice')` call will perform the equivalent of a
|
||||
`SET name Alice` command. It's safe to pass integer arguments where applicable (for
|
||||
example `$redis->expire($key, 60)`), but internally Redis requires all arguments to
|
||||
always be coerced to string values.
|
||||
|
||||
Each of these commands supports async operation and returns a [Promise](#promises)
|
||||
that eventually *fulfills* with its *results* on success or *rejects* with an
|
||||
`Exception` on error. See also the following section about [promises](#promises)
|
||||
for more details.
|
||||
|
||||
### Promises
|
||||
|
||||
Sending commands is async (non-blocking), so you can actually send multiple
|
||||
commands in parallel.
|
||||
Redis will respond to each command request with a response message, pending
|
||||
commands will be pipelined automatically.
|
||||
|
||||
Sending commands uses a [Promise](https://github.com/reactphp/promise)-based
|
||||
interface that makes it easy to react to when a command is completed
|
||||
(i.e. either successfully fulfilled or rejected with an error):
|
||||
|
||||
```php
|
||||
$redis->get($key)->then(function (?string $value) {
|
||||
var_dump($value);
|
||||
}, function (Exception $e) {
|
||||
echo 'Error: ' . $e->getMessage() . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
### PubSub
|
||||
|
||||
This library is commonly used to efficiently transport messages using Redis'
|
||||
[Pub/Sub](https://redis.io/topics/pubsub) (Publish/Subscribe) channels. For
|
||||
instance, this can be used to distribute single messages to a larger number
|
||||
of subscribers (think horizontal scaling for chat-like applications) or as an
|
||||
efficient message transport in distributed systems (microservice architecture).
|
||||
|
||||
The [`PUBLISH` command](https://redis.io/commands/publish) can be used to
|
||||
send a message to all clients currently subscribed to a given channel:
|
||||
|
||||
```php
|
||||
$channel = 'user';
|
||||
$message = json_encode(array('id' => 10));
|
||||
$redis->publish($channel, $message);
|
||||
```
|
||||
|
||||
The [`SUBSCRIBE` command](https://redis.io/commands/subscribe) can be used to
|
||||
subscribe to a channel and then receive incoming PubSub `message` events:
|
||||
|
||||
```php
|
||||
$channel = 'user';
|
||||
$redis->subscribe($channel);
|
||||
|
||||
$redis->on('message', function ($channel, $payload) {
|
||||
// pubsub message received on given $channel
|
||||
var_dump($channel, json_decode($payload));
|
||||
});
|
||||
```
|
||||
|
||||
Likewise, you can use the same client connection to subscribe to multiple
|
||||
channels by simply executing this command multiple times:
|
||||
|
||||
```php
|
||||
$redis->subscribe('user.register');
|
||||
$redis->subscribe('user.join');
|
||||
$redis->subscribe('user.leave');
|
||||
```
|
||||
|
||||
Similarly, the [`PSUBSCRIBE` command](https://redis.io/commands/psubscribe) can
|
||||
be used to subscribe to all channels matching a given pattern and then receive
|
||||
all incoming PubSub messages with the `pmessage` event:
|
||||
|
||||
|
||||
```php
|
||||
$pattern = 'user.*';
|
||||
$redis->psubscribe($pattern);
|
||||
|
||||
$redis->on('pmessage', function ($pattern, $channel, $payload) {
|
||||
// pubsub message received matching given $pattern
|
||||
var_dump($channel, json_decode($payload));
|
||||
});
|
||||
```
|
||||
|
||||
Once you're in a subscribed state, Redis no longer allows executing any other
|
||||
commands on the same client connection. This is commonly worked around by simply
|
||||
creating a second client connection and dedicating one client connection solely
|
||||
for PubSub subscriptions and the other for all other commands.
|
||||
|
||||
The [`UNSUBSCRIBE` command](https://redis.io/commands/unsubscribe) and
|
||||
[`PUNSUBSCRIBE` command](https://redis.io/commands/punsubscribe) can be used to
|
||||
unsubscribe from active subscriptions if you're no longer interested in
|
||||
receiving any further events for the given channel and pattern subscriptions
|
||||
respectively:
|
||||
|
||||
```php
|
||||
$redis->subscribe('user');
|
||||
|
||||
Loop::addTimer(60.0, function () use ($redis) {
|
||||
$redis->unsubscribe('user');
|
||||
});
|
||||
```
|
||||
|
||||
Likewise, once you've unsubscribed the last channel and pattern, the client
|
||||
connection is no longer in a subscribed state and you can issue any other
|
||||
command over this client connection again.
|
||||
|
||||
Each of the above methods follows normal request-response semantics and return
|
||||
a [`Promise`](#promises) to await successful subscriptions. Note that while
|
||||
Redis allows a variable number of arguments for each of these commands, this
|
||||
library is currently limited to single arguments for each of these methods in
|
||||
order to match exactly one response to each command request. As an alternative,
|
||||
the methods can simply be invoked multiple times with one argument each.
|
||||
|
||||
Additionally, can listen for the following PubSub events to get notifications
|
||||
about subscribed/unsubscribed channels and patterns:
|
||||
|
||||
```php
|
||||
$redis->on('subscribe', function ($channel, $total) {
|
||||
// subscribed to given $channel
|
||||
});
|
||||
$redis->on('psubscribe', function ($pattern, $total) {
|
||||
// subscribed to matching given $pattern
|
||||
});
|
||||
$redis->on('unsubscribe', function ($channel, $total) {
|
||||
// unsubscribed from given $channel
|
||||
});
|
||||
$redis->on('punsubscribe', function ($pattern, $total) {
|
||||
// unsubscribed from matching given $pattern
|
||||
});
|
||||
```
|
||||
|
||||
When using the [`createLazyClient()`](#createlazyclient) method, the `unsubscribe`
|
||||
and `punsubscribe` events will be invoked automatically when the underlying
|
||||
connection is lost. This gives you control over re-subscribing to the channels
|
||||
and patterns as appropriate.
|
||||
|
||||
## API
|
||||
|
||||
### Factory
|
||||
|
||||
The `Factory` is responsible for creating your [`Client`](#client) instance.
|
||||
|
||||
```php
|
||||
$factory = new Clue\React\Redis\Factory();
|
||||
```
|
||||
|
||||
This class takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
pass the event loop instance to use for this object. You can use a `null` value
|
||||
here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
|
||||
This value SHOULD NOT be given unless you're sure you want to explicitly use a
|
||||
given event loop instance.
|
||||
|
||||
If you need custom connector settings (DNS resolution, TLS parameters, timeouts,
|
||||
proxy servers etc.), you can explicitly pass a custom instance of the
|
||||
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface):
|
||||
|
||||
```php
|
||||
$connector = new React\Socket\Connector(array(
|
||||
'dns' => '127.0.0.1',
|
||||
'tcp' => array(
|
||||
'bindto' => '192.168.10.1:0'
|
||||
),
|
||||
'tls' => array(
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false
|
||||
)
|
||||
));
|
||||
|
||||
$factory = new Clue\React\Redis\Factory(null, $connector);
|
||||
```
|
||||
|
||||
#### createClient()
|
||||
|
||||
The `createClient(string $uri): PromiseInterface<Client,Exception>` method can be used to
|
||||
create a new [`Client`](#client).
|
||||
|
||||
It helps with establishing a plain TCP/IP or secure TLS connection to Redis
|
||||
and optionally authenticating (AUTH) and selecting the right database (SELECT).
|
||||
|
||||
```php
|
||||
$factory->createClient('localhost:6379')->then(
|
||||
function (Client $redis) {
|
||||
// client connected (and authenticated)
|
||||
},
|
||||
function (Exception $e) {
|
||||
// an error occurred while trying to connect (or authenticate) client
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
The method returns a [Promise](https://github.com/reactphp/promise) that
|
||||
will resolve with a [`Client`](#client)
|
||||
instance on success or will reject with an `Exception` if the URL is
|
||||
invalid or the connection or authentication fails.
|
||||
|
||||
The returned Promise is implemented in such a way that it can be
|
||||
cancelled when it is still pending. Cancelling a pending promise will
|
||||
reject its value with an Exception and will cancel the underlying TCP/IP
|
||||
connection attempt and/or Redis authentication.
|
||||
|
||||
```php
|
||||
$promise = $factory->createClient($uri);
|
||||
|
||||
Loop::addTimer(3.0, function () use ($promise) {
|
||||
$promise->cancel();
|
||||
});
|
||||
```
|
||||
|
||||
The `$redisUri` can be given in the
|
||||
[standard](https://www.iana.org/assignments/uri-schemes/prov/redis) form
|
||||
`[redis[s]://][:auth@]host[:port][/db]`.
|
||||
You can omit the URI scheme and port if you're connecting to the default port 6379:
|
||||
|
||||
```php
|
||||
// both are equivalent due to defaults being applied
|
||||
$factory->createClient('localhost');
|
||||
$factory->createClient('redis://localhost:6379');
|
||||
```
|
||||
|
||||
Redis supports password-based authentication (`AUTH` command). Note that Redis'
|
||||
authentication mechanism does not employ a username, so you can pass the
|
||||
password `h@llo` URL-encoded (percent-encoded) as part of the URI like this:
|
||||
|
||||
```php
|
||||
// all forms are equivalent
|
||||
$factory->createClient('redis://:h%40llo@localhost');
|
||||
$factory->createClient('redis://ignored:h%40llo@localhost');
|
||||
$factory->createClient('redis://localhost?password=h%40llo');
|
||||
```
|
||||
|
||||
You can optionally include a path that will be used to select (SELECT command) the right database:
|
||||
|
||||
```php
|
||||
// both forms are equivalent
|
||||
$factory->createClient('redis://localhost/2');
|
||||
$factory->createClient('redis://localhost?db=2');
|
||||
```
|
||||
|
||||
You can use the [standard](https://www.iana.org/assignments/uri-schemes/prov/rediss)
|
||||
`rediss://` URI scheme if you're using a secure TLS proxy in front of Redis:
|
||||
|
||||
```php
|
||||
$factory->createClient('rediss://redis.example.com:6340');
|
||||
```
|
||||
|
||||
You can use the `redis+unix://` URI scheme if your Redis instance is listening
|
||||
on a Unix domain socket (UDS) path:
|
||||
|
||||
```php
|
||||
$factory->createClient('redis+unix:///tmp/redis.sock');
|
||||
|
||||
// the URI MAY contain `password` and `db` query parameters as seen above
|
||||
$factory->createClient('redis+unix:///tmp/redis.sock?password=secret&db=2');
|
||||
|
||||
// the URI MAY contain authentication details as userinfo as seen above
|
||||
// should be used with care, also note that database can not be passed as path
|
||||
$factory->createClient('redis+unix://:secret@/tmp/redis.sock');
|
||||
```
|
||||
|
||||
This method respects PHP's `default_socket_timeout` setting (default 60s)
|
||||
as a timeout for establishing the connection and waiting for successful
|
||||
authentication. You can explicitly pass a custom timeout value in seconds
|
||||
(or use a negative number to not apply a timeout) like this:
|
||||
|
||||
```php
|
||||
$factory->createClient('localhost?timeout=0.5');
|
||||
```
|
||||
|
||||
#### createLazyClient()
|
||||
|
||||
The `createLazyClient(string $uri): Client` method can be used to
|
||||
create a new [`Client`](#client).
|
||||
|
||||
It helps with establishing a plain TCP/IP or secure TLS connection to Redis
|
||||
and optionally authenticating (AUTH) and selecting the right database (SELECT).
|
||||
|
||||
```php
|
||||
$redis = $factory->createLazyClient('localhost:6379');
|
||||
|
||||
$redis->incr('hello');
|
||||
$redis->end();
|
||||
```
|
||||
|
||||
This method immediately returns a "virtual" connection implementing the
|
||||
[`Client`](#client) that can be used to interface with your Redis database.
|
||||
Internally, it lazily creates the underlying database connection only on
|
||||
demand once the first request is invoked on this instance and will queue
|
||||
all outstanding requests until the underlying connection is ready.
|
||||
Additionally, it will only keep this underlying connection in an "idle" state
|
||||
for 60s by default and will automatically close the underlying connection when
|
||||
it is no longer needed.
|
||||
|
||||
From a consumer side this means that you can start sending commands to the
|
||||
database right away while the underlying connection may still be
|
||||
outstanding. Because creating this underlying connection may take some
|
||||
time, it will enqueue all oustanding commands and will ensure that all
|
||||
commands will be executed in correct order once the connection is ready.
|
||||
In other words, this "virtual" connection behaves just like a "real"
|
||||
connection as described in the `Client` interface and frees you from having
|
||||
to deal with its async resolution.
|
||||
|
||||
If the underlying database connection fails, it will reject all
|
||||
outstanding commands and will return to the initial "idle" state. This
|
||||
means that you can keep sending additional commands at a later time which
|
||||
will again try to open a new underlying connection. Note that this may
|
||||
require special care if you're using transactions (`MULTI`/`EXEC`) that are kept
|
||||
open for longer than the idle period.
|
||||
|
||||
While using PubSub channels (see `SUBSCRIBE` and `PSUBSCRIBE` commands), this client
|
||||
will never reach an "idle" state and will keep pending forever (or until the
|
||||
underlying database connection is lost). Additionally, if the underlying
|
||||
database connection drops, it will automatically send the appropriate `unsubscribe`
|
||||
and `punsubscribe` events for all currently active channel and pattern subscriptions.
|
||||
This allows you to react to these events and restore your subscriptions by
|
||||
creating a new underlying connection repeating the above commands again.
|
||||
|
||||
Note that creating the underlying connection will be deferred until the
|
||||
first request is invoked. Accordingly, any eventual connection issues
|
||||
will be detected once this instance is first used. You can use the
|
||||
`end()` method to ensure that the "virtual" connection will be soft-closed
|
||||
and no further commands can be enqueued. Similarly, calling `end()` on
|
||||
this instance when not currently connected will succeed immediately and
|
||||
will not have to wait for an actual underlying connection.
|
||||
|
||||
Depending on your particular use case, you may prefer this method or the
|
||||
underlying `createClient()` which resolves with a promise. For many
|
||||
simple use cases it may be easier to create a lazy connection.
|
||||
|
||||
The `$redisUri` can be given in the
|
||||
[standard](https://www.iana.org/assignments/uri-schemes/prov/redis) form
|
||||
`[redis[s]://][:auth@]host[:port][/db]`.
|
||||
You can omit the URI scheme and port if you're connecting to the default port 6379:
|
||||
|
||||
```php
|
||||
// both are equivalent due to defaults being applied
|
||||
$factory->createLazyClient('localhost');
|
||||
$factory->createLazyClient('redis://localhost:6379');
|
||||
```
|
||||
|
||||
Redis supports password-based authentication (`AUTH` command). Note that Redis'
|
||||
authentication mechanism does not employ a username, so you can pass the
|
||||
password `h@llo` URL-encoded (percent-encoded) as part of the URI like this:
|
||||
|
||||
```php
|
||||
// all forms are equivalent
|
||||
$factory->createLazyClient('redis://:h%40llo@localhost');
|
||||
$factory->createLazyClient('redis://ignored:h%40llo@localhost');
|
||||
$factory->createLazyClient('redis://localhost?password=h%40llo');
|
||||
```
|
||||
|
||||
You can optionally include a path that will be used to select (SELECT command) the right database:
|
||||
|
||||
```php
|
||||
// both forms are equivalent
|
||||
$factory->createLazyClient('redis://localhost/2');
|
||||
$factory->createLazyClient('redis://localhost?db=2');
|
||||
```
|
||||
|
||||
You can use the [standard](https://www.iana.org/assignments/uri-schemes/prov/rediss)
|
||||
`rediss://` URI scheme if you're using a secure TLS proxy in front of Redis:
|
||||
|
||||
```php
|
||||
$factory->createLazyClient('rediss://redis.example.com:6340');
|
||||
```
|
||||
|
||||
You can use the `redis+unix://` URI scheme if your Redis instance is listening
|
||||
on a Unix domain socket (UDS) path:
|
||||
|
||||
```php
|
||||
$factory->createLazyClient('redis+unix:///tmp/redis.sock');
|
||||
|
||||
// the URI MAY contain `password` and `db` query parameters as seen above
|
||||
$factory->createLazyClient('redis+unix:///tmp/redis.sock?password=secret&db=2');
|
||||
|
||||
// the URI MAY contain authentication details as userinfo as seen above
|
||||
// should be used with care, also note that database can not be passed as path
|
||||
$factory->createLazyClient('redis+unix://:secret@/tmp/redis.sock');
|
||||
```
|
||||
|
||||
This method respects PHP's `default_socket_timeout` setting (default 60s)
|
||||
as a timeout for establishing the underlying connection and waiting for
|
||||
successful authentication. You can explicitly pass a custom timeout value
|
||||
in seconds (or use a negative number to not apply a timeout) like this:
|
||||
|
||||
```php
|
||||
$factory->createLazyClient('localhost?timeout=0.5');
|
||||
```
|
||||
|
||||
By default, this method will keep "idle" connections open for 60s and will
|
||||
then end the underlying connection. The next request after an "idle"
|
||||
connection ended will automatically create a new underlying connection.
|
||||
This ensure you always get a "fresh" connection and as such should not be
|
||||
confused with a "keepalive" or "heartbeat" mechanism, as this will not
|
||||
actively try to probe the connection. You can explicitly pass a custom
|
||||
idle timeout value in seconds (or use a negative number to not apply a
|
||||
timeout) like this:
|
||||
|
||||
```php
|
||||
$factory->createLazyClient('localhost?idle=0.1');
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
The `Client` is responsible for exchanging messages with Redis
|
||||
and keeps track of pending commands.
|
||||
|
||||
Besides defining a few methods, this interface also implements the
|
||||
`EventEmitterInterface` which allows you to react to certain events as documented below.
|
||||
|
||||
#### __call()
|
||||
|
||||
The `__call(string $name, string[] $args): PromiseInterface<mixed,Exception>` method can be used to
|
||||
invoke the given command.
|
||||
|
||||
This is a magic method that will be invoked when calling any Redis command on this instance.
|
||||
Each method call matches the respective [Redis command](https://redis.io/commands).
|
||||
For example, the `$redis->get()` method will invoke the [`GET` command](https://redis.io/commands/get).
|
||||
|
||||
```php
|
||||
$redis->get($key)->then(function (?string $value) {
|
||||
var_dump($value);
|
||||
}, function (Exception $e) {
|
||||
echo 'Error: ' . $e->getMessage() . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
All [Redis commands](https://redis.io/commands) are automatically available as
|
||||
public methods via this magic `__call()` method.
|
||||
Listing all available commands is out of scope here, please refer to the
|
||||
[Redis command reference](https://redis.io/commands).
|
||||
|
||||
Any arguments passed to the method call will be forwarded as command arguments.
|
||||
For example, the `$redis->set('name', 'Alice')` call will perform the equivalent of a
|
||||
`SET name Alice` command. It's safe to pass integer arguments where applicable (for
|
||||
example `$redis->expire($key, 60)`), but internally Redis requires all arguments to
|
||||
always be coerced to string values.
|
||||
|
||||
Each of these commands supports async operation and returns a [Promise](#promises)
|
||||
that eventually *fulfills* with its *results* on success or *rejects* with an
|
||||
`Exception` on error. See also [promises](#promises) for more details.
|
||||
|
||||
#### end()
|
||||
|
||||
The `end():void` method can be used to
|
||||
soft-close the Redis connection once all pending commands are completed.
|
||||
|
||||
#### close()
|
||||
|
||||
The `close():void` method can be used to
|
||||
force-close the Redis connection and reject all pending commands.
|
||||
|
||||
#### error event
|
||||
|
||||
The `error` event will be emitted once a fatal error occurs, such as
|
||||
when the client connection is lost or is invalid.
|
||||
The event receives a single `Exception` argument for the error instance.
|
||||
|
||||
```php
|
||||
$redis->on('error', function (Exception $e) {
|
||||
echo 'Error: ' . $e->getMessage() . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
This event will only be triggered for fatal errors and will be followed
|
||||
by closing the client connection. It is not to be confused with "soft"
|
||||
errors caused by invalid commands.
|
||||
|
||||
#### close event
|
||||
|
||||
The `close` event will be emitted once the client connection closes (terminates).
|
||||
|
||||
```php
|
||||
$redis->on('close', function () {
|
||||
echo 'Connection closed' . PHP_EOL;
|
||||
});
|
||||
```
|
||||
|
||||
See also the [`close()`](#close) method.
|
||||
|
||||
## Install
|
||||
|
||||
The recommended way to install this library is [through Composer](https://getcomposer.org/).
|
||||
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
|
||||
|
||||
This project follows [SemVer](https://semver.org/).
|
||||
This will install the latest supported version:
|
||||
|
||||
```bash
|
||||
$ composer require clue/redis-react:^2.8
|
||||
```
|
||||
|
||||
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
|
||||
|
||||
This project aims to run on any platform and thus does not require any PHP
|
||||
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
|
||||
HHVM.
|
||||
It's *highly recommended to use the latest supported PHP version* for this project.
|
||||
|
||||
## Tests
|
||||
|
||||
To run the test suite, you first need to clone this repo and then install all
|
||||
dependencies [through Composer](https://getcomposer.org/):
|
||||
|
||||
```bash
|
||||
$ composer install
|
||||
```
|
||||
|
||||
To run the test suite, go to the project root and run:
|
||||
|
||||
```bash
|
||||
$ vendor/bin/phpunit
|
||||
```
|
||||
|
||||
The test suite contains both unit tests and functional integration tests.
|
||||
The functional tests require access to a running Redis server instance
|
||||
and will be skipped by default.
|
||||
|
||||
If you don't have access to a running Redis server, you can also use a temporary `Redis` Docker image:
|
||||
|
||||
```bash
|
||||
$ docker run --net=host redis
|
||||
```
|
||||
|
||||
To now run the functional tests, you need to supply *your* login
|
||||
details in an environment variable like this:
|
||||
|
||||
```bash
|
||||
$ REDIS_URI=localhost:6379 vendor/bin/phpunit
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is released under the permissive [MIT license](LICENSE).
|
||||
|
||||
> Did you know that I offer custom development services and issuing invoices for
|
||||
sponsorships of releases and for contributions? Contact me (@clue) for details.
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "clue/redis-react",
|
||||
"description": "Async Redis client implementation, built on top of ReactPHP.",
|
||||
"keywords": ["Redis", "database", "client", "async", "ReactPHP"],
|
||||
"homepage": "https://github.com/clue/reactphp-redis",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"clue/redis-protocol": "^0.3.2",
|
||||
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
|
||||
"react/event-loop": "^1.2",
|
||||
"react/promise": "^3.2 || ^2.0 || ^1.1",
|
||||
"react/promise-timer": "^1.11",
|
||||
"react/socket": "^1.16"
|
||||
},
|
||||
"require-dev": {
|
||||
"clue/block-react": "^1.5",
|
||||
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Clue\\React\\Redis\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Clue\\Tests\\React\\Redis\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\React\Redis;
|
||||
|
||||
use Evenement\EventEmitterInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
/**
|
||||
* Simple interface for executing redis commands
|
||||
*
|
||||
* @event error(Exception $error)
|
||||
* @event close()
|
||||
*
|
||||
* @event message($channel, $message)
|
||||
* @event subscribe($channel, $numberOfChannels)
|
||||
* @event unsubscribe($channel, $numberOfChannels)
|
||||
*
|
||||
* @event pmessage($pattern, $channel, $message)
|
||||
* @event psubscribe($channel, $numberOfChannels)
|
||||
* @event punsubscribe($channel, $numberOfChannels)
|
||||
*/
|
||||
interface Client extends EventEmitterInterface
|
||||
{
|
||||
/**
|
||||
* Invoke the given command and return a Promise that will be fulfilled when the request has been replied to
|
||||
*
|
||||
* This is a magic method that will be invoked when calling any redis
|
||||
* command on this instance.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string[] $args
|
||||
* @return PromiseInterface Promise<mixed,Exception>
|
||||
*/
|
||||
public function __call($name, $args);
|
||||
|
||||
/**
|
||||
* end connection once all pending requests have been replied to
|
||||
*
|
||||
* @return void
|
||||
* @uses self::close() once all replies have been received
|
||||
* @see self::close() for closing the connection immediately
|
||||
*/
|
||||
public function end();
|
||||
|
||||
/**
|
||||
* close connection immediately
|
||||
*
|
||||
* This will emit the "close" event.
|
||||
*
|
||||
* @return void
|
||||
* @see self::end() for closing the connection once the client is idle
|
||||
*/
|
||||
public function close();
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\React\Redis;
|
||||
|
||||
use Clue\Redis\Protocol\Factory as ProtocolFactory;
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\Timer\TimeoutException;
|
||||
use React\Socket\ConnectionInterface;
|
||||
use React\Socket\Connector;
|
||||
use React\Socket\ConnectorInterface;
|
||||
|
||||
class Factory
|
||||
{
|
||||
/** @var LoopInterface */
|
||||
private $loop;
|
||||
|
||||
/** @var ConnectorInterface */
|
||||
private $connector;
|
||||
|
||||
/** @var ProtocolFactory */
|
||||
private $protocol;
|
||||
|
||||
/**
|
||||
* @param ?LoopInterface $loop
|
||||
* @param ?ConnectorInterface $connector
|
||||
* @param ?ProtocolFactory $protocol (internal, should not usually be passed)
|
||||
*/
|
||||
public function __construct($loop = null, $connector = null, $protocol = null)
|
||||
{
|
||||
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #1 ($loop) expected null|React\EventLoop\LoopInterface');
|
||||
}
|
||||
if ($connector !== null && !$connector instanceof ConnectorInterface) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #2 ($connector) expected null|React\Socket\ConnectorInterface');
|
||||
}
|
||||
if ($protocol !== null && !$protocol instanceof ProtocolFactory) { // manual type check to support legacy PHP < 7.1
|
||||
throw new \InvalidArgumentException('Argument #3 ($protocol) expected null|Clue\Redis\Protocol\Factory');
|
||||
}
|
||||
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
$this->connector = $connector ?: new Connector(array(), $this->loop);
|
||||
$this->protocol = $protocol ?: new ProtocolFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Redis client connected to address of given redis instance
|
||||
*
|
||||
* @param string $uri Redis server URI to connect to
|
||||
* @return \React\Promise\PromiseInterface<Client,\Exception> Promise that will
|
||||
* be fulfilled with `Client` on success or rejects with `\Exception` on error.
|
||||
*/
|
||||
public function createClient($uri)
|
||||
{
|
||||
// support `redis+unix://` scheme for Unix domain socket (UDS) paths
|
||||
if (preg_match('/^(redis\+unix:\/\/(?:[^:]*:[^@]*@)?)(.+?)?$/', $uri, $match)) {
|
||||
$parts = parse_url($match[1] . 'localhost/' . $match[2]);
|
||||
} else {
|
||||
if (strpos($uri, '://') === false) {
|
||||
$uri = 'redis://' . $uri;
|
||||
}
|
||||
|
||||
$parts = parse_url($uri);
|
||||
}
|
||||
|
||||
$uri = preg_replace(array('/(:)[^:\/]*(@)/', '/([?&]password=).*?($|&)/'), '$1***$2', $uri);
|
||||
if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('redis', 'rediss', 'redis+unix'))) {
|
||||
return \React\Promise\reject(new \InvalidArgumentException(
|
||||
'Invalid Redis URI given (EINVAL)',
|
||||
defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22
|
||||
));
|
||||
}
|
||||
|
||||
$args = array();
|
||||
parse_str(isset($parts['query']) ? $parts['query'] : '', $args);
|
||||
|
||||
$authority = $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 6379);
|
||||
if ($parts['scheme'] === 'rediss') {
|
||||
$authority = 'tls://' . $authority;
|
||||
} elseif ($parts['scheme'] === 'redis+unix') {
|
||||
$authority = 'unix://' . substr($parts['path'], 1);
|
||||
unset($parts['path']);
|
||||
}
|
||||
$connecting = $this->connector->connect($authority);
|
||||
|
||||
$deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) {
|
||||
// connection cancelled, start with rejecting attempt, then clean up
|
||||
$reject(new \RuntimeException(
|
||||
'Connection to ' . $uri . ' cancelled (ECONNABORTED)',
|
||||
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
|
||||
));
|
||||
|
||||
// either close successful connection or cancel pending connection attempt
|
||||
$connecting->then(function (ConnectionInterface $connection) {
|
||||
$connection->close();
|
||||
}, function () {
|
||||
// ignore to avoid reporting unhandled rejection
|
||||
});
|
||||
$connecting->cancel();
|
||||
});
|
||||
|
||||
$protocol = $this->protocol;
|
||||
$promise = $connecting->then(function (ConnectionInterface $stream) use ($protocol) {
|
||||
return new StreamingClient($stream, $protocol->createResponseParser(), $protocol->createSerializer());
|
||||
}, function (\Exception $e) use ($uri) {
|
||||
throw new \RuntimeException(
|
||||
'Connection to ' . $uri . ' failed: ' . $e->getMessage(),
|
||||
$e->getCode(),
|
||||
$e
|
||||
);
|
||||
});
|
||||
|
||||
// use `?password=secret` query or `user:secret@host` password form URL
|
||||
$pass = isset($args['password']) ? $args['password'] : (isset($parts['pass']) ? rawurldecode($parts['pass']) : null);
|
||||
if (isset($args['password']) || isset($parts['pass'])) {
|
||||
$pass = isset($args['password']) ? $args['password'] : rawurldecode($parts['pass']);
|
||||
$promise = $promise->then(function (StreamingClient $redis) use ($pass, $uri) {
|
||||
return $redis->auth($pass)->then(
|
||||
function () use ($redis) {
|
||||
return $redis;
|
||||
},
|
||||
function (\Exception $e) use ($redis, $uri) {
|
||||
$redis->close();
|
||||
|
||||
$const = '';
|
||||
$errno = $e->getCode();
|
||||
if ($errno === 0) {
|
||||
$const = ' (EACCES)';
|
||||
$errno = $e->getCode() ?: (defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
|
||||
}
|
||||
|
||||
throw new \RuntimeException(
|
||||
'Connection to ' . $uri . ' failed during AUTH command: ' . $e->getMessage() . $const,
|
||||
$errno,
|
||||
$e
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// use `?db=1` query or `/1` path (skip first slash)
|
||||
if (isset($args['db']) || (isset($parts['path']) && $parts['path'] !== '/')) {
|
||||
$db = isset($args['db']) ? $args['db'] : substr($parts['path'], 1);
|
||||
$promise = $promise->then(function (StreamingClient $redis) use ($db, $uri) {
|
||||
return $redis->select($db)->then(
|
||||
function () use ($redis) {
|
||||
return $redis;
|
||||
},
|
||||
function (\Exception $e) use ($redis, $uri) {
|
||||
$redis->close();
|
||||
|
||||
$const = '';
|
||||
$errno = $e->getCode();
|
||||
if ($errno === 0 && strpos($e->getMessage(), 'NOAUTH ') === 0) {
|
||||
$const = ' (EACCES)';
|
||||
$errno = defined('SOCKET_EACCES') ? SOCKET_EACCES : 13;
|
||||
} elseif ($errno === 0) {
|
||||
$const = ' (ENOENT)';
|
||||
$errno = defined('SOCKET_ENOENT') ? SOCKET_ENOENT : 2;
|
||||
}
|
||||
|
||||
throw new \RuntimeException(
|
||||
'Connection to ' . $uri . ' failed during SELECT command: ' . $e->getMessage() . $const,
|
||||
$errno,
|
||||
$e
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
$promise->then(array($deferred, 'resolve'), array($deferred, 'reject'));
|
||||
|
||||
// use timeout from explicit ?timeout=x parameter or default to PHP's default_socket_timeout (60)
|
||||
$timeout = isset($args['timeout']) ? (float) $args['timeout'] : (int) ini_get("default_socket_timeout");
|
||||
if ($timeout < 0) {
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) use ($uri) {
|
||||
if ($e instanceof TimeoutException) {
|
||||
throw new \RuntimeException(
|
||||
'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)',
|
||||
defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110
|
||||
);
|
||||
}
|
||||
throw $e;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Redis client connected to address of given redis instance
|
||||
*
|
||||
* @param string $target
|
||||
* @return Client
|
||||
*/
|
||||
public function createLazyClient($target)
|
||||
{
|
||||
return new LazyClient($target, $this, $this->loop);
|
||||
}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\React\Redis;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\Stream\Util;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class LazyClient extends EventEmitter implements Client
|
||||
{
|
||||
private $target;
|
||||
/** @var Factory */
|
||||
private $factory;
|
||||
private $closed = false;
|
||||
private $promise;
|
||||
|
||||
private $loop;
|
||||
private $idlePeriod = 60.0;
|
||||
private $idleTimer;
|
||||
private $pending = 0;
|
||||
|
||||
private $subscribed = array();
|
||||
private $psubscribed = array();
|
||||
|
||||
/**
|
||||
* @param $target
|
||||
*/
|
||||
public function __construct($target, Factory $factory, LoopInterface $loop)
|
||||
{
|
||||
$args = array();
|
||||
\parse_str((string) \parse_url($target, \PHP_URL_QUERY), $args);
|
||||
if (isset($args['idle'])) {
|
||||
$this->idlePeriod = (float)$args['idle'];
|
||||
}
|
||||
|
||||
$this->target = $target;
|
||||
$this->factory = $factory;
|
||||
$this->loop = $loop;
|
||||
}
|
||||
|
||||
private function client()
|
||||
{
|
||||
if ($this->promise !== null) {
|
||||
return $this->promise;
|
||||
}
|
||||
|
||||
$self = $this;
|
||||
$pending =& $this->promise;
|
||||
$idleTimer=& $this->idleTimer;
|
||||
$subscribed =& $this->subscribed;
|
||||
$psubscribed =& $this->psubscribed;
|
||||
$loop = $this->loop;
|
||||
return $pending = $this->factory->createClient($this->target)->then(function (Client $redis) use ($self, &$pending, &$idleTimer, &$subscribed, &$psubscribed, $loop) {
|
||||
// connection completed => remember only until closed
|
||||
$redis->on('close', function () use (&$pending, $self, &$subscribed, &$psubscribed, &$idleTimer, $loop) {
|
||||
$pending = null;
|
||||
|
||||
// foward unsubscribe/punsubscribe events when underlying connection closes
|
||||
$n = count($subscribed);
|
||||
foreach ($subscribed as $channel => $_) {
|
||||
$self->emit('unsubscribe', array($channel, --$n));
|
||||
}
|
||||
$n = count($psubscribed);
|
||||
foreach ($psubscribed as $pattern => $_) {
|
||||
$self->emit('punsubscribe', array($pattern, --$n));
|
||||
}
|
||||
$subscribed = array();
|
||||
$psubscribed = array();
|
||||
|
||||
if ($idleTimer !== null) {
|
||||
$loop->cancelTimer($idleTimer);
|
||||
$idleTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
// keep track of all channels and patterns this connection is subscribed to
|
||||
$redis->on('subscribe', function ($channel) use (&$subscribed) {
|
||||
$subscribed[$channel] = true;
|
||||
});
|
||||
$redis->on('psubscribe', function ($pattern) use (&$psubscribed) {
|
||||
$psubscribed[$pattern] = true;
|
||||
});
|
||||
$redis->on('unsubscribe', function ($channel) use (&$subscribed) {
|
||||
unset($subscribed[$channel]);
|
||||
});
|
||||
$redis->on('punsubscribe', function ($pattern) use (&$psubscribed) {
|
||||
unset($psubscribed[$pattern]);
|
||||
});
|
||||
|
||||
Util::forwardEvents(
|
||||
$redis,
|
||||
$self,
|
||||
array(
|
||||
'message',
|
||||
'subscribe',
|
||||
'unsubscribe',
|
||||
'pmessage',
|
||||
'psubscribe',
|
||||
'punsubscribe',
|
||||
)
|
||||
);
|
||||
|
||||
return $redis;
|
||||
}, function (\Exception $e) use (&$pending) {
|
||||
// connection failed => discard connection attempt
|
||||
$pending = null;
|
||||
|
||||
throw $e;
|
||||
});
|
||||
}
|
||||
|
||||
public function __call($name, $args)
|
||||
{
|
||||
if ($this->closed) {
|
||||
return \React\Promise\reject(new \RuntimeException(
|
||||
'Connection closed (ENOTCONN)',
|
||||
defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107
|
||||
));
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
return $this->client()->then(function (Client $redis) use ($name, $args, $that) {
|
||||
$that->awake();
|
||||
return \call_user_func_array(array($redis, $name), $args)->then(
|
||||
function ($result) use ($that) {
|
||||
$that->idle();
|
||||
return $result;
|
||||
},
|
||||
function ($error) use ($that) {
|
||||
$that->idle();
|
||||
throw $error;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
if ($this->promise === null) {
|
||||
$this->close();
|
||||
}
|
||||
|
||||
if ($this->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
return $this->client()->then(function (Client $redis) use ($that) {
|
||||
$redis->on('close', function () use ($that) {
|
||||
$that->close();
|
||||
});
|
||||
$redis->end();
|
||||
});
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if ($this->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->closed = true;
|
||||
|
||||
// either close active connection or cancel pending connection attempt
|
||||
if ($this->promise !== null) {
|
||||
$this->promise->then(function (Client $redis) {
|
||||
$redis->close();
|
||||
}, function () {
|
||||
// ignore to avoid reporting unhandled rejection
|
||||
});
|
||||
if ($this->promise !== null) {
|
||||
$this->promise->cancel();
|
||||
$this->promise = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->idleTimer !== null) {
|
||||
$this->loop->cancelTimer($this->idleTimer);
|
||||
$this->idleTimer = null;
|
||||
}
|
||||
|
||||
$this->emit('close');
|
||||
$this->removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function awake()
|
||||
{
|
||||
++$this->pending;
|
||||
|
||||
if ($this->idleTimer !== null) {
|
||||
$this->loop->cancelTimer($this->idleTimer);
|
||||
$this->idleTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function idle()
|
||||
{
|
||||
--$this->pending;
|
||||
|
||||
if ($this->pending < 1 && $this->idlePeriod >= 0 && !$this->subscribed && !$this->psubscribed && $this->promise !== null) {
|
||||
$idleTimer =& $this->idleTimer;
|
||||
$promise =& $this->promise;
|
||||
$idleTimer = $this->loop->addTimer($this->idlePeriod, function () use (&$idleTimer, &$promise) {
|
||||
$promise->then(function (Client $redis) {
|
||||
$redis->close();
|
||||
});
|
||||
$promise = null;
|
||||
$idleTimer = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace Clue\React\Redis;
|
||||
|
||||
use Clue\Redis\Protocol\Factory as ProtocolFactory;
|
||||
use Clue\Redis\Protocol\Model\ErrorReply;
|
||||
use Clue\Redis\Protocol\Model\ModelInterface;
|
||||
use Clue\Redis\Protocol\Model\MultiBulkReply;
|
||||
use Clue\Redis\Protocol\Parser\ParserException;
|
||||
use Clue\Redis\Protocol\Parser\ParserInterface;
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
use Evenement\EventEmitter;
|
||||
use React\Promise\Deferred;
|
||||
use React\Stream\DuplexStreamInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class StreamingClient extends EventEmitter implements Client
|
||||
{
|
||||
private $stream;
|
||||
private $parser;
|
||||
private $serializer;
|
||||
private $requests = array();
|
||||
private $ending = false;
|
||||
private $closed = false;
|
||||
|
||||
private $subscribed = 0;
|
||||
private $psubscribed = 0;
|
||||
|
||||
/**
|
||||
* @param DuplexStreamInterface $stream
|
||||
* @param ?ParserInterface $parser
|
||||
* @param ?SerializerInterface $serializer
|
||||
*/
|
||||
public function __construct(DuplexStreamInterface $stream, $parser = null, $serializer = null)
|
||||
{
|
||||
// manual type checks to support legacy PHP < 7.1
|
||||
assert($parser === null || $parser instanceof ParserInterface);
|
||||
assert($serializer === null || $serializer instanceof SerializerInterface);
|
||||
|
||||
if ($parser === null || $serializer === null) {
|
||||
$factory = new ProtocolFactory();
|
||||
if ($parser === null) {
|
||||
$parser = $factory->createResponseParser();
|
||||
}
|
||||
if ($serializer === null) {
|
||||
$serializer = $factory->createSerializer();
|
||||
}
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
$stream->on('data', function($chunk) use ($parser, $that) {
|
||||
try {
|
||||
$models = $parser->pushIncoming($chunk);
|
||||
} catch (ParserException $error) {
|
||||
$that->emit('error', array(new \UnexpectedValueException(
|
||||
'Invalid data received: ' . $error->getMessage() . ' (EBADMSG)',
|
||||
defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG : 77,
|
||||
$error
|
||||
)));
|
||||
$that->close();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($models as $data) {
|
||||
try {
|
||||
$that->handleMessage($data);
|
||||
} catch (\UnderflowException $error) {
|
||||
$that->emit('error', array($error));
|
||||
$that->close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$stream->on('close', array($this, 'close'));
|
||||
|
||||
$this->stream = $stream;
|
||||
$this->parser = $parser;
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
|
||||
public function __call($name, $args)
|
||||
{
|
||||
$request = new Deferred();
|
||||
$promise = $request->promise();
|
||||
|
||||
$name = strtolower($name);
|
||||
|
||||
// special (p)(un)subscribe commands only accept a single parameter and have custom response logic applied
|
||||
static $pubsubs = array('subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe');
|
||||
|
||||
if ($this->ending) {
|
||||
$request->reject(new \RuntimeException(
|
||||
'Connection ' . ($this->closed ? 'closed' : 'closing'). ' (ENOTCONN)',
|
||||
defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107
|
||||
));
|
||||
} elseif (count($args) !== 1 && in_array($name, $pubsubs)) {
|
||||
$request->reject(new \InvalidArgumentException(
|
||||
'PubSub commands limited to single argument (EINVAL)',
|
||||
defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22
|
||||
));
|
||||
} elseif ($name === 'monitor') {
|
||||
$request->reject(new \BadMethodCallException(
|
||||
'MONITOR command explicitly not supported (ENOTSUP)',
|
||||
defined('SOCKET_ENOTSUP') ? SOCKET_ENOTSUP : (defined('SOCKET_EOPNOTSUPP') ? SOCKET_EOPNOTSUPP : 95)
|
||||
));
|
||||
} else {
|
||||
$this->stream->write($this->serializer->getRequestMessage($name, $args));
|
||||
$this->requests []= $request;
|
||||
}
|
||||
|
||||
if (in_array($name, $pubsubs)) {
|
||||
$that = $this;
|
||||
$subscribed =& $this->subscribed;
|
||||
$psubscribed =& $this->psubscribed;
|
||||
|
||||
$promise->then(function ($array) use ($that, &$subscribed, &$psubscribed) {
|
||||
$first = array_shift($array);
|
||||
|
||||
// (p)(un)subscribe messages are to be forwarded
|
||||
$that->emit($first, $array);
|
||||
|
||||
// remember number of (p)subscribe topics
|
||||
if ($first === 'subscribe' || $first === 'unsubscribe') {
|
||||
$subscribed = $array[1];
|
||||
} else {
|
||||
$psubscribed = $array[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $promise;
|
||||
}
|
||||
|
||||
public function handleMessage(ModelInterface $message)
|
||||
{
|
||||
if (($this->subscribed !== 0 || $this->psubscribed !== 0) && $message instanceof MultiBulkReply) {
|
||||
$array = $message->getValueNative();
|
||||
$first = array_shift($array);
|
||||
|
||||
// pub/sub messages are to be forwarded and should not be processed as request responses
|
||||
if (in_array($first, array('message', 'pmessage'))) {
|
||||
$this->emit($first, $array);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->requests) {
|
||||
throw new \UnderflowException(
|
||||
'Unexpected reply received, no matching request found (ENOMSG)',
|
||||
defined('SOCKET_ENOMSG') ? SOCKET_ENOMSG : 42
|
||||
);
|
||||
}
|
||||
|
||||
$request = array_shift($this->requests);
|
||||
assert($request instanceof Deferred);
|
||||
|
||||
if ($message instanceof ErrorReply) {
|
||||
$request->reject($message);
|
||||
} else {
|
||||
$request->resolve($message->getValueNative());
|
||||
}
|
||||
|
||||
if ($this->ending && !$this->requests) {
|
||||
$this->close();
|
||||
}
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
$this->ending = true;
|
||||
|
||||
if (!$this->requests) {
|
||||
$this->close();
|
||||
}
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if ($this->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ending = true;
|
||||
$this->closed = true;
|
||||
|
||||
$remoteClosed = $this->stream->isReadable() === false && $this->stream->isWritable() === false;
|
||||
$this->stream->close();
|
||||
|
||||
$this->emit('close');
|
||||
|
||||
// reject all remaining requests in the queue
|
||||
while ($this->requests) {
|
||||
$request = array_shift($this->requests);
|
||||
assert($request instanceof Deferred);
|
||||
|
||||
if ($remoteClosed) {
|
||||
$request->reject(new \RuntimeException(
|
||||
'Connection closed by peer (ECONNRESET)',
|
||||
defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104
|
||||
));
|
||||
} else {
|
||||
$request->reject(new \RuntimeException(
|
||||
'Connection closing (ECONNABORTED)',
|
||||
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user