1313
1414use Predis \Connection \Aggregate \ClusterInterface ;
1515use Predis \Connection \Aggregate \RedisCluster ;
16- use Predis \Connection \Factory ;
1716use Predis \Response \Status ;
1817use Symfony \Component \Cache \Exception \CacheException ;
1918use Symfony \Component \Cache \Exception \InvalidArgumentException ;
@@ -37,7 +36,10 @@ trait RedisTrait
3736'retry_interval ' =>0 ,
3837'compression ' =>true ,
3938'tcp_keepalive ' =>0 ,
40- 'lazy ' =>false ,
39+ 'lazy ' =>null ,
40+ 'redis_cluster ' =>false ,
41+ 'dbindex ' =>0 ,
42+ 'failover ' =>'none ' ,
4143 );
4244private $ redis ;
4345private $ marshaller ;
@@ -74,57 +76,87 @@ private function init($redisClient, $namespace, $defaultLifetime, ?MarshallerInt
7476 *
7577 * @throws InvalidArgumentException when the DSN is invalid
7678 *
77- * @return \Redis|\Predis\Client According to the "class" option
79+ * @return \Redis|\RedisCluster|\ Predis\Client According to the "class" option
7880 */
7981public static function createConnection ($ dsn ,array $ options =array ())
8082 {
81- if (0 !==strpos ($ dsn ,'redis:// ' )) {
82- throw new InvalidArgumentException (sprintf ('Invalid Redis DSN: %s does not start with "redis:// " ' ,$ dsn ));
83+ if (0 !==strpos ($ dsn ,'redis: ' )) {
84+ throw new InvalidArgumentException (sprintf ('Invalid Redis DSN: %s does not start with "redis:" ' ,$ dsn ));
8385 }
84- $ params =preg_replace_callback ('#^redis://(?:(?:[^:@]*+:)?([^@]*+)@)?# ' ,function ($ m )use (&$ auth ) {
85- if (isset ($ m [1 ])) {
86- $ auth =$ m [1 ];
86+
87+ if (!\extension_loaded ('redis ' ) && !class_exists (\Predis \Client::class)) {
88+ throw new CacheException (sprintf ('Cannot find the "redis" extension nor the "predis/predis" package: %s ' ,$ dsn ));
89+ }
90+
91+ $ params =preg_replace_callback ('#^redis:(//)?(?:(?:[^:@]*+:)?([^@]*+)@)?# ' ,function ($ m )use (&$ auth ) {
92+ if (isset ($ m [2 ])) {
93+ $ auth =$ m [2 ];
8794 }
8895
89- return 'file:// ' ;
96+ return 'file: ' .( $ m [ 1 ] ?? '' ) ;
9097 },$ dsn );
91- if (false ===$ params =parse_url ($ params )) {
92- throw new InvalidArgumentException (sprintf ('Invalid Redis DSN: %s ' ,$ dsn ));
93- }
94- if (!isset ($ params ['host ' ]) && !isset ($ params ['path ' ])) {
98+
99+ if (false ===$ params =parse_url ($ dsn )) {
95100throw new InvalidArgumentException (sprintf ('Invalid Redis DSN: %s ' ,$ dsn ));
96101 }
97- if (isset ($ params ['path ' ]) &&preg_match ('#/(\d+)$# ' ,$ params ['path ' ],$ m )) {
98- $ params ['dbindex ' ] =$ m [1 ];
99- $ params ['path ' ] =substr ($ params ['path ' ],0 , -\strlen ($ m [0 ]));
100- }
101- if (isset ($ params ['host ' ])) {
102- $ scheme ='tcp ' ;
103- }else {
104- $ scheme ='unix ' ;
105- }
106- $ params +=array (
107- 'host ' =>isset ($ params ['host ' ]) ?$ params ['host ' ] :$ params ['path ' ],
108- 'port ' =>isset ($ params ['host ' ]) ?6379 :null ,
109- 'dbindex ' =>0 ,
110- );
102+
103+ $ query =$ hosts =array ();
104+
111105if (isset ($ params ['query ' ])) {
112106parse_str ($ params ['query ' ],$ query );
113- $ params +=$ query ;
107+
108+ if (isset ($ query ['host ' ])) {
109+ if (!\is_array ($ hosts =$ query ['host ' ])) {
110+ throw new InvalidArgumentException (sprintf ('Invalid Redis DSN: %s ' ,$ dsn ));
111+ }
112+ foreach ($ hostsas $ host =>$ parameters ) {
113+ if (\is_string ($ parameters )) {
114+ parse_str ($ parameters ,$ parameters );
115+ }
116+ if (false ===$ i =strrpos ($ host ,': ' )) {
117+ $ hosts [$ host ] =array ('scheme ' =>'tcp ' ,'host ' =>$ host ,'port ' =>6379 ) +$ parameters ;
118+ }elseif ($ port = (int )substr ($ host ,1 +$ i )) {
119+ $ hosts [$ host ] =array ('scheme ' =>'tcp ' ,'host ' =>substr ($ host ,0 ,$ i ),'port ' =>$ port ) +$ parameters ;
120+ }else {
121+ $ hosts [$ host ] =array ('scheme ' =>'unix ' ,'path ' =>substr ($ host ,0 ,$ i )) +$ parameters ;
122+ }
123+ }
124+ $ hosts =array_values ($ hosts );
125+ }
126+ }
127+
128+ if (isset ($ params ['host ' ]) ||isset ($ params ['path ' ])) {
129+ if (!isset ($ params ['dbindex ' ]) &&isset ($ params ['path ' ]) &&preg_match ('#/(\d+)$# ' ,$ params ['path ' ],$ m )) {
130+ $ params ['dbindex ' ] =$ m [1 ];
131+ $ params ['path ' ] =substr ($ params ['path ' ],0 , -\strlen ($ m [0 ]));
132+ }
133+
134+ if (isset ($ params ['host ' ])) {
135+ array_unshift ($ hosts ,array ('scheme ' =>'tcp ' ,'host ' =>$ params ['host ' ],'port ' =>$ params ['port ' ] ??6379 ));
136+ }else {
137+ array_unshift ($ hosts ,array ('scheme ' =>'unix ' ,'path ' =>$ params ['path ' ]));
138+ }
114139 }
115- $ params +=$ options +self ::$ defaultConnectionOptions ;
116- if (null ===$ params ['class ' ] && !\extension_loaded ('redis ' ) && !class_exists (\Predis \Client::class)) {
117- throw new CacheException (sprintf ('Cannot find the "redis" extension, and "predis/predis" is not installed: %s ' ,$ dsn ));
140+
141+ if (!$ hosts ) {
142+ throw new InvalidArgumentException (sprintf ('Invalid Redis DSN: %s ' ,$ dsn ));
143+ }
144+
145+ $ params +=$ query +$ options +self ::$ defaultConnectionOptions ;
146+
147+ if (null ===$ params ['class ' ] &&\extension_loaded ('redis ' )) {
148+ $ class =$ params ['redis_cluster ' ] ? \RedisCluster::class : (1 <\count ($ hosts ) ? \RedisArray::class : \Redis::class);
149+ }else {
150+ $ class =null ===$ params ['class ' ] ? \Predis \Client::class :$ params ['class ' ];
118151 }
119- $ class =null ===$ params ['class ' ] ? (\extension_loaded ('redis ' ) ? \Redis::class : \Predis \Client::class) :$ params ['class ' ];
120152
121153if (is_a ($ class , \Redis::class,true )) {
122154$ connect =$ params ['persistent ' ] ||$ params ['persistent_id ' ] ?'pconnect ' :'connect ' ;
123155$ redis =new $ class ();
124156
125- $ initializer =function ($ redis )use ($ connect ,$ params ,$ dsn ,$ auth ) {
157+ $ initializer =function ($ redis )use ($ connect ,$ params ,$ dsn ,$ auth, $ hosts ) {
126158try {
127- @$ redis ->{$ connect }($ params [ 'host ' ],$ params [ 'port ' ],$ params ['timeout ' ],$ params ['persistent_id ' ],$ params ['retry_interval ' ]);
159+ @$ redis ->{$ connect }($ hosts [ 0 ][ 'host ' ],$ hosts [ 0 ][ 'port ' ],$ params ['timeout ' ], ( string ) $ params ['persistent_id ' ],$ params ['retry_interval ' ]);
128160 }catch (\RedisException $ e ) {
129161throw new InvalidArgumentException (sprintf ('Redis connection failed (%s): %s ' ,$ e ->getMessage (),$ dsn ));
130162 }
@@ -160,15 +192,82 @@ public static function createConnection($dsn, array $options = array())
160192 }else {
161193$ initializer ($ redis );
162194 }
195+ }elseif (is_a ($ class , \RedisArray::class,true )) {
196+ foreach ($ hostsas $ i =>$ host ) {
197+ $ hosts [$ i ] ='tcp ' ===$ host ['scheme ' ] ?$ host ['host ' ].': ' .$ host ['port ' ] :$ host ['path ' ];
198+ }
199+ $ params ['lazy_connect ' ] =$ params ['lazy ' ] ??true ;
200+ $ params ['connect_timeout ' ] =$ params ['timeout ' ];
201+
202+ try {
203+ $ redis =new $ class ($ hosts ,$ params );
204+ }catch (\RedisClusterException $ e ) {
205+ throw new InvalidArgumentException (sprintf ('Redis connection failed (%s): %s ' ,$ e ->getMessage (),$ dsn ));
206+ }
207+
208+ if (0 <$ params ['tcp_keepalive ' ] &&\defined ('Redis::OPT_TCP_KEEPALIVE ' )) {
209+ $ redis ->setOption (\Redis::OPT_TCP_KEEPALIVE ,$ params ['tcp_keepalive ' ]);
210+ }
211+ if ($ params ['compression ' ] &&\defined ('Redis::COMPRESSION_LZF ' )) {
212+ $ redis ->setOption (\Redis::OPT_COMPRESSION , \Redis::COMPRESSION_LZF );
213+ }
214+ }elseif (is_a ($ class , \RedisCluster::class,true )) {
215+ $ initializer =function ()use ($ class ,$ params ,$ dsn ,$ hosts ) {
216+ foreach ($ hostsas $ i =>$ host ) {
217+ $ hosts [$ i ] ='tcp ' ===$ host ['scheme ' ] ?$ host ['host ' ].': ' .$ host ['port ' ] :$ host ['path ' ];
218+ }
219+
220+ try {
221+ $ redis =new $ class (null ,$ hosts ,$ params ['timeout ' ],$ params ['read_timeout ' ], (bool )$ params ['persistent ' ]);
222+ }catch (\RedisClusterException $ e ) {
223+ throw new InvalidArgumentException (sprintf ('Redis connection failed (%s): %s ' ,$ e ->getMessage (),$ dsn ));
224+ }
225+
226+ if (0 <$ params ['tcp_keepalive ' ] &&\defined ('Redis::OPT_TCP_KEEPALIVE ' )) {
227+ $ redis ->setOption (\Redis::OPT_TCP_KEEPALIVE ,$ params ['tcp_keepalive ' ]);
228+ }
229+ if ($ params ['compression ' ] &&\defined ('Redis::COMPRESSION_LZF ' )) {
230+ $ redis ->setOption (\Redis::OPT_COMPRESSION , \Redis::COMPRESSION_LZF );
231+ }
232+ switch ($ params ['failover ' ]) {
233+ case 'error ' :$ redis ->setOption (\RedisCluster::OPT_SLAVE_FAILOVER , \RedisCluster::FAILOVER_ERROR );break ;
234+ case 'distribute ' :$ redis ->setOption (\RedisCluster::OPT_SLAVE_FAILOVER , \RedisCluster::FAILOVER_DISTRIBUTE );break ;
235+ case 'slaves ' :$ redis ->setOption (\RedisCluster::OPT_SLAVE_FAILOVER , \RedisCluster::FAILOVER_DISTRIBUTE_SLAVES );break ;
236+ }
237+
238+ return $ redis ;
239+ };
240+
241+ $ redis =$ params ['lazy ' ] ?new RedisClusterProxy ($ initializer ) :$ initializer ();
163242 }elseif (is_a ($ class , \Predis \Client::class,true )) {
164- $ params ['scheme ' ] =$ scheme ;
165- $ params ['database ' ] =$ params ['dbindex ' ] ?:null ;
166- $ params ['password ' ] =$ auth ;
167- $ redis =new $ class ((new Factory ())->create ($ params ));
243+ if ($ params ['redis_cluster ' ]) {
244+ $ params ['cluster ' ] ='redis ' ;
245+ }
246+ $ params +=array ('parameters ' =>array ());
247+ $ params ['parameters ' ] +=array (
248+ 'persistent ' =>$ params ['persistent ' ],
249+ 'timeout ' =>$ params ['timeout ' ],
250+ 'read_write_timeout ' =>$ params ['read_timeout ' ],
251+ 'tcp_nodelay ' =>true ,
252+ );
253+ if ($ params ['dbindex ' ]) {
254+ $ params ['parameters ' ]['database ' ] =$ params ['dbindex ' ];
255+ }
256+ if (null !==$ auth ) {
257+ $ params ['parameters ' ]['password ' ] =$ auth ;
258+ }
259+ if (1 ===\count ($ hosts ) && !$ params ['redis_cluster ' ]) {
260+ $ hosts =$ hosts [0 ];
261+ }elseif (\in_array ($ params ['failover ' ],array ('slaves ' ,'distribute ' ),true ) && !isset ($ params ['replication ' ])) {
262+ $ params ['replication ' ] =true ;
263+ $ hosts [0 ] +=array ('alias ' =>'master ' );
264+ }
265+
266+ $ redis =new $ class ($ hosts ,array_diff_key ($ params ,self ::$ defaultConnectionOptions ));
168267 }elseif (class_exists ($ class ,false )) {
169- throw new InvalidArgumentException (sprintf ('"%s" is not a subclass of "Redis" or " Predis\Client" ' ,$ class ));
268+ throw new InvalidArgumentException (sprintf ('"%s" is not a subclass of "Redis", "RedisArray", "RedisCluster" nor " Predis\Client". ' ,$ class ));
170269 }else {
171- throw new InvalidArgumentException (sprintf ('Class "%s" does not exist ' ,$ class ));
270+ throw new InvalidArgumentException (sprintf ('Class "%s" does not exist. ' ,$ class ));
172271 }
173272
174273return $ redis ;
@@ -183,7 +282,6 @@ protected function doFetch(array $ids)
183282return array ();
184283 }
185284
186- $ i = -1 ;
187285$ result =array ();
188286
189287if ($ this ->redis instanceof \Predis \Client) {
@@ -244,6 +342,7 @@ protected function doClear($namespace)
244342$ h ->connect ($ host [0 ],$ host [1 ]);
245343 }
246344 }
345+
247346foreach ($ hostsas $ host ) {
248347if (!isset ($ namespace [0 ])) {
249348$ cleared =$ host ->flushDb () &&$ cleared ;