tipo: modificación del cpanel y gitignore de la carpeta vendor

This commit is contained in:
2026-07-07 11:35:09 -06:00
parent a0c91dc567
commit 676ef0d506
8510 changed files with 1132803 additions and 4 deletions
+2
View File
@@ -0,0 +1,2 @@
github: clue
custom: https://clue.engineering/support
+69
View File
@@ -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
+21
View File
@@ -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.
+189
View File
@@ -0,0 +1,189 @@
# clue/redis-protocol
[![CI status](https://github.com/clue/redis-protocol/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/redis-protocol/actions)
[![code coverage](https://img.shields.io/badge/code%20coverage-100%25-success)](#tests)
[![installs on Packagist](https://img.shields.io/packagist/dt/clue/redis-protocol?color=blue&label=installs%20on%20Packagist)](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
View File
@@ -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
View File
@@ -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();
}
}
+33
View File
@@ -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);
}
}
+34
View File
@@ -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());
}
}
+30
View File
@@ -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);
}
}
+23
View File
@@ -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);
}
+103
View File
@@ -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);
}
}
+50
View File
@@ -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);
}
}
+34
View File
@@ -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);
}
}
+40
View File
@@ -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);
}
+124
View File
@@ -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;
}
}
+148
View File
@@ -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
View File
@@ -0,0 +1,2 @@
github: clue
custom: https://clue.engineering/support
+287
View File
@@ -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
+21
View File
@@ -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.
+660
View File
@@ -0,0 +1,660 @@
# clue/reactphp-redis
[![CI status](https://github.com/clue/reactphp-redis/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-redis/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/clue/redis-react?color=blue&label=installs%20on%20Packagist)](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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
));
}
}
}
}