Skip to content
Async Redis client implementation, built on top of ReactPHP.
Branch: master
Clone or download
Latest commit 426e9df Mar 11, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
examples Improve Exception messages for connection issues Mar 9, 2019
src Improve documentation for PubSub and events Mar 11, 2019
tests Improve Exception messages for connection issues Mar 9, 2019
.gitignore Ignore composer.lock Mar 6, 2015
.travis.yml
CHANGELOG.md
LICENSE
README.md Prepare v2.3.0 release Mar 11, 2019
composer.json Implement "idle" timeout to close underlying connection when unused Mar 8, 2019
phpunit.xml.dist Clean up test suite (namespaces etc.) Feb 24, 2019

README.md

clue/reactphp-redis Build Status

Async Redis client implementation, built on top of ReactPHP.

Redis 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 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

Quickstart example

Once installed, you can use the following code to connect to your local Redis server and send some requests:

$loop = React\EventLoop\Factory::create();
$factory = new Factory($loop);

$client = $factory->createLazyClient('localhost');
$client->set('greeting', 'Hello world');
$client->append('greeting', '!');

$client->get('greeting')->then(function ($greeting) {
    // Hello world!
    echo $greeting . PHP_EOL;
});

$client->incr('invocation')->then(function ($n) {
    echo 'This is invocation #' . $n . PHP_EOL;
});

// end connection once all pending requests have been resolved
$client->end();

$loop->run();

See also the examples.

Usage

Factory

The Factory is responsible for creating your Client instance. It also registers everything with the main EventLoop.

$loop = \React\EventLoop\Factory::create();
$factory = new Factory($loop);

If you need custom DNS, proxy or TLS settings, you can explicitly pass a custom instance of the ConnectorInterface:

$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);

createClient()

The createClient(string $redisUri): PromiseInterface<Client,Exception> method can be used to create a new 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).

$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
    }
);

The method returns a Promise that will resolve with a 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.

$promise = $factory->createClient($redisUri);

$loop->addTimer(3.0, function () use ($promise) {
    $promise->cancel();
});

The $redisUri can be given in the standard form [redis[s]://][:auth@]host[:port][/db]. You can omit the URI scheme and port if you're connecting to the default port 6379:

// 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:

// 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:

// both forms are equivalent
$factory->createClient('redis://localhost/2');
$factory->createClient('redis://localhost?db=2');

You can use the standard rediss:// URI scheme if you're using a secure TLS proxy in front of Redis:

$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:

$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:

$factory->createClient('localhost?timeout=0.5');

createLazyClient()

The createLazyClient(string $redisUri): Client method can be used to create a new 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).

$client = $factory->createLazyClient('redis://localhost:6379');

$client->incr('hello');
$client->end();

This method immediately returns a "virtual" connection implementing the 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 form [redis[s]://][:auth@]host[:port][/db]. You can omit the URI scheme and port if you're connecting to the default port 6379:

// 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:

// 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:

// both forms are equivalent
$factory->createLazyClient('redis://localhost/2');
$factory->createLazyClient('redis://localhost?db=2');

You can use the standard rediss:// URI scheme if you're using a secure TLS proxy in front of Redis:

$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:

$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:

$factory->createLazyClient('localhost?timeout=0.5');

By default, this method will keep "idle" connection 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:

$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.

Commands

All Redis commands are automatically available as public methods like this:

$client->get($key);
$client->set($key, $value);
$client->exists($key);
$client->expire($key, $seconds);
$client->mget($key1, $key2, $key3);

$client->multi();
$client->exec();

$client->publish($channel, $payload);
$client->subscribe($channel);

$client->ping();
$client->select($database);

// many more…

Listing all available commands is out of scope here, please refer to the Redis command reference. All Redis commands are automatically available as public methods via the magic __call() method.

Each of these commands supports async operation and either resolves with its results or rejects with an Exception. Please see the following section about 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-based interface that makes it easy to react to when a command is fulfilled (i.e. either successfully resolved or rejected with an error):

$client->set('hello', 'world');
$client->get('hello')->then(function ($response) {
    // response received for GET command
    echo 'hello ' . $response;
});

PubSub

This library is commonly used to efficiently transport messages using Redis' Pub/Sub (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 can be used to send a message to all clients currently subscribed to a given channel:

$channel = 'user';
$message = json_encode(array('id' => 10));
$client->publish($channel, $message);

The SUBSCRIBE command can be used to subscribe to a channel and then receive incoming PubSub message events:

$channel = 'user';
$client->subscribe($channel);

$client->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:

$client->subscribe('user.register');
$client->subscribe('user.join');
$client->subscribe('user.leave');

Similarly, the PSUBSCRIBE command can be used to subscribe to all channels matching a given pattern and then receive all incoming PubSub messages with the pmessage event:

$pattern = 'user.*';
$client->psubscribe($pattern);

$client->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 and PUNSUBSCRIBE command 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:

$client->subscribe('user');

$loop->addTimer(60.0, function () use ($client) {
    $client->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 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:

$client->on('subscribe', function ($channel, $total) {
    // subscribed to given $channel
});
$client->on('psubscribe', function ($pattern, $total) {
    // subscribed to matching given $pattern
});
$client->on('unsubscribe', function ($channel, $total) {
    // unsubscribed from given $channel
});
$client->on('punsubscribe', function ($pattern, $total) {
    // unsubscribed from matching given $pattern
});

When using the 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.

close()

The close():void method can be used to force-close the Redis connection and reject all pending commands.

end()

The end():void method can be used to soft-close the Redis connection once all pending commands are completed.

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.

$client->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).

$client->on('close', function () {
    echo 'Connection closed' . PHP_EOL;
});

See also the close() method.

Install

The recommended way to install this library is through Composer. New to Composer?

This project follows SemVer. This will install the latest supported version:

$ composer require clue/redis-react:^2.3

See also the CHANGELOG 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 7+ and HHVM. It's highly recommended to use PHP 7+ for this project.

Tests

To run the test suite, you first need to clone this repo and then install all dependencies through Composer:

$ composer install

To run the test suite, go to the project root and run:

$ php 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 want to also run the functional tests, you need to supply your login details in an environment variable like this:

$ REDIS_URI=localhost:6379 php vendor/bin/phpunit

License

This project is released under the permissive MIT 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.

You can’t perform that action at this time.