afa 5 meses atrás
pai
commit
c4abffbcb4
100 arquivos alterados com 8581 adições e 18 exclusões
  1. 6 6
      app/api/controller/Base.php
  2. 22 5
      app/api/controller/Shops.php
  3. 1 1
      app/api/service/auth/ApiAuthService.php
  4. 1 2
      app/api/service/auth/MysqlAdapter.php
  5. 2 1
      composer.json
  6. 70 1
      composer.lock
  7. 1 0
      vendor/composer/autoload_psr4.php
  8. 5 0
      vendor/composer/autoload_static.php
  9. 72 0
      vendor/composer/installed.json
  10. 11 2
      vendor/composer/installed.php
  11. 22 0
      vendor/predis/predis/LICENSE
  12. 648 0
      vendor/predis/predis/README.md
  13. 12 0
      vendor/predis/predis/autoload.php
  14. 53 0
      vendor/predis/predis/composer.json
  15. 64 0
      vendor/predis/predis/src/Autoloader.php
  16. 629 0
      vendor/predis/predis/src/Client.php
  17. 42 0
      vendor/predis/predis/src/ClientConfiguration.php
  18. 400 0
      vendor/predis/predis/src/ClientContextInterface.php
  19. 20 0
      vendor/predis/predis/src/ClientException.php
  20. 456 0
      vendor/predis/predis/src/ClientInterface.php
  21. 512 0
      vendor/predis/predis/src/Cluster/ClusterStrategy.php
  22. 81 0
      vendor/predis/predis/src/Cluster/Distributor/DistributorInterface.php
  23. 22 0
      vendor/predis/predis/src/Cluster/Distributor/EmptyRingException.php
  24. 268 0
      vendor/predis/predis/src/Cluster/Distributor/HashRing.php
  25. 70 0
      vendor/predis/predis/src/Cluster/Distributor/KetamaRing.php
  26. 73 0
      vendor/predis/predis/src/Cluster/Hash/CRC16.php
  27. 29 0
      vendor/predis/predis/src/Cluster/Hash/HashGeneratorInterface.php
  28. 40 0
      vendor/predis/predis/src/Cluster/NullSlotRange.php
  29. 75 0
      vendor/predis/predis/src/Cluster/PredisStrategy.php
  30. 55 0
      vendor/predis/predis/src/Cluster/RedisStrategy.php
  31. 209 0
      vendor/predis/predis/src/Cluster/SimpleSlotMap.php
  32. 417 0
      vendor/predis/predis/src/Cluster/SlotMap.php
  33. 145 0
      vendor/predis/predis/src/Cluster/SlotRange.php
  34. 60 0
      vendor/predis/predis/src/Cluster/StrategyInterface.php
  35. 196 0
      vendor/predis/predis/src/Collection/Iterator/CursorBasedIterator.php
  36. 57 0
      vendor/predis/predis/src/Collection/Iterator/HashKey.php
  37. 42 0
      vendor/predis/predis/src/Collection/Iterator/Keyspace.php
  38. 183 0
      vendor/predis/predis/src/Collection/Iterator/ListKey.php
  39. 46 0
      vendor/predis/predis/src/Collection/Iterator/SetKey.php
  40. 57 0
      vendor/predis/predis/src/Collection/Iterator/SortedSetKey.php
  41. 26 0
      vendor/predis/predis/src/Command/Argument/ArrayableArgument.php
  42. 46 0
      vendor/predis/predis/src/Command/Argument/Geospatial/AbstractBy.php
  43. 43 0
      vendor/predis/predis/src/Command/Argument/Geospatial/ByBox.php
  44. 19 0
      vendor/predis/predis/src/Command/Argument/Geospatial/ByInterface.php
  45. 37 0
      vendor/predis/predis/src/Command/Argument/Geospatial/ByRadius.php
  46. 19 0
      vendor/predis/predis/src/Command/Argument/Geospatial/FromInterface.php
  47. 42 0
      vendor/predis/predis/src/Command/Argument/Geospatial/FromLonLat.php
  48. 36 0
      vendor/predis/predis/src/Command/Argument/Geospatial/FromMember.php
  49. 161 0
      vendor/predis/predis/src/Command/Argument/Search/AggregateArguments.php
  50. 17 0
      vendor/predis/predis/src/Command/Argument/Search/AlterArguments.php
  51. 182 0
      vendor/predis/predis/src/Command/Argument/Search/CommonArguments.php
  52. 191 0
      vendor/predis/predis/src/Command/Argument/Search/CreateArguments.php
  53. 44 0
      vendor/predis/predis/src/Command/Argument/Search/CursorArguments.php
  54. 43 0
      vendor/predis/predis/src/Command/Argument/Search/DropArguments.php
  55. 17 0
      vendor/predis/predis/src/Command/Argument/Search/ExplainArguments.php
  56. 81 0
      vendor/predis/predis/src/Command/Argument/Search/ProfileArguments.php
  57. 75 0
      vendor/predis/predis/src/Command/Argument/Search/SchemaFields/AbstractField.php
  58. 22 0
      vendor/predis/predis/src/Command/Argument/Search/SchemaFields/FieldInterface.php
  59. 33 0
      vendor/predis/predis/src/Command/Argument/Search/SchemaFields/GeoField.php
  60. 57 0
      vendor/predis/predis/src/Command/Argument/Search/SchemaFields/GeoShapeField.php
  61. 33 0
      vendor/predis/predis/src/Command/Argument/Search/SchemaFields/NumericField.php
  62. 51 0
      vendor/predis/predis/src/Command/Argument/Search/SchemaFields/TagField.php
  63. 65 0
      vendor/predis/predis/src/Command/Argument/Search/SchemaFields/TextField.php
  64. 47 0
      vendor/predis/predis/src/Command/Argument/Search/SchemaFields/VectorField.php
  65. 306 0
      vendor/predis/predis/src/Command/Argument/Search/SearchArguments.php
  66. 59 0
      vendor/predis/predis/src/Command/Argument/Search/SpellcheckArguments.php
  67. 28 0
      vendor/predis/predis/src/Command/Argument/Search/SugAddArguments.php
  68. 41 0
      vendor/predis/predis/src/Command/Argument/Search/SugGetArguments.php
  69. 17 0
      vendor/predis/predis/src/Command/Argument/Search/SynUpdateArguments.php
  70. 19 0
      vendor/predis/predis/src/Command/Argument/Server/LimitInterface.php
  71. 42 0
      vendor/predis/predis/src/Command/Argument/Server/LimitOffsetCount.php
  72. 57 0
      vendor/predis/predis/src/Command/Argument/Server/To.php
  73. 49 0
      vendor/predis/predis/src/Command/Argument/Stream/XInfoStreamOptions.php
  74. 30 0
      vendor/predis/predis/src/Command/Argument/TimeSeries/AddArguments.php
  75. 17 0
      vendor/predis/predis/src/Command/Argument/TimeSeries/AlterArguments.php
  76. 162 0
      vendor/predis/predis/src/Command/Argument/TimeSeries/CommonArguments.php
  77. 17 0
      vendor/predis/predis/src/Command/Argument/TimeSeries/CreateArguments.php
  78. 17 0
      vendor/predis/predis/src/Command/Argument/TimeSeries/DecrByArguments.php
  79. 17 0
      vendor/predis/predis/src/Command/Argument/TimeSeries/GetArguments.php
  80. 41 0
      vendor/predis/predis/src/Command/Argument/TimeSeries/IncrByArguments.php
  81. 43 0
      vendor/predis/predis/src/Command/Argument/TimeSeries/InfoArguments.php
  82. 17 0
      vendor/predis/predis/src/Command/Argument/TimeSeries/MGetArguments.php
  83. 44 0
      vendor/predis/predis/src/Command/Argument/TimeSeries/MRangeArguments.php
  84. 85 0
      vendor/predis/predis/src/Command/Argument/TimeSeries/RangeArguments.php
  85. 200 0
      vendor/predis/predis/src/Command/Command.php
  86. 103 0
      vendor/predis/predis/src/Command/CommandInterface.php
  87. 31 0
      vendor/predis/predis/src/Command/Container/ACL.php
  88. 42 0
      vendor/predis/predis/src/Command/Container/AbstractContainer.php
  89. 32 0
      vendor/predis/predis/src/Command/Container/CLIENT.php
  90. 29 0
      vendor/predis/predis/src/Command/Container/CLUSTER.php
  91. 81 0
      vendor/predis/predis/src/Command/Container/ContainerFactory.php
  92. 33 0
      vendor/predis/predis/src/Command/Container/ContainerInterface.php
  93. 33 0
      vendor/predis/predis/src/Command/Container/FUNCTIONS.php
  94. 27 0
      vendor/predis/predis/src/Command/Container/Json/JSONDEBUG.php
  95. 29 0
      vendor/predis/predis/src/Command/Container/Search/FTCONFIG.php
  96. 29 0
      vendor/predis/predis/src/Command/Container/Search/FTCURSOR.php
  97. 30 0
      vendor/predis/predis/src/Command/Container/XGROUP.php
  98. 28 0
      vendor/predis/predis/src/Command/Container/XINFO.php
  99. 143 0
      vendor/predis/predis/src/Command/Factory.php
  100. 42 0
      vendor/predis/predis/src/Command/FactoryInterface.php

+ 6 - 6
app/api/controller/Base.php

@@ -45,10 +45,8 @@ class Base
      * @access protected
      */
     protected function _initialize()
-    {
-      
+    {   
         $token=request()->header('token');
-
         $actionname = $this->request->action();
         $noNeedLoginSet=is_string($this->noNeedLogin)?[$this->noNeedLogin]:$this->noNeedLogin;
         $noNeedLogin = in_array('*',$noNeedLoginSet) || in_array($actionname,$noNeedLoginSet);
@@ -61,9 +59,11 @@ class Base
        
         //获取用户信息
         if(!$noNeedLogin) {
-            $this->userinfo = MysqlAdapter::userinfo($token);
-            if(!$this->userinfo) Response::create(__('请先登录!'), 'html', 401);
-            $this->userinfo = Cache::get('user_info_'.$this->userinfo);
+            $uid= MysqlAdapter::userinfo($token);
+            if(!$uid) Response::create(__('请先登录!'), 'html', 401);
+            $userinfo = Cache::store('redis')->get('user_info_'.$uid);
+            if(!$userinfo) Response::create(__('请先登录!'), 'html', 401);
+            $this->userinfo = $userinfo;
         }
     }
 

+ 22 - 5
app/api/controller/Shops.php

@@ -5,9 +5,9 @@ namespace app\api\controller;
 use app\common\model\UserEnterLog;
 use app\api\validate\Shop as ShopValidate;
 use app\common\model\ShopList;
-use app\common\mode\ShopDelivery;
+use app\common\model\ShopDelivery;
 use think\Exception;
-use think\facade\Request;
+use think\facade\Cache;
 use think\exception\ValidateException;
 
 class Shops extends Base
@@ -47,16 +47,33 @@ class Shops extends Base
     }
 
     //发货记录
-    public function delivery(UserEnterLog $userEnterLog){
-
+    public function delivery(ShopDelivery $shopDelivery){
 
+        $data = $this->request->post();
+        $result = false;
+        try {
+            validate(ShopValidate::class)->scene('add')->check($data);
+           
+            $data['user_id'] = $this->userinfo['id'];
+            $data['variety'] = json_encode($data['variety'], JSON_UNESCAPED_UNICODE);
+            $result = $shopDelivery::create($data);
+            
+        }catch (ValidateException $e) {
+            return $this->error($e->getError());
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+        if ($result === false) {
+            $this->error(__('没有新增任何数据'));
+        }
+        $this->success();
 
     }
 
     //添加记录
     public function create(UserEnterLog $userEnterLog)
     {
-    
+ 
         $data = $this->request->post();
         $result = false;
         try {

+ 1 - 1
app/api/service/auth/ApiAuthService.php

@@ -36,7 +36,7 @@ class ApiAuthService
         $user->loginip = request()->ip();
         $user->save();
 
-        Cache::set('user_info_'.$user->id, $user->toArray(), Config::get('app.user_login.keepalive_time'));
+        Cache::store('redis')->set('user_info_'.$user->id, $user->toArray(), Config::get('app.user_login.keepalive_time'));
         return ['userinfo'=>$user,'token'=>$token];
     }
 

+ 1 - 2
app/api/service/auth/MysqlAdapter.php

@@ -41,7 +41,6 @@ class MysqlAdapter
             $usertoken=UserToken::where('user_id',$user->id)->where('expire','>',$time)->order('id','asc')->find();
             $usertoken->delete();
         }
-
         return $token ;
     }
 
@@ -49,6 +48,6 @@ class MysqlAdapter
     {   
         $token = request()->header('token');
         UserToken::where('token', $token)->delete();
-        Cache::delete('user_info_'.$uid);
+        Cache::store('redis')->delete('user_info_'.$uid);
     }
 }

+ 2 - 1
composer.json

@@ -30,7 +30,8 @@
         "symfony/http-foundation": "^6.3",
         "phpoffice/phpspreadsheet": "^1.29",
         "topthink/think-captcha": "^3.0",
-        "topthink/think-cors": "^1.0"
+        "topthink/think-cors": "^1.0",
+        "predis/predis": "^3.0"
     },
     "require-dev": {
         "symfony/var-dumper": ">=4.2"

+ 70 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "bbed793c5d6b8394cf660d85e47a23e1",
+    "content-hash": "0070e2e721e6eafdd1232dea9e03e199",
     "packages": [
         {
             "name": "composer/pcre",
@@ -764,6 +764,75 @@
             },
             "time": "2025-02-08T02:56:14+00:00"
         },
+        {
+            "name": "predis/predis",
+            "version": "v3.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/predis/predis.git",
+                "reference": "34fb0a7da0330df1bab4280fcac4afdeeccc3edf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/predis/predis/zipball/34fb0a7da0330df1bab4280fcac4afdeeccc3edf",
+                "reference": "34fb0a7da0330df1bab4280fcac4afdeeccc3edf",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": "^7.2 || ^8.0",
+                "psr/http-message": "^1.0|^2.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^3.3",
+                "phpstan/phpstan": "^1.9",
+                "phpunit/phpcov": "^6.0 || ^8.0",
+                "phpunit/phpunit": "^8.0 || ~9.4.4"
+            },
+            "suggest": {
+                "ext-relay": "Faster connection with in-memory caching (>=0.6.2)"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Predis\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Till Krüss",
+                    "homepage": "https://till.im",
+                    "role": "Maintainer"
+                }
+            ],
+            "description": "A flexible and feature-complete Redis/Valkey client for PHP.",
+            "homepage": "http://github.com/predis/predis",
+            "keywords": [
+                "nosql",
+                "predis",
+                "redis"
+            ],
+            "support": {
+                "issues": "https://github.com/predis/predis/issues",
+                "source": "https://github.com/predis/predis/tree/v3.0.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sponsors/tillkruss",
+                    "type": "github"
+                }
+            ],
+            "time": "2025-05-16T18:30:32+00:00"
+        },
         {
             "name": "psr/cache",
             "version": "1.0.1",

+ 1 - 0
vendor/composer/autoload_psr4.php

@@ -24,6 +24,7 @@ return array(
     'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
     'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
     'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
+    'Predis\\' => array($vendorDir . '/predis/predis/src'),
     'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'),
     'Matrix\\' => array($vendorDir . '/markbaker/matrix/classes/src'),
     'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),

+ 5 - 0
vendor/composer/autoload_static.php

@@ -51,6 +51,7 @@ class ComposerStaticInitda8298e611526d4409180cd4bea64bcc
             'Psr\\Http\\Client\\' => 16,
             'Psr\\Container\\' => 14,
             'Psr\\Cache\\' => 10,
+            'Predis\\' => 7,
             'PhpOffice\\PhpSpreadsheet\\' => 25,
         ),
         'M' => 
@@ -154,6 +155,10 @@ class ComposerStaticInitda8298e611526d4409180cd4bea64bcc
         array (
             0 => __DIR__ . '/..' . '/psr/cache/src',
         ),
+        'Predis\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/predis/predis/src',
+        ),
         'PhpOffice\\PhpSpreadsheet\\' => 
         array (
             0 => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet',

+ 72 - 0
vendor/composer/installed.json

@@ -770,6 +770,78 @@
             },
             "install-path": "../phpoffice/phpspreadsheet"
         },
+        {
+            "name": "predis/predis",
+            "version": "v3.0.1",
+            "version_normalized": "3.0.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/predis/predis.git",
+                "reference": "34fb0a7da0330df1bab4280fcac4afdeeccc3edf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/predis/predis/zipball/34fb0a7da0330df1bab4280fcac4afdeeccc3edf",
+                "reference": "34fb0a7da0330df1bab4280fcac4afdeeccc3edf",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": "^7.2 || ^8.0",
+                "psr/http-message": "^1.0|^2.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^3.3",
+                "phpstan/phpstan": "^1.9",
+                "phpunit/phpcov": "^6.0 || ^8.0",
+                "phpunit/phpunit": "^8.0 || ~9.4.4"
+            },
+            "suggest": {
+                "ext-relay": "Faster connection with in-memory caching (>=0.6.2)"
+            },
+            "time": "2025-05-16T18:30:32+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Predis\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Till Krüss",
+                    "homepage": "https://till.im",
+                    "role": "Maintainer"
+                }
+            ],
+            "description": "A flexible and feature-complete Redis/Valkey client for PHP.",
+            "homepage": "http://github.com/predis/predis",
+            "keywords": [
+                "nosql",
+                "predis",
+                "redis"
+            ],
+            "support": {
+                "issues": "https://github.com/predis/predis/issues",
+                "source": "https://github.com/predis/predis/tree/v3.0.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sponsors/tillkruss",
+                    "type": "github"
+                }
+            ],
+            "install-path": "../predis/predis"
+        },
         {
             "name": "psr/cache",
             "version": "1.0.1",

+ 11 - 2
vendor/composer/installed.php

@@ -5,7 +5,7 @@
         'type' => 'project',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
-        'reference' => '23ea46551d04a04801572b25ee7958a879de42ab',
+        'reference' => 'e29333d61654e8d6e72a65e42b000ce868dc1b81',
         'name' => 'topthink/think',
         'dev' => true,
     ),
@@ -100,6 +100,15 @@
             'reference' => 'c80041b1628c4f18030407134fe88303661d4e4e',
             'dev_requirement' => false,
         ),
