vendor/symfony/cache/Traits/RedisTrait.php line 171

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Cache\Traits;
  11. use Predis\Command\Redis\UNLINK;
  12. use Predis\Connection\Aggregate\ClusterInterface;
  13. use Predis\Connection\Aggregate\RedisCluster;
  14. use Predis\Connection\Aggregate\ReplicationInterface;
  15. use Predis\Response\ErrorInterface;
  16. use Predis\Response\Status;
  17. use Symfony\Component\Cache\Exception\CacheException;
  18. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  19. use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
  20. use Symfony\Component\Cache\Marshaller\MarshallerInterface;
  21. /**
  22.  * @author Aurimas Niekis <aurimas@niekis.lt>
  23.  * @author Nicolas Grekas <p@tchwork.com>
  24.  *
  25.  * @internal
  26.  */
  27. trait RedisTrait
  28. {
  29.     private static $defaultConnectionOptions = [
  30.         'class' => null,
  31.         'persistent' => 0,
  32.         'persistent_id' => null,
  33.         'timeout' => 30,
  34.         'read_timeout' => 0,
  35.         'retry_interval' => 0,
  36.         'tcp_keepalive' => 0,
  37.         'lazy' => null,
  38.         'redis_cluster' => false,
  39.         'redis_sentinel' => null,
  40.         'dbindex' => 0,
  41.         'failover' => 'none',
  42.         'ssl' => null// see https://php.net/context.ssl
  43.     ];
  44.     private $redis;
  45.     private $marshaller;
  46.     /**
  47.      * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis
  48.      */
  49.     private function init($redisstring $namespaceint $defaultLifetime, ?MarshallerInterface $marshaller)
  50.     {
  51.         parent::__construct($namespace$defaultLifetime);
  52.         if (preg_match('#[^-+_.A-Za-z0-9]#'$namespace$match)) {
  53.             throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.'$match[0]));
  54.         }
  55.         if (!$redis instanceof \Redis && !$redis instanceof \RedisArray && !$redis instanceof \RedisCluster && !$redis instanceof \Predis\ClientInterface && !$redis instanceof RedisProxy && !$redis instanceof RedisClusterProxy) {
  56.             throw new InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.'__METHOD__get_debug_type($redis)));
  57.         }
  58.         if ($redis instanceof \Predis\ClientInterface && $redis->getOptions()->exceptions) {
  59.             $options = clone $redis->getOptions();
  60.             \Closure::bind(function () { $this->options['exceptions'] = false; }, $options$options)();
  61.             $redis = new $redis($redis->getConnection(), $options);
  62.         }
  63.         $this->redis $redis;
  64.         $this->marshaller $marshaller ?? new DefaultMarshaller();
  65.     }
  66.     /**
  67.      * Creates a Redis connection using a DSN configuration.
  68.      *
  69.      * Example DSN:
  70.      *   - redis://localhost
  71.      *   - redis://example.com:1234
  72.      *   - redis://secret@example.com/13
  73.      *   - redis:///var/run/redis.sock
  74.      *   - redis://secret@/var/run/redis.sock/13
  75.      *
  76.      * @param array $options See self::$defaultConnectionOptions
  77.      *
  78.      * @return \Redis|\RedisArray|\RedisCluster|RedisClusterProxy|RedisProxy|\Predis\ClientInterface According to the "class" option
  79.      *
  80.      * @throws InvalidArgumentException when the DSN is invalid
  81.      */
  82.     public static function createConnection(string $dsn, array $options = [])
  83.     {
  84.         if (str_starts_with($dsn'redis:')) {
  85.             $scheme 'redis';
  86.         } elseif (str_starts_with($dsn'rediss:')) {
  87.             $scheme 'rediss';
  88.         } else {
  89.             throw new InvalidArgumentException(sprintf('Invalid Redis DSN: "%s" does not start with "redis:" or "rediss".'$dsn));
  90.         }
  91.         if (!\extension_loaded('redis') && !class_exists(\Predis\Client::class)) {
  92.             throw new CacheException(sprintf('Cannot find the "redis" extension nor the "predis/predis" package: "%s".'$dsn));
  93.         }
  94.         $params preg_replace_callback('#^'.$scheme.':(//)?(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) {
  95.             if (isset($m[2])) {
  96.                 $auth $m[2];
  97.                 if ('' === $auth) {
  98.                     $auth null;
  99.                 }
  100.             }
  101.             return 'file:'.($m[1] ?? '');
  102.         }, $dsn);
  103.         if (false === $params parse_url($params)) {
  104.             throw new InvalidArgumentException(sprintf('Invalid Redis DSN: "%s".'$dsn));
  105.         }
  106.         $query $hosts = [];
  107.         $tls 'rediss' === $scheme;
  108.         $tcpScheme $tls 'tls' 'tcp';
  109.         if (isset($params['query'])) {
  110.             parse_str($params['query'], $query);
  111.             if (isset($query['host'])) {
  112.                 if (!\is_array($hosts $query['host'])) {
  113.                     throw new InvalidArgumentException(sprintf('Invalid Redis DSN: "%s".'$dsn));
  114.                 }
  115.                 foreach ($hosts as $host => $parameters) {
  116.                     if (\is_string($parameters)) {
  117.                         parse_str($parameters$parameters);
  118.                     }
  119.                     if (false === $i strrpos($host':')) {
  120.                         $hosts[$host] = ['scheme' => $tcpScheme'host' => $host'port' => 6379] + $parameters;
  121.                     } elseif ($port = (int) substr($host$i)) {
  122.                         $hosts[$host] = ['scheme' => $tcpScheme'host' => substr($host0$i), 'port' => $port] + $parameters;
  123.                     } else {
  124.                         $hosts[$host] = ['scheme' => 'unix''path' => substr($host0$i)] + $parameters;
  125.                     }
  126.                 }
  127.                 $hosts array_values($hosts);
  128.             }
  129.         }
  130.         if (isset($params['host']) || isset($params['path'])) {
  131.             if (!isset($params['dbindex']) && isset($params['path'])) {
  132.                 if (preg_match('#/(\d+)?$#'$params['path'], $m)) {
  133.                     $params['dbindex'] = $m[1] ?? '0';
  134.                     $params['path'] = substr($params['path'], 0, -\strlen($m[0]));
  135.                 } elseif (isset($params['host'])) {
  136.                     throw new InvalidArgumentException(sprintf('Invalid Redis DSN: "%s", the "dbindex" parameter must be a number.'$dsn));
  137.                 }
  138.             }
  139.             if (isset($params['host'])) {
  140.                 array_unshift($hosts, ['scheme' => $tcpScheme'host' => $params['host'], 'port' => $params['port'] ?? 6379]);
  141.             } else {
  142.                 array_unshift($hosts, ['scheme' => 'unix''path' => $params['path']]);
  143.             }
  144.         }
  145.         if (!$hosts) {
  146.             throw new InvalidArgumentException(sprintf('Invalid Redis DSN: "%s".'$dsn));
  147.         }
  148.         $params += $query $options self::$defaultConnectionOptions;
  149.         if (isset($params['redis_sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class)) {
  150.             throw new CacheException(sprintf('Redis Sentinel support requires the "predis/predis" package or the "redis" extension v5.2 or higher: "%s".'$dsn));
  151.         }
  152.         if (isset($params['lazy'])) {
  153.             $params['lazy'] = filter_var($params['lazy'], \FILTER_VALIDATE_BOOLEAN);
  154.         }
  155.         $params['redis_cluster'] = filter_var($params['redis_cluster'], \FILTER_VALIDATE_BOOLEAN);
  156.         if ($params['redis_cluster'] && isset($params['redis_sentinel'])) {
  157.             throw new InvalidArgumentException(sprintf('Cannot use both "redis_cluster" and "redis_sentinel" at the same time: "%s".'$dsn));
  158.         }
  159.         if (null === $params['class'] && \extension_loaded('redis')) {
  160.             $class $params['redis_cluster'] ? \RedisCluster::class : (\count($hosts) && !isset($params['redis_sentinel']) ? \RedisArray::class : \Redis::class);
  161.         } else {
  162.             $class $params['class'] ?? \Predis\Client::class;
  163.             if (isset($params['redis_sentinel']) && !is_a($class\Predis\Client::class, true) && !class_exists(\RedisSentinel::class)) {
  164.                 throw new CacheException(sprintf('Cannot use Redis Sentinel: class "%s" does not extend "Predis\Client" and ext-redis >= 5.2 not found: "%s".'$class$dsn));
  165.             }
  166.         }
  167.         if (is_a($class\Redis::class, true)) {
  168.             $connect $params['persistent'] || $params['persistent_id'] ? 'pconnect' 'connect';
  169.             $redis = new $class();
  170.             $initializer = static function ($redis) use ($connect$params$dsn$auth$hosts$tls) {
  171.                 $hostIndex 0;
  172.                 do {
  173.                     $host $hosts[$hostIndex]['host'] ?? $hosts[$hostIndex]['path'];
  174.                     $port $hosts[$hostIndex]['port'] ?? 0;
  175.                     $address false;
  176.                     if (isset($hosts[$hostIndex]['host']) && $tls) {
  177.                         $host 'tls://'.$host;
  178.                     }
  179.                     if (!isset($params['redis_sentinel'])) {
  180.                         break;
  181.                     }
  182.                     $extra = [];
  183.                     if (\defined('Redis::OPT_NULL_MULTIBULK_AS_NULL') && isset($params['auth'])) {
  184.                         $extra = [$params['auth']];
  185.                     }
  186.                     $sentinel = new \RedisSentinel($host$port$params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...$extra);
  187.                     if ($address $sentinel->getMasterAddrByName($params['redis_sentinel'])) {
  188.                         [$host$port] = $address;
  189.                     }
  190.                 } while (++$hostIndex \count($hosts) && !$address);
  191.                 if (isset($params['redis_sentinel']) && !$address) {
  192.                     throw new InvalidArgumentException(sprintf('Failed to retrieve master information from sentinel "%s" and dsn "%s".'$params['redis_sentinel'], $dsn));
  193.                 }
  194.                 try {
  195.                     $extra = [
  196.                         'stream' => $params['ssl'] ?? null,
  197.                     ];
  198.                     if (isset($params['auth'])) {
  199.                         $extra['auth'] = $params['auth'];
  200.                     }
  201.                     @$redis->{$connect}($host$port$params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...\defined('Redis::SCAN_PREFIX') ? [$extra] : []);
  202.                     set_error_handler(function ($type$msg) use (&$error) { $error $msg; });
  203.                     try {
  204.                         $isConnected $redis->isConnected();
  205.                     } finally {
  206.                         restore_error_handler();
  207.                     }
  208.                     if (!$isConnected) {
  209.                         $error preg_match('/^Redis::p?connect\(\): (.*)/'$error ?? ''$error) ? sprintf(' (%s)'$error[1]) : '';
  210.                         throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: '$dsn).$error.'.');
  211.                     }
  212.                     if ((null !== $auth && !$redis->auth($auth))
  213.                         || ($params['dbindex'] && !$redis->select($params['dbindex']))
  214.                     ) {
  215.                         $e preg_replace('/^ERR /'''$redis->getLastError());
  216.                         throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: '$dsn).$e.'.');
  217.                     }
  218.                     if ($params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) {
  219.                         $redis->setOption(\Redis::OPT_TCP_KEEPALIVE$params['tcp_keepalive']);
  220.                     }
  221.                 } catch (\RedisException $e) {
  222.                     throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: '$dsn).$e->getMessage());
  223.                 }
  224.                 return true;
  225.             };
  226.             if ($params['lazy']) {
  227.                 $redis = new RedisProxy($redis$initializer);
  228.             } else {
  229.                 $initializer($redis);
  230.             }
  231.         } elseif (is_a($class\RedisArray::class, true)) {
  232.             foreach ($hosts as $i => $host) {
  233.                 switch ($host['scheme']) {
  234.                     case 'tcp'$hosts[$i] = $host['host'].':'.$host['port']; break;
  235.                     case 'tls'$hosts[$i] = 'tls://'.$host['host'].':'.$host['port']; break;
  236.                     default: $hosts[$i] = $host['path'];
  237.                 }
  238.             }
  239.             $params['lazy_connect'] = $params['lazy'] ?? true;
  240.             $params['connect_timeout'] = $params['timeout'];
  241.             try {
  242.                 $redis = new $class($hosts$params);
  243.             } catch (\RedisClusterException $e) {
  244.                 throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: '$dsn).$e->getMessage());
  245.             }
  246.             if ($params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) {
  247.                 $redis->setOption(\Redis::OPT_TCP_KEEPALIVE$params['tcp_keepalive']);
  248.             }
  249.         } elseif (is_a($class\RedisCluster::class, true)) {
  250.             $initializer = static function () use ($class$params$dsn$hosts) {
  251.                 foreach ($hosts as $i => $host) {
  252.                     switch ($host['scheme']) {
  253.                         case 'tcp'$hosts[$i] = $host['host'].':'.$host['port']; break;
  254.                         case 'tls'$hosts[$i] = 'tls://'.$host['host'].':'.$host['port']; break;
  255.                         default: $hosts[$i] = $host['path'];
  256.                     }
  257.                 }
  258.                 try {
  259.                     $redis = new $class(null$hosts$params['timeout'], $params['read_timeout'], (bool) $params['persistent'], $params['auth'] ?? '', ...\defined('Redis::SCAN_PREFIX') ? [$params['ssl'] ?? null] : []);
  260.                 } catch (\RedisClusterException $e) {
  261.                     throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: '$dsn).$e->getMessage());
  262.                 }
  263.                 if ($params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) {
  264.                     $redis->setOption(\Redis::OPT_TCP_KEEPALIVE$params['tcp_keepalive']);
  265.                 }
  266.                 switch ($params['failover']) {
  267.                     case 'error'$redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER\RedisCluster::FAILOVER_ERROR); break;
  268.                     case 'distribute'$redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER\RedisCluster::FAILOVER_DISTRIBUTE); break;
  269.                     case 'slaves'$redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER\RedisCluster::FAILOVER_DISTRIBUTE_SLAVES); break;
  270.                 }
  271.                 return $redis;
  272.             };
  273.             $redis $params['lazy'] ? new RedisClusterProxy($initializer) : $initializer();
  274.         } elseif (is_a($class\Predis\ClientInterface::class, true)) {
  275.             if ($params['redis_cluster']) {
  276.                 $params['cluster'] = 'redis';
  277.             } elseif (isset($params['redis_sentinel'])) {
  278.                 $params['replication'] = 'sentinel';
  279.                 $params['service'] = $params['redis_sentinel'];
  280.             }
  281.             $params += ['parameters' => []];
  282.             $params['parameters'] += [
  283.                 'persistent' => $params['persistent'],
  284.                 'timeout' => $params['timeout'],
  285.                 'read_write_timeout' => $params['read_timeout'],
  286.                 'tcp_nodelay' => true,
  287.             ];
  288.             if ($params['dbindex']) {
  289.                 $params['parameters']['database'] = $params['dbindex'];
  290.             }
  291.             if (null !== $auth) {
  292.                 $params['parameters']['password'] = $auth;
  293.             }
  294.             if (isset($params['ssl'])) {
  295.                 foreach ($hosts as $i => $host) {
  296.                     if (!isset($host['ssl'])) {
  297.                         $hosts[$i]['ssl'] = $params['ssl'];
  298.                     }
  299.                 }
  300.             }
  301.             if (=== \count($hosts) && !($params['redis_cluster'] || $params['redis_sentinel'])) {
  302.                 $hosts $hosts[0];
  303.             } elseif (\in_array($params['failover'], ['slaves''distribute'], true) && !isset($params['replication'])) {
  304.                 $params['replication'] = true;
  305.                 $hosts[0] += ['alias' => 'master'];
  306.             }
  307.             $params['exceptions'] = false;
  308.             $redis = new $class($hostsarray_diff_key($paramsself::$defaultConnectionOptions));
  309.             if (isset($params['redis_sentinel'])) {
  310.                 $redis->getConnection()->setSentinelTimeout($params['timeout']);
  311.             }
  312.         } elseif (class_exists($classfalse)) {
  313.             throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis", "RedisArray", "RedisCluster" nor "Predis\ClientInterface".'$class));
  314.         } else {
  315.             throw new InvalidArgumentException(sprintf('Class "%s" does not exist.'$class));
  316.         }
  317.         return $redis;
  318.     }
  319.     /**
  320.      * {@inheritdoc}
  321.      */
  322.     protected function doFetch(array $ids)
  323.     {
  324.         if (!$ids) {
  325.             return [];
  326.         }
  327.         $result = [];
  328.         if ($this->redis instanceof \Predis\ClientInterface && $this->redis->getConnection() instanceof ClusterInterface) {
  329.             $values $this->pipeline(function () use ($ids) {
  330.                 foreach ($ids as $id) {
  331.                     yield 'get' => [$id];
  332.                 }
  333.             });
  334.         } else {
  335.             $values $this->redis->mget($ids);
  336.             if (!\is_array($values) || \count($values) !== \count($ids)) {
  337.                 return [];
  338.             }
  339.             $values array_combine($ids$values);
  340.         }
  341.         foreach ($values as $id => $v) {
  342.             if ($v) {
  343.                 $result[$id] = $this->marshaller->unmarshall($v);
  344.             }
  345.         }
  346.         return $result;
  347.     }
  348.     /**
  349.      * {@inheritdoc}
  350.      */
  351.     protected function doHave(string $id)
  352.     {
  353.         return (bool) $this->redis->exists($id);
  354.     }
  355.     /**
  356.      * {@inheritdoc}
  357.      */
  358.     protected function doClear(string $namespace)
  359.     {
  360.         if ($this->redis instanceof \Predis\ClientInterface) {
  361.             $prefix $this->redis->getOptions()->prefix $this->redis->getOptions()->prefix->getPrefix() : '';
  362.             $prefixLen \strlen($prefix ?? '');
  363.         }
  364.         $cleared true;
  365.         $hosts $this->getHosts();
  366.         $host reset($hosts);
  367.         if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) {
  368.             // Predis supports info command only on the master in replication environments
  369.             $hosts = [$host->getClientFor('master')];
  370.         }
  371.         foreach ($hosts as $host) {
  372.             if (!isset($namespace[0])) {
  373.                 $cleared $host->flushDb() && $cleared;
  374.                 continue;
  375.             }
  376.             $info $host->info('Server');
  377.             $info = !$info instanceof ErrorInterface $info['Server'] ?? $info : ['redis_version' => '2.0'];
  378.             if (!$host instanceof \Predis\ClientInterface) {
  379.                 $prefix \defined('Redis::SCAN_PREFIX') && (\Redis::SCAN_PREFIX $host->getOption(\Redis::OPT_SCAN)) ? '' $host->getOption(\Redis::OPT_PREFIX);
  380.                 $prefixLen \strlen($host->getOption(\Redis::OPT_PREFIX) ?? '');
  381.             }
  382.             $pattern $prefix.$namespace.'*';
  383.             if (!version_compare($info['redis_version'], '2.8''>=')) {
  384.                 // As documented in Redis documentation (http://redis.io/commands/keys) using KEYS
  385.                 // can hang your server when it is executed against large databases (millions of items).
  386.                 // Whenever you hit this scale, you should really consider upgrading to Redis 2.8 or above.
  387.                 $unlink version_compare($info['redis_version'], '4.0''>=') ? 'UNLINK' 'DEL';
  388.                 $args $this->redis instanceof \Predis\ClientInterface ? [0$pattern] : [[$pattern], 0];
  389.                 $cleared $host->eval("local keys=redis.call('KEYS',ARGV[1]) for i=1,#keys,5000 do redis.call('$unlink',unpack(keys,i,math.min(i+4999,#keys))) end return 1"$args[0], $args[1]) && $cleared;
  390.                 continue;
  391.             }
  392.             $cursor null;
  393.             do {
  394.                 $keys $host instanceof \Predis\ClientInterface $host->scan($cursor'MATCH'$pattern'COUNT'1000) : $host->scan($cursor$pattern1000);
  395.                 if (isset($keys[1]) && \is_array($keys[1])) {
  396.                     $cursor $keys[0];
  397.                     $keys $keys[1];
  398.                 }
  399.                 if ($keys) {
  400.                     if ($prefixLen) {
  401.                         foreach ($keys as $i => $key) {
  402.                             $keys[$i] = substr($key$prefixLen);
  403.                         }
  404.                     }
  405.                     $this->doDelete($keys);
  406.                 }
  407.             } while ($cursor = (int) $cursor);
  408.         }
  409.         return $cleared;
  410.     }
  411.     /**
  412.      * {@inheritdoc}
  413.      */
  414.     protected function doDelete(array $ids)
  415.     {
  416.         if (!$ids) {
  417.             return true;
  418.         }
  419.         if ($this->redis instanceof \Predis\ClientInterface && $this->redis->getConnection() instanceof ClusterInterface) {
  420.             static $del;
  421.             $del $del ?? (class_exists(UNLINK::class) ? 'unlink' 'del');
  422.             $this->pipeline(function () use ($ids$del) {
  423.                 foreach ($ids as $id) {
  424.                     yield $del => [$id];
  425.                 }
  426.             })->rewind();
  427.         } else {
  428.             static $unlink true;
  429.             if ($unlink) {
  430.                 try {
  431.                     $unlink false !== $this->redis->unlink($ids);
  432.                 } catch (\Throwable $e) {
  433.                     $unlink false;
  434.                 }
  435.             }
  436.             if (!$unlink) {
  437.                 $this->redis->del($ids);
  438.             }
  439.         }
  440.         return true;
  441.     }
  442.     /**
  443.      * {@inheritdoc}
  444.      */
  445.     protected function doSave(array $valuesint $lifetime)
  446.     {
  447.         if (!$values $this->marshaller->marshall($values$failed)) {
  448.             return $failed;
  449.         }
  450.         $results $this->pipeline(function () use ($values$lifetime) {
  451.             foreach ($values as $id => $value) {
  452.                 if (>= $lifetime) {
  453.                     yield 'set' => [$id$value];
  454.                 } else {
  455.                     yield 'setEx' => [$id$lifetime$value];
  456.                 }
  457.             }
  458.         });
  459.         foreach ($results as $id => $result) {
  460.             if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) {
  461.                 $failed[] = $id;
  462.             }
  463.         }
  464.         return $failed;
  465.     }
  466.     private function pipeline(\Closure $generatorobject $redis null): \Generator
  467.     {
  468.         $ids = [];
  469.         $redis $redis ?? $this->redis;
  470.         if ($redis instanceof RedisClusterProxy || $redis instanceof \RedisCluster || ($redis instanceof \Predis\ClientInterface && $redis->getConnection() instanceof RedisCluster)) {
  471.             // phpredis & predis don't support pipelining with RedisCluster
  472.             // see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining
  473.             // see https://github.com/nrk/predis/issues/267#issuecomment-123781423
  474.             $results = [];
  475.             foreach ($generator() as $command => $args) {
  476.                 $results[] = $redis->{$command}(...$args);
  477.                 $ids[] = 'eval' === $command ? ($redis instanceof \Predis\ClientInterface $args[2] : $args[1][0]) : $args[0];
  478.             }
  479.         } elseif ($redis instanceof \Predis\ClientInterface) {
  480.             $results $redis->pipeline(static function ($redis) use ($generator, &$ids) {
  481.                 foreach ($generator() as $command => $args) {
  482.                     $redis->{$command}(...$args);
  483.                     $ids[] = 'eval' === $command $args[2] : $args[0];
  484.                 }
  485.             });
  486.         } elseif ($redis instanceof \RedisArray) {
  487.             $connections $results $ids = [];
  488.             foreach ($generator() as $command => $args) {
  489.                 $id 'eval' === $command $args[1][0] : $args[0];
  490.                 if (!isset($connections[$h $redis->_target($id)])) {
  491.                     $connections[$h] = [$redis->_instance($h), -1];
  492.                     $connections[$h][0]->multi(\Redis::PIPELINE);
  493.                 }
  494.                 $connections[$h][0]->{$command}(...$args);
  495.                 $results[] = [$h, ++$connections[$h][1]];
  496.                 $ids[] = $id;
  497.             }
  498.             foreach ($connections as $h => $c) {
  499.                 $connections[$h] = $c[0]->exec();
  500.             }
  501.             foreach ($results as $k => [$h$c]) {
  502.                 $results[$k] = $connections[$h][$c];
  503.             }
  504.         } else {
  505.             $redis->multi(\Redis::PIPELINE);
  506.             foreach ($generator() as $command => $args) {
  507.                 $redis->{$command}(...$args);
  508.                 $ids[] = 'eval' === $command $args[1][0] : $args[0];
  509.             }
  510.             $results $redis->exec();
  511.         }
  512.         if (!$redis instanceof \Predis\ClientInterface && 'eval' === $command && $redis->getLastError()) {
  513.             $e = new \RedisException($redis->getLastError());
  514.             $results array_map(function ($v) use ($e) { return false === $v $e $v; }, (array) $results);
  515.         }
  516.         if (\is_bool($results)) {
  517.             return;
  518.         }
  519.         foreach ($ids as $k => $id) {
  520.             yield $id => $results[$k];
  521.         }
  522.     }
  523.     private function getHosts(): array
  524.     {
  525.         $hosts = [$this->redis];
  526.         if ($this->redis instanceof \Predis\ClientInterface) {
  527.             $connection $this->redis->getConnection();
  528.             if ($connection instanceof ClusterInterface && $connection instanceof \Traversable) {
  529.                 $hosts = [];
  530.                 foreach ($connection as $c) {
  531.                     $hosts[] = new \Predis\Client($c);
  532.                 }
  533.             }
  534.         } elseif ($this->redis instanceof \RedisArray) {
  535.             $hosts = [];
  536.             foreach ($this->redis->_hosts() as $host) {
  537.                 $hosts[] = $this->redis->_instance($host);
  538.             }
  539.         } elseif ($this->redis instanceof RedisClusterProxy || $this->redis instanceof \RedisCluster) {
  540.             $hosts = [];
  541.             foreach ($this->redis->_masters() as $host) {
  542.                 $hosts[] = new RedisClusterNodeProxy($host$this->redis);
  543.             }
  544.         }
  545.         return $hosts;
  546.     }
  547. }