mohannadelemary/laravel-redis-connection-clients
A Laravel package that provides Redis connection client wrappers, making it easy to configure, resolve, and reuse multiple Redis connections across your application for cache, queues, sessions, and custom workloads. Simplifies connection setup and keeps Redis access consistent and organized in Larav...
This package lets a single Redis connection (or cluster) opt out of the global database.redis.client setting and use its own client instead — phpredis, predis, or any client registered through Redis::extend(). Nothing else changes: connections without a client key keep behaving exactly as they do today.
Install
composer require <vendor>/<package> # v0.1.0 — pin it: "0.1.*"
Requirements to verify before you start: PHP 8.3+, Laravel 12 or 13. There is no migration, no published config file, and no env var of its own — the whole feature lives in config/database.php.
Where to look first
config/database.php → redis array and redis → clusters array. That is the only surface area.Redis facade alias.client) is placed at connection level, not inside options.First use case
The most common one: your app is on phpredis, but one connection needs predis — a cluster shape phpredis handles badly, a TLS/proxy setup, or a third-party package that type-hints \Predis\Client. Instead of flipping REDIS_CLIENT for the whole app and breaking everything else:
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
// ...
],
'analytics' => [
'client' => 'predis', // only this connection uses predis
'host' => env('ANALYTICS_REDIS_HOST'),
// ...
],
],
Then Redis::connection('analytics') gives you a \Predis\Client while Redis::connection('default') stays phpredis.
Pin the client per logical role, not per host. The useful pattern is naming connections after the consumer (cache, session, horizon, analytics, pubsub) and choosing the client per consumer. Everything that already resolves connections by name — cache, sessions, queues, Horizon — then picks up the right client for free:
// config/cache.php
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => 'cache', // -> uses that connection's `client`
],
],
// config/session.php
'connection' => 'session',
// config/queue.php
'redis' => ['connection' => 'default'],
Clusters get the key at cluster level, alongside seeds/options, not inside an individual node:
'clusters' => [
'default' => [
'client' => 'predis',
'seeds' => [['host' => '10.0.0.1', 'port' => 6379]],
'options' => ['cluster' => 'redis'], // predis needs this; keep it
],
],
Custom clients via Redis::extend(). Register the resolver in a service provider's register() (see gotchas about ordering), then name it:
// AppServiceProvider::register()
Redis::extend('instrumented', function (array $config) {
return new InstrumentedRedisClient($config);
});
// config/database.php
'analytics' => [
'client' => 'instrumented',
'host' => env('ANALYTICS_REDIS_HOST'),
],
This is the real payoff of the package: you can roll out a logging/metrics-wrapped client on one connection without forking the Redis manager or swapping the global client.
Incremental migration workflow. Move one connection at a time: add client to a non-critical connection → deploy → verify key reads/writes and latency → move the next. Because the feature is strictly opt-in, rollback is deleting one line.
Debugging which client you actually got:
$conn = Redis::connection('analytics');
dump($conn->client()::class); // \Redis vs \Predis\Client vs your wrapper
config('database.redis.analytics.client');
Compare against config('database.redis.client') to confirm the override is present in the compiled config (i.e. after config:cache), not just in your local file.
Testing. Client resolution happens when a connection is first resolved, and the manager memoizes it. In tests, set config in setUp() and purge before asserting:
config()->set('database.redis.analytics.client', 'predis');
Redis::purge('analytics');
Connections are memoized — config changes after the first connect do nothing. Redis::connection('cache') returns a cached instance for the rest of the process. If you flip the client key in a test or a runtime config()->set(), you must call Redis::purge('cache') (or Redis::purge() for all) or you'll silently keep testing the old client.
The failure mode is at connect time, not boot time. A typo in the client name ('predis ', 'Predis', a name you never registered with extend()) won't fail php artisan config:cache, route:list, or a healthcheck that doesn't touch Redis. It blows up on the first real command against that connection. Smoke-test each overridden connection right after deploy (Redis::connection('analytics')->ping()).
Redis::extend() registration order matters. The resolver name must exist in the manager before the connection is first resolved. Register in AppServiceProvider::register(), not in a controller, job, or a boot() that runs after something else already connected. In long-running workers (queue, Horizon, Octane) a connection resolved once is reused for the life of the worker — a resolver registered lazily will never be picked up.
Serialization compatibility across clients. Switching one connection between phpredis and predis changes defaults for serializer and how values/keys are encoded. Keys written by a phpredis connection may not deserialize cleanly from a predis connection. If two connections share a keyspace (cache + session on the same server, say), keep them on the same client, or explicitly set options.serializer on both. This is the pitfall most likely to bite in production and it won't show up in a fresh environment.
options is still merged globally. Per-connection options (prefix, serializer, parameters, cluster) continue to work as before and layer on top of database.redis.options. The client key does not affect them, and options.cluster => 'redis' is still required for a predis cluster — don't drop it when adding client.
Prefixes are unchanged, but be explicit. If you're splitting consumers across connections for isolation, set options.prefix per connection rather than relying on the global one, otherwise your "isolated" connections share a keyspace.
Global fallback still governs everything else. database.redis.client remains the default for default, and for any connection where you forget the key. There's no "client per environment" helper — if you need env-driven per-connection clients, read a dedicated env var in the connection array ('client' => env('ANALYTICS_REDIS_CLIENT', 'predis')), which is fine because it's plain config.
Version pinning. v0.1.0, supports Laravel 12/13 only, PHP 8.3+. On Laravel 11 or PHP 8.2 this is a hard blocker — check with composer why-not <vendor>/<package> 0.1.0 before debugging odd behaviour. Expect the config contract to be volatile pre-1.0; pin 0.1.* and re-read the README on each bump, since the opt-in nature is the whole API and a rename of the client key would be a silent breaking change.
Octane/serverless. Per-worker memoization means the client choice is fixed per worker lifetime, which is fine — but after a config change (e.g. deploy with a different client), you need a full worker recycle, not just a config reload.
Extension points beyond the config key. Two worth knowing: Redis::extend() for wrapping/adapting clients, and resolving the manager directly (app('redis')) if you need to build connections outside the facade — both still respect the per-connection client key. If you need conditional logic (client A in one region, B in another), do it in the config array or in an extend() resolver rather than post-hoc swapping the manager.
How can I help you explore Laravel packages today?