+        'predis/predis' => array(
+            'pretty_version' => 'v3.0.1',
+            'version' => '3.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../predis/predis',
+            'aliases' => array(),
+            'reference' => '34fb0a7da0330df1bab4280fcac4afdeeccc3edf',
+            'dev_requirement' => false,
+        ),
         'psr/cache' => array(
             'pretty_version' => '1.0.1',
             'version' => '1.0.1.0',
@@ -223,7 +232,7 @@
             'type' => 'project',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
-            'reference' => '23ea46551d04a04801572b25ee7958a879de42ab',
+            'reference' => 'e29333d61654e8d6e72a65e42b000ce868dc1b81',
             'dev_requirement' => false,
         ),
         'topthink/think-annotation' => array(

+ 22 - 0
vendor/predis/predis/LICENSE

@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2009-2020 Daniele Alessandri (original work)
+Copyright (c) 2021-2024 Till Krüss (modified work)
+
+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.

+ 648 - 0
vendor/predis/predis/README.md

@@ -0,0 +1,648 @@
+# Predis #
+
+[![Software license][ico-license]](LICENSE)
+[![Latest stable][ico-version-stable]][link-releases]
+[![Latest development][ico-version-dev]][link-releases]
+[![Monthly installs][ico-downloads-monthly]][link-downloads]
+[![Build status][ico-build]][link-actions]
+[![Coverage Status][ico-coverage]][link-coverage]
+
+A flexible and feature-complete [Redis](http://redis.io) / [Valkey](https://github.com/valkey-io/valkey) client for PHP 7.2 and newer.
+
+More details about this project can be found on the [frequently asked questions](FAQ.md).
+
+
+## Main features ##
+
+- Support for Redis from __3.0__ to __8.0__.
+- Support for clustering using client-side sharding and pluggable keyspace distributors.
+- Support for [redis-cluster](http://redis.io/topics/cluster-tutorial) (Redis >= 3.0).
+- Support for master-slave replication setups and [redis-sentinel](http://redis.io/topics/sentinel).
+- Transparent key prefixing of keys using a customizable prefix strategy.
+- Command pipelining on both single nodes and clusters (client-side sharding only).
+- Abstraction for Redis transactions (Redis >= 2.0) and CAS operations (Redis >= 2.2).
+- Abstraction for Lua scripting (Redis >= 2.6) and automatic switching between `EVALSHA` or `EVAL`.
+- Abstraction for `SCAN`, `SSCAN`, `ZSCAN` and `HSCAN` (Redis >= 2.8) based on PHP iterators.
+- Connections are established lazily by the client upon the first command and can be persisted.
+- Connections can be established via TCP/IP (also TLS/SSL-encrypted) or UNIX domain sockets.
+- Support for custom connection classes for providing different network or protocol backends.
+- Flexible system for defining custom commands and override the default ones.
+
+
+## How to _install_ and use Predis ##
+
+This library can be found on [Packagist](http://packagist.org/packages/predis/predis) for an easier
+management of projects dependencies using [Composer](http://packagist.org/about-composer).
+Compressed archives of each release are [available on GitHub](https://github.com/predis/predis/releases).
+
+```shell
+composer require predis/predis
+```
+
+
+### Loading the library ###
+
+Predis relies on the autoloading features of PHP to load its files when needed and complies with the
+[PSR-4 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md).
+Autoloading is handled automatically when dependencies are managed through Composer, but it is also
+possible to leverage its own autoloader in projects or scripts lacking any autoload facility:
+
+```php
+// Prepend a base path if Predis is not available in your "include_path".
+require 'Predis/Autoloader.php';
+
+Predis\Autoloader::register();
+```
+
+
+### Connecting to Redis ###
+
+When creating a client instance without passing any connection parameter, Predis assumes `127.0.0.1`
+and `6379` as default host and port. The default timeout for the `connect()` operation is 5 seconds:
+
+```php
+$client = new Predis\Client();
+$client->set('foo', 'bar');
+$value = $client->get('foo');
+```
+
+Connection parameters can be supplied either in the form of URI strings or named arrays. The latter
+is the preferred way to supply parameters, but URI strings can be useful when parameters are read
+from non-structured or partially-structured sources:
+
+```php
+// Parameters passed using a named array:
+$client = new Predis\Client([
+    'scheme' => 'tcp',
+    'host'   => '10.0.0.1',
+    'port'   => 6379,
+]);
+
+// Same set of parameters, passed using an URI string:
+$client = new Predis\Client('tcp://10.0.0.1:6379');
+```
+
+Password protected servers can be accessed by adding `password` to the parameters set. When ACLs are
+enabled on Redis >= 6.0, both `username` and `password` are required for user authentication.
+
+It is also possible to connect to local instances of Redis using UNIX domain sockets, in this case
+the parameters must use the `unix` scheme and specify a path for the socket file:
+
+```php
+$client = new Predis\Client(['scheme' => 'unix', 'path' => '/path/to/redis.sock']);
+$client = new Predis\Client('unix:/path/to/redis.sock');
+```
+
+The client can leverage TLS/SSL encryption to connect to secured remote Redis instances without the
+need to configure an SSL proxy like stunnel. This can be useful when connecting to nodes running on
+various cloud hosting providers. Encryption can be enabled with using the `tls` scheme and an array
+of suitable [options](http://php.net/manual/context.ssl.php) passed via the `ssl` parameter:
+
+```php
+// Named array of connection parameters:
+$client = new Predis\Client([
+  'scheme' => 'tls',
+  'ssl'    => ['cafile' => 'private.pem', 'verify_peer' => true],
+]);
+
+// Same set of parameters, but using an URI string:
+$client = new Predis\Client('tls://127.0.0.1?ssl[cafile]=private.pem&ssl[verify_peer]=1');
+```
+
+The connection schemes [`redis`](http://www.iana.org/assignments/uri-schemes/prov/redis) (alias of
+`tcp`) and [`rediss`](http://www.iana.org/assignments/uri-schemes/prov/rediss) (alias of `tls`) are
+also supported, with the difference that URI strings containing these schemes are parsed following
+the rules described on their respective IANA provisional registration documents.
+
+The actual list of supported connection parameters can vary depending on each connection backend so
+it is recommended to refer to their specific documentation or implementation for details.
+
+Predis can aggregate multiple connections when providing an array of connection parameters and the
+appropriate option to instruct the client about how to aggregate them (clustering, replication or a
+custom aggregation logic). Named arrays and URI strings can be mixed when providing configurations
+for each node:
+
+```php
+$client = new Predis\Client([
+    'tcp://10.0.0.1?alias=first-node', ['host' => '10.0.0.2', 'alias' => 'second-node'],
+], [
+    'cluster' => 'predis',
+]);
+```
+
+See the [aggregate connections](#aggregate-connections) section of this document for more details.
+
+Connections to Redis are lazy meaning that the client connects to a server only if and when needed.
+While it is recommended to let the client do its own stuff under the hood, there may be times when
+it is still desired to have control of when the connection is opened or closed: this can easily be
+achieved by invoking `$client->connect()` and `$client->disconnect()`. Please note that the effect
+of these methods on aggregate connections may differ depending on each specific implementation.
+
+
+### Client configuration ###
+
+Many aspects and behaviors of the client can be configured by passing specific client options to the
+second argument of `Predis\Client::__construct()`:
+
+```php
+$client = new Predis\Client($parameters, ['prefix' => 'sample:']);
+```
+
+Options are managed using a mini DI-alike container and their values can be lazily initialized only
+when needed. The client options supported by default in Predis are:
+
+  - `prefix`: prefix string applied to every key found in commands.
+  - `exceptions`: whether the client should throw or return responses upon Redis errors.
+  - `connections`: list of connection backends or a connection factory instance.
+  - `cluster`: specifies a cluster backend (`predis`, `redis` or callable).
+  - `replication`: specifies a replication backend (`predis`, `sentinel` or callable).
+  - `aggregate`: configures the client with a custom aggregate connection (callable).
+  - `parameters`: list of default connection parameters for aggregate connections.
+  - `commands`: specifies a command factory instance to use through the library.
+  - `readTimeout`: (cluster only) Timeout between read operations while loop over connections.
+
+Users can also provide custom options with values or callable objects (for lazy initialization) that
+are stored in the options container for later use through the library.
+
+
+### Aggregate connections ###
+
+Aggregate connections are the foundation upon which Predis implements clustering and replication and
+they are used to group multiple connections to single Redis nodes and hide the specific logic needed
+to handle them properly depending on the context. Aggregate connections usually require an array of
+connection parameters along with the appropriate client option when creating a new client instance.
+
+#### Cluster ####
+
+Predis can be configured to work in clustering mode with a traditional client-side sharding approach
+to create a cluster of independent nodes and distribute the keyspace among them. This approach needs
+some sort of external health monitoring of nodes and requires the keyspace to be rebalanced manually
+when nodes are added or removed:
+
+```php
+$parameters = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
+$options    = ['cluster' => 'predis'];
+
+$client = new Predis\Client($parameters);
+```
+
+Along with Redis 3.0, a new supervised and coordinated type of clustering was introduced in the form
+of [redis-cluster](http://redis.io/topics/cluster-tutorial). This kind of approach uses a different
+algorithm to distribute the keyspaces, with Redis nodes coordinating themselves by communicating via
+a gossip protocol to handle health status, rebalancing, nodes discovery and request redirection. In
+order to connect to a cluster managed by redis-cluster, the client requires a list of its nodes (not
+necessarily complete since it will automatically discover new nodes if necessary) and the `cluster`
+client options set to `redis`:
+
+```php
+$parameters = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
+$options    = ['cluster' => 'redis'];
+
+$client = new Predis\Client($parameters, $options);
+```
+
+#### Redis Gears with cluster ####
+
+Since Redis v7.2, Redis Gears module is a part of Redis Stack bundle. Client supports a variety of
+Redis Gears commands that can be used with OSS cluster API. Currently, before using any Redis
+Gears commands against OSS cluster Redis server needs to be aware of cluster topology.
+
+`REDISGEARS_2.REFRESHCLUSTER` command should be called against **each master node** (read replicas
+should be ignored) **on cluster creation and each time cluster topology changes**.
+
+In most cases this actions should be performed from the CLI interface by the administrator, DevOPS
+or even Kubernetes, depends on your infrastructure managing process. However, client provides an API
+to do this programmatically.
+
+```php
+/** @var \Predis\Connection\Cluster\ClusterInterface $connection */
+$connection->executeCommandOnEachNode(
+    new \Predis\Command\RawCommand('REDISGEARS_2.REFRESHCLUSTER')
+);
+```
+
+#### Replication ####
+
+The client can be configured to operate in a single master / multiple slaves setup to provide better
+service availability. When using replication, Predis recognizes read-only commands and sends them to
+a random slave in order to provide some sort of load-balancing and switches to the master as soon as
+it detects a command that performs any kind of operation that would end up modifying the keyspace or
+the value of a key. Instead of raising a connection error when a slave fails, the client attempts to
+fall back to a different slave among the ones provided in the configuration.
+
+The basic configuration needed to use the client in replication mode requires one Redis server to be
+identified as the master (this can be done via connection parameters by setting the `role` parameter
+to `master`) and one or more slaves (in this case setting `role` to `slave` for slaves is optional):
+
+```php
+$parameters = ['tcp://10.0.0.1?role=master', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
+$options    = ['replication' => 'predis'];
+
+$client = new Predis\Client($parameters, $options);
+```
+
+The above configuration has a static list of servers and relies entirely on the client's logic, but
+it is possible to rely on [`redis-sentinel`](http://redis.io/topics/sentinel) for a more robust HA
+environment with sentinel servers acting as a source of authority for clients for service discovery.
+The minimum configuration required by the client to work with redis-sentinel is a list of connection
+parameters pointing to a bunch of sentinel instances, the `replication` option set to `sentinel` and
+the `service` option set to the name of the service:
+
+```php
+$sentinels = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
+$options   = ['replication' => 'sentinel', 'service' => 'mymaster'];
+
+$client = new Predis\Client($sentinels, $options);
+```
+
+If the master and slave nodes are configured to require an authentication from clients, a password
+must be provided via the global `parameters` client option. This option can also be used to specify
+a different database index. The client options array would then look like this:
+
+```php
+$options = [
+    'replication' => 'sentinel',
+    'service' => 'mymaster',
+    'parameters' => [
+        'password' => $secretpassword,
+        'database' => 10,
+    ],
+];
+```
+
+While Predis is able to distinguish commands performing write and read-only operations, `EVAL` and
+`EVALSHA` represent a corner case in which the client switches to the master node because it cannot
+tell when a Lua script is safe to be executed on slaves. While this is indeed the default behavior,
+when certain Lua scripts do not perform write operations it is possible to provide an hint to tell
+the client to stick with slaves for their execution:
+
+```php
+$parameters = ['tcp://10.0.0.1?role=master', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
+$options    = ['replication' => function () {
+    // Set scripts that won't trigger a switch from a slave to the master node.
+    $strategy = new Predis\Replication\ReplicationStrategy();
+    $strategy->setScriptReadOnly($LUA_SCRIPT);
+
+    return new Predis\Connection\Replication\MasterSlaveReplication($strategy);
+}];
+
+$client = new Predis\Client($parameters, $options);
+$client->eval($LUA_SCRIPT, 0);             // Sticks to slave using `eval`...
+$client->evalsha(sha1($LUA_SCRIPT), 0);    // ... and `evalsha`, too.
+```
+
+The [`examples`](examples/) directory contains a few scripts that demonstrate how the client can be
+configured and used to leverage replication in both basic and complex scenarios.
+
+
+### Command pipelines ###
+
+Pipelining can help with performances when many commands need to be sent to a server by reducing the
+latency introduced by network round-trip timings. Pipelining also works with aggregate connections.
+The client can execute the pipeline inside a callable block or return a pipeline instance with the
+ability to chain commands thanks to its fluent interface:
+
+```php
+// Executes a pipeline inside the given callable block:
+$responses = $client->pipeline(function ($pipe) {
+    for ($i = 0; $i < 1000; $i++) {
+        $pipe->set("key:$i", str_pad($i, 4, '0', 0));
+        $pipe->get("key:$i");
+    }
+});
+
+// Returns a pipeline that can be chained thanks to its fluent interface:
+$responses = $client->pipeline()->set('foo', 'bar')->get('foo')->execute();
+```
+
+
+### Transactions ###
+
+The client provides an abstraction for Redis transactions based on `MULTI` and `EXEC` with a similar
+interface to command pipelines:
+
+```php
+// Executes a transaction inside the given callable block:
+$responses = $client->transaction(function ($tx) {
+    $tx->set('foo', 'bar');
+    $tx->get('foo');
+});
+
+// Returns a transaction that can be chained thanks to its fluent interface:
+$responses = $client->transaction()->set('foo', 'bar')->get('foo')->execute();
+```
+
+This abstraction can perform check-and-set operations thanks to `WATCH` and `UNWATCH` and provides
+automatic retries of transactions aborted by Redis when `WATCH`ed keys are touched. For an example
+of a transaction using CAS you can see [the following example](examples/transaction_using_cas.php).
+
+#### Support for clustered connections ####
+
+Since Predis v3.0 transactions could be used with clustered connections. However, it has some limitations due to the
+fact that Redis doesn't support distributed transactions. All keys in the transaction context should operate on the same
+hash slot, due to this limitation it's recommended to use `{}` syntax to make sure that all keys will be mapped to the same hash
+slot. Apart from it no additional configuration needed on a client side.
+
+```php
+$redis = $this->getClient();
+
+$response = $redis->transaction(function (MultiExec $tx) {
+    $tx->set('{foo}foo', 'value');
+    $tx->set('{foo}bar', 'value');
+    $tx->set('{foo}baz', 'value');
+});
+
+// ['OK', 'OK', 'OK']
+```
+
+
+### Adding new commands ###
+
+While we try to update Predis to stay up to date with all the commands available in Redis, you might
+prefer to stick with an old version of the library or provide a different way to filter arguments or
+parse responses for specific commands. To achieve that, Predis provides the ability to implement new
+command classes to define or override commands in the default command factory used by the client:
+
+```php
+// Define a new command by extending Predis\Command\Command:
+class BrandNewRedisCommand extends Predis\Command\Command
+{
+    public function getId()
+    {
+        return 'NEWCMD';
+    }
+}
+
+// Inject your command in the current command factory:
+$client = new Predis\Client($parameters, [
+    'commands' => [
+        'newcmd' => 'BrandNewRedisCommand',
+    ],
+]);
+
+$response = $client->newcmd();
+```
+
+There is also a method to send raw commands without filtering their arguments or parsing responses.
+Users must provide the list of arguments for the command as an array, following the signatures as
+defined by the [Redis documentation for commands](http://redis.io/commands):
+
+```php
+$response = $client->executeRaw(['SET', 'foo', 'bar']);
+```
+
+
+### Script commands ###
+
+While it is possible to leverage [Lua scripting](http://redis.io/commands/eval) on Redis 2.6+ using
+directly [`EVAL`](http://redis.io/commands/eval) and [`EVALSHA`](http://redis.io/commands/evalsha),
+Predis offers script commands as an higher level abstraction built upon them to make things simple.
+Script commands can be registered in the command factory used by the client and are accessible as if
+they were plain Redis commands, but they define Lua scripts that get transmitted to the server for
+remote execution. Internally they use [`EVALSHA`](http://redis.io/commands/evalsha) by default and
+identify a script by its SHA1 hash to save bandwidth, but [`EVAL`](http://redis.io/commands/eval)
+is used as a fall back when needed:
+
+```php
+// Define a new script command by extending Predis\Command\ScriptCommand:
+class ListPushRandomValue extends Predis\Command\ScriptCommand
+{
+    public function getKeysCount()
+    {
+        return 1;
+    }
+
+    public function getScript()
+    {
+        return <<<LUA
+math.randomseed(ARGV[1])
+local rnd = tostring(math.random())
+redis.call('lpush', KEYS[1], rnd)
+return rnd
+LUA;
+    }
+}
+
+// Inject the script command in the current command factory:
+$client = new Predis\Client($parameters, [
+    'commands' => [
+        'lpushrand' => 'ListPushRandomValue',
+    ],
+]);
+
+$response = $client->lpushrand('random_values', $seed = mt_rand());
+```
+
+
+### Customizable connection backends ###
+
+Predis can use different connection backends to connect to Redis. The builtin Relay integration
+leverages the [Relay](https://github.com/cachewerk/relay) extension for PHP for major performance
+gains, by caching a partial replica of the Redis dataset in PHP shared runtime memory.
+
+```php
+$client = new Predis\Client('tcp://127.0.0.1', [
+    'connections' => 'relay',
+]);
+```
+
+Developers can create their own connection classes to support whole new network backends, extend
+existing classes or provide completely different implementations. Connection classes must implement
+`Predis\Connection\NodeConnectionInterface` or extend `Predis\Connection\AbstractConnection`:
+
+```php
+class MyConnectionClass implements Predis\Connection\NodeConnectionInterface
+{
+    // Implementation goes here...
+}
+
+// Use MyConnectionClass to handle connections for the `tcp` scheme:
+$client = new Predis\Client('tcp://127.0.0.1', [
+    'connections' => ['tcp' => 'MyConnectionClass'],
+]);
+```
+
+For a more in-depth insight on how to create new connection backends you can refer to the actual
+implementation of the standard connection classes available in the `Predis\Connection` namespace.
+
+## RESP3 ##
+
+### Connection ###
+To establish the connection using the [RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md) protocol, you need to set parameter `protocol => 3`. The default protocol is RESP2.
+
+You can pass parameter as configuration option in array or as a query parameter in `redis_url`
+
+```php
+  // Configuration option
+  $client = new \Predis\Client(['protocol' => 3]);
+
+  // Redis URL
+  $client = new \Predis\Client('redis://localhost:6379?protocol=3');
+
+  // ["proto" => "3"]
+  $client->executeRaw(['HELLO']);
+```
+
+### Command responses ###
+RESP3 protocol introduce a variety of new [response types](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#resp3-types),
+so on the client-side we have more explicit understanding on data types we retrieve from server. Here's some examples to show the difference
+between RESP2 and RESP3 responses.
+
+#### Float responses ####
+``` php
+// RESP2 connection
+$client = new \Predis\Client();
+
+$client->geoadd('my_geo', 11.111, 22.222, 'member1');
+
+// [[0 => string(20) "11.11099988222122192", 1 => string(20) "22.22200052541037252"]]
+// RESP2 returns float values as simple strings.
+var_dump($client->geopos('my_geo', ['member1']));
+
+// RESP3 connection
+$client = new \Predis\Client(['protocol' => 3]);
+
+// [[0 => float(11.110999882221222), 1 => float(22.222000525410373)]]
+// RESP3 introduces new double type, that corresponds to PHP float.
+var_dump($client->geopos('my_geo', ['member1']));
+```
+
+#### Aggregate types ####
+In RESP3 new aggregate type [Map](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#map-type)
+was introduced, that represents the sequence of field-value pairs. So it simplifies parsing, since we don't need to specify
+parsing strategy per command (RESP2) and instead relies on the type defined by protocol (RESP3).
+
+In most cases RESP2 responses shouldn't differ from RESP3, since we added additional parsing for those
+command that return field-value pairs. However, since RESP2 requires additional parsing, it could be that some commands
+had lack of it and return unhandled responses. In this case there would be difference like this:
+
+```php
+$client = new \Predis\Client();
+
+// RESP2: ['field', 'value]
+$client->commandThatReturnsFieldValuePair('key');
+
+$client = new \Predis\Client(['protocol' => 3]);
+
+// RESP3: ['field' => 'value]
+$client->commandThatReturnsFieldValuePair('key');
+```
+
+Feel free to open PR or GitHub issue if you face those protocol mismatching.
+
+### Push notifications ###
+RESP3 introduce a concept of [push connection](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#push-type),
+is the one where server could send asynchronous data to client which was not explicitly requested. Predis 3.0 provides
+an API to establish this kind of connection as separate blocking process (worker) and invoke callbacks depends on push
+notification message type.
+
+#### Consumer ####
+First of all, you need to set up a consumer connection and provide an optional callback that will be executed before
+event loop will be started. It allows you to subscribe on channels, enable keys invalidations tracking or enable monitor
+connection, any Redis command to let server know that you want to receive push notification within this connection.
+
+```php
+// Make sure that RESP3 protocol enabled and read_write_timeout set 0,
+// so connection won't be killed by timeout.
+$client = new Predis\Client(['read_write_timeout' => 0, 'protocol' => 3]);
+
+// Create push notifications consumer.
+// Provides callback where current consumer subscribes to few channels before
+// enter the loop.
+$push = $client->push(static function (ClientInterface $client) {
+    $response = $client->subscribe('channel', 'control');
+    $status = ($response[2] === 1) ? 'OK' : 'FAILED';
+    echo "Channel subscription status: {$status}\n";
+});
+```
+
+#### Dispatcher loop ####
+Dispatcher object allows you to attach a callback to given push notification type and run the actual worker process that
+listen for incoming push notifications. To be able to stop blocking process in runtime you can specify a condition and
+call `$dispatcher->stop()` method from given callback. In this example we're waiting for specific message `terminate`
+within `control` channel that we subscribed to before entering the loop.
+
+```php
+// Storage for incoming notifications.
+$messages = [];
+
+// Create dispatcher for push notifications.
+$dispatcher = new Predis\Consumer\Push\DispatcherLoop($push);
+
+$dispatcher->attachCallback(
+    PushResponseInterface::MESSAGE_DATA_TYPE,
+    static function (array $payload, DispatcherLoopInterface $dispatcher) {
+        global $messages;
+        [$channel, $message] = $payload;
+
+        if ($channel === 'control' && $message === 'terminate') {
+            echo "Terminating notification consumer.\n";
+            $dispatcher->stop();
+
+            return;
+        }
+
+        $messages[] = $message;
+        echo "Received message: {$message}\n";
+    }
+);
+
+// Run consumer loop with attached callbacks.
+$dispatcher->run();
+
+// Count all messages that were received during consumer loop.
+$messagesCount = count($messages);
+echo "We received: {$messagesCount} messages\n";
+```
+
+This example shows a simple script to count all incoming messages from push notifications that we receive from
+subscribed channels until stop condition will be met. Examples available in `examples/` folder.
+
+### Sharded pub/sub ###
+From Redis 7.0, sharded Pub/Sub is introduced in which shard channels are assigned to slots by the same algorithm used
+to assign keys to slots.
+
+Predis 3.0 provides an API that allows to use pub/sub for Cluster connections using sharded pub/sub from Redis.
+You don't need to specify any additional configuration to enable sharded pub/sub, it will be automatically enabled if
+Cluster connection is using.
+
+Implementation looks pretty much the same as Push notification, so you need to set up consumer
+and run it over Dispatcher loop object. All examples available in `examples/` folder.
+## Development ##
+
+
+### Reporting bugs and contributing code ###
+
+Contributions to Predis are highly appreciated either in the form of pull requests for new features,
+bug fixes, or just bug reports. We only ask you to adhere to issue and pull request templates.
+
+
+### Test suite ###
+
+__ATTENTION__: Do not ever run the test suite shipped with Predis against instances of Redis running
+in production environments or containing data you are interested in!
+
+Predis has a comprehensive test suite covering every aspect of the library and that can optionally
+perform integration tests against a running instance of Redis (required >= 2.4.0 in order to verify
+the correct behavior of the implementation of each command. Integration tests for unsupported Redis
+commands are automatically skipped. If you do not have Redis up and running, integration tests can
+be disabled. See [the tests README](tests/README.md) for more details about testing this library.
+
+Predis uses GitHub Actions for continuous integration and the history for past and current builds can be
+found [on its actions page](https://github.com/predis/predis/actions).
+
+### License ###
+
+The code for Predis is distributed under the terms of the MIT license (see [LICENSE](LICENSE)).
+
+[ico-license]: https://img.shields.io/github/license/predis/predis.svg?style=flat-square
+[ico-version-stable]: https://img.shields.io/github/v/tag/predis/predis?label=stable&style=flat-square
+[ico-version-dev]: https://img.shields.io/github/v/tag/predis/predis?include_prereleases&label=pre-release&style=flat-square
+[ico-downloads-monthly]: https://img.shields.io/packagist/dm/predis/predis.svg?style=flat-square
+[ico-build]: https://img.shields.io/github/actions/workflow/status/predis/predis/tests.yml?branch=main&style=flat-square
+[ico-coverage]: https://img.shields.io/coverallsCoverage/github/predis/predis?style=flat-square
+
+[link-releases]: https://github.com/predis/predis/releases
+[link-actions]: https://github.com/predis/predis/actions
+[link-downloads]: https://packagist.org/packages/predis/predis/stats
+[link-coverage]: https://coveralls.io/github/predis/predis

+ 12 - 0
vendor/predis/predis/autoload.php

@@ -0,0 +1,12 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+require __DIR__.'/src/Autoloader.php';
+
+Predis\Autoloader::register();

+ 53 - 0
vendor/predis/predis/composer.json

@@ -0,0 +1,53 @@
+{
+    "name": "predis/predis",
+    "type": "library",
+    "description": "A flexible and feature-complete Redis/Valkey client for PHP.",
+    "keywords": ["nosql", "redis", "predis"],
+    "homepage": "http://github.com/predis/predis",
+    "license": "MIT",
+    "support": {
+        "issues": "https://github.com/predis/predis/issues"
+    },
+    "authors": [
+        {
+            "name": "Till Krüss",
+            "homepage": "https://till.im",
+            "role": "Maintainer"
+        }
+    ],
+    "funding": [
+        {
+            "type": "github",
+            "url": "https://github.com/sponsors/tillkruss"
+        }
+    ],
+    "require": {
+        "php": "^7.2 || ^8.0",
+        "psr/http-message": "^1.0|^2.0"
+    },
+    "require-dev": {
+        "friendsofphp/php-cs-fixer": "^3.3",
+        "phpstan/phpstan": "^1.9",
+        "phpunit/phpunit": "^8.0 || ~9.4.4",
+        "phpunit/phpcov": "^6.0 || ^8.0"
+    },
+    "suggest": {
+        "ext-relay": "Faster connection with in-memory caching (>=0.6.2)"
+    },
+    "scripts": {
+        "phpstan": "phpstan analyse",
+        "style": "php-cs-fixer fix --diff --dry-run",
+        "style:fix": "php-cs-fixer fix"
+    },
+    "autoload": {
+        "psr-4": {
+            "Predis\\": "src/"
+        }
+    },
+    "config": {
+        "sort-packages": true,
+        "preferred-install": "dist"
+    },
+    "minimum-stability": "dev",
+    "prefer-stable": true
+}

+ 64 - 0
vendor/predis/predis/src/Autoloader.php

@@ -0,0 +1,64 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis;
+
+/**
+ * Implements a lightweight PSR-0 compliant autoloader for Predis.
+ *
+ * @author Eric Naeseth <eric@thumbtack.com>
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ * @codeCoverageIgnore
+ */
+class Autoloader
+{
+    private $directory;
+    private $prefix;
+    private $prefixLength;
+
+    /**
+     * @param string $baseDirectory Base directory where the source files are located.
+     */
+    public function __construct($baseDirectory = __DIR__)
+    {
+        $this->directory = $baseDirectory;
+        $this->prefix = __NAMESPACE__ . '\\';
+        $this->prefixLength = strlen($this->prefix);
+    }
+
+    /**
+     * Registers the autoloader class with the PHP SPL autoloader.
+     *
+     * @param bool $prepend Prepend the autoloader on the stack instead of appending it.
+     */
+    public static function register($prepend = false)
+    {
+        spl_autoload_register([new self(), 'autoload'], true, $prepend);
+    }
+
+    /**
+     * Loads a class from a file using its fully qualified name.
+     *
+     * @param string $className Fully qualified name of a class.
+     */
+    public function autoload($className)
+    {
+        if (0 === strpos($className, $this->prefix)) {
+            $parts = explode('\\', substr($className, $this->prefixLength));
+            $filepath = $this->directory . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts) . '.php';
+
+            if (is_file($filepath)) {
+                require $filepath;
+            }
+        }
+    }
+}

+ 629 - 0
vendor/predis/predis/src/Client.php

@@ -0,0 +1,629 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis;
+
+use ArrayIterator;
+use InvalidArgumentException;
+use IteratorAggregate;
+use Predis\Command\CommandInterface;
+use Predis\Command\Container\ContainerFactory;
+use Predis\Command\Container\ContainerInterface;
+use Predis\Command\RawCommand;
+use Predis\Command\ScriptCommand;
+use Predis\Configuration\Options;
+use Predis\Configuration\OptionsInterface;
+use Predis\Connection\ConnectionInterface;
+use Predis\Connection\Parameters;
+use Predis\Connection\ParametersInterface;
+use Predis\Connection\RelayConnection;
+use Predis\Consumer\PubSub\Consumer as PubSubConsumer;
+use Predis\Consumer\PubSub\RelayConsumer as RelayPubSubConsumer;
+use Predis\Consumer\Push\Consumer as PushConsumer;
+use Predis\Monitor\Consumer as MonitorConsumer;
+use Predis\Pipeline\Atomic;
+use Predis\Pipeline\FireAndForget;
+use Predis\Pipeline\Pipeline;
+use Predis\Pipeline\RelayAtomic;
+use Predis\Pipeline\RelayPipeline;
+use Predis\Response\ErrorInterface as ErrorResponseInterface;
+use Predis\Response\ResponseInterface;
+use Predis\Response\ServerException;
+use Predis\Transaction\MultiExec as MultiExecTransaction;
+use ReturnTypeWillChange;
+use RuntimeException;
+use Traversable;
+
+/**
+ * Client class used for connecting and executing commands on Redis.
+ *
+ * This is the main high-level abstraction of Predis upon which various other
+ * abstractions are built. Internally it aggregates various other classes each
+ * one with its own responsibility and scope.
+ *
+ * @template-implements \IteratorAggregate<string, static>
+ */
+class Client implements ClientInterface, IteratorAggregate
+{
+    public const VERSION = '3.0.1';
+
+    /** @var OptionsInterface */
+    private $options;
+
+    /** @var ConnectionInterface */
+    private $connection;
+
+    /** @var Command\FactoryInterface */
+    private $commands;
+
+    /**
+     * @param mixed $parameters Connection parameters for one or more servers.
+     * @param mixed $options    Options to configure some behaviours of the client.
+     */
+    public function __construct($parameters = null, $options = null)
+    {
+        $this->options = static::createOptions($options ?? new Options());
+        $this->connection = static::createConnection($this->options, $parameters ?? new Parameters());
+        $this->commands = $this->options->commands;
+    }
+
+    /**
+     * Creates a new set of client options for the client.
+     *
+     * @param array|OptionsInterface $options Set of client options
+     *
+     * @return OptionsInterface
+     * @throws InvalidArgumentException
+     */
+    protected static function createOptions($options)
+    {
+        if (is_array($options)) {
+            return new Options($options);
+        } elseif ($options instanceof OptionsInterface) {
+            return $options;
+        } else {
+            throw new InvalidArgumentException('Invalid type for client options');
+        }
+    }
+
+    /**
+     * Creates single or aggregate connections from supplied arguments.
+     *
+     * This method accepts the following types to create a connection instance:
+     *
+     *  - Array (dictionary: single connection, indexed: aggregate connections)
+     *  - String (URI for a single connection)
+     *  - Callable (connection initializer callback)
+     *  - Instance of Predis\Connection\ParametersInterface (used as-is)
+     *  - Instance of Predis\Connection\ConnectionInterface (returned as-is)
+     *
+     * When a callable is passed, it receives the original set of client options
+     * and must return an instance of Predis\Connection\ConnectionInterface.
+     *
+     * Connections are created using the connection factory (in case of single
+     * connections) or a specialized aggregate connection initializer (in case
+     * of cluster and replication) retrieved from the supplied client options.
+     *
+     * @param OptionsInterface $options    Client options container
+     * @param mixed            $parameters Connection parameters
+     *
+     * @return ConnectionInterface
+     * @throws InvalidArgumentException
+     */
+    protected static function createConnection(OptionsInterface $options, $parameters)
+    {
+        if ($parameters instanceof ConnectionInterface) {
+            return $parameters;
+        }
+
+        if ($parameters instanceof ParametersInterface || is_string($parameters)) {
+            return $options->connections->create($parameters);
+        }
+
+        if (is_array($parameters)) {
+            if (!isset($parameters[0])) {
+                return $options->connections->create($parameters);
+            } elseif ($options->defined('cluster') && $initializer = $options->cluster) {
+                return $initializer($parameters, true);
+            } elseif ($options->defined('replication') && $initializer = $options->replication) {
+                return $initializer($parameters, true);
+            } elseif ($options->defined('aggregate') && $initializer = $options->aggregate) {
+                return $initializer($parameters, false);
+            } else {
+                throw new InvalidArgumentException(
+                    'Array of connection parameters requires `cluster`, `replication` or `aggregate` client option'
+                );
+            }
+        }
+
+        if (is_callable($parameters)) {
+            $connection = call_user_func($parameters, $options);
+
+            if (!$connection instanceof ConnectionInterface) {
+                throw new InvalidArgumentException('Callable parameters must return a valid connection');
+            }
+
+            return $connection;
+        }
+
+        throw new InvalidArgumentException('Invalid type for connection parameters');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getCommandFactory()
+    {
+        return $this->commands;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getOptions()
+    {
+        return $this->options;
+    }
+
+    /**
+     * Creates a new client using a specific underlying connection.
+     *
+     * This method allows to create a new client instance by picking a specific
+     * connection out of an aggregate one, with the same options of the original
+     * client instance.
+     *
+     * The specified selector defines which logic to use to look for a suitable
+     * connection by the specified value. Supported selectors are:
+     *
+     *   - `id`
+     *   - `key`
+     *   - `slot`
+     *   - `command`
+     *   - `alias`
+     *   - `role`
+     *
+     * Internally the client relies on duck-typing and follows this convention:
+     *
+     *   $selector string => getConnectionBy$selector($value) method
+     *
+     * This means that support for specific selectors may vary depending on the
+     * actual logic implemented by connection classes and there is no interface
+     * binding a connection class to implement any of these.
+     *
+     * @param string $selector Type of selector.
+     * @param mixed  $value    Value to be used by the selector.
+     *
+     * @return ClientInterface
+     */
+    public function getClientBy($selector, $value)
+    {
+        $selector = strtolower($selector);
+
+        if (!in_array($selector, ['id', 'key', 'slot', 'role', 'alias', 'command'])) {
+            throw new InvalidArgumentException("Invalid selector type: `$selector`");
+        }
+
+        if (!method_exists($this->connection, $method = "getConnectionBy$selector")) {
+            $class = get_class($this->connection);
+            throw new InvalidArgumentException("Selecting connection by $selector is not supported by $class");
+        }
+
+        if (!$connection = $this->connection->$method($value)) {
+            throw new InvalidArgumentException("Cannot find a connection by $selector matching `$value`");
+        }
+
+        return new static($connection, $this->getOptions());
+    }
+
+    /**
+     * Opens the underlying connection and connects to the server.
+     */
+    public function connect()
+    {
+        $this->connection->connect();
+    }
+
+    /**
+     * Closes the underlying connection and disconnects from the server.
+     */
+    public function disconnect()
+    {
+        $this->connection->disconnect();
+    }
+
+    /**
+     * Closes the underlying connection and disconnects from the server.
+     *
+     * This is the same as `Client::disconnect()` as it does not actually send
+     * the `QUIT` command to Redis, but simply closes the connection.
+     */
+    public function quit()
+    {
+        $this->disconnect();
+    }
+
+    /**
+     * Returns the current state of the underlying connection.
+     *
+     * @return bool
+     */
+    public function isConnected()
+    {
+        return $this->connection->isConnected();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getConnection()
+    {
+        return $this->connection;
+    }
+
+    /**
+     * Applies the configured serializer and compression to given value.
+     *
+     * @param  mixed  $value
+     * @return string
+     */
+    public function pack($value)
+    {
+        return $this->connection instanceof RelayConnection
+            ? $this->connection->pack($value)
+            : $value;
+    }
+
+    /**
+     * Deserializes and decompresses to given value.
+     *
+     * @param  mixed  $value
+     * @return string
+     */
+    public function unpack($value)
+    {
+        return $this->connection instanceof RelayConnection
+            ? $this->connection->unpack($value)
+            : $value;
+    }
+
+    /**
+     * Executes a command without filtering its arguments, parsing the response,
+     * applying any prefix to keys or throwing exceptions on Redis errors even
+     * regardless of client options.
+     *
+     * It is possible to identify Redis error responses from normal responses
+     * using the second optional argument which is populated by reference.
+     *
+     * @param array $arguments Command arguments as defined by the command signature.
+     * @param bool  $error     Set to TRUE when Redis returned an error response.
+     *
+     * @return mixed
+     */
+    public function executeRaw(array $arguments, &$error = null)
+    {
+        $error = false;
+        $commandID = array_shift($arguments);
+
+        $response = $this->connection->executeCommand(
+            new RawCommand($commandID, $arguments)
+        );
+
+        if ($response instanceof ResponseInterface) {
+            if ($response instanceof ErrorResponseInterface) {
+                $error = true;
+            }
+
+            return (string) $response;
+        }
+
+        return $response;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __call($commandID, $arguments)
+    {
+        return $this->executeCommand(
+            $this->createCommand($commandID, $arguments)
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function createCommand($commandID, $arguments = [])
+    {
+        return $this->commands->create($commandID, $arguments);
+    }
+
+    /**
+     * @param  string             $name
+     * @return ContainerInterface
+     */
+    public function __get(string $name)
+    {
+        return ContainerFactory::create($this, $name);
+    }
+
+    /**
+     * @param  string $name
+     * @param  mixed  $value
+     * @return mixed
+     */
+    public function __set(string $name, $value)
+    {
+        throw new RuntimeException('Not allowed');
+    }
+
+    /**
+     * @param  string $name
+     * @return mixed
+     */
+    public function __isset(string $name)
+    {
+        throw new RuntimeException('Not allowed');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function executeCommand(CommandInterface $command)
+    {
+        $response = $this->connection->executeCommand($command);
+        $parameters = $this->connection->getParameters();
+
+        if ($response instanceof ResponseInterface) {
+            if ($response instanceof ErrorResponseInterface) {
+                $response = $this->onErrorResponse($command, $response);
+            }
+
+            return $response;
+        }
+
+        if ($parameters->protocol === 2) {
+            return $command->parseResponse($response);
+        }
+
+        return $command->parseResp3Response($response);
+    }
+
+    /**
+     * Handles -ERR responses returned by Redis.
+     *
+     * @param CommandInterface       $command  Redis command that generated the error.
+     * @param ErrorResponseInterface $response Instance of the error response.
+     *
+     * @return mixed
+     * @throws ServerException
+     */
+    protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $response)
+    {
+        if ($command instanceof ScriptCommand && $response->getErrorType() === 'NOSCRIPT') {
+            $response = $this->executeCommand($command->getEvalCommand());
+
+            if (!$response instanceof ResponseInterface) {
+                $response = $command->parseResponse($response);
+            }
+
+            return $response;
+        }
+
+        if ($this->options->exceptions) {
+            throw new ServerException($response->getMessage());
+        }
+
+        return $response;
+    }
+
+    /**
+     * Executes the specified initializer method on `$this` by adjusting the
+     * actual invocation depending on the arity (0, 1 or 2 arguments). This is
+     * simply an utility method to create Redis contexts instances since they
+     * follow a common initialization path.
+     *
+     * @param string $initializer Method name.
+     * @param array  $argv        Arguments for the method.
+     *
+     * @return mixed
+     */
+    private function sharedContextFactory($initializer, $argv = null)
+    {
+        switch (count($argv)) {
+            case 0:
+                return $this->$initializer();
+
+            case 1:
+                return is_array($argv[0])
+                    ? $this->$initializer($argv[0])
+                    : $this->$initializer(null, $argv[0]);
+
+            case 2:
+                [$arg0, $arg1] = $argv;
+
+                return $this->$initializer($arg0, $arg1);
+
+            default:
+                return $this->$initializer($this, $argv);
+        }
+    }
+
+    /**
+     * Creates a new pipeline context and returns it, or returns the results of
+     * a pipeline executed inside the optionally provided callable object.
+     *
+     * @param mixed ...$arguments Array of options, a callable for execution, or both.
+     *
+     * @return Pipeline|array
+     */
+    public function pipeline(...$arguments)
+    {
+        return $this->sharedContextFactory('createPipeline', func_get_args());
+    }
+
+    /**
+     * Actual pipeline context initializer method.
+     *
+     * @param array|null $options  Options for the context.
+     * @param mixed      $callable Optional callable used to execute the context.
+     *
+     * @return Pipeline|array
+     */
+    protected function createPipeline(?array $options = null, $callable = null)
+    {
+        if (isset($options['atomic']) && $options['atomic']) {
+            $class = Atomic::class;
+        } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) {
+            $class = FireAndForget::class;
+        } else {
+            $class = Pipeline::class;
+        }
+
+        if ($this->connection instanceof RelayConnection) {
+            if (isset($options['atomic']) && $options['atomic']) {
+                $class = RelayAtomic::class;
+            } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) {
+                throw new NotSupportedException('The "relay" extension does not support fire-and-forget pipelines.');
+            } else {
+                $class = RelayPipeline::class;
+            }
+        }
+
+        /*
+         * @var ClientContextInterface
+         */
+        $pipeline = new $class($this);
+
+        if (isset($callable)) {
+            return $pipeline->execute($callable);
+        }
+
+        return $pipeline;
+    }
+
+    /**
+     * Creates a new transaction context and returns it, or returns the results
+     * of a transaction executed inside the optionally provided callable object.
+     *
+     * @param mixed ...$arguments Array of options, a callable for execution, or both.
+     *
+     * @return MultiExecTransaction|array
+     */
+    public function transaction(...$arguments)
+    {
+        return $this->sharedContextFactory('createTransaction', func_get_args());
+    }
+
+    /**
+     * Actual transaction context initializer method.
+     *
+     * @param array|null $options  Options for the context.
+     * @param mixed      $callable Optional callable used to execute the context.
+     *
+     * @return MultiExecTransaction|array
+     */
+    protected function createTransaction(?array $options = null, $callable = null)
+    {
+        $transaction = new MultiExecTransaction($this, $options);
+
+        if (isset($callable)) {
+            return $transaction->execute($callable);
+        }
+
+        return $transaction;
+    }
+
+    /**
+     * Creates a new publish/subscribe context and returns it, or starts its loop
+     * inside the optionally provided callable object.
+     *
+     * @param mixed ...$arguments Array of options, a callable for execution, or both.
+     *
+     * @return PubSubConsumer|null
+     */
+    public function pubSubLoop(...$arguments)
+    {
+        return $this->sharedContextFactory('createPubSub', func_get_args());
+    }
+
+    /**
+     * Creates new push notifications consumer.
+     *
+     * @param  callable|null $preLoopCallback Callback that should be called on client before enter a loop.
+     * @return PushConsumer
+     */
+    public function push(?callable $preLoopCallback = null): PushConsumer
+    {
+        return new PushConsumer($this, $preLoopCallback);
+    }
+
+    /**
+     * Actual publish/subscribe context initializer method.
+     *
+     * @param array|null $options  Options for the context.
+     * @param mixed      $callable Optional callable used to execute the context.
+     *
+     * @return PubSubConsumer|null
+     */
+    protected function createPubSub(?array $options = null, $callable = null)
+    {
+        if ($this->connection instanceof RelayConnection) {
+            $pubsub = new RelayPubSubConsumer($this, $options);
+        } else {
+            $pubsub = new PubSubConsumer($this, $options);
+        }
+
+        if (!isset($callable)) {
+            return $pubsub;
+        }
+
+        foreach ($pubsub as $message) {
+            if (call_user_func($callable, $pubsub, $message) === false) {
+                $pubsub->stop();
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Creates a new monitor consumer and returns it.
+     *
+     * @return MonitorConsumer
+     */
+    public function monitor()
+    {
+        return new MonitorConsumer($this);
+    }
+
+    /**
+     * @return Traversable<string, static>
+     */
+    #[ReturnTypeWillChange]
+    public function getIterator()
+    {
+        $clients = [];
+        $connection = $this->getConnection();
+
+        if (!$connection instanceof Traversable) {
+            return new ArrayIterator([
+                (string) $connection => new static($connection, $this->getOptions()),
+            ]);
+        }
+
+        foreach ($connection as $node) {
+            $clients[(string) $node] = new static($node, $this->getOptions());
+        }
+
+        return new ArrayIterator($clients);
+    }
+}

+ 42 - 0
vendor/predis/predis/src/ClientConfiguration.php

@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis;
+
+class ClientConfiguration
+{
+    /**
+     * @var array{modules: array}|string[][]
+     */
+    private static $config = [
+        'modules' => [
+            ['name' => 'Json', 'commandPrefix' => 'JSON'],
+            ['name' => 'BloomFilter', 'commandPrefix' => 'BF'],
+            ['name' => 'CuckooFilter', 'commandPrefix' => 'CF'],
+            ['name' => 'CountMinSketch', 'commandPrefix' => 'CMS'],
+            ['name' => 'TDigest', 'commandPrefix' => 'TDIGEST'],
+            ['name' => 'TopK', 'commandPrefix' => 'TOPK'],
+            ['name' => 'Search', 'commandPrefix' => 'FT'],
+            ['name' => 'TimeSeries', 'commandPrefix' => 'TS'],
+        ],
+    ];
+
+    /**
+     * Returns available modules with configuration.
+     *
+     * @return array|string[][]
+     */
+    public static function getModules(): array
+    {
+        return self::$config['modules'];
+    }
+}

+ 400 - 0
vendor/predis/predis/src/ClientContextInterface.php

@@ -0,0 +1,400 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis;
+
+use Predis\Command\Argument\Geospatial\ByInterface;
+use Predis\Command\Argument\Geospatial\FromInterface;
+use Predis\Command\Argument\Search\AggregateArguments;
+use Predis\Command\Argument\Search\AlterArguments;
+use Predis\Command\Argument\Search\CreateArguments;
+use Predis\Command\Argument\Search\DropArguments;
+use Predis\Command\Argument\Search\ExplainArguments;
+use Predis\Command\Argument\Search\ProfileArguments;
+use Predis\Command\Argument\Search\SchemaFields\FieldInterface;
+use Predis\Command\Argument\Search\SearchArguments;
+use Predis\Command\Argument\Search\SugAddArguments;
+use Predis\Command\Argument\Search\SugGetArguments;
+use Predis\Command\Argument\Search\SynUpdateArguments;
+use Predis\Command\Argument\Server\LimitOffsetCount;
+use Predis\Command\Argument\Server\To;
+use Predis\Command\Argument\TimeSeries\AddArguments;
+use Predis\Command\Argument\TimeSeries\AlterArguments as TSAlterArguments;
+use Predis\Command\Argument\TimeSeries\CreateArguments as TSCreateArguments;
+use Predis\Command\Argument\TimeSeries\DecrByArguments;
+use Predis\Command\Argument\TimeSeries\GetArguments;
+use Predis\Command\Argument\TimeSeries\IncrByArguments;
+use Predis\Command\Argument\TimeSeries\InfoArguments;
+use Predis\Command\Argument\TimeSeries\MGetArguments;
+use Predis\Command\Argument\TimeSeries\MRangeArguments;
+use Predis\Command\Argument\TimeSeries\RangeArguments;
+use Predis\Command\CommandInterface;
+use Predis\Command\Container\ACL;
+use Predis\Command\Container\CLIENT;
+use Predis\Command\Container\FUNCTIONS;
+use Predis\Command\Container\Json\JSONDEBUG;
+use Predis\Command\Container\Search\FTCONFIG;
+use Predis\Command\Container\Search\FTCURSOR;
+use Predis\Command\Container\XGROUP;
+
+/**
+ * Interface defining a client-side context such as a pipeline or transaction.
+ *
+ * @method $this copy(string $source, string $destination, int $db = -1, bool $replace = false)
+ * @method $this del(array|string $keys)
+ * @method $this dump($key)
+ * @method $this exists($key)
+ * @method $this expire($key, $seconds, string $expireOption = '')
+ * @method $this expireat($key, $timestamp, string $expireOption = '')
+ * @method $this expiretime(string $key)
+ * @method $this keys($pattern)
+ * @method $this move($key, $db)
+ * @method $this object($subcommand, $key)
+ * @method $this persist($key)
+ * @method $this pexpire($key, $milliseconds, string $option = null)
+ * @method $this pexpireat($key, $timestamp, string $option = null)
+ * @method $this pttl($key)
+ * @method $this randomkey()
+ * @method $this rename($key, $target)
+ * @method $this renamenx($key, $target)
+ * @method $this scan($cursor, ?array $options = null)
+ * @method $this sort($key, ?array $options = null)
+ * @method $this sort_ro(string $key, ?string $byPattern = null, ?LimitOffsetCount $limit = null, array $getPatterns = [], ?string $sorting = null, bool $alpha = false)
+ * @method $this ttl($key)
+ * @method $this type($key)
+ * @method $this append($key, $value)
+ * @method $this bfadd(string $key, $item)
+ * @method $this bfexists(string $key, $item)
+ * @method $this bfinfo(string $key, string $modifier = '')
+ * @method $this bfinsert(string $key, int $capacity = -1, float $error = -1, int $expansion = -1, bool $noCreate = false, bool $nonScaling = false, string ...$item)
+ * @method $this bfloadchunk(string $key, int $iterator, $data)
+ * @method $this bfmadd(string $key, ...$item)
+ * @method $this bfmexists(string $key, ...$item)
+ * @method $this bfreserve(string $key, float $errorRate, int $capacity, int $expansion = -1, bool $nonScaling = false)
+ * @method $this bfscandump(string $key, int $iterator)
+ * @method $this bitcount(string $key, $start = null, $end = null, string $index = 'byte')
+ * @method $this bitop($operation, $destkey, $key)
+ * @method $this bitfield($key, $subcommand, ...$subcommandArg)
+ * @method $this bitfield_ro(string $key, ?array $encodingOffsetMap = null)
+ * @method $this bitpos($key, $bit, $start = null, $end = null, string $index = 'byte')
+ * @method $this blmpop(int $timeout, array $keys, string $modifier = 'left', int $count = 1)
+ * @method $this bzpopmax(array $keys, int $timeout)
+ * @method $this bzpopmin(array $keys, int $timeout)
+ * @method $this bzmpop(int $timeout, array $keys, string $modifier = 'min', int $count = 1)
+ * @method $this cfadd(string $key, $item)
+ * @method $this cfaddnx(string $key, $item)
+ * @method $this cfcount(string $key, $item)
+ * @method $this cfdel(string $key, $item)
+ * @method $this cfexists(string $key, $item)
+ * @method $this cfloadchunk(string $key, int $iterator, $data)
+ * @method $this cfmexists(string $key, ...$item)
+ * @method $this cfinfo(string $key)
+ * @method $this cfinsert(string $key, int $capacity = -1, bool $noCreate = false, string ...$item)
+ * @method $this cfinsertnx(string $key, int $capacity = -1, bool $noCreate = false, string ...$item)
+ * @method $this cfreserve(string $key, int $capacity, int $bucketSize = -1, int $maxIterations = -1, int $expansion = -1)
+ * @method $this cfscandump(string $key, int $iterator)
+ * @method $this cmsincrby(string $key, string|int...$itemIncrementDictionary)
+ * @method $this cmsinfo(string $key)
+ * @method $this cmsinitbydim(string $key, int $width, int $depth)
+ * @method $this cmsinitbyprob(string $key, float $errorRate, float $probability)
+ * @method $this cmsmerge(string $destination, array $sources, array $weights = [])
+ * @method $this cmsquery(string $key, string ...$item)
+ * @method $this decr($key)
+ * @method $this decrby($key, $decrement)
+ * @method $this failover(?To $to = null, bool $abort = false, int $timeout = -1)
+ * @method $this fcall(string $function, array $keys, ...$args)
+ * @method $this fcall_ro(string $function, array $keys, ...$args)
+ * @method $this ft_list()
+ * @method $this ftaggregate(string $index, string $query, ?AggregateArguments $arguments = null)
+ * @method $this ftaliasadd(string $alias, string $index)
+ * @method $this ftaliasdel(string $alias)
+ * @method $this ftaliasupdate(string $alias, string $index)
+ * @method $this ftalter(string $index, FieldInterface[] $schema, ?AlterArguments $arguments = null)
+ * @method $this ftcreate(string $index, FieldInterface[] $schema, ?CreateArguments $arguments = null)
+ * @method $this ftdictadd(string $dict, ...$term)
+ * @method $this ftdictdel(string $dict, ...$term)
+ * @method $this ftdictdump(string $dict)
+ * @method $this ftdropindex(string $index, ?DropArguments $arguments = null)
+ * @method $this ftexplain(string $index, string $query, ?ExplainArguments $arguments = null)
+ * @method $this ftinfo(string $index)
+ * @method $this ftprofile(string $index, ProfileArguments $arguments)
+ * @method $this ftsearch(string $index, string $query, ?SearchArguments $arguments = null)
+ * @method $this ftspellcheck(string $index, string $query, ?SearchArguments $arguments = null)
+ * @method $this ftsugadd(string $key, string $string, float $score, ?SugAddArguments $arguments = null)
+ * @method $this ftsugdel(string $key, string $string)
+ * @method $this ftsugget(string $key, string $prefix, ?SugGetArguments $arguments = null)
+ * @method $this ftsuglen(string $key)
+ * @method $this ftsyndump(string $index)
+ * @method $this ftsynupdate(string $index, string $synonymGroupId, ?SynUpdateArguments $arguments = null, string ...$terms)
+ * @method $this fttagvals(string $index, string $fieldName)
+ * @method $this get($key)
+ * @method $this getbit($key, $offset)
+ * @method $this getex(string $key, $modifier = '', $value = false)
+ * @method $this getrange($key, $start, $end)
+ * @method $this getdel(string $key)
+ * @method $this getset($key, $value)
+ * @method $this incr($key)
+ * @method $this incrby($key, $increment)
+ * @method $this incrbyfloat($key, $increment)
+ * @method $this mget(array $keys)
+ * @method $this mset(array $dictionary)
+ * @method $this msetnx(array $dictionary)
+ * @method $this psetex($key, $milliseconds, $value)
+ * @method $this set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
+ * @method $this setbit($key, $offset, $value)
+ * @method $this setex($key, $seconds, $value)
+ * @method $this setnx($key, $value)
+ * @method $this setrange($key, $offset, $value)
+ * @method $this strlen($key)
+ * @method $this hdel($key, array $fields)
+ * @method $this hexists($key, $field)
+ * @method $this hexpire(string $key, int $seconds, array $fields, string $flag = null)
+ * @method $this hexpireat(string $key, int $unixTimeSeconds, array $fields, string $flag = null)
+ * @method $this hexpiretime(string $key, array $fields)
+ * @method $this hpersist(string $key, array $fields)
+ * @method $this hpexpire(string $key, int $milliseconds, array $fields, string $flag = null)
+ * @method $this hpexpireat(string $key, int $unixTimeMilliseconds, array $fields, string $flag = null)
+ * @method $this hpexpiretime(string $key, array $fields)
+ * @method $this hget($key, $field)
+ * @method $this hgetex(string $key, array $fields, string $modifier = HGETEX::NULL)
+ * @method $this hgetall($key)
+ * @method $this hgetdel(string $key, array $fields)
+ * @method $this hincrby($key, $field, $increment)
+ * @method $this hincrbyfloat($key, $field, $increment)
+ * @method $this hkeys($key)
+ * @method $this hlen($key)
+ * @method $this hmget($key, array $fields)
+ * @method $this hmset($key, array $dictionary)
+ * @method $this hrandfield(string $key, int $count = 1, bool $withValues = false)
+ * @method $this hscan($key, $cursor, ?array $options = null)
+ * @method $this hset($key, $field, $value)
+ * @method $this hsetex(string $key, array $fieldValueMap, string $setModifier = HSETEX::SET_NULL, string $ttlModifier = HSETEX::TTL_NULL, int|bool $ttlModifierValue = false)
+ * @method $this hsetnx($key, $field, $value)
+ * @method $this httl(string $key, array $fields)
+ * @method $this hpttl(string $key, array $fields)
+ * @method $this hvals($key)
+ * @method $this hstrlen($key, $field)
+ * @method $this jsonarrappend(string $key, string $path = '$', ...$value)
+ * @method $this jsonarrindex(string $key, string $path, string $value, int $start = 0, int $stop = 0)
+ * @method $this jsonarrinsert(string $key, string $path, int $index, string ...$value)
+ * @method $this jsonarrlen(string $key, string $path = '$')
+ * @method $this jsonarrpop(string $key, string $path = '$', int $index = -1)
+ * @method $this jsonarrtrim(string $key, string $path, int $start, int $stop)
+ * @method $this jsonclear(string $key, string $path = '$')
+ * @method $this jsondel(string $key, string $path = '$')
+ * @method $this jsonforget(string $key, string $path = '$')
+ * @method $this jsonget(string $key, string $indent = '', string $newline = '', string $space = '', string ...$paths)
+ * @method $this jsonnumincrby(string $key, string $path, int $value)
+ * @method $this jsonmerge(string $key, string $path, string $value)
+ * @method $this jsonmget(array $keys, string $path)
+ * @method $this jsonmset(string ...$keyPathValue)
+ * @method $this jsonobjkeys(string $key, string $path = '$')
+ * @method $this jsonobjlen(string $key, string $path = '$')
+ * @method $this jsonresp(string $key, string $path = '$')
+ * @method $this jsonset(string $key, string $path, string $value, ?string $subcommand = null)
+ * @method $this jsonstrappend(string $key, string $path, string $value)
+ * @method $this jsonstrlen(string $key, string $path = '$')
+ * @method $this jsontoggle(string $key, string $path)
+ * @method $this jsontype(string $key, string $path = '$')
+ * @method $this blmove(string $source, string $destination, string $where, string $to, int $timeout)
+ * @method $this blpop(array|string $keys, $timeout)
+ * @method $this brpop(array|string $keys, $timeout)
+ * @method $this brpoplpush($source, $destination, $timeout)
+ * @method $this lcs(string $key1, string $key2, bool $len = false, bool $idx = false, int $minMatchLen = 0, bool $withMatchLen = false)
+ * @method $this lindex($key, $index)
+ * @method $this linsert($key, $whence, $pivot, $value)
+ * @method $this llen($key)
+ * @method $this lmove(string $source, string $destination, string $where, string $to)
+ * @method $this lmpop(array $keys, string $modifier = 'left', int $count = 1)
+ * @method $this lpop($key)
+ * @method $this lpush($key, array $values)
+ * @method $this lpushx($key, array $values)
+ * @method $this lrange($key, $start, $stop)
+ * @method $this lrem($key, $count, $value)
+ * @method $this lset($key, $index, $value)
+ * @method $this ltrim($key, $start, $stop)
+ * @method $this rpop($key)
+ * @method $this rpoplpush($source, $destination)
+ * @method $this rpush($key, array $values)
+ * @method $this rpushx($key, array $values)
+ * @method $this sadd($key, array $members)
+ * @method $this scard($key)
+ * @method $this sdiff(array|string $keys)
+ * @method $this sdiffstore($destination, array|string $keys)
+ * @method $this sinter(array|string $keys)
+ * @method $this sintercard(array $keys, int $limit = 0)
+ * @method $this sinterstore($destination, array|string $keys)
+ * @method $this sismember($key, $member)
+ * @method $this smembers($key)
+ * @method $this smismember(string $key, string ...$members)
+ * @method $this smove($source, $destination, $member)
+ * @method $this spop($key, $count = null)
+ * @method $this srandmember($key, $count = null)
+ * @method $this srem($key, $member)
+ * @method $this sscan($key, $cursor, ?array $options = null)
+ * @method $this ssubscribe(string ...$shardChannels)
+ * @method $this subscribe(string ...$channels)
+ * @method $this sunsubscribe(?string ...$shardChannels = null)
+ * @method $this sunion(array|string $keys)
+ * @method $this sunionstore($destination, array|string $keys)
+ * @method $this tdigestadd(string $key, float ...$value)
+ * @method $this tdigestbyrank(string $key, int ...$rank)
+ * @method $this tdigestbyrevrank(string $key, int ...$reverseRank)
+ * @method $this tdigestcdf(string $key, int ...$value)
+ * @method $this tdigestcreate(string $key, int $compression = 0)
+ * @method $this tdigestinfo(string $key)
+ * @method $this tdigestmax(string $key)
+ * @method $this tdigestmerge(string $destinationKey, array $sourceKeys, int $compression = 0, bool $override = false)
+ * @method $this tdigestquantile(string $key, float ...$quantile)
+ * @method $this tdigestmin(string $key)
+ * @method $this tdigestrank(string $key, ...$value)
+ * @method $this tdigestreset(string $key)
+ * @method $this tdigestrevrank(string $key, float ...$value)
+ * @method $this tdigesttrimmed_mean(string $key, float $lowCutQuantile, float $highCutQuantile)
+ * @method $this topkadd(string $key, ...$items)
+ * @method $this topkincrby(string $key, ...$itemIncrement)
+ * @method $this topkinfo(string $key)
+ * @method $this topklist(string $key, bool $withCount = false)
+ * @method $this topkquery(string $key, ...$items)
+ * @method $this topkreserve(string $key, int $topK, int $width = 8, int $depth = 7, float $decay = 0.9)
+ * @method $this tsadd(string $key, int $timestamp, float $value, ?AddArguments $arguments = null)
+ * @method $this tsalter(string $key, ?TSAlterArguments $arguments = null)
+ * @method $this tscreate(string $key, ?TSCreateArguments $arguments = null)
+ * @method $this tscreaterule(string $sourceKey, string $destKey, string $aggregator, int $bucketDuration, int $alignTimestamp = 0)
+ * @method $this tsdecrby(string $key, float $value, ?DecrByArguments $arguments = null)
+ * @method $this tsdel(string $key, int $fromTimestamp, int $toTimestamp)
+ * @method $this tsdeleterule(string $sourceKey, string $destKey)
+ * @method $this tsget(string $key, ?GetArguments $arguments = null)
+ * @method $this tsincrby(string $key, float $value, ?IncrByArguments $arguments = null)
+ * @method $this tsinfo(string $key, ?InfoArguments $arguments = null)
+ * @method $this tsmadd(mixed ...$keyTimestampValue)
+ * @method $this tsmget(MGetArguments $arguments, string ...$filterExpression)
+ * @method $this tsmrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments)
+ * @method $this tsmrevrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments)
+ * @method $this tsqueryindex(string ...$filterExpression)
+ * @method $this tsrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null)
+ * @method $this tsrevrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null)
+ * @method $this zadd($key, array $membersAndScoresDictionary)
+ * @method $this zcard($key)
+ * @method $this zcount($key, $min, $max)
+ * @method $this zdiff(array $keys, bool $withScores = false)
+ * @method $this zdiffstore(string $destination, array $keys)
+ * @method $this zincrby($key, $increment, $member)
+ * @method $this zintercard(array $keys, int $limit = 0)
+ * @method $this zinterstore(string $destination, array $keys, int[] $weights = [], string $aggregate = 'sum')
+ * @method $this zinter(array $keys, int[] $weights = [], string $aggregate = 'sum', bool $withScores = false)
+ * @method $this zmpop(array $keys, string $modifier = 'min', int $count = 1)
+ * @method $this zmscore(string $key, string ...$member)
+ * @method $this zrandmember(string $key, int $count = 1, bool $withScores = false)
+ * @method $this zrange($key, $start, $stop, ?array $options = null)
+ * @method $this zrangebyscore($key, $min, $max, ?array $options = null)
+ * @method $this zrangestore(string $destination, string $source, int|string $min, string|int $max, string|bool $by = false, bool $reversed = false, bool $limit = false, int $offset = 0, int $count = 0)
+ * @method $this zrank($key, $member)
+ * @method $this zrem($key, $member)
+ * @method $this zremrangebyrank($key, $start, $stop)
+ * @method $this zremrangebyscore($key, $min, $max)
+ * @method $this zrevrange($key, $start, $stop, ?array $options = null)
+ * @method $this zrevrangebyscore($key, $max, $min, ?array $options = null)
+ * @method $this zrevrank($key, $member)
+ * @method $this zunion(array $keys, int[] $weights = [], string $aggregate = 'sum', bool $withScores = false)
+ * @method $this zunionstore(string $destination, array $keys, int[] $weights = [], string $aggregate = 'sum')
+ * @method $this zscore($key, $member)
+ * @method $this zscan($key, $cursor, ?array $options = null)
+ * @method $this zrangebylex($key, $start, $stop, ?array $options = null)
+ * @method $this zrevrangebylex($key, $start, $stop, ?array $options = null)
+ * @method $this zremrangebylex($key, $min, $max)
+ * @method $this zlexcount($key, $min, $max)
+ * @method $this pexpiretime(string $key)
+ * @method $this pfadd($key, array $elements)
+ * @method $this pfmerge($destinationKey, array|string $sourceKeys)
+ * @method $this pfcount(array|string $keys)
+ * @method $this pubsub($subcommand, $argument)
+ * @method $this publish($channel, $message)
+ * @method $this discard()
+ * @method $this exec()
+ * @method $this multi()
+ * @method $this unwatch()
+ * @method $this waitaof(int $numLocal, int $numReplicas, int $timeout)
+ * @method $this unsubscribe(string ...$channels)
+ * @method $this watch($key)
+ * @method $this eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
+ * @method $this eval_ro(string $script, array $keys, ...$argument)
+ * @method $this evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
+ * @method $this evalsha_ro(string $sha1, array $keys, ...$argument)
+ * @method $this script($subcommand, $argument = null)
+ * @method $this shutdown(?bool $noSave = null, bool $now = false, bool $force = false, bool $abort = false)
+ * @method $this auth($password)
+ * @method $this echo($message)
+ * @method $this ping($message = null)
+ * @method $this select($database)
+ * @method $this bgrewriteaof()
+ * @method $this bgsave()
+ * @method $this config($subcommand, $argument = null)
+ * @method $this dbsize()
+ * @method $this flushall()
+ * @method $this flushdb()
+ * @method $this info(string ...$section = null)
+ * @method $this lastsave()
+ * @method $this save()
+ * @method $this slaveof($host, $port)
+ * @method $this slowlog($subcommand, $argument = null)
+ * @method $this spublish(string $shardChannel, string $message)
+ * @method $this time()
+ * @method $this command($subcommand, $argument = null)
+ * @method $this geoadd($key, $longitude, $latitude, $member)
+ * @method $this geohash($key, array $members)
+ * @method $this geopos($key, array $members)
+ * @method $this geodist($key, $member1, $member2, $unit = null)
+ * @method $this georadius($key, $longitude, $latitude, $radius, $unit, ?array $options = null)
+ * @method $this georadiusbymember($key, $member, $radius, $unit, ?array $options = null)
+ * @method $this geosearch(string $key, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $withCoord = false, bool $withDist = false, bool $withHash = false)
+ * @method $this geosearchstore(string $destination, string $source, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $storeDist = false)
+ *
+ * Container commands
+ * @property CLIENT    $client
+ * @property FUNCTIONS $function
+ * @property FTCONFIG  $ftconfig
+ * @property FTCURSOR  $ftcursor
+ * @property JSONDEBUG $jsondebug
+ * @property ACL       $acl
+ * @property XGROUP    $xgroup
+ */
+interface ClientContextInterface
+{
+    /**
+     * Sends the specified command instance to Redis.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return mixed
+     */
+    public function executeCommand(CommandInterface $command);
+
+    /**
+     * Sends the specified command with its arguments to Redis.
+     *
+     * @param string $method    Command ID.
+     * @param array  $arguments Arguments for the command.
+     *
+     * @return mixed
+     */
+    public function __call($method, $arguments);
+
+    /**
+     * Starts the execution of the context.
+     *
+     * @param mixed $callable Optional callback for execution.
+     *
+     * @return array
+     */
+    public function execute($callable = null);
+}

+ 20 - 0
vendor/predis/predis/src/ClientException.php

@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis;
+
+/**
+ * Exception class that identifies client-side errors.
+ */
+class ClientException extends PredisException
+{
+}

+ 456 - 0
vendor/predis/predis/src/ClientInterface.php

@@ -0,0 +1,456 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis;
+
+use Predis\Command\Argument\Geospatial\ByInterface;
+use Predis\Command\Argument\Geospatial\FromInterface;
+use Predis\Command\Argument\Search\AggregateArguments;
+use Predis\Command\Argument\Search\AlterArguments;
+use Predis\Command\Argument\Search\CreateArguments;
+use Predis\Command\Argument\Search\DropArguments;
+use Predis\Command\Argument\Search\ExplainArguments;
+use Predis\Command\Argument\Search\ProfileArguments;
+use Predis\Command\Argument\Search\SchemaFields\FieldInterface;
+use Predis\Command\Argument\Search\SearchArguments;
+use Predis\Command\Argument\Search\SugAddArguments;
+use Predis\Command\Argument\Search\SugGetArguments;
+use Predis\Command\Argument\Search\SynUpdateArguments;
+use Predis\Command\Argument\Server\LimitOffsetCount;
+use Predis\Command\Argument\Server\To;
+use Predis\Command\Argument\TimeSeries\AddArguments;
+use Predis\Command\Argument\TimeSeries\AlterArguments as TSAlterArguments;
+use Predis\Command\Argument\TimeSeries\CreateArguments as TSCreateArguments;
+use Predis\Command\Argument\TimeSeries\DecrByArguments;
+use Predis\Command\Argument\TimeSeries\GetArguments;
+use Predis\Command\Argument\TimeSeries\IncrByArguments;
+use Predis\Command\Argument\TimeSeries\InfoArguments;
+use Predis\Command\Argument\TimeSeries\MGetArguments;
+use Predis\Command\Argument\TimeSeries\MRangeArguments;
+use Predis\Command\Argument\TimeSeries\RangeArguments;
+use Predis\Command\CommandInterface;
+use Predis\Command\Container\ACL;
+use Predis\Command\Container\CLIENT;
+use Predis\Command\Container\FUNCTIONS;
+use Predis\Command\Container\Json\JSONDEBUG;
+use Predis\Command\Container\Search\FTCONFIG;
+use Predis\Command\Container\Search\FTCURSOR;
+use Predis\Command\Container\XGROUP;
+use Predis\Command\Container\XINFO;
+use Predis\Command\FactoryInterface;
+use Predis\Configuration\OptionsInterface;
+use Predis\Connection\ConnectionInterface;
+use Predis\Response\Status;
+
+/**
+ * Interface defining a client able to execute commands against Redis.
+ *
+ * All the commands exposed by the client generally have the same signature as
+ * described by the Redis documentation, but some of them offer an additional
+ * and more friendly interface to ease programming which is described in the
+ * following list of methods:
+ *
+ * @method int               copy(string $source, string $destination, int $db = -1, bool $replace = false)
+ * @method int               del(string[]|string $keyOrKeys, string ...$keys = null)
+ * @method string|null       dump(string $key)
+ * @method int               exists(string $key)
+ * @method int               expire(string $key, int $seconds, string $expireOption = '')
+ * @method int               expireat(string $key, int $timestamp, string $expireOption = '')
+ * @method int               expiretime(string $key)
+ * @method array             keys(string $pattern)
+ * @method int               move(string $key, int $db)
+ * @method mixed             object($subcommand, string $key)
+ * @method int               persist(string $key)
+ * @method int               pexpire(string $key, int $milliseconds, string $option = null)
+ * @method int               pexpireat(string $key, int $timestamp, string $option = null)
+ * @method int               pttl(string $key)
+ * @method string|null       randomkey()
+ * @method mixed             rename(string $key, string $target)
+ * @method int               renamenx(string $key, string $target)
+ * @method array             scan($cursor, ?array $options = null)
+ * @method array             sort(string $key, ?array $options = null)
+ * @method array             sort_ro(string $key, ?string $byPattern = null, ?LimitOffsetCount $limit = null, array $getPatterns = [], ?string $sorting = null, bool $alpha = false)
+ * @method int               ttl(string $key)
+ * @method mixed             type(string $key)
+ * @method int               append(string $key, $value)
+ * @method mixed             bfadd(string $key, $item)
+ * @method mixed             bfexists(string $key, $item)
+ * @method array             bfinfo(string $key, string $modifier = '')
+ * @method array             bfinsert(string $key, int $capacity = -1, float $error = -1, int $expansion = -1, bool $noCreate = false, bool $nonScaling = false, string ...$item)
+ * @method Status            bfloadchunk(string $key, int $iterator, $data)
+ * @method array             bfmadd(string $key, ...$item)
+ * @method array             bfmexists(string $key, ...$item)
+ * @method Status            bfreserve(string $key, float $errorRate, int $capacity, int $expansion = -1, bool $nonScaling = false)
+ * @method array             bfscandump(string $key, int $iterator)
+ * @method int               bitcount(string $key, $start = null, $end = null, string $index = 'byte')
+ * @method int               bitop($operation, $destkey, $key)
+ * @method array|null        bitfield(string $key, $subcommand, ...$subcommandArg)
+ * @method array|null        bitfield_ro(string $key, ?array $encodingOffsetMap = null)
+ * @method int               bitpos(string $key, $bit, $start = null, $end = null, string $index = 'byte')
+ * @method array             blmpop(int $timeout, array $keys, string $modifier = 'left', int $count = 1)
+ * @method array             bzpopmax(array $keys, int $timeout)
+ * @method array             bzpopmin(array $keys, int $timeout)
+ * @method array             bzmpop(int $timeout, array $keys, string $modifier = 'min', int $count = 1)
+ * @method mixed             cfadd(string $key, $item)
+ * @method mixed             cfaddnx(string $key, $item)
+ * @method int               cfcount(string $key, $item)
+ * @method mixed             cfdel(string $key, $item)
+ * @method mixed             cfexists(string $key, $item)
+ * @method Status            cfloadchunk(string $key, int $iterator, $data)
+ * @method int               cfmexists(string $key, ...$item)
+ * @method array             cfinfo(string $key)
+ * @method array             cfinsert(string $key, int $capacity = -1, bool $noCreate = false, string ...$item)
+ * @method array             cfinsertnx(string $key, int $capacity = -1, bool $noCreate = false, string ...$item)
+ * @method Status            cfreserve(string $key, int $capacity, int $bucketSize = -1, int $maxIterations = -1, int $expansion = -1)
+ * @method array             cfscandump(string $key, int $iterator)
+ * @method array             cmsincrby(string $key, string|int ...$itemIncrementDictionary)
+ * @method array             cmsinfo(string $key)
+ * @method Status            cmsinitbydim(string $key, int $width, int $depth)
+ * @method Status            cmsinitbyprob(string $key, float $errorRate, float $probability)
+ * @method Status            cmsmerge(string $destination, array $sources, array $weights = [])
+ * @method array             cmsquery(string $key, string ...$item)
+ * @method int               decr(string $key)
+ * @method int               decrby(string $key, int $decrement)
+ * @method Status            failover(?To $to = null, bool $abort = false, int $timeout = -1)
+ * @method mixed             fcall(string $function, array $keys, ...$args)
+ * @method mixed             fcall_ro(string $function, array $keys, ...$args)
+ * @method array             ft_list()
+ * @method array             ftaggregate(string $index, string $query, ?AggregateArguments $arguments = null)
+ * @method Status            ftaliasadd(string $alias, string $index)
+ * @method Status            ftaliasdel(string $alias)
+ * @method Status            ftaliasupdate(string $alias, string $index)
+ * @method Status            ftalter(string $index, FieldInterface[] $schema, ?AlterArguments $arguments = null)
+ * @method Status            ftcreate(string $index, FieldInterface[] $schema, ?CreateArguments $arguments = null)
+ * @method int               ftdictadd(string $dict, ...$term)
+ * @method int               ftdictdel(string $dict, ...$term)
+ * @method array             ftdictdump(string $dict)
+ * @method Status            ftdropindex(string $index, ?DropArguments $arguments = null)
+ * @method string            ftexplain(string $index, string $query, ?ExplainArguments $arguments = null)
+ * @method array             ftinfo(string $index)
+ * @method array             ftprofile(string $index, ProfileArguments $arguments)
+ * @method array             ftsearch(string $index, string $query, ?SearchArguments $arguments = null)
+ * @method array             ftspellcheck(string $index, string $query, ?SearchArguments $arguments = null)
+ * @method int               ftsugadd(string $key, string $string, float $score, ?SugAddArguments $arguments = null)
+ * @method int               ftsugdel(string $key, string $string)
+ * @method array             ftsugget(string $key, string $prefix, ?SugGetArguments $arguments = null)
+ * @method int               ftsuglen(string $key)
+ * @method array             ftsyndump(string $index)
+ * @method Status            ftsynupdate(string $index, string $synonymGroupId, ?SynUpdateArguments $arguments = null, string ...$terms)
+ * @method array             fttagvals(string $index, string $fieldName)
+ * @method string|null       get(string $key)
+ * @method int               getbit(string $key, $offset)
+ * @method int|null          getex(string $key, $modifier = '', $value = false)
+ * @method string            getrange(string $key, $start, $end)
+ * @method string            getdel(string $key)
+ * @method string|null       getset(string $key, $value)
+ * @method int               incr(string $key)
+ * @method int               incrby(string $key, int $increment)
+ * @method string            incrbyfloat(string $key, int|float $increment)
+ * @method array             mget(string[]|string $keyOrKeys, string ...$keys = null)
+ * @method mixed             mset(array $dictionary)
+ * @method int               msetnx(array $dictionary)
+ * @method Status            psetex(string $key, $milliseconds, $value)
+ * @method Status|null       set(string $key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
+ * @method int               setbit(string $key, $offset, $value)
+ * @method Status            setex(string $key, $seconds, $value)
+ * @method int               setnx(string $key, $value)
+ * @method int               setrange(string $key, $offset, $value)
+ * @method int               strlen(string $key)
+ * @method int               hdel(string $key, array $fields)
+ * @method int               hexists(string $key, string $field)
+ * @method array|null        hexpire(string $key, int $seconds, array $fields, string $flag = null)
+ * @method array|null        hexpireat(string $key, int $unixTimeSeconds, array $fields, string $flag = null)
+ * @method array|null        hexpiretime(string $key, array $fields)
+ * @method array|null        hpersist(string $key, array $fields)
+ * @method array|null        hpexpire(string $key, int $milliseconds, array $fields, string $flag = null)
+ * @method array|null        hpexpireat(string $key, int $unixTimeMilliseconds, array $fields, string $flag = null)
+ * @method array|null        hpexpiretime(string $key, array $fields)
+ * @method string|null       hget(string $key, string $field)
+ * @method array|null        hgetex(string $key, array $fields, string $modifier = HGETEX::NULL, int|bool $modifierValue = false)
+ * @method array             hgetall(string $key)
+ * @method array             hgetdel(string $key, array $fields)
+ * @method int               hincrby(string $key, string $field, int $increment)
+ * @method string            hincrbyfloat(string $key, string $field, int|float $increment)
+ * @method array             hkeys(string $key)
+ * @method int               hlen(string $key)
+ * @method array             hmget(string $key, array $fields)
+ * @method mixed             hmset(string $key, array $dictionary)
+ * @method array             hrandfield(string $key, int $count = 1, bool $withValues = false)
+ * @method array             hscan(string $key, $cursor, ?array $options = null)
+ * @method int               hset(string $key, string $field, string $value)
+ * @method int               hsetex(string $key, array $fieldValueMap, string $setModifier = HSETEX::SET_NULL, string $ttlModifier = HSETEX::TTL_NULL, int|bool $ttlModifierValue = false)
+ * @method int               hsetnx(string $key, string $field, string $value)
+ * @method array|null        httl(string $key, array $fields)
+ * @method array|null        hpttl(string $key, array $fields)
+ * @method array             hvals(string $key)
+ * @method int               hstrlen(string $key, string $field)
+ * @method array             jsonarrappend(string $key, string $path = '$', ...$value)
+ * @method array             jsonarrindex(string $key, string $path, string $value, int $start = 0, int $stop = 0)
+ * @method array             jsonarrinsert(string $key, string $path, int $index, string ...$value)
+ * @method array             jsonarrlen(string $key, string $path = '$')
+ * @method array             jsonarrpop(string $key, string $path = '$', int $index = -1)
+ * @method int               jsonclear(string $key, string $path = '$')
+ * @method array             jsonarrtrim(string $key, string $path, int $start, int $stop)
+ * @method int               jsondel(string $key, string $path = '$')
+ * @method int               jsonforget(string $key, string $path = '$')
+ * @method mixed             jsonget(string $key, string $indent = '', string $newline = '', string $space = '', string ...$paths)
+ * @method mixed             jsonnumincrby(string $key, string $path, int $value)
+ * @method Status            jsonmerge(string $key, string $path, string $value)
+ * @method array             jsonmget(array $keys, string $path)
+ * @method Status            jsonmset(string ...$keyPathValue)
+ * @method array             jsonobjkeys(string $key, string $path = '$')
+ * @method array             jsonobjlen(string $key, string $path = '$')
+ * @method array             jsonresp(string $key, string $path = '$')
+ * @method string            jsonset(string $key, string $path, string $value, ?string $subcommand = null)
+ * @method array             jsonstrappend(string $key, string $path, string $value)
+ * @method array             jsonstrlen(string $key, string $path = '$')
+ * @method array             jsontoggle(string $key, string $path)
+ * @method array             jsontype(string $key, string $path = '$')
+ * @method string            blmove(string $source, string $destination, string $where, string $to, int $timeout)
+ * @method array|null        blpop(array|string $keys, int|float $timeout)
+ * @method array|null        brpop(array|string $keys, int|float $timeout)
+ * @method string|null       brpoplpush(string $source, string $destination, int|float $timeout)
+ * @method mixed             lcs(string $key1, string $key2, bool $len = false, bool $idx = false, int $minMatchLen = 0, bool $withMatchLen = false)
+ * @method string|null       lindex(string $key, int $index)
+ * @method int               linsert(string $key, $whence, $pivot, $value)
+ * @method int               llen(string $key)
+ * @method string            lmove(string $source, string $destination, string $where, string $to)
+ * @method array|null        lmpop(array $keys, string $modifier = 'left', int $count = 1)
+ * @method string|null       lpop(string $key)
+ * @method int               lpush(string $key, array $values)
+ * @method int               lpushx(string $key, array $values)
+ * @method string[]          lrange(string $key, int $start, int $stop)
+ * @method int               lrem(string $key, int $count, string $value)
+ * @method mixed             lset(string $key, int $index, string $value)
+ * @method mixed             ltrim(string $key, int $start, int $stop)
+ * @method string|null       rpop(string $key)
+ * @method string|null       rpoplpush(string $source, string $destination)
+ * @method int               rpush(string $key, array $values)
+ * @method int               rpushx(string $key, array $values)
+ * @method int               sadd(string $key, array $members)
+ * @method int               scard(string $key)
+ * @method string[]          sdiff(array|string $keys)
+ * @method int               sdiffstore(string $destination, array|string $keys)
+ * @method string[]          sinter(array|string $keys)
+ * @method int               sintercard(array $keys, int $limit = 0)
+ * @method int               sinterstore(string $destination, array|string $keys)
+ * @method int               sismember(string $key, string $member)
+ * @method string[]          smembers(string $key)
+ * @method array             smismember(string $key, string ...$members)
+ * @method int               smove(string $source, string $destination, string $member)
+ * @method string|array|null spop(string $key, ?int $count = null)
+ * @method string|null       srandmember(string $key, ?int $count = null)
+ * @method int               srem(string $key, array|string $member)
+ * @method array             sscan(string $key, int $cursor, array $options = null)
+ * @method array             ssubscribe(string ...$shardChannels)
+ * @method array             subscribe(string ...$channels)
+ * @method string[]          sunion(array|string $keys)
+ * @method int               sunionstore(string $destination, array|string $keys)
+ * @method array             sunsubscribe(?string ...$shardChannels = null)
+ * @method int               touch(string[]|string $keyOrKeys, string ...$keys = null)
+ * @method Status            tdigestadd(string $key, float ...$value)
+ * @method array             tdigestbyrank(string $key, int ...$rank)
+ * @method array             tdigestbyrevrank(string $key, int ...$reverseRank)
+ * @method array             tdigestcdf(string $key, int ...$value)
+ * @method Status            tdigestcreate(string $key, int $compression = 0)
+ * @method array             tdigestinfo(string $key)
+ * @method mixed             tdigestmax(string $key)
+ * @method Status            tdigestmerge(string $destinationKey, array $sourceKeys, int $compression = 0, bool $override = false)
+ * @method string[]          tdigestquantile(string $key, float ...$quantile)
+ * @method mixed             tdigestmin(string $key)
+ * @method array             tdigestrank(string $key, float ...$value)
+ * @method Status            tdigestreset(string $key)
+ * @method array             tdigestrevrank(string $key, float ...$value)
+ * @method string            tdigesttrimmed_mean(string $key, float $lowCutQuantile, float $highCutQuantile)
+ * @method array             topkadd(string $key, ...$items)
+ * @method array             topkincrby(string $key, ...$itemIncrement)
+ * @method array             topkinfo(string $key)
+ * @method array             topklist(string $key, bool $withCount = false)
+ * @method array             topkquery(string $key, ...$items)
+ * @method Status            topkreserve(string $key, int $topK, int $width = 8, int $depth = 7, float $decay = 0.9)
+ * @method int               tsadd(string $key, int $timestamp, float $value, ?AddArguments $arguments = null)
+ * @method Status            tsalter(string $key, ?TSAlterArguments $arguments = null)
+ * @method Status            tscreate(string $key, ?TSCreateArguments $arguments = null)
+ * @method Status            tscreaterule(string $sourceKey, string $destKey, string $aggregator, int $bucketDuration, int $alignTimestamp = 0)
+ * @method int               tsdecrby(string $key, float $value, ?DecrByArguments $arguments = null)
+ * @method int               tsdel(string $key, int $fromTimestamp, int $toTimestamp)
+ * @method Status            tsdeleterule(string $sourceKey, string $destKey)
+ * @method array             tsget(string $key, ?GetArguments $arguments = null)
+ * @method int               tsincrby(string $key, float $value, ?IncrByArguments $arguments = null)
+ * @method array             tsinfo(string $key, ?InfoArguments $arguments = null)
+ * @method array             tsmadd(mixed ...$keyTimestampValue)
+ * @method array             tsmget(MGetArguments $arguments, string ...$filterExpression)
+ * @method array             tsmrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments)
+ * @method array             tsmrevrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments)
+ * @method array             tsqueryindex(string ...$filterExpression)
+ * @method array             tsrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null)
+ * @method array             tsrevrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null)
+ * @method string            xadd(string $key, array $dictionary, string $id = '*', array $options = null)
+ * @method array             xautoclaim(string $key, string $group, string $consumer, int $minIdleTime, string $start, ?int $count = null, bool $justId = false)
+ * @method int               xdel(string $key, string ...$id)
+ * @method int               xlen(string $key)
+ * @method array             xrevrange(string $key, string $end, string $start, ?int $count = null)
+ * @method array             xrange(string $key, string $start, string $end, ?int $count = null)
+ * @method array|null        xread(int $count = null, int $block = null, array $streams = null, string ...$id)
+ * @method array             xreadgroup(string $group, string $consumer, ?int $count = null, ?int $blockMs = null, bool $noAck = false, string ...$keyOrId)
+ * @method string            xtrim(string $key, array|string $strategy, string $threshold, array $options = null)
+ * @method int               zadd(string $key, array $membersAndScoresDictionary)
+ * @method int               zcard(string $key)
+ * @method string            zcount(string $key, int|string $min, int|string $max)
+ * @method array             zdiff(array $keys, bool $withScores = false)
+ * @method int               zdiffstore(string $destination, array $keys)
+ * @method string            zincrby(string $key, int $increment, string $member)
+ * @method int               zintercard(array $keys, int $limit = 0)
+ * @method int               zinterstore(string $destination, array $keys, int[] $weights = [], string $aggregate = 'sum')
+ * @method array             zinter(array $keys, int[] $weights = [], string $aggregate = 'sum', bool $withScores = false)
+ * @method array             zmpop(array $keys, string $modifier = 'min', int $count = 1)
+ * @method array             zmscore(string $key, string ...$member)
+ * @method array             zpopmin(string $key, int $count = 1)
+ * @method array             zpopmax(string $key, int $count = 1)
+ * @method mixed             zrandmember(string $key, int $count = 1, bool $withScores = false)
+ * @method array             zrange(string $key, int|string $start, int|string $stop, ?array $options = null)
+ * @method array             zrangebyscore(string $key, int|string $min, int|string $max, ?array $options = null)
+ * @method int               zrangestore(string $destination, string $source, int|string $min, int|string $max, string|bool $by = false, bool $reversed = false, bool $limit = false, int $offset = 0, int $count = 0)
+ * @method int|null          zrank(string $key, string $member)
+ * @method int               zrem(string $key, string ...$member)
+ * @method int               zremrangebyrank(string $key, int|string $start, int|string $stop)
+ * @method int               zremrangebyscore(string $key, int|string $min, int|string $max)
+ * @method array             zrevrange(string $key, int|string $start, int|string $stop, ?array $options = null)
+ * @method array             zrevrangebyscore(string $key, int|string $max, int|string $min, ?array $options = null)
+ * @method int|null          zrevrank(string $key, string $member)
+ * @method array             zunion(array $keys, int[] $weights = [], string $aggregate = 'sum', bool $withScores = false)
+ * @method int               zunionstore(string $destination, array $keys, int[] $weights = [], string $aggregate = 'sum')
+ * @method string|null       zscore(string $key, string $member)
+ * @method array             zscan(string $key, int $cursor, ?array $options = null)
+ * @method array             zrangebylex(string $key, string $start, string $stop, ?array $options = null)
+ * @method array             zrevrangebylex(string $key, string $start, string $stop, ?array $options = null)
+ * @method int               zremrangebylex(string $key, string $min, string $max)
+ * @method int               zlexcount(string $key, string $min, string $max)
+ * @method int               pexpiretime(string $key)
+ * @method int               pfadd(string $key, array $elements)
+ * @method mixed             pfmerge(string $destinationKey, array|string $sourceKeys)
+ * @method int               pfcount(string[]|string $keyOrKeys, string ...$keys = null)
+ * @method mixed             pubsub($subcommand, $argument)
+ * @method int               publish($channel, $message)
+ * @method mixed             discard()
+ * @method array|null        exec()
+ * @method mixed             multi()
+ * @method mixed             unwatch()
+ * @method array             unsubscribe(string ...$channels)
+ * @method array             waitaof(int $numLocal, int $numReplicas, int $timeout)
+ * @method mixed             watch(string[]|string $keyOrKeys)
+ * @method mixed             eval(string $script, int $numkeys, string ...$keyOrArg = null)
+ * @method mixed             eval_ro(string $script, array $keys, ...$argument)
+ * @method mixed             evalsha(string $script, int $numkeys, string ...$keyOrArg = null)
+ * @method mixed             evalsha_ro(string $sha1, array $keys, ...$argument)
+ * @method mixed             script($subcommand, $argument = null)
+ * @method Status            shutdown(?bool $noSave = null, bool $now = false, bool $force = false, bool $abort = false)
+ * @method mixed             auth(string $password)
+ * @method string            echo(string $message)
+ * @method mixed             ping(?string $message = null)
+ * @method mixed             select(int $database)
+ * @method mixed             bgrewriteaof()
+ * @method mixed             bgsave()
+ * @method mixed             config($subcommand, $argument = null)
+ * @method int               dbsize()
+ * @method mixed             flushall()
+ * @method mixed             flushdb()
+ * @method array             info(string ...$section = null)
+ * @method int               lastsave()
+ * @method mixed             save()
+ * @method mixed             slaveof(string $host, int $port)
+ * @method mixed             slowlog($subcommand, $argument = null)
+ * @method int               spublish(string $shardChannel, string $message)
+ * @method array             time()
+ * @method array             command($subcommand, $argument = null)
+ * @method int               geoadd(string $key, $longitude, $latitude, $member)
+ * @method array             geohash(string $key, array $members)
+ * @method array             geopos(string $key, array $members)
+ * @method string|null       geodist(string $key, $member1, $member2, $unit = null)
+ * @method array             georadius(string $key, $longitude, $latitude, $radius, $unit, ?array $options = null)
+ * @method array             georadiusbymember(string $key, $member, $radius, $unit, ?array $options = null)
+ * @method array             geosearch(string $key, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $withCoord = false, bool $withDist = false, bool $withHash = false)
+ * @method int               geosearchstore(string $destination, string $source, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $storeDist = false)
+ *
+ * Container commands
+ * @property CLIENT    $client
+ * @property FUNCTIONS $function
+ * @property FTCONFIG  $ftconfig
+ * @property FTCURSOR  $ftcursor
+ * @property JSONDEBUG $jsondebug
+ * @property ACL       $acl
+ * @property XGROUP    $xgroup
+ * @property XINFO     $xinfo
+ */
+interface ClientInterface
+{
+    /**
+     * Returns the command factory used by the client.
+     *
+     * @return FactoryInterface
+     */
+    public function getCommandFactory();
+
+    /**
+     * Returns the client options specified upon initialization.
+     *
+     * @return OptionsInterface
+     */
+    public function getOptions();
+
+    /**
+     * Opens the underlying connection to the server.
+     */
+    public function connect();
+
+    /**
+     * Closes the underlying connection from the server.
+     */
+    public function disconnect();
+
+    /**
+     * Returns the underlying connection instance.
+     *
+     * @return ConnectionInterface
+     */
+    public function getConnection();
+
+    /**
+     * Creates a new instance of the specified Redis command.
+     *
+     * @param string $method    Command ID.
+     * @param array  $arguments Arguments for the command.
+     *
+     * @return CommandInterface
+     */
+    public function createCommand($method, $arguments = []);
+
+    /**
+     * Executes the specified Redis command.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return mixed
+     */
+    public function executeCommand(CommandInterface $command);
+
+    /**
+     * Creates a Redis command with the specified arguments and sends a request
+     * to the server.
+     *
+     * @param string $method    Command ID.
+     * @param array  $arguments Arguments for the command.
+     *
+     * @return mixed
+     */
+    public function __call($method, $arguments);
+}

+ 512 - 0
vendor/predis/predis/src/Cluster/ClusterStrategy.php

@@ -0,0 +1,512 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster;
+
+use InvalidArgumentException;
+use Predis\Command\CommandInterface;
+use Predis\Command\ScriptCommand;
+
+/**
+ * Common class implementing the logic needed to support clustering strategies.
+ */
+abstract class ClusterStrategy implements StrategyInterface
+{
+    protected $commands;
+
+    public function __construct()
+    {
+        $this->commands = $this->getDefaultCommands();
+    }
+
+    /**
+     * Returns the default map of supported commands with their handlers.
+     *
+     * @return array
+     */
+    protected function getDefaultCommands()
+    {
+        $getKeyFromFirstArgument = [$this, 'getKeyFromFirstArgument'];
+        $getKeyFromAllArguments = [$this, 'getKeyFromAllArguments'];
+
+        return [
+            /* commands operating on the key space */
+            'EXISTS' => $getKeyFromAllArguments,
+            'DEL' => $getKeyFromAllArguments,
+            'TYPE' => $getKeyFromFirstArgument,
+            'EXPIRE' => $getKeyFromFirstArgument,
+            'EXPIREAT' => $getKeyFromFirstArgument,
+            'PERSIST' => $getKeyFromFirstArgument,
+            'PEXPIRE' => $getKeyFromFirstArgument,
+            'PEXPIREAT' => $getKeyFromFirstArgument,
+            'TTL' => $getKeyFromFirstArgument,
+            'PTTL' => $getKeyFromFirstArgument,
+            'SORT' => [$this, 'getKeyFromSortCommand'],
+            'DUMP' => $getKeyFromFirstArgument,
+            'RESTORE' => $getKeyFromFirstArgument,
+            'FLUSHDB' => [$this, 'getFakeKey'],
+
+            /* commands operating on string values */
+            'APPEND' => $getKeyFromFirstArgument,
+            'DECR' => $getKeyFromFirstArgument,
+            'DECRBY' => $getKeyFromFirstArgument,
+            'GET' => $getKeyFromFirstArgument,
+            'GETBIT' => $getKeyFromFirstArgument,
+            'MGET' => $getKeyFromAllArguments,
+            'SET' => $getKeyFromFirstArgument,
+            'GETRANGE' => $getKeyFromFirstArgument,
+            'GETSET' => $getKeyFromFirstArgument,
+            'INCR' => $getKeyFromFirstArgument,
+            'INCRBY' => $getKeyFromFirstArgument,
+            'INCRBYFLOAT' => $getKeyFromFirstArgument,
+            'SETBIT' => $getKeyFromFirstArgument,
+            'SETEX' => $getKeyFromFirstArgument,
+            'MSET' => [$this, 'getKeyFromInterleavedArguments'],
+            'MSETNX' => [$this, 'getKeyFromInterleavedArguments'],
+            'SETNX' => $getKeyFromFirstArgument,
+            'SETRANGE' => $getKeyFromFirstArgument,
+            'STRLEN' => $getKeyFromFirstArgument,
+            'SUBSTR' => $getKeyFromFirstArgument,
+            'BITOP' => [$this, 'getKeyFromBitOp'],
+            'BITCOUNT' => $getKeyFromFirstArgument,
+            'BITFIELD' => $getKeyFromFirstArgument,
+
+            /* commands operating on lists */
+            'LINSERT' => $getKeyFromFirstArgument,
+            'LINDEX' => $getKeyFromFirstArgument,
+            'LLEN' => $getKeyFromFirstArgument,
+            'LPOP' => $getKeyFromFirstArgument,
+            'RPOP' => $getKeyFromFirstArgument,
+            'RPOPLPUSH' => $getKeyFromAllArguments,
+            'BLPOP' => [$this, 'getKeyFromBlockingListCommands'],
+            'BRPOP' => [$this, 'getKeyFromBlockingListCommands'],
+            'BRPOPLPUSH' => [$this, 'getKeyFromBlockingListCommands'],
+            'LPUSH' => $getKeyFromFirstArgument,
+            'LPUSHX' => $getKeyFromFirstArgument,
+            'RPUSH' => $getKeyFromFirstArgument,
+            'RPUSHX' => $getKeyFromFirstArgument,
+            'LRANGE' => $getKeyFromFirstArgument,
+            'LREM' => $getKeyFromFirstArgument,
+            'LSET' => $getKeyFromFirstArgument,
+            'LTRIM' => $getKeyFromFirstArgument,
+
+            /* commands operating on sets */
+            'SADD' => $getKeyFromFirstArgument,
+            'SCARD' => $getKeyFromFirstArgument,
+            'SDIFF' => $getKeyFromAllArguments,
+            'SDIFFSTORE' => $getKeyFromAllArguments,
+            'SINTER' => $getKeyFromAllArguments,
+            'SINTERSTORE' => $getKeyFromAllArguments,
+            'SUNION' => $getKeyFromAllArguments,
+            'SUNIONSTORE' => $getKeyFromAllArguments,
+            'SISMEMBER' => $getKeyFromFirstArgument,
+            'SMEMBERS' => $getKeyFromFirstArgument,
+            'SSCAN' => $getKeyFromFirstArgument,
+            'SPOP' => $getKeyFromFirstArgument,
+            'SRANDMEMBER' => $getKeyFromFirstArgument,
+            'SREM' => $getKeyFromFirstArgument,
+
+            /* commands operating on sorted sets */
+            'ZADD' => $getKeyFromFirstArgument,
+            'ZCARD' => $getKeyFromFirstArgument,
+            'ZCOUNT' => $getKeyFromFirstArgument,
+            'ZINCRBY' => $getKeyFromFirstArgument,
+            'ZINTERSTORE' => [$this, 'getKeyFromZsetAggregationCommands'],
+            'ZRANGE' => $getKeyFromFirstArgument,
+            'ZRANGEBYSCORE' => $getKeyFromFirstArgument,
+            'ZRANK' => $getKeyFromFirstArgument,
+            'ZREM' => $getKeyFromFirstArgument,
+            'ZREMRANGEBYRANK' => $getKeyFromFirstArgument,
+            'ZREMRANGEBYSCORE' => $getKeyFromFirstArgument,
+            'ZREVRANGE' => $getKeyFromFirstArgument,
+            'ZREVRANGEBYSCORE' => $getKeyFromFirstArgument,
+            'ZREVRANK' => $getKeyFromFirstArgument,
+            'ZSCORE' => $getKeyFromFirstArgument,
+            'ZUNIONSTORE' => [$this, 'getKeyFromZsetAggregationCommands'],
+            'ZSCAN' => $getKeyFromFirstArgument,
+            'ZLEXCOUNT' => $getKeyFromFirstArgument,
+            'ZRANGEBYLEX' => $getKeyFromFirstArgument,
+            'ZREMRANGEBYLEX' => $getKeyFromFirstArgument,
+            'ZREVRANGEBYLEX' => $getKeyFromFirstArgument,
+
+            /* commands operating on hashes */
+            'HDEL' => $getKeyFromFirstArgument,
+            'HEXISTS' => $getKeyFromFirstArgument,
+            'HGET' => $getKeyFromFirstArgument,
+            'HGETALL' => $getKeyFromFirstArgument,
+            'HMGET' => $getKeyFromFirstArgument,
+            'HMSET' => $getKeyFromFirstArgument,
+            'HINCRBY' => $getKeyFromFirstArgument,
+            'HINCRBYFLOAT' => $getKeyFromFirstArgument,
+            'HKEYS' => $getKeyFromFirstArgument,
+            'HLEN' => $getKeyFromFirstArgument,
+            'HSET' => $getKeyFromFirstArgument,
+            'HSETNX' => $getKeyFromFirstArgument,
+            'HVALS' => $getKeyFromFirstArgument,
+            'HSCAN' => $getKeyFromFirstArgument,
+            'HSTRLEN' => $getKeyFromFirstArgument,
+
+            /* commands operating on HyperLogLog */
+            'PFADD' => $getKeyFromFirstArgument,
+            'PFCOUNT' => $getKeyFromAllArguments,
+            'PFMERGE' => $getKeyFromAllArguments,
+
+            /* scripting */
+            'EVAL' => [$this, 'getKeyFromScriptingCommands'],
+            'EVALSHA' => [$this, 'getKeyFromScriptingCommands'],
+            'EVAL_RO' => [$this, 'getKeyFromScriptingCommands'],
+            'EVALSHA_RO' => [$this, 'getKeyFromScriptingCommands'],
+
+            /* server */
+            'INFO' => [$this, 'getFakeKey'],
+
+            /* commands performing geospatial operations */
+            'GEOADD' => $getKeyFromFirstArgument,
+            'GEOHASH' => $getKeyFromFirstArgument,
+            'GEOPOS' => $getKeyFromFirstArgument,
+            'GEODIST' => $getKeyFromFirstArgument,
+            'GEORADIUS' => [$this, 'getKeyFromGeoradiusCommands'],
+            'GEORADIUSBYMEMBER' => [$this, 'getKeyFromGeoradiusCommands'],
+
+            /* sharded pubsub */
+            'SSUBSCRIBE' => $getKeyFromAllArguments,
+            'SUNSUBSCRIBE' => [$this, 'getKeyFromSUnsubscribeCommand'],
+            'SPUBLISH' => $getKeyFromFirstArgument,
+
+            /* cluster */
+            'CLUSTER' => [$this, 'getFakeKey'],
+        ];
+    }
+
+    /**
+     * Returns the list of IDs for the supported commands.
+     *
+     * @return array
+     */
+    public function getSupportedCommands()
+    {
+        return array_keys($this->commands);
+    }
+
+    /**
+     * Sets an handler for the specified command ID.
+     *
+     * The signature of the callback must have a single parameter of type
+     * Predis\Command\CommandInterface.
+     *
+     * When the callback argument is omitted or NULL, the previously associated
+     * handler for the specified command ID is removed.
+     *
+     * @param string $commandID Command ID.
+     * @param mixed  $callback  A valid callable object, or NULL to unset the handler.
+     *
+     * @throws InvalidArgumentException
+     */
+    public function setCommandHandler($commandID, $callback = null)
+    {
+        $commandID = strtoupper($commandID);
+
+        if (!isset($callback)) {
+            unset($this->commands[$commandID]);
+
+            return;
+        }
+
+        if (!is_callable($callback)) {
+            throw new InvalidArgumentException(
+                'The argument must be a callable object or NULL.'
+            );
+        }
+
+        $this->commands[$commandID] = $callback;
+    }
+
+    /**
+     * Get fake key for commands with no key argument.
+     *
+     * @return string
+     */
+    protected function getFakeKey(): string
+    {
+        return 'key';
+    }
+
+    /**
+     * Extracts the key from the first argument of a command instance.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return string
+     */
+    protected function getKeyFromFirstArgument(CommandInterface $command)
+    {
+        return $command->getArgument(0);
+    }
+
+    /**
+     * Extracts the key from a command with multiple keys only when all keys in
+     * the arguments array produce the same hash.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return string|null
+     */
+    protected function getKeyFromAllArguments(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+
+        if (!$this->checkSameSlotForKeys($arguments)) {
+            return null;
+        }
+
+        return $arguments[0];
+    }
+
+    /**
+     * Extracts the key from a command with multiple keys only when all keys in
+     * the arguments array produce the same hash.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return string|null
+     */
+    protected function getKeyFromInterleavedArguments(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+        $keys = [];
+
+        for ($i = 0; $i < count($arguments); $i += 2) {
+            $keys[] = $arguments[$i];
+        }
+
+        if (!$this->checkSameSlotForKeys($keys)) {
+            return null;
+        }
+
+        return $arguments[0];
+    }
+
+    /**
+     * Extracts the key from SORT command.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return string|null
+     */
+    protected function getKeyFromSortCommand(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+        $firstKey = $arguments[0];
+
+        if (1 === $argc = count($arguments)) {
+            return $firstKey;
+        }
+
+        $keys = [$firstKey];
+
+        for ($i = 1; $i < $argc; ++$i) {
+            if (strtoupper($arguments[$i]) === 'STORE') {
+                $keys[] = $arguments[++$i];
+            }
+        }
+
+        if (!$this->checkSameSlotForKeys($keys)) {
+            return null;
+        }
+
+        return $firstKey;
+    }
+
+    /**
+     * Extracts the key from BLPOP and BRPOP commands.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return string|null
+     */
+    protected function getKeyFromBlockingListCommands(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+
+        if (!$this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) {
+            return null;
+        }
+
+        return $arguments[0];
+    }
+
+    /**
+     * Extracts the key from BITOP command.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return string|null
+     */
+    protected function getKeyFromBitOp(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+
+        if (!$this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) {
+            return null;
+        }
+
+        return $arguments[1];
+    }
+
+    /**
+     * Extracts the key from GEORADIUS and GEORADIUSBYMEMBER commands.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return string|null
+     */
+    protected function getKeyFromGeoradiusCommands(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+        $argc = count($arguments);
+        $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4;
+
+        if ($argc > $startIndex) {
+            $keys = [$arguments[0]];
+
+            for ($i = $startIndex; $i < $argc; ++$i) {
+                $argument = strtoupper($arguments[$i]);
+                if ($argument === 'STORE' || $argument === 'STOREDIST') {
+                    $keys[] = $arguments[++$i];
+                }
+            }
+
+            if (!$this->checkSameSlotForKeys($keys)) {
+                return null;
+            }
+        }
+
+        return $arguments[0];
+    }
+
+    /**
+     * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return string|null
+     */
+    protected function getKeyFromZsetAggregationCommands(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+        $keys = array_merge([$arguments[0]], array_slice($arguments, 2, $arguments[1]));
+
+        if (!$this->checkSameSlotForKeys($keys)) {
+            return null;
+        }
+
+        return $arguments[0];
+    }
+
+    /**
+     * Extracts key from SUNSUBSCRIBE command if it's given.
+     *
+     * @param  CommandInterface $command
+     * @return string
+     */
+    protected function getKeyFromSUnsubscribeCommand(CommandInterface $command): ?string
+    {
+        $arguments = $command->getArguments();
+
+        // SUNSUBSCRIBE command could be called without arguments, so it doesn't matter on each node it will be called.
+        if (empty($arguments)) {
+            return 'fake';
+        }
+
+        return $this->getKeyFromAllArguments($command);
+    }
+
+    /**
+     * Extracts the key from EVAL and EVALSHA commands.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return string|null
+     */
+    protected function getKeyFromScriptingCommands(CommandInterface $command)
+    {
+        $keys = $command instanceof ScriptCommand
+            ? $command->getKeys()
+            : array_slice($args = $command->getArguments(), 2, $args[1]);
+
+        if (!$keys || !$this->checkSameSlotForKeys($keys)) {
+            return null;
+        }
+
+        return $keys[0];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSlot(CommandInterface $command)
+    {
+        $slot = $command->getSlot();
+
+        if (!isset($slot) && isset($this->commands[$cmdID = $command->getId()])) {
+            $key = call_user_func($this->commands[$cmdID], $command);
+
+            if (isset($key)) {
+                $slot = $this->getSlotByKey($key);
+                $command->setSlot($slot);
+            }
+        }
+
+        return $slot;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function checkSameSlotForKeys(array $keys): bool
+    {
+        if (!$count = count($keys)) {
+            return false;
+        }
+
+        $currentSlot = $this->getSlotByKey($keys[0]);
+
+        for ($i = 1; $i < $count; ++$i) {
+            $nextSlot = $this->getSlotByKey($keys[$i]);
+
+            if ($currentSlot !== $nextSlot) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns only the hashable part of a key (delimited by "{...}"), or the
+     * whole key if a key tag is not found in the string.
+     *
+     * @param string $key A key.
+     *
+     * @return string
+     */
+    protected function extractKeyTag($key)
+    {
+        if (false !== $start = strpos($key, '{')) {
+            if (false !== ($end = strpos($key, '}', $start)) && $end !== ++$start) {
+                $key = substr($key, $start, $end - $start);
+            }
+        }
+
+        return $key;
+    }
+}

+ 81 - 0
vendor/predis/predis/src/Cluster/Distributor/DistributorInterface.php

@@ -0,0 +1,81 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster\Distributor;
+
+use Predis\Cluster\Hash\HashGeneratorInterface;
+
+/**
+ * A distributor implements the logic to automatically distribute keys among
+ * several nodes for client-side sharding.
+ */
+interface DistributorInterface
+{
+    /**
+     * Adds a node to the distributor with an optional weight.
+     *
+     * @param mixed $node   Node object.
+     * @param int   $weight Weight for the node.
+     */
+    public function add($node, $weight = null);
+
+    /**
+     * Removes a node from the distributor.
+     *
+     * @param mixed $node Node object.
+     */
+    public function remove($node);
+
+    /**
+     * Returns the corresponding slot of a node from the distributor using the
+     * computed hash of a key.
+     *
+     * @param mixed $hash
+     *
+     * @return mixed
+     */
+    public function getSlot($hash);
+
+    /**
+     * Returns a node from the distributor using its assigned slot ID.
+     *
+     * @param mixed $slot
+     *
+     * @return mixed|null
+     */
+    public function getBySlot($slot);
+
+    /**
+     * Returns a node from the distributor using the computed hash of a key.
+     *
+     * @param mixed $hash
+     *
+     * @return mixed
+     */
+    public function getByHash($hash);
+
+    /**
+     * Returns a node from the distributor mapping to the specified value.
+     *
+     * @param string $value
+     *
+     * @return mixed
+     */
+    public function get($value);
+
+    /**
+     * Returns the underlying hash generator instance.
+     *
+     * @return HashGeneratorInterface
+     */
+    public function getHashGenerator();
+}

+ 22 - 0
vendor/predis/predis/src/Cluster/Distributor/EmptyRingException.php

@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster\Distributor;
+
+use Exception;
+
+/**
+ * Exception class that identifies empty rings.
+ */
+class EmptyRingException extends Exception
+{
+}

+ 268 - 0
vendor/predis/predis/src/Cluster/Distributor/HashRing.php

@@ -0,0 +1,268 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster\Distributor;
+
+use Predis\Cluster\Hash\HashGeneratorInterface;
+
+/**
+ * This class implements an hashring-based distributor that uses the same
+ * algorithm of memcache to distribute keys in a cluster using client-side
+ * sharding.
+ * @author Lorenzo Castelli <lcastelli@gmail.com>
+ */
+class HashRing implements DistributorInterface, HashGeneratorInterface
+{
+    public const DEFAULT_REPLICAS = 128;
+    public const DEFAULT_WEIGHT = 100;
+
+    private $ring;
+    private $ringKeys;
+    private $ringKeysCount;
+    private $replicas;
+    private $nodeHashCallback;
+    private $nodes = [];
+
+    /**
+     * @param int   $replicas         Number of replicas in the ring.
+     * @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes.
+     */
+    public function __construct($replicas = self::DEFAULT_REPLICAS, $nodeHashCallback = null)
+    {
+        $this->replicas = $replicas;
+        $this->nodeHashCallback = $nodeHashCallback;
+    }
+
+    /**
+     * Adds a node to the ring with an optional weight.
+     *
+     * @param mixed $node   Node object.
+     * @param int   $weight Weight for the node.
+     */
+    public function add($node, $weight = null)
+    {
+        // In case of collisions in the hashes of the nodes, the node added
+        // last wins, thus the order in which nodes are added is significant.
+        $this->nodes[] = [
+            'object' => $node,
+            'weight' => (int) $weight ?: $this::DEFAULT_WEIGHT,
+        ];
+
+        $this->reset();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function remove($node)
+    {
+        // A node is removed by resetting the ring so that it's recreated from
+        // scratch, in order to reassign possible hashes with collisions to the
+        // right node according to the order in which they were added in the
+        // first place.
+        for ($i = 0; $i < count($this->nodes); ++$i) {
+            if ($this->nodes[$i]['object'] === $node) {
+                array_splice($this->nodes, $i, 1);
+                $this->reset();
+
+                break;
+            }
+        }
+    }
+
+    /**
+     * Resets the distributor.
+     */
+    private function reset()
+    {
+        unset(
+            $this->ring,
+            $this->ringKeys,
+            $this->ringKeysCount
+        );
+    }
+
+    /**
+     * Returns the initialization status of the distributor.
+     *
+     * @return bool
+     */
+    private function isInitialized()
+    {
+        return isset($this->ringKeys);
+    }
+
+    /**
+     * Calculates the total weight of all the nodes in the distributor.
+     *
+     * @return int
+     */
+    private function computeTotalWeight()
+    {
+        $totalWeight = 0;
+
+        foreach ($this->nodes as $node) {
+            $totalWeight += $node['weight'];
+        }
+
+        return $totalWeight;
+    }
+
+    /**
+     * Initializes the distributor.
+     */
+    private function initialize()
+    {
+        if ($this->isInitialized()) {
+            return;
+        }
+
+        if (!$this->nodes) {
+            throw new EmptyRingException('Cannot initialize an empty hashring.');
+        }
+
+        $this->ring = [];
+        $totalWeight = $this->computeTotalWeight();
+        $nodesCount = count($this->nodes);
+
+        foreach ($this->nodes as $node) {
+            $weightRatio = $node['weight'] / $totalWeight;
+            $this->addNodeToRing($this->ring, $node, $nodesCount, $this->replicas, $weightRatio);
+        }
+
+        ksort($this->ring, SORT_NUMERIC);
+        $this->ringKeys = array_keys($this->ring);
+        $this->ringKeysCount = count($this->ringKeys);
+    }
+
+    /**
+     * Implements the logic needed to add a node to the hashring.
+     *
+     * @param array $ring        Source hashring.
+     * @param mixed $node        Node object to be added.
+     * @param int   $totalNodes  Total number of nodes.
+     * @param int   $replicas    Number of replicas in the ring.
+     * @param float $weightRatio Weight ratio for the node.
+     */
+    protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio)
+    {
+        $nodeObject = $node['object'];
+        $nodeHash = $this->getNodeHash($nodeObject);
+        $replicas = (int) round($weightRatio * $totalNodes * $replicas);
+
+        for ($i = 0; $i < $replicas; ++$i) {
+            $key = $this->hash("$nodeHash:$i");
+            $ring[$key] = $nodeObject;
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getNodeHash($nodeObject)
+    {
+        if (!isset($this->nodeHashCallback)) {
+            return (string) $nodeObject;
+        }
+
+        return call_user_func($this->nodeHashCallback, $nodeObject);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function hash($value)
+    {
+        return crc32($value);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getByHash($hash)
+    {
+        return $this->ring[$this->getSlot($hash)];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getBySlot($slot)
+    {
+        $this->initialize();
+
+        if (isset($this->ring[$slot])) {
+            return $this->ring[$slot];
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSlot($hash)
+    {
+        $this->initialize();
+
+        $ringKeys = $this->ringKeys;
+        $upper = $this->ringKeysCount - 1;
+        $lower = 0;
+
+        while ($lower <= $upper) {
+            $index = ($lower + $upper) >> 1;
+            $item = $ringKeys[$index];
+
+            if ($item > $hash) {
+                $upper = $index - 1;
+            } elseif ($item < $hash) {
+                $lower = $index + 1;
+            } else {
+                return $item;
+            }
+        }
+
+        return $ringKeys[$this->wrapAroundStrategy($upper, $lower, $this->ringKeysCount)];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function get($value)
+    {
+        $hash = $this->hash($value);
+
+        return $this->getByHash($hash);
+    }
+
+    /**
+     * Implements a strategy to deal with wrap-around errors during binary searches.
+     *
+     * @param int $upper
+     * @param int $lower
+     * @param int $ringKeysCount
+     *
+     * @return int
+     */
+    protected function wrapAroundStrategy($upper, $lower, $ringKeysCount)
+    {
+        // Binary search for the last item in ringkeys with a value less or
+        // equal to the key. If no such item exists, return the last item.
+        return $upper >= 0 ? $upper : $ringKeysCount - 1;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getHashGenerator()
+    {
+        return $this;
+    }
+}

+ 70 - 0
vendor/predis/predis/src/Cluster/Distributor/KetamaRing.php

@@ -0,0 +1,70 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster\Distributor;
+
+/**
+ * This class implements an hashring-based distributor that uses the same
+ * algorithm of libketama to distribute keys in a cluster using client-side
+ * sharding.
+ * @author Lorenzo Castelli <lcastelli@gmail.com>
+ */
+class KetamaRing extends HashRing
+{
+    public const DEFAULT_REPLICAS = 160;
+
+    /**
+     * @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes.
+     */
+    public function __construct($nodeHashCallback = null)
+    {
+        parent::__construct($this::DEFAULT_REPLICAS, $nodeHashCallback);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio)
+    {
+        $nodeObject = $node['object'];
+        $nodeHash = $this->getNodeHash($nodeObject);
+        $replicas = (int) floor($weightRatio * $totalNodes * ($replicas / 4));
+
+        for ($i = 0; $i < $replicas; ++$i) {
+            $unpackedDigest = unpack('V4', md5("$nodeHash-$i", true));
+
+            foreach ($unpackedDigest as $key) {
+                $ring[$key] = $nodeObject;
+            }
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function hash($value)
+    {
+        $hash = unpack('V', md5($value, true));
+
+        return $hash[1];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function wrapAroundStrategy($upper, $lower, $ringKeysCount)
+    {
+        // Binary search for the first item in ringkeys with a value greater
+        // or equal to the key. If no such item exists, return the first item.
+        return $lower < $ringKeysCount ? $lower : 0;
+    }
+}

+ 73 - 0
vendor/predis/predis/src/Cluster/Hash/CRC16.php

@@ -0,0 +1,73 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster\Hash;
+
+/**
+ * Hash generator implementing the CRC-CCITT-16 algorithm used by redis-cluster.
+ */
+class CRC16 implements HashGeneratorInterface
+{
+    private static $CCITT_16 = [
+        0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
+        0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
+        0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
+        0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
+        0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
+        0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
+        0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
+        0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
+        0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
+        0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
+        0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
+        0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
+        0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
+        0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
+        0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
+        0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
+        0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
+        0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
+        0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
+        0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
+        0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
+        0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
+        0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
+        0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
+        0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
+        0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
+        0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
+        0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
+        0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
+        0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
+        0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
+        0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0,
+    ];
+
+    /**
+     * {@inheritdoc}
+     */
+    public function hash($value)
+    {
+        // CRC-CCITT-16 algorithm
+        $crc = 0;
+        $CCITT_16 = self::$CCITT_16;
+
+        $value = (string) $value;
+        $strlen = strlen($value);
+
+        for ($i = 0; $i < $strlen; ++$i) {
+            $crc = (($crc << 8) ^ $CCITT_16[($crc >> 8) ^ ord($value[$i])]) & 0xFFFF;
+        }
+
+        return $crc;
+    }
+}

+ 29 - 0
vendor/predis/predis/src/Cluster/Hash/HashGeneratorInterface.php

@@ -0,0 +1,29 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster\Hash;
+
+/**
+ * An hash generator implements the logic used to calculate the hash of a key to
+ * distribute operations among Redis nodes.
+ */
+interface HashGeneratorInterface
+{
+    /**
+     * Generates an hash from a string to be used for distribution.
+     *
+     * @param string $value String value.
+     *
+     * @return int
+     */
+    public function hash($value);
+}

+ 40 - 0
vendor/predis/predis/src/Cluster/NullSlotRange.php

@@ -0,0 +1,40 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster;
+
+/**
+ * Represents the gap between slot ranges.
+ */
+class NullSlotRange extends SlotRange
+{
+    public function __construct(int $start, int $end)
+    {
+        parent::__construct($start, $end, '');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return [];
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function count(): int
+    {
+        return 0;
+    }
+}

+ 75 - 0
vendor/predis/predis/src/Cluster/PredisStrategy.php

@@ -0,0 +1,75 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster;
+
+use Predis\Cluster\Distributor\DistributorInterface;
+use Predis\Cluster\Distributor\HashRing;
+
+/**
+ * Default cluster strategy used by Predis to handle client-side sharding.
+ */
+class PredisStrategy extends ClusterStrategy
+{
+    protected $distributor;
+
+    /**
+     * @param DistributorInterface|null $distributor Optional distributor instance.
+     */
+    public function __construct(?DistributorInterface $distributor = null)
+    {
+        parent::__construct();
+
+        $this->distributor = $distributor ?: new HashRing();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSlotByKey($key)
+    {
+        $key = $this->extractKeyTag($key);
+        $hash = $this->distributor->hash($key);
+
+        return $this->distributor->getSlot($hash);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function checkSameSlotForKeys(array $keys): bool
+    {
+        if (!$count = count($keys)) {
+            return false;
+        }
+
+        $currentKey = $this->extractKeyTag($keys[0]);
+
+        for ($i = 1; $i < $count; ++$i) {
+            $nextKey = $this->extractKeyTag($keys[$i]);
+
+            if ($currentKey !== $nextKey) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDistributor()
+    {
+        return $this->distributor;
+    }
+}

+ 55 - 0
vendor/predis/predis/src/Cluster/RedisStrategy.php

@@ -0,0 +1,55 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster;
+
+use Predis\Cluster\Hash\CRC16;
+use Predis\Cluster\Hash\HashGeneratorInterface;
+use Predis\NotSupportedException;
+
+/**
+ * Default class used by Predis to calculate hashes out of keys of
+ * commands supported by redis-cluster.
+ */
+class RedisStrategy extends ClusterStrategy
+{
+    protected $hashGenerator;
+
+    /**
+     * @param HashGeneratorInterface|null $hashGenerator Hash generator instance.
+     */
+    public function __construct(?HashGeneratorInterface $hashGenerator = null)
+    {
+        parent::__construct();
+
+        $this->hashGenerator = $hashGenerator ?: new CRC16();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSlotByKey($key)
+    {
+        $key = $this->extractKeyTag($key);
+
+        return $this->hashGenerator->hash($key) & 0x3FFF;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDistributor()
+    {
+        $class = get_class($this);
+        throw new NotSupportedException("$class does not provide an external distributor");
+    }
+}

+ 209 - 0
vendor/predis/predis/src/Cluster/SimpleSlotMap.php

@@ -0,0 +1,209 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster;
+
+use ArrayAccess;
+use ArrayIterator;
+use Countable;
+use IteratorAggregate;
+use OutOfBoundsException;
+use Predis\Connection\NodeConnectionInterface;
+use ReturnTypeWillChange;
+use Traversable;
+
+/**
+ * Slot map for redis-cluster.
+ */
+class SimpleSlotMap implements ArrayAccess, IteratorAggregate, Countable
+{
+    private $slots = [];
+
+    /**
+     * Checks if the given slot is valid.
+     *
+     * @param int $slot Slot index.
+     *
+     * @return bool
+     */
+    public static function isValid($slot)
+    {
+        return $slot >= 0x0000 && $slot <= 0x3FFF;
+    }
+
+    /**
+     * Checks if the given slot range is valid.
+     *
+     * @param int $first Initial slot of the range.
+     * @param int $last  Last slot of the range.
+     *
+     * @return bool
+     */
+    public static function isValidRange($first, $last)
+    {
+        return $first >= 0x0000 && $first <= 0x3FFF && $last >= 0x0000 && $last <= 0x3FFF && $first <= $last;
+    }
+
+    /**
+     * Resets the slot map.
+     */
+    public function reset()
+    {
+        $this->slots = [];
+    }
+
+    /**
+     * Checks if the slot map is empty.
+     *
+     * @return bool
+     */
+    public function isEmpty()
+    {
+        return empty($this->slots);
+    }
+
+    /**
+     * Returns the current slot map as a dictionary of $slot => $node.
+     *
+     * The order of the slots in the dictionary is not guaranteed.
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        return $this->slots;
+    }
+
+    /**
+     * Returns the list of unique nodes in the slot map.
+     *
+     * @return array
+     */
+    public function getNodes()
+    {
+        return array_keys(array_flip($this->slots));
+    }
+
+    /**
+     * Assigns the specified slot range to a node.
+     *
+     * @param int                            $first      Initial slot of the range.
+     * @param int                            $last       Last slot of the range.
+     * @param NodeConnectionInterface|string $connection ID or connection instance.
+     *
+     * @throws OutOfBoundsException
+     */
+    public function setSlots($first, $last, $connection)
+    {
+        if (!static::isValidRange($first, $last)) {
+            throw new OutOfBoundsException("Invalid slot range $first-$last for `$connection`");
+        }
+
+        $this->slots += array_fill($first, $last - $first + 1, (string) $connection);
+    }
+
+    /**
+     * Returns the specified slot range.
+     *
+     * @param int $first Initial slot of the range.
+     * @param int $last  Last slot of the range.
+     *
+     * @return array
+     */
+    public function getSlots($first, $last)
+    {
+        if (!static::isValidRange($first, $last)) {
+            throw new OutOfBoundsException("Invalid slot range $first-$last");
+        }
+
+        return array_intersect_key($this->slots, array_fill($first, $last - $first + 1, null));
+    }
+
+    /**
+     * Checks if the specified slot is assigned.
+     *
+     * @param int $slot Slot index.
+     *
+     * @return bool
+     */
+    #[ReturnTypeWillChange]
+    public function offsetExists($slot)
+    {
+        return isset($this->slots[$slot]);
+    }
+
+    /**
+     * Returns the node assigned to the specified slot.
+     *
+     * @param int $slot Slot index.
+     *
+     * @return string|null
+     */
+    #[ReturnTypeWillChange]
+    public function offsetGet($slot)
+    {
+        return $this->slots[$slot] ?? null;
+    }
+
+    /**
+     * Assigns the specified slot to a node.
+     *
+     * @param int                            $slot       Slot index.
+     * @param NodeConnectionInterface|string $connection ID or connection instance.
+     *
+     * @return void
+     */
+    #[ReturnTypeWillChange]
+    public function offsetSet($slot, $connection)
+    {
+        if (!static::isValid($slot)) {
+            throw new OutOfBoundsException("Invalid slot $slot for `$connection`");
+        }
+
+        $this->slots[(int) $slot] = (string) $connection;
+    }
+
+    /**
+     * Returns the node assigned to the specified slot.
+     *
+     * @param int $slot Slot index.
+     *
+     * @return void
+     */
+    #[ReturnTypeWillChange]
+    public function offsetUnset($slot)
+    {
+        unset($this->slots[$slot]);
+    }
+
+    /**
+     * Returns the current number of assigned slots.
+     *
+     * @return int
+     */
+    #[ReturnTypeWillChange]
+    public function count()
+    {
+        return count($this->slots);
+    }
+
+    /**
+     * Returns an iterator over the slot map.
+     *
+     * @return Traversable<int, string>
+     */
+    #[ReturnTypeWillChange]
+    public function getIterator()
+    {
+        return new ArrayIterator($this->slots);
+    }
+}

+ 417 - 0
vendor/predis/predis/src/Cluster/SlotMap.php

@@ -0,0 +1,417 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster;
+
+use ArrayAccess;
+use ArrayIterator;
+use Countable;
+use IteratorAggregate;
+use OutOfBoundsException;
+use Predis\Connection\NodeConnectionInterface;
+use ReturnTypeWillChange;
+use Traversable;
+
+/**
+ * Compact slot map for redis-cluster.
+ */
+class SlotMap implements ArrayAccess, IteratorAggregate, Countable
+{
+    /**
+     * Slot ranges list.
+     *
+     * @var SlotRange[]
+     */
+    private $slotRanges = [];
+
+    /**
+     * Checks if the given slot is valid.
+     *
+     * @param int $slot Slot index.
+     *
+     * @return bool
+     */
+    public static function isValid($slot)
+    {
+        return $slot >= 0 && $slot <= SlotRange::MAX_SLOTS;
+    }
+
+    /**
+     * Checks if the given slot range is valid.
+     *
+     * @param int $first Initial slot of the range.
+     * @param int $last  Last slot of the range.
+     *
+     * @return bool
+     */
+    public static function isValidRange($first, $last)
+    {
+        return SlotRange::isValidRange($first, $last);
+    }
+
+    /**
+     * Resets the slot map.
+     */
+    public function reset()
+    {
+        $this->slotRanges = [];
+    }
+
+    /**
+     * Checks if the slot map is empty.
+     *
+     * @return bool
+     */
+    public function isEmpty()
+    {
+        return empty($this->slotRanges);
+    }
+
+    /**
+     * Returns the current slot map as a dictionary of $slot => $node.
+     *
+     * The order of the slots in the dictionary is not guaranteed.
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        return array_reduce(
+            $this->slotRanges,
+            function ($carry, $slotRange) {
+                return $carry + $slotRange->toArray();
+            },
+            []
+        );
+    }
+
+    /**
+     * Returns the list of unique nodes in the slot map.
+     *
+     * @return array
+     */
+    public function getNodes()
+    {
+        return array_unique(array_map(
+            function ($slotRange) {
+                return $slotRange->getConnection();
+            },
+            $this->slotRanges
+        ));
+    }
+
+    /**
+     * Returns the list of slot ranges.
+     *
+     * @return SlotRange[]
+     */
+    public function getSlotRanges()
+    {
+        return $this->slotRanges;
+    }
+
+    /**
+     * Assigns the specified slot range to a node.
+     *
+     * @param int                            $first      Initial slot of the range.
+     * @param int                            $last       Last slot of the range.
+     * @param NodeConnectionInterface|string $connection ID or connection instance.
+     *
+     * @throws OutOfBoundsException
+     */
+    public function setSlots($first, $last, $connection)
+    {
+        if (!static::isValidRange($first, $last)) {
+            throw new OutOfBoundsException("Invalid slot range $first-$last for `$connection`");
+        }
+
+        $targetSlotRange = new SlotRange($first, $last, (string) $connection);
+
+        // Get gaps of slot ranges list.
+        $gaps = $this->getGaps($this->slotRanges);
+
+        $results = $this->slotRanges;
+
+        foreach ($gaps as $gap) {
+            if (!$gap->hasIntersectionWith($targetSlotRange)) {
+                continue;
+            }
+
+            // Get intersection of the gap and target slot range.
+            $results[] = new SlotRange(
+                max($gap->getStart(), $targetSlotRange->getStart()),
+                min($gap->getEnd(), $targetSlotRange->getEnd()),
+                $targetSlotRange->getConnection()
+            );
+        }
+
+        $this->sortSlotRanges($results);
+
+        $results = $this->compactSlotRanges($results);
+
+        $this->slotRanges = $results;
+    }
+
+    /**
+     * Returns the specified slot range.
+     *
+     * @param int $first Initial slot of the range.
+     * @param int $last  Last slot of the range.
+     *
+     * @return array<int, string>
+     */
+    public function getSlots($first, $last)
+    {
+        if (!static::isValidRange($first, $last)) {
+            throw new OutOfBoundsException("Invalid slot range $first-$last");
+        }
+
+        $placeHolder = new NullSlotRange($first, $last);
+
+        $intersections = [];
+        foreach ($this->slotRanges as $slotRange) {
+            if (!$placeHolder->hasIntersectionWith($slotRange)) {
+                continue;
+            }
+
+            $intersections[] = new SlotRange(
+                max($placeHolder->getStart(), $slotRange->getStart()),
+                min($placeHolder->getEnd(), $slotRange->getEnd()),
+                $slotRange->getConnection()
+            );
+        }
+
+        return array_reduce(
+            $intersections,
+            function ($carry, $slotRange) {
+                return $carry + $slotRange->toArray();
+            },
+            []
+        );
+    }
+
+    /**
+     * Checks if the specified slot is assigned.
+     *
+     * @param int $slot Slot index.
+     *
+     * @return bool
+     */
+    #[ReturnTypeWillChange]
+    public function offsetExists($slot)
+    {
+        return $this->findRangeBySlot($slot) !== false;
+    }
+
+    /**
+     * Returns the node assigned to the specified slot.
+     *
+     * @param int $slot Slot index.
+     *
+     * @return string|null
+     */
+    #[ReturnTypeWillChange]
+    public function offsetGet($slot)
+    {
+        $found = $this->findRangeBySlot($slot);
+
+        return $found ? $found->getConnection() : null;
+    }
+
+    /**
+     * Assigns the specified slot to a node.
+     *
+     * @param int                            $slot       Slot index.
+     * @param NodeConnectionInterface|string $connection ID or connection instance.
+     *
+     * @return void
+     */
+    #[ReturnTypeWillChange]
+    public function offsetSet($slot, $connection)
+    {
+        if (!static::isValid($slot)) {
+            throw new OutOfBoundsException("Invalid slot $slot for `$connection`");
+        }
+
+        $this->offsetUnset($slot);
+        $this->setSlots($slot, $slot, $connection);
+    }
+
+    /**
+     * Returns the node assigned to the specified slot.
+     *
+     * @param int $slot Slot index.
+     *
+     * @return void
+     */
+    #[ReturnTypeWillChange]
+    public function offsetUnset($slot)
+    {
+        if (!static::isValid($slot)) {
+            throw new OutOfBoundsException("Invalid slot $slot");
+        }
+
+        $results = [];
+        foreach ($this->slotRanges as $slotRange) {
+            if (!$slotRange->hasSlot($slot)) {
+                $results[] = $slotRange;
+            }
+
+            if (static::isValidRange($slotRange->getStart(), $slot - 1)) {
+                $results[] = new SlotRange($slotRange->getStart(), $slot - 1, $slotRange->getConnection());
+            }
+
+            if (static::isValidRange($slot + 1, $slotRange->getEnd())) {
+                $results[] = new SlotRange($slot + 1, $slotRange->getEnd(), $slotRange->getConnection());
+            }
+        }
+
+        $this->slotRanges = $results;
+    }
+
+    /**
+     * Returns the current number of assigned slots.
+     *
+     * @return int
+     */
+    #[ReturnTypeWillChange]
+    public function count()
+    {
+        return array_sum(array_map(
+            function ($slotRange) {
+                return $slotRange->count();
+            },
+            $this->slotRanges
+        ));
+    }
+
+    /**
+     * Returns an iterator over the slot map.
+     *
+     * @return Traversable<int, string>
+     */
+    #[ReturnTypeWillChange]
+    public function getIterator()
+    {
+        return new ArrayIterator($this->toArray());
+    }
+
+    /**
+     * Find the slot range which contains the specific slot index.
+     *
+     * @param int $slot Slot index.
+     *
+     * @return SlotRange|false The slot range object or false if not found.
+     */
+    protected function findRangeBySlot(int $slot)
+    {
+        foreach ($this->slotRanges as $slotRange) {
+            if ($slotRange->hasSlot($slot)) {
+                return $slotRange;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Get gaps between sorted slot ranges with NullSlotRange object.
+     *
+     * @param SlotRange[] $slotRanges
+     *
+     * @return SlotRange[]
+     */
+    protected function getGaps(array $slotRanges)
+    {
+        if (empty($slotRanges)) {
+            return [
+                new NullSlotRange(0, SlotRange::MAX_SLOTS),
+            ];
+        }
+        $gaps = [];
+        $count = count($slotRanges);
+        $i = 0;
+        foreach ($slotRanges as $key => $slotRange) {
+            $start = $slotRange->getStart();
+            $end = $slotRange->getEnd();
+            if (static::isValidRange($i, $start - 1)) {
+                $gaps[] = new NullSlotRange($i, $start - 1);
+            }
+
+            $i = $end + 1;
+
+            if ($key === $count - 1) {
+                if (static::isValidRange($i, SlotRange::MAX_SLOTS)) {
+                    $gaps[] = new NullSlotRange($i, SlotRange::MAX_SLOTS);
+                }
+            }
+        }
+
+        return $gaps;
+    }
+
+    /**
+     * Sort slot ranges by start index.
+     *
+     * @param SlotRange[] $slotRanges
+     *
+     * @return void
+     */
+    protected function sortSlotRanges(array &$slotRanges)
+    {
+        usort(
+            $slotRanges,
+            function (SlotRange $a, SlotRange $b) {
+                if ($a->getStart() == $b->getStart()) {
+                    return 0;
+                }
+
+                return $a->getStart() < $b->getStart() ? -1 : 1;
+            }
+        );
+    }
+
+    /**
+     * Compact adjacent slot ranges with the same connection.
+     *
+     * @param SlotRange[] $slotRanges
+     *
+     * @return SlotRange[]
+     */
+    protected function compactSlotRanges(array $slotRanges)
+    {
+        if (empty($slotRanges)) {
+            return [];
+        }
+
+        $compacted = [];
+        $count = count($slotRanges);
+        $i = 0;
+        $carry = $slotRanges[0];
+        while ($i < $count) {
+            $next = $slotRanges[$i + 1] ?? null;
+            if (
+                !is_null($next)
+                && ($carry->getEnd() + 1) === $next->getStart()
+                && $carry->getConnection() === $next->getConnection()
+            ) {
+                $carry = new SlotRange($carry->getStart(), $next->getEnd(), $carry->getConnection());
+            } else {
+                $compacted[] = $carry;
+                $carry = $next;
+            }
+            $i++;
+        }
+
+        return array_values($compacted);
+    }
+}

+ 145 - 0
vendor/predis/predis/src/Cluster/SlotRange.php

@@ -0,0 +1,145 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster;
+
+use Countable;
+use OutOfBoundsException;
+
+/**
+ * Represents a range of slots in a Redis cluster.
+ */
+class SlotRange implements Countable
+{
+    /**
+     * Maximum number of slots in a Redis cluster is 16384.
+     */
+    public const MAX_SLOTS = 0x3FFF;
+
+    /**
+     * Starting slot of the range.
+     *
+     * @var int
+     */
+    protected $start;
+
+    /**
+     * Ending slot of the range.
+     *
+     * @var int
+     */
+    protected $end;
+
+    /**
+     * Connection to the server hosting this slot range.
+     *
+     * @var string
+     */
+    protected $connection;
+
+    public function __construct(int $start, int $end, string $connection)
+    {
+        if (!static::isValidRange($start, $end)) {
+            throw new OutOfBoundsException("Invalid slot range $start-$end for `$connection`");
+        }
+        $this->start = $start;
+        $this->end = $end;
+        $this->connection = $connection;
+    }
+
+    /**
+     * Checks if a slot range is valid.
+     *
+     * @param int $first
+     * @param int $last
+     *
+     * @return bool
+     */
+    public static function isValidRange($first, $last)
+    {
+        return $first >= 0x0000 && $first <= self::MAX_SLOTS && $last >= 0x0000 && $last <= self::MAX_SLOTS && $first <= $last;
+    }
+
+    /**
+     * Returns the start slot index of this range.
+     *
+     * @return int
+     */
+    public function getStart()
+    {
+        return $this->start;
+    }
+
+    /**
+     * Returns the end slot index of this range.
+     *
+     * @return int
+     */
+    public function getEnd()
+    {
+        return $this->end;
+    }
+
+    /**
+     * Returns the connection to the server hosting this slot range.
+     *
+     * @return string
+     */
+    public function getConnection()
+    {
+        return $this->connection;
+    }
+
+    /**
+     * Checks if the specific slot is contained in this range.
+     *
+     * @param int $slot
+     *
+     * @return bool
+     */
+    public function hasSlot(int $slot)
+    {
+        return $this->start <= $slot && $this->end >= $slot;
+    }
+
+    /**
+     * Returns an array of connection strings for each slot in this range.
+     *
+     * @return string[]
+     */
+    public function toArray(): array
+    {
+        return array_fill($this->start, $this->end - $this->start + 1, $this->connection);
+    }
+
+    /**
+     * Returns the number of slots in this range.
+     *
+     * @return int
+     */
+    public function count(): int
+    {
+        return $this->end - $this->start + 1;
+    }
+
+    /**
+     * Checks if this range has an intersection with the given slot range.
+     *
+     * @param SlotRange $slotRange
+     *
+     * @return bool
+     */
+    public function hasIntersectionWith(SlotRange $slotRange): bool
+    {
+        return $this->start <= $slotRange->getEnd() && $this->end >= $slotRange->getStart();
+    }
+}

+ 60 - 0
vendor/predis/predis/src/Cluster/StrategyInterface.php

@@ -0,0 +1,60 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Cluster;
+
+use Predis\Cluster\Distributor\DistributorInterface;
+use Predis\Command\CommandInterface;
+
+/**
+ * Interface for classes defining the strategy used to calculate an hash out of
+ * keys extracted from supported commands.
+ *
+ * This is mostly useful to support clustering via client-side sharding.
+ */
+interface StrategyInterface
+{
+    /**
+     * Returns a slot for the given command used for clustering distribution or
+     * NULL when this is not possible.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return int|null
+     */
+    public function getSlot(CommandInterface $command);
+
+    /**
+     * Returns a slot for the given key used for clustering distribution or NULL
+     * when this is not possible.
+     *
+     * @param string $key Key string.
+     *
+     * @return int|null
+     */
+    public function getSlotByKey($key);
+
+    /**
+     * Returns a distributor instance to be used by the cluster.
+     *
+     * @return DistributorInterface
+     */
+    public function getDistributor();
+
+    /**
+     * Checks if the specified array of keys will generate the same hash.
+     *
+     * @param  array $keys
+     * @return bool
+     */
+    public function checkSameSlotForKeys(array $keys): bool;
+}

+ 196 - 0
vendor/predis/predis/src/Collection/Iterator/CursorBasedIterator.php

@@ -0,0 +1,196 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use Iterator;
+use Predis\ClientInterface;
+use Predis\NotSupportedException;
+use ReturnTypeWillChange;
+
+/**
+ * Provides the base implementation for a fully-rewindable PHP iterator that can
+ * incrementally iterate over cursor-based collections stored on Redis using the
+ * commands in the `SCAN` family.
+ *
+ * Given their incremental nature with multiple fetches, these kind of iterators
+ * offer limited guarantees about the returned elements because the collection
+ * can change several times during the iteration process.
+ *
+ * @see http://redis.io/commands/scan
+ */
+abstract class CursorBasedIterator implements Iterator
+{
+    protected $client;
+    protected $match;
+    protected $count;
+
+    protected $valid;
+    protected $fetchmore;
+    protected $elements;
+    protected $cursor;
+    protected $position;
+    protected $current;
+
+    /**
+     * @param ClientInterface $client Client connected to Redis.
+     * @param string          $match  Pattern to match during the server-side iteration.
+     * @param int             $count  Hint used by Redis to compute the number of results per iteration.
+     */
+    public function __construct(ClientInterface $client, $match = null, $count = null)
+    {
+        $this->client = $client;
+        $this->match = $match;
+        $this->count = $count;
+
+        $this->reset();
+    }
+
+    /**
+     * Ensures that the client supports the specified Redis command required to
+     * fetch elements from the server to perform the iteration.
+     *
+     * @param ClientInterface $client    Client connected to Redis.
+     * @param string          $commandID Command ID.
+     *
+     * @throws NotSupportedException
+     */
+    protected function requiredCommand(ClientInterface $client, $commandID)
+    {
+        if (!$client->getCommandFactory()->supports($commandID)) {
+            throw new NotSupportedException("'$commandID' is not supported by the current command factory.");
+        }
+    }
+
+    /**
+     * Resets the inner state of the iterator.
+     */
+    protected function reset()
+    {
+        $this->valid = true;
+        $this->fetchmore = true;
+        $this->elements = [];
+        $this->cursor = 0;
+        $this->position = -1;
+        $this->current = null;
+    }
+
+    /**
+     * Returns an array of options for the `SCAN` command.
+     *
+     * @return array
+     */
+    protected function getScanOptions()
+    {
+        $options = [];
+
+        if (strlen(strval($this->match)) > 0) {
+            $options['MATCH'] = $this->match;
+        }
+
+        if ($this->count > 0) {
+            $options['COUNT'] = $this->count;
+        }
+
+        return $options;
+    }
+
+    /**
+     * Fetches a new set of elements from the remote collection, effectively
+     * advancing the iteration process.
+     *
+     * @return array
+     */
+    abstract protected function executeCommand();
+
+    /**
+     * Populates the local buffer of elements fetched from the server during
+     * the iteration.
+     */
+    protected function fetch()
+    {
+        [$cursor, $elements] = $this->executeCommand();
+
+        if (!$cursor) {
+            $this->fetchmore = false;
+        }
+
+        $this->cursor = $cursor;
+        $this->elements = $elements;
+    }
+
+    /**
+     * Extracts next values for key() and current().
+     */
+    protected function extractNext()
+    {
+        ++$this->position;
+        $this->current = array_shift($this->elements);
+    }
+
+    /**
+     * @return void
+     */
+    #[ReturnTypeWillChange]
+    public function rewind()
+    {
+        $this->reset();
+        $this->next();
+    }
+
+    /**
+     * @return mixed
+     */
+    #[ReturnTypeWillChange]
+    public function current()
+    {
+        return $this->current;
+    }
+
+    /**
+     * @return int|null
+     */
+    #[ReturnTypeWillChange]
+    public function key()
+    {
+        return $this->position;
+    }
+
+    /**
+     * @return void
+     */
+    #[ReturnTypeWillChange]
+    public function next()
+    {
+        tryFetch:
+            if (!$this->elements && $this->fetchmore) {
+                $this->fetch();
+            }
+
+        if ($this->elements) {
+            $this->extractNext();
+        } elseif ($this->cursor) {
+            goto tryFetch;
+        } else {
+            $this->valid = false;
+        }
+    }
+
+    /**
+     * @return bool
+     */
+    #[ReturnTypeWillChange]
+    public function valid()
+    {
+        return $this->valid;
+    }
+}

+ 57 - 0
vendor/predis/predis/src/Collection/Iterator/HashKey.php

@@ -0,0 +1,57 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use Predis\ClientInterface;
+
+/**
+ * Abstracts the iteration of fields and values of an hash by leveraging the
+ * HSCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
+ *
+ * @see http://redis.io/commands/scan
+ */
+class HashKey extends CursorBasedIterator
+{
+    protected $key;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct(ClientInterface $client, $key, $match = null, $count = null)
+    {
+        $this->requiredCommand($client, 'HSCAN');
+
+        parent::__construct($client, $match, $count);
+
+        $this->key = $key;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function executeCommand()
+    {
+        return $this->client->hscan($this->key, $this->cursor, $this->getScanOptions());
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function extractNext()
+    {
+        $this->position = key($this->elements);
+        $this->current = current($this->elements);
+
+        unset($this->elements[$this->position]);
+    }
+}

+ 42 - 0
vendor/predis/predis/src/Collection/Iterator/Keyspace.php

@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use Predis\ClientInterface;
+
+/**
+ * Abstracts the iteration of the keyspace on a Redis instance by leveraging the
+ * SCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
+ *
+ * @see http://redis.io/commands/scan
+ */
+class Keyspace extends CursorBasedIterator
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct(ClientInterface $client, $match = null, $count = null)
+    {
+        $this->requiredCommand($client, 'SCAN');
+
+        parent::__construct($client, $match, $count);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function executeCommand()
+    {
+        return $this->client->scan($this->cursor, $this->getScanOptions());
+    }
+}

+ 183 - 0
vendor/predis/predis/src/Collection/Iterator/ListKey.php

@@ -0,0 +1,183 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use InvalidArgumentException;
+use Iterator;
+use Predis\ClientInterface;
+use Predis\NotSupportedException;
+use ReturnTypeWillChange;
+
+/**
+ * Abstracts the iteration of items stored in a list by leveraging the LRANGE
+ * command wrapped in a fully-rewindable PHP iterator.
+ *
+ * This iterator tries to emulate the behaviour of cursor-based iterators based
+ * on the SCAN-family of commands introduced in Redis <= 2.8, meaning that due
+ * to its incremental nature with multiple fetches it can only offer limited
+ * guarantees on the returned elements because the collection can change several
+ * times (trimmed, deleted, overwritten) during the iteration process.
+ *
+ * @see http://redis.io/commands/lrange
+ */
+class ListKey implements Iterator
+{
+    protected $client;
+    protected $count;
+    protected $key;
+
+    protected $valid;
+    protected $fetchmore;
+    protected $elements;
+    protected $position;
+    protected $current;
+
+    /**
+     * @param ClientInterface $client Client connected to Redis.
+     * @param string          $key    Redis list key.
+     * @param int             $count  Number of items retrieved on each fetch operation.
+     *
+     * @throws InvalidArgumentException
+     */
+    public function __construct(ClientInterface $client, $key, $count = 10)
+    {
+        $this->requiredCommand($client, 'LRANGE');
+
+        if ((false === $count = filter_var($count, FILTER_VALIDATE_INT)) || $count < 0) {
+            throw new InvalidArgumentException('The $count argument must be a positive integer.');
+        }
+
+        $this->client = $client;
+        $this->key = $key;
+        $this->count = $count;
+
+        $this->reset();
+    }
+
+    /**
+     * Ensures that the client instance supports the specified Redis command
+     * required to fetch elements from the server to perform the iteration.
+     *
+     * @param ClientInterface $client    Client connected to Redis.
+     * @param string          $commandID Command ID.
+     *
+     * @throws NotSupportedException
+     */
+    protected function requiredCommand(ClientInterface $client, $commandID)
+    {
+        if (!$client->getCommandFactory()->supports($commandID)) {
+            throw new NotSupportedException("'$commandID' is not supported by the current command factory.");
+        }
+    }
+
+    /**
+     * Resets the inner state of the iterator.
+     */
+    protected function reset()
+    {
+        $this->valid = true;
+        $this->fetchmore = true;
+        $this->elements = [];
+        $this->position = -1;
+        $this->current = null;
+    }
+
+    /**
+     * Fetches a new set of elements from the remote collection, effectively
+     * advancing the iteration process.
+     *
+     * @return array
+     */
+    protected function executeCommand()
+    {
+        return $this->client->lrange($this->key, $this->position + 1, $this->position + $this->count);
+    }
+
+    /**
+     * Populates the local buffer of elements fetched from the server during the
+     * iteration.
+     */
+    protected function fetch()
+    {
+        $elements = $this->executeCommand();
+
+        if (count($elements) < $this->count) {
+            $this->fetchmore = false;
+        }
+
+        $this->elements = $elements;
+    }
+
+    /**
+     * Extracts next values for key() and current().
+     */
+    protected function extractNext()
+    {
+        ++$this->position;
+        $this->current = array_shift($this->elements);
+    }
+
+    /**
+     * @return void
+     */
+    #[ReturnTypeWillChange]
+    public function rewind()
+    {
+        $this->reset();
+        $this->next();
+    }
+
+    /**
+     * @return mixed
+     */
+    #[ReturnTypeWillChange]
+    public function current()
+    {
+        return $this->current;
+    }
+
+    /**
+     * @return int|null
+     */
+    #[ReturnTypeWillChange]
+    public function key()
+    {
+        return $this->position;
+    }
+
+    /**
+     * @return void
+     */
+    #[ReturnTypeWillChange]
+    public function next()
+    {
+        if (!$this->elements && $this->fetchmore) {
+            $this->fetch();
+        }
+
+        if ($this->elements) {
+            $this->extractNext();
+        } else {
+            $this->valid = false;
+        }
+    }
+
+    /**
+     * @return bool
+     */
+    #[ReturnTypeWillChange]
+    public function valid()
+    {
+        return $this->valid;
+    }
+}

+ 46 - 0
vendor/predis/predis/src/Collection/Iterator/SetKey.php

@@ -0,0 +1,46 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use Predis\ClientInterface;
+
+/**
+ * Abstracts the iteration of members stored in a set by leveraging the SSCAN
+ * command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
+ *
+ * @see http://redis.io/commands/scan
+ */
+class SetKey extends CursorBasedIterator
+{
+    protected $key;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct(ClientInterface $client, $key, $match = null, $count = null)
+    {
+        $this->requiredCommand($client, 'SSCAN');
+
+        parent::__construct($client, $match, $count);
+
+        $this->key = $key;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function executeCommand()
+    {
+        return $this->client->sscan($this->key, $this->cursor, $this->getScanOptions());
+    }
+}

+ 57 - 0
vendor/predis/predis/src/Collection/Iterator/SortedSetKey.php

@@ -0,0 +1,57 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Collection\Iterator;
+
+use Predis\ClientInterface;
+
+/**
+ * Abstracts the iteration of members stored in a sorted set by leveraging the
+ * ZSCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
+ *
+ * @see http://redis.io/commands/scan
+ */
+class SortedSetKey extends CursorBasedIterator
+{
+    protected $key;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct(ClientInterface $client, $key, $match = null, $count = null)
+    {
+        $this->requiredCommand($client, 'ZSCAN');
+
+        parent::__construct($client, $match, $count);
+
+        $this->key = $key;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function executeCommand()
+    {
+        return $this->client->zscan($this->key, $this->cursor, $this->getScanOptions());
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function extractNext()
+    {
+        $this->position = key($this->elements);
+        $this->current = current($this->elements);
+
+        unset($this->elements[$this->position]);
+    }
+}

+ 26 - 0
vendor/predis/predis/src/Command/Argument/ArrayableArgument.php

@@ -0,0 +1,26 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument;
+
+/**
+ * Allows to use object-oriented approach to handle complex conditional arguments.
+ */
+interface ArrayableArgument
+{
+    /**
+     * Get the instance as an array.
+     *
+     * @return array
+     */
+    public function toArray(): array;
+}

+ 46 - 0
vendor/predis/predis/src/Command/Argument/Geospatial/AbstractBy.php

@@ -0,0 +1,46 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Geospatial;
+
+use UnexpectedValueException;
+
+abstract class AbstractBy implements ByInterface
+{
+    /**
+     * @var string[]
+     */
+    private static $unitEnum = ['m', 'km', 'ft', 'mi'];
+
+    /**
+     * @var string
+     */
+    protected $unit;
+
+    /**
+     * {@inheritDoc}
+     */
+    abstract public function toArray(): array;
+
+    /**
+     * @param  string $unit
+     * @return void
+     */
+    protected function setUnit(string $unit): void
+    {
+        if (!in_array($unit, self::$unitEnum, true)) {
+            throw new UnexpectedValueException('Wrong value given for unit');
+        }
+
+        $this->unit = $unit;
+    }
+}

+ 43 - 0
vendor/predis/predis/src/Command/Argument/Geospatial/ByBox.php

@@ -0,0 +1,43 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Geospatial;
+
+class ByBox extends AbstractBy
+{
+    private const KEYWORD = 'BYBOX';
+
+    /**
+     * @var int
+     */
+    private $width;
+
+    /**
+     * @var int
+     */
+    private $height;
+
+    public function __construct(int $width, int $height, string $unit)
+    {
+        $this->width = $width;
+        $this->height = $height;
+        $this->setUnit($unit);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return [self::KEYWORD, $this->width, $this->height, $this->unit];
+    }
+}

+ 19 - 0
vendor/predis/predis/src/Command/Argument/Geospatial/ByInterface.php

@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Geospatial;
+
+use Predis\Command\Argument\ArrayableArgument;
+
+interface ByInterface extends ArrayableArgument
+{
+}

+ 37 - 0
vendor/predis/predis/src/Command/Argument/Geospatial/ByRadius.php

@@ -0,0 +1,37 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Geospatial;
+
+class ByRadius extends AbstractBy
+{
+    private const KEYWORD = 'BYRADIUS';
+
+    /**
+     * @var int
+     */
+    private $radius;
+
+    public function __construct(int $radius, string $unit)
+    {
+        $this->radius = $radius;
+        $this->setUnit($unit);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return [self::KEYWORD, $this->radius, $this->unit];
+    }
+}

+ 19 - 0
vendor/predis/predis/src/Command/Argument/Geospatial/FromInterface.php

@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Geospatial;
+
+use Predis\Command\Argument\ArrayableArgument;
+
+interface FromInterface extends ArrayableArgument
+{
+}

+ 42 - 0
vendor/predis/predis/src/Command/Argument/Geospatial/FromLonLat.php

@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Geospatial;
+
+class FromLonLat implements FromInterface
+{
+    private const KEYWORD = 'FROMLONLAT';
+
+    /**
+     * @var float
+     */
+    private $longitude;
+
+    /**
+     * @var float
+     */
+    private $latitude;
+
+    public function __construct(float $longitude, float $latitude)
+    {
+        $this->longitude = $longitude;
+        $this->latitude = $latitude;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return [self::KEYWORD, $this->longitude, $this->latitude];
+    }
+}

+ 36 - 0
vendor/predis/predis/src/Command/Argument/Geospatial/FromMember.php

@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Geospatial;
+
+class FromMember implements FromInterface
+{
+    private const KEYWORD = 'FROMMEMBER';
+
+    /**
+     * @var string
+     */
+    private $member;
+
+    public function __construct(string $member)
+    {
+        $this->member = $member;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return [self::KEYWORD, $this->member];
+    }
+}

+ 161 - 0
vendor/predis/predis/src/Command/Argument/Search/AggregateArguments.php

@@ -0,0 +1,161 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+class AggregateArguments extends CommonArguments
+{
+    /**
+     * @var string[]
+     */
+    private $sortingEnum = [
+        'asc' => 'ASC',
+        'desc' => 'DESC',
+    ];
+
+    /**
+     * Loads document attributes from the source document.
+     *
+     * @param  string ...$fields Could be just '*' to load all fields
+     * @return $this
+     */
+    public function load(string ...$fields): self
+    {
+        $arguments = func_get_args();
+
+        $this->arguments[] = 'LOAD';
+
+        if ($arguments[0] === '*') {
+            $this->arguments[] = '*';
+
+            return $this;
+        }
+
+        $this->arguments[] = count($arguments);
+        $this->arguments = array_merge($this->arguments, $arguments);
+
+        return $this;
+    }
+
+    /**
+     * Loads document attributes from the source document.
+     *
+     * @param  string ...$properties
+     * @return $this
+     */
+    public function groupBy(string ...$properties): self
+    {
+        $arguments = func_get_args();
+
+        array_push($this->arguments, 'GROUPBY', count($arguments));
+        $this->arguments = array_merge($this->arguments, $arguments);
+
+        return $this;
+    }
+
+    /**
+     * Groups the results in the pipeline based on one or more properties.
+     *
+     * If you want to add alias property to your argument just add "true" value in arguments enumeration,
+     * next value will be considered as alias to previous one.
+     *
+     * Example: 'argument', true, 'name' => 'argument' AS 'name'
+     *
+     * @param  string      $function
+     * @param  string|bool ...$argument
+     * @return $this
+     */
+    public function reduce(string $function, ...$argument): self
+    {
+        $arguments = func_get_args();
+        $functionValue = array_shift($arguments);
+        $argumentsCounter = 0;
+
+        for ($i = 0, $iMax = count($arguments); $i < $iMax; $i++) {
+            if (true === $arguments[$i]) {
+                $arguments[$i] = 'AS';
+                $i++;
+                continue;
+            }
+
+            $argumentsCounter++;
+        }
+
+        array_push($this->arguments, 'REDUCE', $functionValue);
+        $this->arguments = array_merge($this->arguments, [$argumentsCounter], $arguments);
+
+        return $this;
+    }
+
+    /**
+     * Sorts the pipeline up until the point of SORTBY, using a list of properties.
+     *
+     * @param  int    $max
+     * @param  string ...$properties Enumeration of properties, including sorting direction (ASC, DESC)
+     * @return $this
+     */
+    public function sortBy(int $max = 0, ...$properties): self
+    {
+        $arguments = func_get_args();
+        $maxValue = array_shift($arguments);
+
+        $this->arguments[] = 'SORTBY';
+        $this->arguments = array_merge($this->arguments, [count($arguments)], $arguments);
+
+        if ($maxValue !== 0) {
+            array_push($this->arguments, 'MAX', $maxValue);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Applies a 1-to-1 transformation on one or more properties and either stores the result
+     * as a new property down the pipeline or replaces any property using this transformation.
+     *
+     * @param  string $expression
+     * @param  string $as
+     * @return $this
+     */
+    public function apply(string $expression, string $as = ''): self
+    {
+        array_push($this->arguments, 'APPLY', $expression);
+
+        if ($as !== '') {
+            array_push($this->arguments, 'AS', $as);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Scan part of the results with a quicker alternative than LIMIT.
+     *
+     * @param  int   $readSize
+     * @param  int   $idleTime
+     * @return $this
+     */
+    public function withCursor(int $readSize = 0, int $idleTime = 0): self
+    {
+        $this->arguments[] = 'WITHCURSOR';
+
+        if ($readSize !== 0) {
+            array_push($this->arguments, 'COUNT', $readSize);
+        }
+
+        if ($idleTime !== 0) {
+            array_push($this->arguments, 'MAXIDLE', $idleTime);
+        }
+
+        return $this;
+    }
+}

+ 17 - 0
vendor/predis/predis/src/Command/Argument/Search/AlterArguments.php

@@ -0,0 +1,17 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+class AlterArguments extends CommonArguments
+{
+}

+ 182 - 0
vendor/predis/predis/src/Command/Argument/Search/CommonArguments.php

@@ -0,0 +1,182 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+use Predis\Command\Argument\ArrayableArgument;
+
+class CommonArguments implements ArrayableArgument
+{
+    /**
+     * @var array
+     */
+    protected $arguments = [];
+
+    /**
+     * Adds default language for documents within an index.
+     *
+     * @param  string $defaultLanguage
+     * @return $this
+     */
+    public function language(string $defaultLanguage = 'english'): self
+    {
+        $this->arguments[] = 'LANGUAGE';
+        $this->arguments[] = $defaultLanguage;
+
+        return $this;
+    }
+
+    /**
+     * Selects the dialect version under which to execute the query.
+     * If not specified, the query will execute under the default dialect version
+     * set during module initial loading or via FT.CONFIG SET command.
+     *
+     * @param  string $dialect
+     * @return $this
+     */
+    public function dialect(string $dialect): self
+    {
+        $this->arguments[] = 'DIALECT';
+        $this->arguments[] = $dialect;
+
+        return $this;
+    }
+
+    /**
+     * If set, does not scan and index.
+     *
+     * @return $this
+     */
+    public function skipInitialScan(): self
+    {
+        $this->arguments[] = 'SKIPINITIALSCAN';
+
+        return $this;
+    }
+
+    /**
+     * Adds an arbitrary, binary safe payload that is exposed to custom scoring functions.
+     *
+     * @param  string $payload
+     * @return $this
+     */
+    public function payload(string $payload): self
+    {
+        $this->arguments[] = 'PAYLOAD';
+        $this->arguments[] = $payload;
+
+        return $this;
+    }
+
+    /**
+     * Also returns the relative internal score of each document.
+     *
+     * @return $this
+     */
+    public function withScores(): self
+    {
+        $this->arguments[] = 'WITHSCORES';
+
+        return $this;
+    }
+
+    /**
+     * Retrieves optional document payloads.
+     *
+     * @return $this
+     */
+    public function withPayloads(): self
+    {
+        $this->arguments[] = 'WITHPAYLOADS';
+
+        return $this;
+    }
+
+    /**
+     * Does not try to use stemming for query expansion but searches the query terms verbatim.
+     *
+     * @return $this
+     */
+    public function verbatim(): self
+    {
+        $this->arguments[] = 'VERBATIM';
+
+        return $this;
+    }
+
+    /**
+     * Overrides the timeout parameter of the module.
+     *
+     * @param  int   $timeout
+     * @return $this
+     */
+    public function timeout(int $timeout): self
+    {
+        $this->arguments[] = 'TIMEOUT';
+        $this->arguments[] = $timeout;
+
+        return $this;
+    }
+
+    /**
+     * Adds an arbitrary, binary safe payload that is exposed to custom scoring functions.
+     *
+     * @param  int   $offset
+     * @param  int   $num
+     * @return $this
+     */
+    public function limit(int $offset, int $num): self
+    {
+        array_push($this->arguments, 'LIMIT', $offset, $num);
+
+        return $this;
+    }
+
+    /**
+     * Adds filter expression into index.
+     *
+     * @param  string $filter
+     * @return $this
+     */
+    public function filter(string $filter): self
+    {
+        $this->arguments[] = 'FILTER';
+        $this->arguments[] = $filter;
+
+        return $this;
+    }
+
+    /**
+     * Defines one or more value parameters. Each parameter has a name and a value.
+     *
+     * Example: ['name1', 'value1', 'name2', 'value2'...]
+     *
+     * @param  array $nameValuesDictionary
+     * @return $this
+     */
+    public function params(array $nameValuesDictionary): self
+    {
+        $this->arguments[] = 'PARAMS';
+        $this->arguments[] = count($nameValuesDictionary);
+        $this->arguments = array_merge($this->arguments, $nameValuesDictionary);
+
+        return $this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return $this->arguments;
+    }
+}

+ 191 - 0
vendor/predis/predis/src/Command/Argument/Search/CreateArguments.php

@@ -0,0 +1,191 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+use InvalidArgumentException;
+
+class CreateArguments extends CommonArguments
+{
+    /**
+     * @var string[]
+     */
+    private $supportedDataTypesEnum = [
+        'hash' => 'HASH',
+        'json' => 'JSON',
+    ];
+
+    /**
+     * Specify data type for given index. To index JSON you must have the RedisJSON module to be installed.
+     *
+     * @param  string $modifier
+     * @return $this
+     */
+    public function on(string $modifier = 'HASH'): self
+    {
+        if (in_array(strtoupper($modifier), $this->supportedDataTypesEnum)) {
+            $this->arguments[] = 'ON';
+            $this->arguments[] = $this->supportedDataTypesEnum[strtolower($modifier)];
+
+            return $this;
+        }
+
+        $enumValues = implode(', ', array_values($this->supportedDataTypesEnum));
+        throw new InvalidArgumentException("Wrong modifier value given. Currently supports: {$enumValues}");
+    }
+
+    /**
+     * Adds one or more prefixes into index.
+     *
+     * @param  array $prefixes
+     * @return $this
+     */
+    public function prefix(array $prefixes): self
+    {
+        $this->arguments[] = 'PREFIX';
+        $this->arguments[] = count($prefixes);
+        $this->arguments = array_merge($this->arguments, $prefixes);
+
+        return $this;
+    }
+
+    /**
+     * Document attribute set as document language.
+     *
+     * @param  string $languageAttribute
+     * @return $this
+     */
+    public function languageField(string $languageAttribute): self
+    {
+        $this->arguments[] = 'LANGUAGE_FIELD';
+        $this->arguments[] = $languageAttribute;
+
+        return $this;
+    }
+
+    /**
+     * Default score for documents in the index.
+     *
+     * @param  float $defaultScore
+     * @return $this
+     */
+    public function score(float $defaultScore = 1.0): self
+    {
+        $this->arguments[] = 'SCORE';
+        $this->arguments[] = $defaultScore;
+
+        return $this;
+    }
+
+    /**
+     * Document attribute that used as the document rank based on the user ranking.
+     *
+     * @param  string $scoreAttribute
+     * @return $this
+     */
+    public function scoreField(string $scoreAttribute): self
+    {
+        $this->arguments[] = 'SCORE_FIELD';
+        $this->arguments[] = $scoreAttribute;
+
+        return $this;
+    }
+
+    /**
+     * Forces RediSearch to encode indexes as if there were more than 32 text attributes.
+     *
+     * @return $this
+     */
+    public function maxTextFields(): self
+    {
+        $this->arguments[] = 'MAXTEXTFIELDS';
+
+        return $this;
+    }
+
+    /**
+     * Does not store term offsets for documents.
+     *
+     * @return $this
+     */
+    public function noOffsets(): self
+    {
+        $this->arguments[] = 'NOOFFSETS';
+
+        return $this;
+    }
+
+    /**
+     * Creates a lightweight temporary index that expires after a specified period of inactivity, in seconds.
+     *
+     * @param  int   $seconds
+     * @return $this
+     */
+    public function temporary(int $seconds): self
+    {
+        $this->arguments[] = 'TEMPORARY';
+        $this->arguments[] = $seconds;
+
+        return $this;
+    }
+
+    /**
+     * Conserves storage space and memory by disabling highlighting support.
+     *
+     * @return $this
+     */
+    public function noHl(): self
+    {
+        $this->arguments[] = 'NOHL';
+
+        return $this;
+    }
+
+    /**
+     * Does not store attribute bits for each term.
+     *
+     * @return $this
+     */
+    public function noFields(): self
+    {
+        $this->arguments[] = 'NOFIELDS';
+
+        return $this;
+    }
+
+    /**
+     * Avoids saving the term frequencies in the index.
+     *
+     * @return $this
+     */
+    public function noFreqs(): self
+    {
+        $this->arguments[] = 'NOFREQS';
+
+        return $this;
+    }
+
+    /**
+     * Sets the index with a custom stopword list, to be ignored during indexing and search time.
+     *
+     * @param  array $stopWords
+     * @return $this
+     */
+    public function stopWords(array $stopWords): self
+    {
+        $this->arguments[] = 'STOPWORDS';
+        $this->arguments[] = count($stopWords);
+        $this->arguments = array_merge($this->arguments, $stopWords);
+
+        return $this;
+    }
+}

+ 44 - 0
vendor/predis/predis/src/Command/Argument/Search/CursorArguments.php

@@ -0,0 +1,44 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+use Predis\Command\Argument\ArrayableArgument;
+
+class CursorArguments implements ArrayableArgument
+{
+    /**
+     * @var array
+     */
+    protected $arguments = [];
+
+    /**
+     * Is number of results to read. This parameter overrides COUNT specified in FT.AGGREGATE.
+     *
+     * @param  int   $readSize
+     * @return $this
+     */
+    public function count(int $readSize): self
+    {
+        array_push($this->arguments, 'COUNT', $readSize);
+
+        return $this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return $this->arguments;
+    }
+}

+ 43 - 0
vendor/predis/predis/src/Command/Argument/Search/DropArguments.php

@@ -0,0 +1,43 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+use Predis\Command\Argument\ArrayableArgument;
+
+class DropArguments implements ArrayableArgument
+{
+    /**
+     * @var array
+     */
+    protected $arguments = [];
+
+    /**
+     * Drop operation that, if set, deletes the actual document hashes.
+     *
+     * @return $this
+     */
+    public function dd(): self
+    {
+        $this->arguments[] = 'DD';
+
+        return $this;
+    }
+
+    /**
+     * @return array
+     */
+    public function toArray(): array
+    {
+        return $this->arguments;
+    }
+}

+ 17 - 0
vendor/predis/predis/src/Command/Argument/Search/ExplainArguments.php

@@ -0,0 +1,17 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+class ExplainArguments extends CommonArguments
+{
+}

+ 81 - 0
vendor/predis/predis/src/Command/Argument/Search/ProfileArguments.php

@@ -0,0 +1,81 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+use Predis\Command\Argument\ArrayableArgument;
+
+class ProfileArguments implements ArrayableArgument
+{
+    /**
+     * @var array
+     */
+    protected $arguments = [];
+
+    /**
+     * Adds search context.
+     *
+     * @return $this
+     */
+    public function search(): self
+    {
+        $this->arguments[] = 'SEARCH';
+
+        return $this;
+    }
+
+    /**
+     * Adds aggregate context.
+     *
+     * @return $this
+     */
+    public function aggregate(): self
+    {
+        $this->arguments[] = 'AGGREGATE';
+
+        return $this;
+    }
+
+    /**
+     * Removes details of reader iterator.
+     *
+     * @return $this
+     */
+    public function limited(): self
+    {
+        $this->arguments[] = 'LIMITED';
+
+        return $this;
+    }
+
+    /**
+     * Is query string, as if sent to FT.SEARCH.
+     *
+     * @param  string $query
+     * @return $this
+     */
+    public function query(string $query): self
+    {
+        $this->arguments[] = 'QUERY';
+        $this->arguments[] = $query;
+
+        return $this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return $this->arguments;
+    }
+}

+ 75 - 0
vendor/predis/predis/src/Command/Argument/Search/SchemaFields/AbstractField.php

@@ -0,0 +1,75 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search\SchemaFields;
+
+abstract class AbstractField implements FieldInterface
+{
+    public const SORTABLE = true;
+    public const NOT_SORTABLE = false;
+    public const SORTABLE_UNF = 'UNF';
+
+    /**
+     * @var array
+     */
+    protected $fieldArguments = [];
+
+    /**
+     * @param  string      $fieldType
+     * @param  string      $identifier
+     * @param  string      $alias
+     * @param  bool|string $sortable
+     * @param  bool        $noIndex
+     * @param  bool        $allowsMissing
+     * @return void
+     */
+    protected function setCommonOptions(
+        string $fieldType,
+        string $identifier,
+        string $alias = '',
+        $sortable = self::NOT_SORTABLE,
+        bool $noIndex = false,
+        bool $allowsMissing = false
+    ): void {
+        $this->fieldArguments[] = $identifier;
+
+        if ($alias !== '') {
+            $this->fieldArguments[] = 'AS';
+            $this->fieldArguments[] = $alias;
+        }
+
+        $this->fieldArguments[] = $fieldType;
+
+        if ($sortable === self::SORTABLE) {
+            $this->fieldArguments[] = 'SORTABLE';
+        } elseif ($sortable === self::SORTABLE_UNF) {
+            $this->fieldArguments[] = 'SORTABLE';
+            $this->fieldArguments[] = 'UNF';
+        }
+
+        if ($noIndex) {
+            $this->fieldArguments[] = 'NOINDEX';
+        }
+
+        if ($allowsMissing) {
+            $this->fieldArguments[] = 'INDEXMISSING';
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return $this->fieldArguments;
+    }
+}

+ 22 - 0
vendor/predis/predis/src/Command/Argument/Search/SchemaFields/FieldInterface.php

@@ -0,0 +1,22 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search\SchemaFields;
+
+use Predis\Command\Argument\ArrayableArgument;
+
+/**
+ * Represents field in search schema.
+ */
+interface FieldInterface extends ArrayableArgument
+{
+}

+ 33 - 0
vendor/predis/predis/src/Command/Argument/Search/SchemaFields/GeoField.php

@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search\SchemaFields;
+
+class GeoField extends AbstractField
+{
+    /**
+     * @param string      $identifier
+     * @param string      $alias
+     * @param bool|string $sortable
+     * @param bool        $noIndex
+     * @param bool        $allowsMissing
+     */
+    public function __construct(
+        string $identifier,
+        string $alias = '',
+        $sortable = self::NOT_SORTABLE,
+        bool $noIndex = false,
+        bool $allowsMissing = false
+    ) {
+        $this->setCommonOptions('GEO', $identifier, $alias, $sortable, $noIndex, $allowsMissing);
+    }
+}

+ 57 - 0
vendor/predis/predis/src/Command/Argument/Search/SchemaFields/GeoShapeField.php

@@ -0,0 +1,57 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search\SchemaFields;
+
+class GeoShapeField extends AbstractField
+{
+    public const COORD_FLAT = 'FLAT';
+
+    /**
+     * @param string      $identifier
+     * @param string      $alias
+     * @param bool|string $sortable
+     * @param bool        $noIndex
+     * @param string|null $coordSystem Constants that represents available systems available on a class level.
+     */
+    public function __construct(
+        string $identifier,
+        string $alias = '',
+        $sortable = self::NOT_SORTABLE,
+        bool $noIndex = false,
+        ?string $coordSystem = null
+    ) {
+        $this->fieldArguments[] = $identifier;
+
+        if ($alias !== '') {
+            $this->fieldArguments[] = 'AS';
+            $this->fieldArguments[] = $alias;
+        }
+
+        $this->fieldArguments[] = 'GEOSHAPE';
+
+        if (null !== $coordSystem) {
+            $this->fieldArguments[] = $coordSystem;
+        }
+
+        if ($sortable === self::SORTABLE) {
+            $this->fieldArguments[] = 'SORTABLE';
+        } elseif ($sortable === self::SORTABLE_UNF) {
+            $this->fieldArguments[] = 'SORTABLE';
+            $this->fieldArguments[] = 'UNF';
+        }
+
+        if ($noIndex) {
+            $this->fieldArguments[] = 'NOINDEX';
+        }
+    }
+}

+ 33 - 0
vendor/predis/predis/src/Command/Argument/Search/SchemaFields/NumericField.php

@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search\SchemaFields;
+
+class NumericField extends AbstractField
+{
+    /**
+     * @param string      $identifier
+     * @param string      $alias
+     * @param bool|string $sortable
+     * @param bool        $noIndex
+     * @param bool        $allowsMissing
+     */
+    public function __construct(
+        string $identifier,
+        string $alias = '',
+        $sortable = self::NOT_SORTABLE,
+        bool $noIndex = false,
+        bool $allowsMissing = false
+    ) {
+        $this->setCommonOptions('NUMERIC', $identifier, $alias, $sortable, $noIndex, $allowsMissing);
+    }
+}

+ 51 - 0
vendor/predis/predis/src/Command/Argument/Search/SchemaFields/TagField.php

@@ -0,0 +1,51 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search\SchemaFields;
+
+class TagField extends AbstractField
+{
+    /**
+     * @param string      $identifier
+     * @param string      $alias
+     * @param bool|string $sortable
+     * @param bool        $noIndex
+     * @param string      $separator
+     * @param bool        $caseSensitive
+     * @param bool        $allowsEmpty
+     */
+    public function __construct(
+        string $identifier,
+        string $alias = '',
+        $sortable = self::NOT_SORTABLE,
+        bool $noIndex = false,
+        string $separator = ',',
+        bool $caseSensitive = false,
+        bool $allowsEmpty = false,
+        bool $allowsMissing = false
+    ) {
+        $this->setCommonOptions('TAG', $identifier, $alias, $sortable, $noIndex, $allowsMissing);
+
+        if ($separator !== ',') {
+            $this->fieldArguments[] = 'SEPARATOR';
+            $this->fieldArguments[] = $separator;
+        }
+
+        if ($caseSensitive) {
+            $this->fieldArguments[] = 'CASESENSITIVE';
+        }
+
+        if ($allowsEmpty) {
+            $this->fieldArguments[] = 'INDEXEMPTY';
+        }
+    }
+}

+ 65 - 0
vendor/predis/predis/src/Command/Argument/Search/SchemaFields/TextField.php

@@ -0,0 +1,65 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search\SchemaFields;
+
+class TextField extends AbstractField
+{
+    /**
+     * @param string      $identifier
+     * @param string      $alias
+     * @param bool|string $sortable
+     * @param bool        $noIndex
+     * @param bool        $noStem
+     * @param string      $phonetic
+     * @param int         $weight
+     * @param bool        $withSuffixTrie
+     * @param bool        $allowsEmpty
+     * @param bool        $allowsMissing
+     */
+    public function __construct(
+        string $identifier,
+        string $alias = '',
+        $sortable = self::NOT_SORTABLE,
+        bool $noIndex = false,
+        bool $noStem = false,
+        string $phonetic = '',
+        int $weight = 1,
+        bool $withSuffixTrie = false,
+        bool $allowsEmpty = false,
+        bool $allowsMissing = false
+    ) {
+        $this->setCommonOptions('TEXT', $identifier, $alias, $sortable, $noIndex, $allowsMissing);
+
+        if ($noStem) {
+            $this->fieldArguments[] = 'NOSTEM';
+        }
+
+        if ($phonetic !== '') {
+            $this->fieldArguments[] = 'PHONETIC';
+            $this->fieldArguments[] = $phonetic;
+        }
+
+        if ($weight !== 1) {
+            $this->fieldArguments[] = 'WEIGHT';
+            $this->fieldArguments[] = $weight;
+        }
+
+        if ($withSuffixTrie) {
+            $this->fieldArguments[] = 'WITHSUFFIXTRIE';
+        }
+
+        if ($allowsEmpty) {
+            $this->fieldArguments[] = 'INDEXEMPTY';
+        }
+    }
+}

+ 47 - 0
vendor/predis/predis/src/Command/Argument/Search/SchemaFields/VectorField.php

@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search\SchemaFields;
+
+class VectorField extends AbstractField
+{
+    /**
+     * @var array
+     */
+    protected $fieldArguments = [];
+
+    /**
+     * @param string $fieldName
+     * @param string $algorithm
+     * @param array  $attributeNameValueDictionary
+     * @param string $alias
+     */
+    public function __construct(
+        string $fieldName,
+        string $algorithm,
+        array $attributeNameValueDictionary,
+        string $alias = ''
+    ) {
+        $this->setCommonOptions('VECTOR', $fieldName, $alias);
+
+        array_push($this->fieldArguments, $algorithm, count($attributeNameValueDictionary));
+        $this->fieldArguments = array_merge($this->fieldArguments, $attributeNameValueDictionary);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return $this->fieldArguments;
+    }
+}

+ 306 - 0
vendor/predis/predis/src/Command/Argument/Search/SearchArguments.php

@@ -0,0 +1,306 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+use InvalidArgumentException;
+
+class SearchArguments extends CommonArguments
+{
+    /**
+     * @var string[]
+     */
+    private $sortingEnum = [
+        'asc' => 'ASC',
+        'desc' => 'DESC',
+    ];
+
+    /**
+     * Returns the document ids and not the content.
+     *
+     * @return $this
+     */
+    public function noContent(): self
+    {
+        $this->arguments[] = 'NOCONTENT';
+
+        return $this;
+    }
+
+    /**
+     * Returns the value of the sorting key, right after the id and score and/or payload, if requested.
+     *
+     * @return $this
+     */
+    public function withSortKeys(): self
+    {
+        $this->arguments[] = 'WITHSORTKEYS';
+
+        return $this;
+    }
+
+    /**
+     * Limits results to those having numeric values ranging between min and max,
+     * if numeric_attribute is defined as a numeric attribute in FT.CREATE.
+     * Min and max follow ZRANGE syntax, and can be -inf, +inf, and use( for exclusive ranges.
+     * Multiple numeric filters for different attributes are supported in one query.
+     *
+     * @param  array ...$filter Should contain: numeric_field, min and max. Example: ['numeric_field', 1, 10]
+     * @return $this
+     */
+    public function searchFilter(array ...$filter): self
+    {
+        $arguments = func_get_args();
+
+        foreach ($arguments as $argument) {
+            array_push($this->arguments, 'FILTER', ...$argument);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Filter the results to a given radius from lon and lat. Radius is given as a number and units.
+     *
+     * @param  array ...$filter Should contain: geo_field, lon, lat, radius, unit. Example: ['geo_field', 34.1231, 35.1231, 300, km]
+     * @return $this
+     */
+    public function geoFilter(array ...$filter): self
+    {
+        $arguments = func_get_args();
+
+        foreach ($arguments as $argument) {
+            array_push($this->arguments, 'GEOFILTER', ...$argument);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Limits the result to a given set of keys specified in the list.
+     *
+     * @param  array $keys
+     * @return $this
+     */
+    public function inKeys(array $keys): self
+    {
+        $this->arguments[] = 'INKEYS';
+        $this->arguments[] = count($keys);
+        $this->arguments = array_merge($this->arguments, $keys);
+
+        return $this;
+    }
+
+    /**
+     * Filters the results to those appearing only in specific attributes of the document, like title or URL.
+     *
+     * @param  array $fields
+     * @return $this
+     */
+    public function inFields(array $fields): self
+    {
+        $this->arguments[] = 'INFIELDS';
+        $this->arguments[] = count($fields);
+        $this->arguments = array_merge($this->arguments, $fields);
+
+        return $this;
+    }
+
+    /**
+     * Limits the attributes returned from the document.
+     * Num is the number of attributes following the keyword.
+     * If num is 0, it acts like NOCONTENT.
+     * Identifier is either an attribute name (for hashes and JSON) or a JSON Path expression (for JSON).
+     * Property is an optional name used in the result. If not provided, the identifier is used in the result.
+     *
+     * If you want to add alias property to your identifier just add "true" value in identifier enumeration,
+     * next value will be considered as alias to previous one.
+     *
+     * Example: 'identifier', true, 'property' => 'identifier' AS 'property'
+     *
+     * @param  int         $count
+     * @param  string|bool ...$identifier
+     * @return $this
+     */
+    public function addReturn(int $count, ...$identifier): self
+    {
+        $arguments = func_get_args();
+
+        $this->arguments[] = 'RETURN';
+
+        for ($i = 1, $iMax = count($arguments); $i < $iMax; $i++) {
+            if (true === $arguments[$i]) {
+                $arguments[$i] = 'AS';
+            }
+        }
+
+        $this->arguments = array_merge($this->arguments, $arguments);
+
+        return $this;
+    }
+
+    /**
+     * Returns only the sections of the attribute that contain the matched text.
+     *
+     * @param  array  $fields
+     * @param  int    $frags
+     * @param  int    $len
+     * @param  string $separator
+     * @return $this
+     */
+    public function summarize(array $fields = [], int $frags = 0, int $len = 0, string $separator = ''): self
+    {
+        $this->arguments[] = 'SUMMARIZE';
+
+        if (!empty($fields)) {
+            $this->arguments[] = 'FIELDS';
+            $this->arguments[] = count($fields);
+            $this->arguments = array_merge($this->arguments, $fields);
+        }
+
+        if ($frags !== 0) {
+            $this->arguments[] = 'FRAGS';
+            $this->arguments[] = $frags;
+        }
+
+        if ($len !== 0) {
+            $this->arguments[] = 'LEN';
+            $this->arguments[] = $len;
+        }
+
+        if ($separator !== '') {
+            $this->arguments[] = 'SEPARATOR';
+            $this->arguments[] = $separator;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Formats occurrences of matched text.
+     *
+     * @param  array  $fields
+     * @param  string $openTag
+     * @param  string $closeTag
+     * @return $this
+     */
+    public function highlight(array $fields = [], string $openTag = '', string $closeTag = ''): self
+    {
+        $this->arguments[] = 'HIGHLIGHT';
+
+        if (!empty($fields)) {
+            $this->arguments[] = 'FIELDS';
+            $this->arguments[] = count($fields);
+            $this->arguments = array_merge($this->arguments, $fields);
+        }
+
+        if ($openTag !== '' && $closeTag !== '') {
+            array_push($this->arguments, 'TAGS', $openTag, $closeTag);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Allows a maximum of N intervening number of unmatched offsets between phrase terms.
+     * In other words, the slop for exact phrases is 0.
+     *
+     * @param  int   $slop
+     * @return $this
+     */
+    public function slop(int $slop): self
+    {
+        $this->arguments[] = 'SLOP';
+        $this->arguments[] = $slop;
+
+        return $this;
+    }
+
+    /**
+     * Puts the query terms in the same order in the document as in the query, regardless of the offsets between them.
+     * Typically used in conjunction with SLOP.
+     *
+     * @return $this
+     */
+    public function inOrder(): self
+    {
+        $this->arguments[] = 'INORDER';
+
+        return $this;
+    }
+
+    /**
+     * Uses a custom query expander instead of the stemmer.
+     *
+     * @param  string $expander
+     * @return $this
+     */
+    public function expander(string $expander): self
+    {
+        $this->arguments[] = 'EXPANDER';
+        $this->arguments[] = $expander;
+
+        return $this;
+    }
+
+    /**
+     * Uses a custom scoring function you define.
+     *
+     * @param  string $scorer
+     * @return $this
+     */
+    public function scorer(string $scorer): self
+    {
+        $this->arguments[] = 'SCORER';
+        $this->arguments[] = $scorer;
+
+        return $this;
+    }
+
+    /**
+     * Returns a textual description of how the scores were calculated.
+     * Using this options requires the WITHSCORES option.
+     *
+     * @return $this
+     */
+    public function explainScore(): self
+    {
+        $this->arguments[] = 'EXPLAINSCORE';
+
+        return $this;
+    }
+
+    /**
+     * Orders the results by the value of this attribute.
+     * This applies to both text and numeric attributes.
+     * Attributes needed for SORTBY should be declared as SORTABLE in the index, in order to be available with very low latency.
+     * Note that this adds memory overhead.
+     *
+     * @param  string $sortAttribute
+     * @param  string $orderBy
+     * @return $this
+     */
+    public function sortBy(string $sortAttribute, string $orderBy = 'asc'): self
+    {
+        $this->arguments[] = 'SORTBY';
+        $this->arguments[] = $sortAttribute;
+
+        if (in_array(strtoupper($orderBy), $this->sortingEnum)) {
+            $this->arguments[] = $this->sortingEnum[strtolower($orderBy)];
+        } else {
+            $enumValues = implode(', ', array_values($this->sortingEnum));
+            throw new InvalidArgumentException("Wrong order direction value given. Currently supports: {$enumValues}");
+        }
+
+        return $this;
+    }
+}

+ 59 - 0
vendor/predis/predis/src/Command/Argument/Search/SpellcheckArguments.php

@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+use InvalidArgumentException;
+
+class SpellcheckArguments extends CommonArguments
+{
+    /**
+     * @var string[]
+     */
+    private $termsEnum = [
+        'include' => 'INCLUDE',
+        'exclude' => 'EXCLUDE',
+    ];
+
+    /**
+     * Is maximum Levenshtein distance for spelling suggestions (default: 1, max: 4).
+     *
+     * @return $this
+     */
+    public function distance(int $distance): self
+    {
+        $this->arguments[] = 'DISTANCE';
+        $this->arguments[] = $distance;
+
+        return $this;
+    }
+
+    /**
+     * Specifies an inclusion (INCLUDE) or exclusion (EXCLUDE) of a custom dictionary named {dict}.
+     *
+     * @param  string $dictionary
+     * @param  string $modifier
+     * @param  string ...$terms
+     * @return $this
+     */
+    public function terms(string $dictionary, string $modifier = 'INCLUDE', string ...$terms): self
+    {
+        if (!in_array(strtoupper($modifier), $this->termsEnum)) {
+            $enumValues = implode(', ', array_values($this->termsEnum));
+            throw new InvalidArgumentException("Wrong modifier value given. Currently supports: {$enumValues}");
+        }
+
+        array_push($this->arguments, 'TERMS', $this->termsEnum[strtolower($modifier)], $dictionary, ...$terms);
+
+        return $this;
+    }
+}

+ 28 - 0
vendor/predis/predis/src/Command/Argument/Search/SugAddArguments.php

@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+class SugAddArguments extends CommonArguments
+{
+    /**
+     * Adds INCR modifier.
+     *
+     * @return $this
+     */
+    public function incr(): self
+    {
+        $this->arguments[] = 'INCR';
+
+        return $this;
+    }
+}

+ 41 - 0
vendor/predis/predis/src/Command/Argument/Search/SugGetArguments.php

@@ -0,0 +1,41 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+class SugGetArguments extends CommonArguments
+{
+    /**
+     * Performs a fuzzy prefix search, including prefixes at Levenshtein distance of 1 from the prefix sent.
+     *
+     * @return $this
+     */
+    public function fuzzy(): self
+    {
+        $this->arguments[] = 'FUZZY';
+
+        return $this;
+    }
+
+    /**
+     * Limits the results to a maximum of num (default: 5).
+     *
+     * @param  int   $num
+     * @return $this
+     */
+    public function max(int $num): self
+    {
+        array_push($this->arguments, 'MAX', $num);
+
+        return $this;
+    }
+}

+ 17 - 0
vendor/predis/predis/src/Command/Argument/Search/SynUpdateArguments.php

@@ -0,0 +1,17 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Search;
+
+class SynUpdateArguments extends CommonArguments
+{
+}

+ 19 - 0
vendor/predis/predis/src/Command/Argument/Server/LimitInterface.php

@@ -0,0 +1,19 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Server;
+
+use Predis\Command\Argument\ArrayableArgument;
+
+interface LimitInterface extends ArrayableArgument
+{
+}

+ 42 - 0
vendor/predis/predis/src/Command/Argument/Server/LimitOffsetCount.php

@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Server;
+
+class LimitOffsetCount implements LimitInterface
+{
+    private const KEYWORD = 'LIMIT';
+
+    /**
+     * @var int
+     */
+    private $offset;
+
+    /**
+     * @var int
+     */
+    private $count;
+
+    public function __construct(int $offset, int $count)
+    {
+        $this->offset = $offset;
+        $this->count = $count;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return [self::KEYWORD, $this->offset, $this->count];
+    }
+}

+ 57 - 0
vendor/predis/predis/src/Command/Argument/Server/To.php

@@ -0,0 +1,57 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Server;
+
+use Predis\Command\Argument\ArrayableArgument;
+
+class To implements ArrayableArgument
+{
+    private const KEYWORD = 'TO';
+    private const FORCE_KEYWORD = 'FORCE';
+
+    /**
+     * @var string
+     */
+    private $host;
+
+    /**
+     * @var int
+     */
+    private $port;
+
+    /**
+     * @var bool
+     */
+    private $isForce;
+
+    public function __construct(string $host, int $port, bool $isForce = false)
+    {
+        $this->host = $host;
+        $this->port = $port;
+        $this->isForce = $isForce;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        $arguments = [self::KEYWORD, $this->host, $this->port];
+
+        if ($this->isForce) {
+            $arguments[] = self::FORCE_KEYWORD;
+        }
+
+        return $arguments;
+    }
+}

+ 49 - 0
vendor/predis/predis/src/Command/Argument/Stream/XInfoStreamOptions.php

@@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\Stream;
+
+use Predis\Command\Argument\ArrayableArgument;
+
+class XInfoStreamOptions implements ArrayableArgument
+{
+    /**
+     * @var array
+     */
+    protected $options = [];
+
+    /**
+     * Modifier provides a more verbose reply.
+     * The COUNT option can be used to limit the number of stream and PEL entries that are returned.
+     *
+     * @param  int|null $count
+     * @return self
+     */
+    public function full(?int $count = null): self
+    {
+        $this->options[] = 'FULL';
+
+        if (null !== $count) {
+            array_push($this->options, 'COUNT', $count);
+        }
+
+        return $this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return $this->options;
+    }
+}

+ 30 - 0
vendor/predis/predis/src/Command/Argument/TimeSeries/AddArguments.php

@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\TimeSeries;
+
+class AddArguments extends CommonArguments
+{
+    /**
+     * Is overwrite key and database configuration for DUPLICATE_POLICY,
+     * the policy for handling samples with identical timestamps.
+     *
+     * @param  string $policy
+     * @return $this
+     */
+    public function onDuplicate(string $policy = self::POLICY_BLOCK): self
+    {
+        array_push($this->arguments, 'ON_DUPLICATE', $policy);
+
+        return $this;
+    }
+}

+ 17 - 0
vendor/predis/predis/src/Command/Argument/TimeSeries/AlterArguments.php

@@ -0,0 +1,17 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\TimeSeries;
+
+class AlterArguments extends CommonArguments
+{
+}

+ 162 - 0
vendor/predis/predis/src/Command/Argument/TimeSeries/CommonArguments.php

@@ -0,0 +1,162 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\TimeSeries;
+
+use Predis\Command\Argument\ArrayableArgument;
+use UnexpectedValueException;
+
+class CommonArguments implements ArrayableArgument
+{
+    public const POLICY_BLOCK = 'BLOCK';
+    public const POLICY_FIRST = 'FIRST';
+    public const POLICY_LAST = 'LAST';
+    public const POLICY_MIN = 'MIN';
+    public const POLICY_MAX = 'MAX';
+    public const POLICY_SUM = 'SUM';
+
+    public const ENCODING_UNCOMPRESSED = 'UNCOMPRESSED';
+    public const ENCODING_COMPRESSED = 'COMPRESSED';
+
+    /**
+     * @var array
+     */
+    protected $arguments = [];
+
+    /**
+     * Is maximum age for samples compared to the highest reported timestamp, in milliseconds.
+     *
+     * @param  int   $retentionPeriod
+     * @return $this
+     */
+    public function retentionMsecs(int $retentionPeriod): self
+    {
+        array_push($this->arguments, 'RETENTION', $retentionPeriod);
+
+        return $this;
+    }
+
+    /**
+     * Ignore samples with given time or value difference.
+     *
+     * @param  int   $maxTimeDiff Non-negative integer value in milliseconds
+     * @param  float $maxValDiff  Non-negative float value
+     * @return $this
+     */
+    public function ignore(int $maxTimeDiff, float $maxValDiff): self
+    {
+        if ($maxTimeDiff < 0 || $maxValDiff < 0) {
+            throw new UnexpectedValueException('Ignore does not accept negative values');
+        }
+
+        array_push($this->arguments, 'IGNORE', $maxTimeDiff, $maxValDiff);
+
+        return $this;
+    }
+
+    /**
+     * Is initial allocation size, in bytes, for the data part of each new chunk.
+     *
+     * @param  int   $size
+     * @return $this
+     */
+    public function chunkSize(int $size): self
+    {
+        array_push($this->arguments, 'CHUNK_SIZE', $size);
+
+        return $this;
+    }
+
+    /**
+     * Is policy for handling insertion of multiple samples with identical timestamps.
+     *
+     * @param  string $policy
+     * @return $this
+     */
+    public function duplicatePolicy(string $policy = self::POLICY_BLOCK): self
+    {
+        array_push($this->arguments, 'DUPLICATE_POLICY', $policy);
+
+        return $this;
+    }
+
+    /**
+     * Is set of label-value pairs that represent metadata labels of the key and serve as a secondary index.
+     *
+     * @param  mixed ...$labelValuePair
+     * @return $this
+     */
+    public function labels(...$labelValuePair): self
+    {
+        array_push($this->arguments, 'LABELS', ...$labelValuePair);
+
+        return $this;
+    }
+
+    /**
+     * Specifies the series samples encoding format.
+     *
+     * @param  string $encoding
+     * @return $this
+     */
+    public function encoding(string $encoding = self::ENCODING_COMPRESSED): self
+    {
+        array_push($this->arguments, 'ENCODING', $encoding);
+
+        return $this;
+    }
+
+    /**
+     * Is used when a time series is a compaction.
+     * With LATEST, TS.GET reports the compacted value of the latest, possibly partial, bucket.
+     *
+     * @return $this
+     */
+    public function latest(): self
+    {
+        $this->arguments[] = 'LATEST';
+
+        return $this;
+    }
+
+    /**
+     * Includes in the reply all label-value pairs representing metadata labels of the time series.
+     *
+     * @return $this
+     */
+    public function withLabels(): self
+    {
+        $this->arguments[] = 'WITHLABELS';
+
+        return $this;
+    }
+
+    /**
+     * Returns a subset of the label-value pairs that represent metadata labels of the time series.
+     *
+     * @return $this
+     */
+    public function selectedLabels(string ...$labels): self
+    {
+        array_push($this->arguments, 'SELECTED_LABELS', ...$labels);
+
+        return $this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return $this->arguments;
+    }
+}

+ 17 - 0
vendor/predis/predis/src/Command/Argument/TimeSeries/CreateArguments.php

@@ -0,0 +1,17 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\TimeSeries;
+
+class CreateArguments extends CommonArguments
+{
+}

+ 17 - 0
vendor/predis/predis/src/Command/Argument/TimeSeries/DecrByArguments.php

@@ -0,0 +1,17 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\TimeSeries;
+
+class DecrByArguments extends IncrByArguments
+{
+}

+ 17 - 0
vendor/predis/predis/src/Command/Argument/TimeSeries/GetArguments.php

@@ -0,0 +1,17 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\TimeSeries;
+
+class GetArguments extends CommonArguments
+{
+}

+ 41 - 0
vendor/predis/predis/src/Command/Argument/TimeSeries/IncrByArguments.php

@@ -0,0 +1,41 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\TimeSeries;
+
+class IncrByArguments extends CommonArguments
+{
+    /**
+     * Is (integer) UNIX sample timestamp in milliseconds or * to set the timestamp according to the server clock.
+     *
+     * @param  string|int $timeStamp
+     * @return $this
+     */
+    public function timestamp($timeStamp): self
+    {
+        array_push($this->arguments, 'TIMESTAMP', $timeStamp);
+
+        return $this;
+    }
+
+    /**
+     * Changes data storage from compressed (default) to uncompressed.
+     *
+     * @return $this
+     */
+    public function uncompressed(): self
+    {
+        $this->arguments[] = 'UNCOMPRESSED';
+
+        return $this;
+    }
+}

+ 43 - 0
vendor/predis/predis/src/Command/Argument/TimeSeries/InfoArguments.php

@@ -0,0 +1,43 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\TimeSeries;
+
+use Predis\Command\Argument\ArrayableArgument;
+
+class InfoArguments implements ArrayableArgument
+{
+    /**
+     * @var array
+     */
+    private $arguments = [];
+
+    /**
+     * Is an optional flag to get a more detailed information about the chunks.
+     *
+     * @return $this
+     */
+    public function debug(): self
+    {
+        $this->arguments[] = 'DEBUG';
+
+        return $this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function toArray(): array
+    {
+        return $this->arguments;
+    }
+}

+ 17 - 0
vendor/predis/predis/src/Command/Argument/TimeSeries/MGetArguments.php

@@ -0,0 +1,17 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\TimeSeries;
+
+class MGetArguments extends CommonArguments
+{
+}

+ 44 - 0
vendor/predis/predis/src/Command/Argument/TimeSeries/MRangeArguments.php

@@ -0,0 +1,44 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\TimeSeries;
+
+class MRangeArguments extends RangeArguments
+{
+    /**
+     * Filters time series based on their labels and label values.
+     *
+     * @param  string ...$filterExpressions
+     * @return $this
+     */
+    public function filter(string ...$filterExpressions): self
+    {
+        array_push($this->arguments, 'FILTER', ...$filterExpressions);
+
+        return $this;
+    }
+
+    /**
+     * Splits time series into groups, each group contains time series that share the same
+     * value for the provided label name, then aggregates results in each group.
+     *
+     * @param  string $label
+     * @param  string $reducer
+     * @return $this
+     */
+    public function groupBy(string $label, string $reducer): self
+    {
+        array_push($this->arguments, 'GROUPBY', $label, 'REDUCE', $reducer);
+
+        return $this;
+    }
+}

+ 85 - 0
vendor/predis/predis/src/Command/Argument/TimeSeries/RangeArguments.php

@@ -0,0 +1,85 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Argument\TimeSeries;
+
+class RangeArguments extends CommonArguments
+{
+    /**
+     * Filters samples by a list of specific timestamps.
+     *
+     * @param  int   ...$ts
+     * @return $this
+     */
+    public function filterByTs(int ...$ts): self
+    {
+        array_push($this->arguments, 'FILTER_BY_TS', ...$ts);
+
+        return $this;
+    }
+
+    /**
+     * Filters samples by minimum and maximum values.
+     *
+     * @param  int   $min
+     * @param  int   $max
+     * @return $this
+     */
+    public function filterByValue(int $min, int $max): self
+    {
+        array_push($this->arguments, 'FILTER_BY_VALUE', $min, $max);
+
+        return $this;
+    }
+
+    /**
+     * Limits the number of returned samples.
+     *
+     * @param  int   $count
+     * @return $this
+     */
+    public function count(int $count): self
+    {
+        array_push($this->arguments, 'COUNT', $count);
+
+        return $this;
+    }
+
+    /**
+     * Aggregates samples into time buckets.
+     *
+     * @param  string $aggregator
+     * @param  int    $bucketDuration  Is duration of each bucket, in milliseconds.
+     * @param  int    $align           It controls the time bucket timestamps by changing the reference timestamp on which a bucket is defined.
+     * @param  int    $bucketTimestamp Controls how bucket timestamps are reported.
+     * @param  bool   $empty           Is a flag, which, when specified, reports aggregations also for empty buckets.
+     * @return $this
+     */
+    public function aggregation(string $aggregator, int $bucketDuration, int $align = 0, int $bucketTimestamp = 0, bool $empty = false): self
+    {
+        if ($align > 0) {
+            array_push($this->arguments, 'ALIGN', $align);
+        }
+
+        array_push($this->arguments, 'AGGREGATION', $aggregator, $bucketDuration);
+
+        if ($bucketTimestamp > 0) {
+            array_push($this->arguments, 'BUCKETTIMESTAMP', $bucketTimestamp);
+        }
+
+        if (true === $empty) {
+            $this->arguments[] = 'EMPTY';
+        }
+
+        return $this;
+    }
+}

+ 200 - 0
vendor/predis/predis/src/Command/Command.php

@@ -0,0 +1,200 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command;
+
+use Predis\ClientConfiguration;
+use UnexpectedValueException;
+
+/**
+ * Base class for Redis commands.
+ */
+abstract class Command implements CommandInterface
+{
+    private $slot;
+    private $arguments = [];
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setArguments(array $arguments)
+    {
+        $this->arguments = $arguments;
+        unset($this->slot);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setRawArguments(array $arguments)
+    {
+        $this->arguments = $arguments;
+        unset($this->slot);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getArguments()
+    {
+        return $this->arguments;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getArgument($index)
+    {
+        if (isset($this->arguments[$index])) {
+            return $this->arguments[$index];
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setSlot($slot)
+    {
+        $this->slot = $slot;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSlot()
+    {
+        return $this->slot ?? null;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function parseResponse($data)
+    {
+        return $data;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function parseResp3Response($data)
+    {
+        return $data;
+    }
+
+    /**
+     * Normalizes the arguments array passed to a Redis command.
+     *
+     * @param array $arguments Arguments for a command.
+     *
+     * @return array
+     */
+    public static function normalizeArguments(array $arguments)
+    {
+        if (count($arguments) === 1 && isset($arguments[0]) && is_array($arguments[0])) {
+            return $arguments[0];
+        }
+
+        return $arguments;
+    }
+
+    /**
+     * Normalizes the arguments array passed to a variadic Redis command.
+     *
+     * @param array $arguments Arguments for a command.
+     *
+     * @return array
+     */
+    public static function normalizeVariadic(array $arguments)
+    {
+        if (count($arguments) === 2 && is_array($arguments[1])) {
+            return array_merge([$arguments[0]], $arguments[1]);
+        }
+
+        return $arguments;
+    }
+
+    /**
+     * Remove all false values from arguments.
+     *
+     * @return void
+     */
+    public function filterArguments(): void
+    {
+        $this->arguments = array_filter($this->arguments, static function ($argument) {
+            return $argument !== false && $argument !== null;
+        });
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function serializeCommand(): string
+    {
+        $commandID = $this->getId();
+        $arguments = $this->getArguments();
+
+        $cmdlen = strlen($commandID);
+        $reqlen = count($arguments) + 1;
+
+        $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n";
+
+        foreach ($arguments as $argument) {
+            $arglen = strlen(strval($argument));
+            $buffer .= "\${$arglen}\r\n{$argument}\r\n";
+        }
+
+        return $buffer;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public static function deserializeCommand(string $serializedCommand): CommandInterface
+    {
+        if ($serializedCommand[0] !== '*') {
+            throw new UnexpectedValueException('Invalid serializing format');
+        }
+
+        $commandArray = explode("\r\n", $serializedCommand);
+        $commandId = $commandArray[2];
+        $classPath = __NAMESPACE__ . '\Redis\\';
+
+        // Check if given command is a module command.
+        if (count($commandIdArray = explode('.', $commandId)) > 1) {
+            // Fetch module configuration to resolve namespace.
+            $moduleConfiguration = array_filter(
+                ClientConfiguration::getModules(),
+                static function ($module) use ($commandIdArray) {
+                    return $module['commandPrefix'] === $commandIdArray[0];
+                }
+            );
+
+            $commandClass = strtoupper($commandIdArray[0] . $commandIdArray[1]);
+            $classPath .= array_shift($moduleConfiguration)['name'] . '\\' . $commandClass;
+        } else {
+            $classPath .= $commandIdArray[0];
+        }
+
+        $command = new $classPath();
+        $arguments = [];
+
+        for ($i = 4, $iMax = count($commandArray); $i < $iMax; $i++) {
+            $arguments[] = $commandArray[$i];
+            ++$i;
+        }
+
+        $command->setArguments($arguments);
+
+        return $command;
+    }
+}

+ 103 - 0
vendor/predis/predis/src/Command/CommandInterface.php

@@ -0,0 +1,103 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command;
+
+/**
+ * Defines an abstraction representing a Redis command.
+ */
+interface CommandInterface
+{
+    /**
+     * Returns the ID of the Redis command. By convention, command identifiers
+     * must always be uppercase.
+     *
+     * @return string
+     */
+    public function getId();
+
+    /**
+     * Assign the specified slot to the command for clustering distribution.
+     *
+     * @param int $slot Slot ID.
+     */
+    public function setSlot($slot);
+
+    /**
+     * Returns the assigned slot of the command for clustering distribution.
+     *
+     * @return int|null
+     */
+    public function getSlot();
+
+    /**
+     * Sets the arguments for the command.
+     *
+     * @param array $arguments List of arguments.
+     */
+    public function setArguments(array $arguments);
+
+    /**
+     * Sets the raw arguments for the command without processing them.
+     *
+     * @param array $arguments List of arguments.
+     */
+    public function setRawArguments(array $arguments);
+
+    /**
+     * Gets the arguments of the command.
+     *
+     * @return array
+     */
+    public function getArguments();
+
+    /**
+     * Gets the argument of the command at the specified index.
+     *
+     * @param int $index Index of the desired argument.
+     *
+     * @return mixed|null
+     */
+    public function getArgument($index);
+
+    /**
+     * Parses a raw response and returns a PHP object.
+     *
+     * @param string|array|null $data Binary string containing the whole response.
+     *
+     * @return mixed
+     */
+    public function parseResponse($data);
+
+    /**
+     * Parses RESP3 protocol response and returns a PHP object.
+     *
+     * @param  mixed $data
+     * @return mixed
+     */
+    public function parseResp3Response($data);
+
+    /**
+     * Returns RESP-formatted representation of command.
+     *
+     * @return string
+     */
+    public function serializeCommand(): string;
+
+    /**
+     * Creates command object from given serialized representation.
+     *
+     * @param  string $serializedCommand
+     * @return static
+     */
+    public static function deserializeCommand(string $serializedCommand): CommandInterface;
+}

+ 31 - 0
vendor/predis/predis/src/Command/Container/ACL.php

@@ -0,0 +1,31 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Container;
+
+use Predis\Response\Status;
+
+/**
+ * @method array  cat(string $category = null)
+ * @method Status dryRun(string $username, string $command, ...$arguments)
+ * @method int    delUser(string ...$username)
+ * @method array  getUser(string $username)
+ * @method Status setUser(string $username, string ...$rules)
+ * @method string whoami()
+ */
+class ACL extends AbstractContainer
+{
+    public function getContainerCommandId(): string
+    {
+        return 'acl';
+    }
+}

+ 42 - 0
vendor/predis/predis/src/Command/Container/AbstractContainer.php

@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Container;
+
+use Predis\ClientInterface;
+
+abstract class AbstractContainer implements ContainerInterface
+{
+    /**
+     * @var ClientInterface
+     */
+    protected $client;
+
+    public function __construct(ClientInterface $client)
+    {
+        $this->client = $client;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function __call(string $subcommandID, array $arguments)
+    {
+        array_unshift($arguments, strtoupper($subcommandID));
+
+        return $this->client->executeCommand(
+            $this->client->createCommand($this->getContainerCommandId(), $arguments)
+        );
+    }
+
+    abstract public function getContainerCommandId(): string;
+}

+ 32 - 0
vendor/predis/predis/src/Command/Container/CLIENT.php

@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Container;
+
+use Predis\Response\Status;
+
+/**
+ * @method string getName()
+ * @method Status kill(...$arguments)
+ * @method string list(string $type = null, int ...$clientId)
+ * @method Status noEvict(bool $enable = null)
+ * @method Status noTouch(bool $enable = null)
+ * @method Status setInfo(string $modifier = null, string $value = null)
+ * @method Status setName(string $connectionName)
+ */
+class CLIENT extends AbstractContainer
+{
+    public function getContainerCommandId(): string
+    {
+        return 'CLIENT';
+    }
+}

+ 29 - 0
vendor/predis/predis/src/Command/Container/CLUSTER.php

@@ -0,0 +1,29 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Container;
+
+use Predis\Response\Status;
+
+/**
+ * @method Status addSlotsRange(int ...$startEndSlots)
+ * @method Status delSlotsRange(int ...$startEndSlots)
+ * @method array  links()
+ * @method array  shards()
+ */
+class CLUSTER extends AbstractContainer
+{
+    public function getContainerCommandId(): string
+    {
+        return 'CLUSTER';
+    }
+}

+ 81 - 0
vendor/predis/predis/src/Command/Container/ContainerFactory.php

@@ -0,0 +1,81 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Container;
+
+use Predis\ClientConfiguration;
+use Predis\ClientInterface;
+use UnexpectedValueException;
+
+class ContainerFactory
+{
+    private const CONTAINER_NAMESPACE = "Predis\Command\Container";
+
+    /**
+     * Mappings for class names that corresponds to PHP reserved words.
+     *
+     * @var array
+     */
+    private static $specialMappings = [
+        'FUNCTION' => FUNCTIONS::class,
+    ];
+
+    /**
+     * Creates container command.
+     *
+     * @param  ClientInterface    $client
+     * @param  string             $containerCommandID
+     * @return ContainerInterface
+     */
+    public static function create(ClientInterface $client, string $containerCommandID): ContainerInterface
+    {
+        $containerCommandID = strtoupper($containerCommandID);
+        $commandModule = self::resolveCommandModuleByPrefix($containerCommandID);
+
+        if (null !== $commandModule) {
+            if (class_exists($containerClass = self::CONTAINER_NAMESPACE . '\\' . $commandModule . '\\' . $containerCommandID)) {
+                return new $containerClass($client);
+            }
+
+            throw new UnexpectedValueException('Given module container command is not supported.');
+        }
+
+        if (class_exists($containerClass = self::CONTAINER_NAMESPACE . '\\' . $containerCommandID)) {
+            return new $containerClass($client);
+        }
+
+        if (array_key_exists($containerCommandID, self::$specialMappings)) {
+            $containerClass = self::$specialMappings[$containerCommandID];
+
+            return new $containerClass($client);
+        }
+
+        throw new UnexpectedValueException('Given container command is not supported.');
+    }
+
+    /**
+     * @param  string      $commandID
+     * @return string|null
+     */
+    private static function resolveCommandModuleByPrefix(string $commandID): ?string
+    {
+        $modules = ClientConfiguration::getModules();
+
+        foreach ($modules as $module) {
+            if (preg_match("/^{$module['commandPrefix']}/", $commandID)) {
+                return $module['name'];
+            }
+        }
+
+        return null;
+    }
+}

+ 33 - 0
vendor/predis/predis/src/Command/Container/ContainerInterface.php

@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Container;
+
+interface ContainerInterface
+{
+    /**
+     * Creates Redis container command with subcommand as virtual method name
+     * and sends a request to the server.
+     *
+     * @param  string $subcommandID
+     * @param  array  $arguments
+     * @return mixed
+     */
+    public function __call(string $subcommandID, array $arguments);
+
+    /**
+     * Returns containerCommandId of specific container command.
+     *
+     * @return string
+     */
+    public function getContainerCommandId(): string;
+}

+ 33 - 0
vendor/predis/predis/src/Command/Container/FUNCTIONS.php

@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Container;
+
+use Predis\Response\Status;
+
+/**
+ * @method Status delete(string $libraryName)
+ * @method string dump()
+ * @method Status flush(?string $mode = null)
+ * @method Status kill()
+ * @method array  list(string $libraryNamePattern = null, bool $withCode = false)
+ * @method string load(string $functionCode, bool $replace = 'false')
+ * @method Status restore(string $value, string $policy = null)
+ * @method array  stats()
+ */
+class FUNCTIONS extends AbstractContainer
+{
+    public function getContainerCommandId(): string
+    {
+        return 'FUNCTIONS';
+    }
+}

+ 27 - 0
vendor/predis/predis/src/Command/Container/Json/JSONDEBUG.php

@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Container\Json;
+
+use Predis\Command\Container\AbstractContainer;
+
+/**
+ * @method array memory(string $key, string $path)
+ * @method array help()
+ */
+class JSONDEBUG extends AbstractContainer
+{
+    public function getContainerCommandId(): string
+    {
+        return 'JSONDEBUG';
+    }
+}

+ 29 - 0
vendor/predis/predis/src/Command/Container/Search/FTCONFIG.php

@@ -0,0 +1,29 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Container\Search;
+
+use Predis\Command\Container\AbstractContainer;
+use Predis\Response\Status;
+
+/**
+ * @method array  get(string $option)
+ * @method array  help(string $option)
+ * @method Status set(string $option, $value)
+ */
+class FTCONFIG extends AbstractContainer
+{
+    public function getContainerCommandId(): string
+    {
+        return 'FTCONFIG';
+    }
+}

+ 29 - 0
vendor/predis/predis/src/Command/Container/Search/FTCURSOR.php

@@ -0,0 +1,29 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Container\Search;
+
+use Predis\Command\Argument\Search\CursorArguments;
+use Predis\Command\Container\AbstractContainer;
+use Predis\Response\Status;
+
+/**
+ * @method Status del(string $index, int $cursorId)
+ * @method array  read(string $index, int $cursorId, ?CursorArguments $arguments = null)
+ */
+class FTCURSOR extends AbstractContainer
+{
+    public function getContainerCommandId(): string
+    {
+        return 'FTCURSOR';
+    }
+}

+ 30 - 0
vendor/predis/predis/src/Command/Container/XGROUP.php

@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Container;
+
+use Predis\Response\Status;
+
+/**
+ * @method Status create(string $key, string $group, string $id, bool $mkStream = false, ?string $entriesRead = null)
+ * @method int    createConsumer(string $key, string $group, string $consumer)
+ * @method int    delConsumer(string $key, string $group, string $consumer)
+ * @method int    destroy(string $key, string $group)
+ * @method Status setId(string $key, string $group, string $id, ?string $entriesRead = null)
+ */
+class XGROUP extends AbstractContainer
+{
+    public function getContainerCommandId(): string
+    {
+        return 'xgroup';
+    }
+}

+ 28 - 0
vendor/predis/predis/src/Command/Container/XINFO.php

@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command\Container;
+
+use Predis\Command\Argument\Stream\XInfoStreamOptions;
+
+/**
+ * @method array consumers(string $key, string $group)
+ * @method array groups(string $key)
+ * @method array stream(string $key, XInfoStreamOptions $options = null)
+ */
+class XINFO extends AbstractContainer
+{
+    public function getContainerCommandId(): string
+    {
+        return 'XINFO';
+    }
+}

+ 143 - 0
vendor/predis/predis/src/Command/Factory.php

@@ -0,0 +1,143 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command;
+
+use InvalidArgumentException;
+use Predis\ClientException;
+use Predis\Command\Processor\ProcessorInterface;
+
+/**
+ * Base command factory class.
+ *
+ * This class provides all of the common functionalities required for a command
+ * factory to create new instances of Redis commands objects. It also allows to
+ * define or undefine command handler classes for each command ID.
+ */
+abstract class Factory implements FactoryInterface
+{
+    protected $commands = [];
+    protected $processor;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function supports(string ...$commandIDs): bool
+    {
+        foreach ($commandIDs as $commandID) {
+            if ($this->getCommandClass($commandID) === null) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the FQCN of a class that represents the specified command ID.
+     *
+     * @codeCoverageIgnore
+     *
+     * @param string $commandID Command ID
+     *
+     * @return string|null
+     */
+    public function getCommandClass(string $commandID): ?string
+    {
+        return $this->commands[strtoupper($commandID)] ?? null;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function create(string $commandID, array $arguments = []): CommandInterface
+    {
+        if (!$commandClass = $this->getCommandClass($commandID)) {
+            $commandID = strtoupper($commandID);
+
+            throw new ClientException("Command `$commandID` is not a registered Redis command.");
+        }
+
+        $command = new $commandClass();
+        $command->setArguments($arguments);
+
+        if (isset($this->processor)) {
+            $this->processor->process($command);
+        }
+
+        return $command;
+    }
+
+    /**
+     * Defines a command in the factory.
+     *
+     * Only classes implementing Predis\Command\CommandInterface are allowed to
+     * handle a command. If the command specified by its ID is already handled
+     * by the factory, the underlying command class is replaced by the new one.
+     *
+     * @param string $commandID    Command ID
+     * @param string $commandClass FQCN of a class implementing Predis\Command\CommandInterface
+     *
+     * @throws InvalidArgumentException
+     */
+    public function define(string $commandID, string $commandClass): void
+    {
+        if (!is_a($commandClass, 'Predis\Command\CommandInterface', true)) {
+            throw new InvalidArgumentException(
+                "Class $commandClass must implement Predis\Command\CommandInterface"
+            );
+        }
+
+        $this->commands[strtoupper($commandID)] = $commandClass;
+    }
+
+    /**
+     * Undefines a command in the factory.
+     *
+     * When the factory already has a class handler associated to the specified
+     * command ID it is removed from the map of known commands. Nothing happens
+     * when the command is not handled by the factory.
+     *
+     * @param string $commandID Command ID
+     */
+    public function undefine(string $commandID): void
+    {
+        unset($this->commands[strtoupper($commandID)]);
+    }
+
+    /**
+     * Sets a command processor for processing command arguments.
+     *
+     * Command processors are used to process and transform arguments of Redis
+     * commands before their newly created instances are returned to the caller
+     * of "create()".
+     *
+     * A NULL value can be used to effectively unset any processor if previously
+     * set for the command factory.
+     *
+     * @param ProcessorInterface|null $processor Command processor or NULL value.
+     */
+    public function setProcessor(?ProcessorInterface $processor): void
+    {
+        $this->processor = $processor;
+    }
+
+    /**
+     * Returns the current command processor.
+     *
+     * @return ProcessorInterface|null
+     */
+    public function getProcessor(): ?ProcessorInterface
+    {
+        return $this->processor;
+    }
+}

+ 42 - 0
vendor/predis/predis/src/Command/FactoryInterface.php

@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) 2009-2020 Daniele Alessandri
+ * (c) 2021-2025 Till Krüss
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command;
+
+/**
+ * Command factory interface.
+ *
+ * A command factory is used through the library to create instances of commands
+ * classes implementing Predis\Command\CommandInterface mapped to Redis commands
+ * by their command ID string (SET, GET, etc...).
+ */
+interface FactoryInterface
+{
+    /**
+     * Checks if the command factory supports the specified list of commands.
+     *
+     * @param string ...$commandIDs List of command IDs
+     *
+     * @return bool
+     */
+    public function supports(string ...$commandIDs): bool;
+
+    /**
+     * Creates a new command instance.
+     *
+     * @param string $commandID Command ID
+     * @param array  $arguments Arguments for the command
+     *
+     * @return CommandInterface
+     */
+    public function create(string $commandID, array $arguments = []): CommandInterface;
+}

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff