From 3b61bb61d91fd0280f0b511d718940a02f92acab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Fri, 21 Jun 2024 22:36:40 +0200 Subject: [PATCH] refactor: `RedisResourceManager` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `RedisResourceManager` is now adapted to work the same way as `RedisClusterResourceManager` while this also removes a bunch of methods which are not meant to be part of the resource manager. - `Redis` adapter now allows `RedisResourceManagerInterface` to be set via `Redis#setResourceManager` - `RedisClusterResourceManagerInterface#hasSerializationSupport` now requires both `StorageInterface` and `PluginCapableInterface` - `RedisCluster#setResourceManager` is not marked as internal anymore - `RedisClusterResourceManagerInterface#getVersion` - `RedisClusterResourceManagerInterface#getLibOption` Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .gitattributes | 1 + benchmark/RedisClusterStorageAdapterBench.php | 19 +- ...hIgbinarySerializerStorageAdapterBench.php | 19 +- ...erWithPhpSerializerStorageAdapterBench.php | 19 +- benchmark/RedisStorageAdapterBench.php | 19 +- ...hIgbinarySerializerStorageAdapterBench.php | 29 + ...isWithPhpSerializerStorageAdapterBench.php | 29 + composer.json | 5 +- composer.lock | 110 ++- psalm-baseline.xml | 83 +- psalm.xml | 8 + psalm/Redis.stub.php | 34 + src/Redis.php | 313 +++++--- src/RedisCluster.php | 65 +- src/RedisClusterOptions.php | 7 +- src/RedisClusterResourceManager.php | 94 +-- src/RedisClusterResourceManagerInterface.php | 10 +- src/RedisOptions.php | 377 +++++++-- src/RedisResourceManager.php | 757 ++---------------- src/RedisResourceManagerInterface.php | 19 + .../RedisClusterStorageCreationTrait.php | 3 + test/integration/Laminas/RedisClusterTest.php | 12 +- .../Laminas/RedisFromExtensionAsset.php | 18 - .../Laminas/RedisStorageCreationTrait.php | 12 +- test/integration/Laminas/RedisTest.php | 113 +-- .../RedisClusterWithPhpIgbinaryTest.php | 21 +- .../RedisClusterWithPhpSerializeTest.php | 21 +- .../RedisClusterWithoutSerializerTest.php | 21 +- .../CacheItemPool/RedisIntegrationTest.php | 31 - .../RedisWithPhpIgbinaryTest.php | 27 + .../RedisWithPhpSerializeTest.php | 27 + .../RedisWithoutSerializerTest.php | 27 + ...p => RedisClusterWithPhpSerializeTest.php} | 2 +- ...nTest.php => RedisWithPhpIgbinaryTest.php} | 7 +- .../SimpleCache/RedisWithPhpSerializeTest.php | 20 + .../RedisWithoutSerializerTest.php | 20 + .../InvalidConfigurationExceptionTest.php | 2 +- test/unit/RedisClusterOptionsFromIniTest.php | 10 +- test/unit/RedisClusterOptionsTest.php | 24 +- test/unit/RedisClusterResourceManagerTest.php | 25 +- test/unit/RedisClusterTest.php | 46 +- test/unit/RedisOptionsTest.php | 93 ++- test/unit/RedisResourceManagerTest.php | 432 ++-------- test/unit/RedisTest.php | 83 ++ 44 files changed, 1298 insertions(+), 1816 deletions(-) create mode 100644 benchmark/RedisWithIgbinarySerializerStorageAdapterBench.php create mode 100644 benchmark/RedisWithPhpSerializerStorageAdapterBench.php create mode 100644 psalm/Redis.stub.php create mode 100644 src/RedisResourceManagerInterface.php delete mode 100644 test/integration/Laminas/RedisFromExtensionAsset.php delete mode 100644 test/integration/Psr/CacheItemPool/RedisIntegrationTest.php create mode 100644 test/integration/Psr/CacheItemPool/RedisWithPhpIgbinaryTest.php create mode 100644 test/integration/Psr/CacheItemPool/RedisWithPhpSerializeTest.php create mode 100644 test/integration/Psr/CacheItemPool/RedisWithoutSerializerTest.php rename test/integration/Psr/SimpleCache/{RedisClusterWithPhpSerializerTest.php => RedisClusterWithPhpSerializeTest.php} (84%) rename test/integration/Psr/SimpleCache/{RedisIntegrationTest.php => RedisWithPhpIgbinaryTest.php} (68%) create mode 100644 test/integration/Psr/SimpleCache/RedisWithPhpSerializeTest.php create mode 100644 test/integration/Psr/SimpleCache/RedisWithoutSerializerTest.php create mode 100644 test/unit/RedisTest.php diff --git a/.gitattributes b/.gitattributes index 5827614..75c8a58 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,4 @@ /test/ export-ignore /benchmark/ export-ignore /phpbench.json export-ignore +/psalm export-ignore \ No newline at end of file diff --git a/benchmark/RedisClusterStorageAdapterBench.php b/benchmark/RedisClusterStorageAdapterBench.php index 5197659..99fc6d9 100644 --- a/benchmark/RedisClusterStorageAdapterBench.php +++ b/benchmark/RedisClusterStorageAdapterBench.php @@ -5,26 +5,25 @@ namespace LaminasBench\Cache; use Laminas\Cache\Storage\Adapter\Benchmark\AbstractStorageAdapterBenchmark; +use Laminas\Cache\Storage\Adapter\RedisClusterOptions; use LaminasTest\Cache\Storage\Adapter\Laminas\RedisClusterStorageCreationTrait; -use PhpBench\Benchmark\Metadata\Annotations\Iterations; -use PhpBench\Benchmark\Metadata\Annotations\Revs; -use PhpBench\Benchmark\Metadata\Annotations\Warmup; +use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\Revs; +use PhpBench\Attributes\Warmup; use Redis; /** - * @Revs(100) - * @Iterations(10) - * @Warmup(1) + * @template-extends AbstractStorageAdapterBenchmark */ +#[Revs(100)] +#[Iterations(10)] +#[Warmup(1)] class RedisClusterStorageAdapterBench extends AbstractStorageAdapterBenchmark { use RedisClusterStorageCreationTrait; public function __construct() { - parent::__construct($this->createRedisClusterStorage( - Redis::SERIALIZER_NONE, - true - )); + parent::__construct($this->createRedisClusterStorage(Redis::SERIALIZER_NONE, true)); } } diff --git a/benchmark/RedisClusterWithIgbinarySerializerStorageAdapterBench.php b/benchmark/RedisClusterWithIgbinarySerializerStorageAdapterBench.php index 05b8b33..4cb5e66 100644 --- a/benchmark/RedisClusterWithIgbinarySerializerStorageAdapterBench.php +++ b/benchmark/RedisClusterWithIgbinarySerializerStorageAdapterBench.php @@ -5,26 +5,25 @@ namespace LaminasBench\Cache; use Laminas\Cache\Storage\Adapter\Benchmark\AbstractStorageAdapterBenchmark; +use Laminas\Cache\Storage\Adapter\RedisClusterOptions; use LaminasTest\Cache\Storage\Adapter\Laminas\RedisClusterStorageCreationTrait; -use PhpBench\Benchmark\Metadata\Annotations\Iterations; -use PhpBench\Benchmark\Metadata\Annotations\Revs; -use PhpBench\Benchmark\Metadata\Annotations\Warmup; +use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\Revs; +use PhpBench\Attributes\Warmup; use Redis; /** - * @Revs(100) - * @Iterations(10) - * @Warmup(1) + * @template-extends AbstractStorageAdapterBenchmark */ +#[Revs(100)] +#[Iterations(10)] +#[Warmup(1)] class RedisClusterWithIgbinarySerializerStorageAdapterBench extends AbstractStorageAdapterBenchmark { use RedisClusterStorageCreationTrait; public function __construct() { - parent::__construct($this->createRedisClusterStorage( - Redis::SERIALIZER_IGBINARY, - false - )); + parent::__construct($this->createRedisClusterStorage(Redis::SERIALIZER_IGBINARY, false)); } } diff --git a/benchmark/RedisClusterWithPhpSerializerStorageAdapterBench.php b/benchmark/RedisClusterWithPhpSerializerStorageAdapterBench.php index 3dca4a9..fc6da63 100644 --- a/benchmark/RedisClusterWithPhpSerializerStorageAdapterBench.php +++ b/benchmark/RedisClusterWithPhpSerializerStorageAdapterBench.php @@ -5,26 +5,25 @@ namespace LaminasBench\Cache; use Laminas\Cache\Storage\Adapter\Benchmark\AbstractStorageAdapterBenchmark; +use Laminas\Cache\Storage\Adapter\RedisClusterOptions; use LaminasTest\Cache\Storage\Adapter\Laminas\RedisClusterStorageCreationTrait; -use PhpBench\Benchmark\Metadata\Annotations\Iterations; -use PhpBench\Benchmark\Metadata\Annotations\Revs; -use PhpBench\Benchmark\Metadata\Annotations\Warmup; +use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\Revs; +use PhpBench\Attributes\Warmup; use Redis; /** - * @Revs(100) - * @Iterations(10) - * @Warmup(1) + * @template-extends AbstractStorageAdapterBenchmark */ +#[Revs(100)] +#[Iterations(10)] +#[Warmup(1)] class RedisClusterWithPhpSerializerStorageAdapterBench extends AbstractStorageAdapterBenchmark { use RedisClusterStorageCreationTrait; public function __construct() { - parent::__construct($this->createRedisClusterStorage( - Redis::SERIALIZER_PHP, - false - )); + parent::__construct($this->createRedisClusterStorage(Redis::SERIALIZER_PHP, false)); } } diff --git a/benchmark/RedisStorageAdapterBench.php b/benchmark/RedisStorageAdapterBench.php index 42beaa7..b7176f8 100644 --- a/benchmark/RedisStorageAdapterBench.php +++ b/benchmark/RedisStorageAdapterBench.php @@ -5,26 +5,25 @@ namespace LaminasBench\Cache; use Laminas\Cache\Storage\Adapter\Benchmark\AbstractStorageAdapterBenchmark; +use Laminas\Cache\Storage\Adapter\RedisOptions; use LaminasTest\Cache\Storage\Adapter\Laminas\RedisStorageCreationTrait; -use PhpBench\Benchmark\Metadata\Annotations\Iterations; -use PhpBench\Benchmark\Metadata\Annotations\Revs; -use PhpBench\Benchmark\Metadata\Annotations\Warmup; +use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\Revs; +use PhpBench\Attributes\Warmup; use Redis; /** - * @Revs(100) - * @Iterations(10) - * @Warmup(1) + * @template-extends AbstractStorageAdapterBenchmark */ +#[Revs(100)] +#[Iterations(10)] +#[Warmup(1)] class RedisStorageAdapterBench extends AbstractStorageAdapterBenchmark { use RedisStorageCreationTrait; public function __construct() { - parent::__construct($this->createRedisStorage( - Redis::SERIALIZER_NONE, - true - )); + parent::__construct($this->createRedisStorage(Redis::SERIALIZER_NONE, true)); } } diff --git a/benchmark/RedisWithIgbinarySerializerStorageAdapterBench.php b/benchmark/RedisWithIgbinarySerializerStorageAdapterBench.php new file mode 100644 index 0000000..4dc4f5e --- /dev/null +++ b/benchmark/RedisWithIgbinarySerializerStorageAdapterBench.php @@ -0,0 +1,29 @@ + + */ +#[Revs(100)] +#[Iterations(10)] +#[Warmup(1)] +class RedisWithIgbinarySerializerStorageAdapterBench extends AbstractStorageAdapterBenchmark +{ + use RedisStorageCreationTrait; + + public function __construct() + { + parent::__construct($this->createRedisStorage(Redis::SERIALIZER_IGBINARY, false)); + } +} diff --git a/benchmark/RedisWithPhpSerializerStorageAdapterBench.php b/benchmark/RedisWithPhpSerializerStorageAdapterBench.php new file mode 100644 index 0000000..097a584 --- /dev/null +++ b/benchmark/RedisWithPhpSerializerStorageAdapterBench.php @@ -0,0 +1,29 @@ + + */ +#[Revs(100)] +#[Iterations(10)] +#[Warmup(1)] +class RedisWithPhpSerializerStorageAdapterBench extends AbstractStorageAdapterBenchmark +{ + use RedisStorageCreationTrait; + + public function __construct() + { + parent::__construct($this->createRedisStorage(Redis::SERIALIZER_PHP, false)); + } +} diff --git a/composer.json b/composer.json index 66f684a..0406ba9 100644 --- a/composer.json +++ b/composer.json @@ -15,9 +15,8 @@ "laminas/laminas-cache-storage-implementation": "2.0" }, "require-dev": { - "laminas/laminas-cache": "4.0.x-dev || ^4.0", - "laminas/laminas-cache-storage-adapter-benchmark": "2.0.x-dev || ^2.0", - "laminas/laminas-cache-storage-adapter-test": "3.0.x-dev || ^3.0", + "laminas/laminas-cache-storage-adapter-benchmark": "^2.1", + "laminas/laminas-cache-storage-adapter-test": "^4.0.1", "laminas/laminas-coding-standard": "~2.5.0", "laminas/laminas-serializer": "^3.0", "psalm/plugin-phpunit": "^0.19.0", diff --git a/composer.lock b/composer.lock index e960cd1..d07ae9d 100644 --- a/composer.lock +++ b/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": "0657da4c55a689dc914ea827ccc81dc5", + "content-hash": "1982fc29d99b0ccfd7188edae6388d14", "packages": [ { "name": "brick/varexporter", @@ -57,16 +57,16 @@ }, { "name": "laminas/laminas-cache", - "version": "4.1.x-dev", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/laminas/laminas-cache.git", - "reference": "512dc92dead683f7713e6e40162fe40e619dd2ad" + "reference": "6bf742fed94e573842a4195715c7758bd8e11882" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-cache/zipball/512dc92dead683f7713e6e40162fe40e619dd2ad", - "reference": "512dc92dead683f7713e6e40162fe40e619dd2ad", + "url": "https://api.github.com/repos/laminas/laminas-cache/zipball/6bf742fed94e573842a4195715c7758bd8e11882", + "reference": "6bf742fed94e573842a4195715c7758bd8e11882", "shasum": "" }, "require": { @@ -108,7 +108,6 @@ "laminas/laminas-cli": "The laminas-cli binary can be used to consume commands provided by this component", "laminas/laminas-serializer": "Laminas\\Serializer component" }, - "default-branch": true, "type": "library", "extra": { "laminas": { @@ -147,7 +146,7 @@ "type": "community_bridge" } ], - "time": "2024-06-15T21:40:37+00:00" + "time": "2024-06-21T18:42:43+00:00" }, { "name": "laminas/laminas-eventmanager", @@ -1560,30 +1559,28 @@ }, { "name": "laminas/laminas-cache-storage-adapter-benchmark", - "version": "2.1.x-dev", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-cache-storage-adapter-benchmark.git", - "reference": "602130de494df8d6cef8b7b59bb7da5b8e78d96d" + "reference": "a93216250daa5d41d379e446c6fd385d0849e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-cache-storage-adapter-benchmark/zipball/602130de494df8d6cef8b7b59bb7da5b8e78d96d", - "reference": "602130de494df8d6cef8b7b59bb7da5b8e78d96d", + "url": "https://api.github.com/repos/laminas/laminas-cache-storage-adapter-benchmark/zipball/a93216250daa5d41d379e446c6fd385d0849e31d", + "reference": "a93216250daa5d41d379e446c6fd385d0849e31d", "shasum": "" }, "require": { - "laminas/laminas-cache": "^4.0", + "laminas/laminas-cache": "^4.0.1", "laminas/laminas-serializer": "^3.0", "php": "~8.1.0 || ~8.2.0 || ~8.3.0", "phpbench/phpbench": "^1.2.15" }, "require-dev": { - "laminas/laminas-cache": "4.0.x-dev || ^4.0", "laminas/laminas-coding-standard": "~2.5.0", "vimeo/psalm": "^5.24.0" }, - "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -1612,24 +1609,24 @@ "type": "community_bridge" } ], - "time": "2024-05-14T20:59:20+00:00" + "time": "2024-06-21T17:41:00+00:00" }, { "name": "laminas/laminas-cache-storage-adapter-test", - "version": "3.1.x-dev", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/laminas/laminas-cache-storage-adapter-test.git", - "reference": "20f81115336b7de59eddd2932ab04623a8000096" + "reference": "7d052d8ca221cda3e7afdc5beb49ab884b6c726f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-cache-storage-adapter-test/zipball/20f81115336b7de59eddd2932ab04623a8000096", - "reference": "20f81115336b7de59eddd2932ab04623a8000096", + "url": "https://api.github.com/repos/laminas/laminas-cache-storage-adapter-test/zipball/7d052d8ca221cda3e7afdc5beb49ab884b6c726f", + "reference": "7d052d8ca221cda3e7afdc5beb49ab884b6c726f", "shasum": "" }, "require": { - "laminas/laminas-cache": "^4.0", + "laminas/laminas-cache": "^4.0.3", "php": "~8.1.0 || ~8.2.0 || ~8.3.0", "phpunit/phpunit": "^10.5", "psr/cache": "^2.0 || ^3.0", @@ -1638,12 +1635,11 @@ }, "require-dev": { "composer-plugin-api": "^2", - "laminas/laminas-cache": "4.0.x-dev || ^4.0", "laminas/laminas-coding-standard": "~2.5.0", "psalm/plugin-phpunit": "^0.19.0", - "vimeo/psalm": "^5.15.0" + "vimeo/psalm": "^5.15.0", + "webmozart/assert": "^1.11" }, - "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -1672,7 +1668,7 @@ "type": "community_bridge" } ], - "time": "2024-06-16T17:11:01+00:00" + "time": "2024-06-21T20:30:05+00:00" }, { "name": "laminas/laminas-coding-standard", @@ -2820,16 +2816,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.21", + "version": "10.5.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "ac837816fa52078f7a5e17ed774f256a72a51af6" + "reference": "5f124e3e3e561006047b532fd0431bf5bb6b9015" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ac837816fa52078f7a5e17ed774f256a72a51af6", - "reference": "ac837816fa52078f7a5e17ed774f256a72a51af6", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5f124e3e3e561006047b532fd0431bf5bb6b9015", + "reference": "5f124e3e3e561006047b532fd0431bf5bb6b9015", "shasum": "" }, "require": { @@ -2901,7 +2897,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.24" }, "funding": [ { @@ -2917,7 +2913,7 @@ "type": "tidelift" } ], - "time": "2024-06-15T09:13:15+00:00" + "time": "2024-06-20T13:09:54+00:00" }, { "name": "psalm/plugin-phpunit", @@ -4578,16 +4574,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", "shasum": "" }, "require": { @@ -4637,7 +4633,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" }, "funding": [ { @@ -4653,20 +4649,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", "shasum": "" }, "require": { @@ -4715,7 +4711,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" }, "funding": [ { @@ -4731,20 +4727,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { @@ -4796,7 +4792,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" }, "funding": [ { @@ -4812,20 +4808,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { @@ -4876,7 +4872,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -4892,7 +4888,7 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/process", @@ -5391,11 +5387,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "laminas/laminas-cache": 20, - "laminas/laminas-cache-storage-adapter-benchmark": 20, - "laminas/laminas-cache-storage-adapter-test": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 623876d..3f9ba3a 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,25 +1,5 @@ - - - - - - - - - - - - - - - - - - - - @@ -40,23 +20,11 @@ keys($prefix . '*')]]> keys($prefix . '*')]]> - - - resourceId]]> - resourceId]]> - - - - - - - initialized]]> - @@ -86,56 +54,23 @@ - - - - - - - - - - - - - - - - + + + + + - - + - - - - - resources[$id]]]> - resources[$id]]]> - resources[$id]]]> - resources[$id]]]> - resources[$id]]]> - resources[$id]]]> - resources[$id]]]> - - - - - - storage->getItem('key')]]> - - info()]]> - - - - - + + + diff --git a/psalm.xml b/psalm.xml index 096fa14..0689e4d 100644 --- a/psalm.xml +++ b/psalm.xml @@ -18,6 +18,9 @@ + + + @@ -55,6 +58,11 @@ + + + + + diff --git a/psalm/Redis.stub.php b/psalm/Redis.stub.php new file mode 100644 index 0000000..ba7aa07 --- /dev/null +++ b/psalm/Redis.stub.php @@ -0,0 +1,34 @@ +, + * readTimeout?: float, + * connectTimeout?: float, + * retryInterval?: non-negative-int, + * persistent?: bool|non-empty-string, + * auth?: null|array{0:non-empty-string,1?:non-empty-string}|non-empty-string, + * ssl?: SSLContextArrayShape, + * backoff?: array{ + * algorithm?: Redis::BACKOFF_ALGORITHM_*, + * base?: non-negative-int, + * cap?: non-negative-int + * } + * } + */ +class Redis +{ + /** + * @param RedisOptions|null} $options + */ + public function __construct(array|null $options = null) + { + } +} \ No newline at end of file diff --git a/src/Redis.php b/src/Redis.php index 09cedab..7119307 100644 --- a/src/Redis.php +++ b/src/Redis.php @@ -6,21 +6,21 @@ use Laminas\Cache\Exception; use Laminas\Cache\Storage\AbstractMetadataCapableAdapter; +use Laminas\Cache\Storage\Adapter\Exception\RedisRuntimeException; use Laminas\Cache\Storage\Adapter\Redis\Metadata; -use Laminas\Cache\Storage\Adapter\RedisResourceManager; use Laminas\Cache\Storage\Capabilities; use Laminas\Cache\Storage\ClearByNamespaceInterface; use Laminas\Cache\Storage\ClearByPrefixInterface; use Laminas\Cache\Storage\FlushableInterface; use Laminas\Cache\Storage\TotalSpaceCapableInterface; -use Redis as RedisResource; -use RedisException as RedisResourceException; +use Redis as RedisFromExtension; +use RedisException as RedisFromExtensionException; use Webmozart\Assert\Assert; -use function array_combine; -use function array_filter; use function array_key_exists; use function assert; +use function is_array; +use function is_string; use function round; use function version_compare; @@ -33,25 +33,14 @@ final class Redis extends AbstractMetadataCapableAdapter implements FlushableInterface, TotalSpaceCapableInterface { - /** - * Has this instance be initialized - */ - private bool $initialized = false; - - /** - * The redis resource manager - */ - private ?RedisResourceManager $resourceManager = null; + private RedisFromExtension|null $resource; - /** - * The redis resource id - */ - private ?string $resourceId = null; + private RedisResourceManagerInterface|null $resourceManager; /** * The namespace prefix */ - private string $namespacePrefix = ''; + private string|null $namespacePrefix; /** * @param null|iterable|RedisOptions $options @@ -59,38 +48,33 @@ final class Redis extends AbstractMetadataCapableAdapter implements public function __construct(iterable|RedisOptions|null $options = null) { parent::__construct($options); - - // reset initialized flag on update option(s) - $initialized = &$this->initialized; - $this->getEventManager()->attach('option', static function () use (&$initialized): void { - $initialized = false; + $this->resourceManager = null; + $this->resource = null; + $this->namespacePrefix = null; + $this->getEventManager()->attach('option', function (): void { + $this->resource = null; + $this->namespacePrefix = null; }); } - private function getRedisResource(): RedisResource + private function getRedisResource(): RedisFromExtension { - if ($this->initialized) { - return $this->resourceManager->getResource($this->resourceId); + if ($this->resource !== null) { + return $this->resource; } - $options = $this->getOptions(); - - // get resource manager and resource id - $this->resourceManager = $options->getResourceManager(); - $this->resourceId = $options->getResourceId(); + $resourceManager = $this->getResourceManager(); + $this->resource = $resourceManager->getResource(); + $options = $this->getOptions(); // init namespace prefix - $namespace = $options->getNamespace(); + $namespace = $options->getNamespace(); + $this->namespacePrefix = ''; if ($namespace !== '') { $this->namespacePrefix = $namespace . $options->getNamespaceSeparator(); - } else { - $this->namespacePrefix = ''; } - // update initialized flag - $this->initialized = true; - - return $this->resourceManager->getResource($this->resourceId); + return $this->resource; } /** @@ -126,20 +110,15 @@ protected function internalGetItem( bool|null &$success = null, mixed &$casToken = null ): mixed { - $redis = $this->getRedisResource(); - try { - $value = $redis->get($this->namespacePrefix . $normalizedKey); - } catch (RedisResourceException $e) { - throw new Exception\RuntimeException($redis->getLastError() ?? $e->getMessage(), $e->getCode(), $e); - } - - if ($value === false) { + $normalizedKeys = [$normalizedKey]; + $values = $this->internalGetItems($normalizedKeys); + if (! array_key_exists($normalizedKey, $values)) { $success = false; return null; } - $success = true; - $casToken = $value; + $value = $casToken = $values[$normalizedKey]; + $success = true; return $value; } @@ -148,23 +127,38 @@ protected function internalGetItem( */ protected function internalGetItems(array $normalizedKeys): array { - $redis = $this->getRedisResource(); - $namespacedKeys = []; foreach ($normalizedKeys as $normalizedKey) { - $namespacedKeys[] = $this->namespacePrefix . $normalizedKey; + $namespacedKeys[] = $this->createNamespacedKey($normalizedKey); } + $redis = $this->getRedisResource(); + try { - $results = $redis->mGet($namespacedKeys); - } catch (RedisResourceException $e) { - throw new Exception\RuntimeException($redis->getLastError() ?? $e->getMessage(), $e->getCode(), $e); - } - //combine the key => value pairs and remove all missing values - return array_filter( - array_combine($normalizedKeys, $results), - static fn($value): bool => $value !== false - ); + $resultsByIndex = $redis->mget($namespacedKeys); + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException( + $exception, + $redis + ); + } + + if (! is_array($resultsByIndex)) { + throw RedisRuntimeException::fromInternalRedisError($redis); + } + + $result = []; + foreach ($resultsByIndex as $keyIndex => $value) { + $normalizedKey = $normalizedKeys[$keyIndex]; + $namespacedKey = $namespacedKeys[$keyIndex]; + if ($value === false && ! $this->isFalseReturnValuePersisted($redis, $namespacedKey)) { + continue; + } + + $result[$normalizedKey] = $value; + } + + return $result; } /** @@ -174,9 +168,9 @@ protected function internalHasItem(string $normalizedKey): bool { $redis = $this->getRedisResource(); try { - return (bool) $redis->exists($this->namespacePrefix . $normalizedKey); - } catch (RedisResourceException $e) { - throw new Exception\RuntimeException($redis->getLastError() ?? $e->getMessage(), $e->getCode(), $e); + return (bool) $redis->exists($this->createNamespacedKey($normalizedKey)); + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException($exception, $redis); } } @@ -191,19 +185,24 @@ protected function internalSetItem(string $normalizedKey, mixed $value): bool try { if ($ttl) { - if ($options->getResourceManager()->getMajorVersion($options->getResourceId()) < 2) { - throw new Exception\UnsupportedMethodCallException("To use ttl you need version >= 2.0.0"); + if ($this->getCapabilities()->ttlSupported === false) { + throw new Exception\UnsupportedMethodCallException( + 'To use ttl you need redis-server version >= 2.0.0', + ); } $success = $redis->setex( - $this->namespacePrefix . $normalizedKey, + $this->createNamespacedKey($normalizedKey), (int) $ttl, $this->preSerialize($value) ) !== false; } else { - $success = $redis->set($this->namespacePrefix . $normalizedKey, $this->preSerialize($value)) !== false; + $success = $redis->set( + $this->createNamespacedKey($normalizedKey), + $this->preSerialize($value) + ) !== false; } - } catch (RedisResourceException $e) { - throw new Exception\RuntimeException($redis->getLastError() ?? $e->getMessage(), $e->getCode(), $e); + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException($exception, $redis); } return $success; @@ -220,15 +219,17 @@ protected function internalSetItems(array $normalizedKeyValuePairs): array $namespacedKeyValuePairs = []; foreach ($normalizedKeyValuePairs as $normalizedKey => $value) { - $namespacedKeyValuePairs[$this->namespacePrefix . $normalizedKey] = $this->preSerialize($value); + $namespacedKeyValuePairs[$this->createNamespacedKey($normalizedKey)] = $this->preSerialize($value); } try { if ($ttl > 0) { - //check if ttl is supported - if ($options->getResourceManager()->getMajorVersion($options->getResourceId()) < 2) { - throw new Exception\UnsupportedMethodCallException("To use ttl you need version >= 2.0.0"); + if ($this->getCapabilities()->ttlSupported === false) { + throw new Exception\UnsupportedMethodCallException( + 'To use ttl you need redis-server version >= 2.0.0', + ); } + //mSet does not allow ttl, so use transaction $transaction = $redis->multi(); foreach ($namespacedKeyValuePairs as $key => $value) { @@ -238,11 +239,12 @@ protected function internalSetItems(array $normalizedKeyValuePairs): array } else { $success = $redis->mSet($namespacedKeyValuePairs) !== false; } - } catch (RedisResourceException $e) { - throw new Exception\RuntimeException($redis->getLastError() ?? $e->getMessage(), $e->getCode(), $e); + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException($exception, $redis); } + if (! $success) { - throw new Exception\RuntimeException($redis->getLastError() ?? 'no last error'); + throw RedisRuntimeException::fromInternalRedisError($redis); } return []; @@ -259,8 +261,10 @@ protected function internalAddItem(string $normalizedKey, mixed $value): bool try { if ($ttl > 0) { - if ($options->getResourceManager()->getMajorVersion($options->getResourceId()) < 2) { - throw new Exception\UnsupportedMethodCallException("To use ttl you need version >= 2.0.0"); + if ($this->getCapabilities()->ttlSupported === false) { + throw new Exception\UnsupportedMethodCallException( + 'To use ttl you need redis-server version >= 2.0.0', + ); } /** @@ -268,19 +272,19 @@ protected function internalAddItem(string $normalizedKey, mixed $value): bool * This means we only set the ttl after the key/value has been successfully set. */ $success = $redis->setnx( - $this->namespacePrefix . $normalizedKey, + $this->createNamespacedKey($normalizedKey), $this->preSerialize($value) ) !== false; if ($success) { - $redis->expire($this->namespacePrefix . $normalizedKey, (int) $ttl); + $redis->expire($this->createNamespacedKey($normalizedKey), (int) $ttl); } return $success; } - return $redis->setnx($this->namespacePrefix . $normalizedKey, $this->preSerialize($value)) !== false; - } catch (RedisResourceException $e) { - throw new Exception\RuntimeException($redis->getLastError() ?? $e->getMessage(), $e->getCode(), $e); + return $redis->setnx($this->createNamespacedKey($normalizedKey), $this->preSerialize($value)) !== false; + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException($exception, $redis); } } @@ -292,9 +296,9 @@ protected function internalTouchItem(string $normalizedKey): bool $redis = $this->getRedisResource(); try { $ttl = $this->getOptions()->getTtl(); - return (bool) $redis->expire($this->namespacePrefix . $normalizedKey, (int) $ttl); - } catch (RedisResourceException $e) { - throw new Exception\RuntimeException($redis->getLastError() ?? $e->getMessage(), $e->getCode(), $e); + return (bool) $redis->expire($this->createNamespacedKey($normalizedKey), (int) $ttl); + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException($exception, $redis); } } @@ -305,9 +309,9 @@ protected function internalRemoveItem(string $normalizedKey): bool { $redis = $this->getRedisResource(); try { - return (bool) $redis->del($this->namespacePrefix . $normalizedKey); - } catch (RedisResourceException $e) { - throw new Exception\RuntimeException($redis->getLastError() ?? $e->getMessage(), $e->getCode(), $e); + return (bool) $redis->del($this->createNamespacedKey($normalizedKey)); + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException($exception, $redis); } } @@ -319,8 +323,8 @@ public function flush(): bool $redis = $this->getRedisResource(); try { return $redis->flushDB(); - } catch (RedisResourceException $e) { - throw new Exception\RuntimeException($redis->getLastError() ?? $e->getMessage(), $e->getCode(), $e); + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException($exception, $redis); } } @@ -371,8 +375,8 @@ public function getTotalSpace(): int $redis = $this->getRedisResource(); try { $info = $redis->info(); - } catch (RedisResourceException $e) { - throw new Exception\RuntimeException($redis->getLastError() ?? $e->getMessage(), $e->getCode(), $e); + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException($exception, $redis); } Assert::isMap($info); @@ -391,11 +395,9 @@ protected function internalGetCapabilities(): Capabilities return $this->capabilities; } - $options = $this->getOptions(); - $resourceMgr = $options->getResourceManager(); - $serializer = $resourceMgr->getLibOption($options->getResourceId(), RedisResource::OPT_SERIALIZER); - $redisVersion = $resourceMgr->getMajorVersion($options->getResourceId()); - $maxKeyLength = version_compare((string) $redisVersion, '3', '<') ? 255 : 512_000_000; + $redisSerializerOptionUsed = $this->getResourceManager()->hasSerializationSupport($this); + $redisVersion = $this->getRedisVersion(); + $maxKeyLength = version_compare($redisVersion, '3', '<') ? 255 : 512_000_000; $supportedDataTypes = [ 'NULL' => 'string', @@ -408,7 +410,7 @@ protected function internalGetCapabilities(): Capabilities 'resource' => false, ]; - if ($serializer !== null) { + if ($redisSerializerOptionUsed === true) { $supportedDataTypes = [ 'NULL' => true, 'boolean' => true, @@ -421,35 +423,66 @@ protected function internalGetCapabilities(): Capabilities ]; } - $this->capabilities = new Capabilities( + return $this->capabilities = new Capabilities( maxKeyLength: $maxKeyLength, - ttlSupported: $redisVersion >= 2, + ttlSupported: version_compare($redisVersion, '2', 'ge'), namespaceIsPrefix: true, supportedDataTypes: $supportedDataTypes, ttlPrecision: 1, usesRequestTime: false, ); + } + + public function getRedisVersion(): string + { + $options = $this->getOptions(); + $versionFromOptions = $options->getRedisVersion(); + if ($versionFromOptions !== '') { + return $versionFromOptions; + } + + $redis = $this->getRedisResource(); + try { + $info = $redis->info(); + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException($exception, $redis); + } + + if (! is_array($info)) { + return '0.0.0-unknown'; + } + + if (! isset($info['redis_version']) || ! is_string($info['redis_version'])) { + return '0.0.0-unknown'; + } - return $this->capabilities; + $version = $info['redis_version']; + assert($version !== ''); + $options->setRedisVersion($version); + + return $version; + } + + public function setResourceManager(RedisResourceManagerInterface $resourceManager): void + { + $this->resourceManager = $resourceManager; } /** * {@inheritDoc} - * - * @throws Exception\ExceptionInterface */ protected function internalGetMetadata(string $normalizedKey): Metadata|null { $redis = $this->getRedisResource(); try { - $redisVersion = $this->resourceManager->getVersion($this->resourceId); + $redisVersion = $this->getRedisVersion(); if (version_compare($redisVersion, '2.8', '>=')) { // redis >= 2.8 // The command 'pttl' returns -2 if the item does not exist // and -1 if the item has no associated expire - $pttl = $redis->pttl($this->namespacePrefix . $normalizedKey); + $pttl = $redis->pttl($this->createNamespacedKey($normalizedKey)); if ($pttl <= -2) { return null; } @@ -466,7 +499,7 @@ protected function internalGetMetadata(string $normalizedKey): Metadata|null if (version_compare($redisVersion, '2.6', '>=')) { // redis >= 2.6, < 2.8 // The command 'pttl' returns -1 if the item does not exist or the item has no associated expire - $pttl = $redis->pttl($this->namespacePrefix . $normalizedKey); + $pttl = $redis->pttl($this->createNamespacedKey($normalizedKey)); if ($pttl <= -1) { if (! $this->internalHasItem($normalizedKey)) { return null; @@ -486,7 +519,7 @@ protected function internalGetMetadata(string $normalizedKey): Metadata|null // The command 'ttl' returns 0 if the item does not exist same as if the item is going to be expired // NOTE: In case of ttl=0 we return false because the item is going to be expired in a very near future // and then doesn't exist anymore - $ttl = $redis->ttl($this->namespacePrefix . $normalizedKey); + $ttl = $redis->ttl($this->createNamespacedKey($normalizedKey)); if ($ttl <= -1) { if (! $this->internalHasItem($normalizedKey)) { return null; @@ -500,8 +533,8 @@ protected function internalGetMetadata(string $normalizedKey): Metadata|null } elseif (! $this->internalHasItem($normalizedKey)) { return null; } - } catch (RedisResourceException $e) { - throw new Exception\RuntimeException($redis->getLastError() ?? $e->getMessage(), $e->getCode(), $e); + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException($exception, $redis); } return new Metadata(remainingTimeToLive: null); @@ -516,13 +549,63 @@ protected function internalGetMetadata(string $normalizedKey): Metadata|null */ private function preSerialize(mixed $value): mixed { - $options = $this->getOptions(); - $resourceMgr = $options->getResourceManager(); - $serializer = $resourceMgr->getLibOption($options->getResourceId(), RedisResource::OPT_SERIALIZER); - if ($serializer === null) { + $resourceManager = $this->getResourceManager(); + if (! $resourceManager->hasSerializationSupport($this)) { return (string) $value; } return $value; } + + private function getResourceManager(): RedisResourceManagerInterface + { + if ($this->resourceManager !== null) { + return $this->resourceManager; + } + + return $this->resourceManager = new RedisResourceManager($this->getOptions()); + } + + /** + * @param non-empty-string|int $key + * @return non-empty-string + */ + private function createNamespacedKey(string|int $key): string + { + if ($this->namespacePrefix !== null) { + return $this->namespacePrefix . $key; + } + + $options = $this->getOptions(); + $namespace = $options->getNamespace(); + $this->namespacePrefix = $namespace; + if ($namespace !== '') { + $this->namespacePrefix = $namespace . $options->getNamespaceSeparator(); + } + + return $this->namespacePrefix . $key; + } + + /** + * This method verifies that the return value from {@see RedisClusterFromExtension::get} or + * {@see RedisClusterFromExtension::mget} is `false` because the key does not exist or because the keys value + * is `false` at type-level. + */ + private function isFalseReturnValuePersisted(RedisFromExtension $redis, string $key): bool + { + $serializer = $this + ->getOptions() + ->getLibOption(RedisFromExtension::OPT_SERIALIZER, RedisFromExtension::SERIALIZER_NONE); + if ($serializer === RedisFromExtension::SERIALIZER_NONE) { + return false; + } + + try { + /** @psalm-var 0|1 $exists */ + $exists = $redis->exists($key); + return (bool) $exists; + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException($exception, $redis); + } + } } diff --git a/src/RedisCluster.php b/src/RedisCluster.php index 5d494ee..764c782 100644 --- a/src/RedisCluster.php +++ b/src/RedisCluster.php @@ -5,6 +5,7 @@ namespace Laminas\Cache\Storage\Adapter; use Laminas\Cache\Exception; +use Laminas\Cache\Exception\RuntimeException; use Laminas\Cache\Storage\AbstractMetadataCapableAdapter; use Laminas\Cache\Storage\Adapter\Exception\MetadataErrorException; use Laminas\Cache\Storage\Adapter\Exception\RedisRuntimeException; @@ -23,6 +24,7 @@ use function count; use function is_array; use function is_int; +use function is_string; use function version_compare; /** @@ -49,6 +51,7 @@ public function __construct($options = null) $this->resource = null; $this->namespacePrefix = null; parent::__construct($options); + $eventManager = $this->getEventManager(); $eventManager->attach('option', function (): void { $this->resource = null; @@ -401,16 +404,6 @@ private function getSupportedDatatypes(bool $serializer): array ]; } - /** - * @psalm-param RedisClusterOptions::OPT_* $option - * @return mixed - */ - private function getLibOption(int $option) - { - $resourceManager = $this->getResourceManager(); - return $resourceManager->getLibOption($option); - } - private function searchAndDelete(string $prefix, string $namespace): bool { $redis = $this->getRedisResource(); @@ -441,7 +434,10 @@ private function clusterException( */ private function isFalseReturnValuePersisted(RedisClusterFromExtension $redis, string $key): bool { - $serializer = $this->getLibOption(RedisFromExtension::OPT_SERIALIZER); + $serializer = $this + ->getOptions() + ->getLibOption(RedisFromExtension::OPT_SERIALIZER, RedisFromExtension::SERIALIZER_NONE); + if ($serializer === RedisFromExtension::SERIALIZER_NONE) { return false; } @@ -531,10 +527,30 @@ private function detectTtlForKey(RedisClusterFromExtension $redis, string $names return null; } - private function getRedisVersion(): string + public function getRedisVersion(): string { - $resourceManager = $this->getResourceManager(); - return $resourceManager->getVersion(); + $options = $this->getOptions(); + $versionFromOptions = $options->getRedisVersion(); + if ($versionFromOptions) { + return $versionFromOptions; + } + + $redisCluster = $this->getRedisResource(); + try { + $info = $this->info($redisCluster); + } catch (RedisClusterException $exception) { + throw RedisRuntimeException::fromClusterException($exception, $redisCluster); + } + + if (! isset($info['redis_version']) || ! is_string($info['redis_version'])) { + return '0.0.0-unknown'; + } + + $version = $info['redis_version']; + assert($version !== ''); + $options->setRedisVersion($version); + + return $version; } private function hasSerializationSupport(): bool @@ -552,11 +568,26 @@ private function getResourceManager(): RedisClusterResourceManagerInterface return $this->resourceManager = new RedisClusterResourceManager($this->getOptions()); } - /** - * @internal This is only used for unit testing. There should be no need to use this method in upstream projects. - */ public function setResourceManager(RedisClusterResourceManagerInterface $resourceManager): void { $this->resourceManager = $resourceManager; } + + private function info(RedisClusterFromExtension $resource): array + { + $options = $this->getOptions(); + if ($options->hasName()) { + $name = $options->getName(); + + return $resource->info($name); + } + + $seeds = $options->getSeeds(); + if ($seeds === []) { + throw new RuntimeException('Neither the node name nor any seed is configured.'); + } + + $seed = $seeds[0]; + return $resource->info($seed); + } } diff --git a/src/RedisClusterOptions.php b/src/RedisClusterOptions.php index 670732f..f9c7567 100644 --- a/src/RedisClusterOptions.php +++ b/src/RedisClusterOptions.php @@ -69,8 +69,7 @@ final class RedisClusterOptions extends AdapterOptions private ?SslContext $sslContext = null; /** - * @param iterable|null|AdapterOptions $options - * @psalm-param iterable|null|AdapterOptions $options + * @param iterable|null|AdapterOptions $options */ public function __construct($options = null) { @@ -243,10 +242,8 @@ public function getLibOptions(): array /** * @psalm-param RedisClusterOptions::OPT_* $option - * @param mixed $default - * @return mixed */ - public function getLibOption(int $option, $default = null) + public function getLibOption(int $option, mixed $default = null): mixed { return $this->libOptions[$option] ?? $default; } diff --git a/src/RedisClusterResourceManager.php b/src/RedisClusterResourceManager.php index 3d7e61d..8fc5494 100644 --- a/src/RedisClusterResourceManager.php +++ b/src/RedisClusterResourceManager.php @@ -4,57 +4,29 @@ namespace Laminas\Cache\Storage\Adapter; -use Laminas\Cache\Exception\RuntimeException; use Laminas\Cache\Storage\Adapter\Exception\InvalidRedisClusterConfigurationException; use Laminas\Cache\Storage\Adapter\Exception\RedisRuntimeException; -use Laminas\Cache\Storage\Adapter\RedisClusterOptions; use Laminas\Cache\Storage\Plugin\PluginInterface; use Laminas\Cache\Storage\Plugin\Serializer; use Laminas\Cache\Storage\PluginCapableInterface; +use Laminas\Cache\Storage\StorageInterface; use Redis as RedisFromExtension; use RedisCluster as RedisClusterFromExtension; use RedisClusterException; use Throwable; -use function array_key_exists; -use function assert; - /** - * @psalm-type RedisClusterInfoType = array&array{redis_version:string} + * @uses PluginCapableInterface */ final class RedisClusterResourceManager implements RedisClusterResourceManagerInterface { private RedisClusterOptions $options; - /** @psalm-var array */ - private array $libraryOptions = []; - public function __construct(RedisClusterOptions $options) { $this->options = $options; } - public function getVersion(): string - { - $versionFromOptions = $this->options->getRedisVersion(); - if ($versionFromOptions) { - return $versionFromOptions; - } - - $resource = $this->getResource(); - try { - $info = $this->info($resource); - } catch (RedisClusterException $exception) { - throw RedisRuntimeException::fromClusterException($exception, $resource); - } - - $version = $info['redis_version']; - assert($version !== ''); - $this->options->setRedisVersion($version); - - return $version; - } - public function getResource(): RedisClusterFromExtension { try { @@ -66,8 +38,7 @@ public function getResource(): RedisClusterFromExtension $libraryOptions = $this->options->getLibOptions(); try { - $resource = $this->applyLibraryOptions($resource, $libraryOptions); - $this->libraryOptions = $this->mergeLibraryOptionsFromCluster($libraryOptions, $resource); + $resource = $this->applyLibraryOptions($resource, $libraryOptions); } catch (RedisClusterException $exception) { throw RedisRuntimeException::fromClusterException($exception, $resource); } @@ -170,37 +141,7 @@ private function applyLibraryOptions( return $resource; } - /** - * @psalm-param array $options - * @psalm-return array - */ - private function mergeLibraryOptionsFromCluster(array $options, RedisClusterFromExtension $resource): array - { - foreach (RedisClusterOptions::LIBRARY_OPTIONS as $option) { - if (array_key_exists($option, $options)) { - continue; - } - - $options[$option] = $resource->getOption($option); - } - - return $options; - } - - /** - * @psalm-param RedisClusterOptions::OPT_* $option - * @return mixed - */ - public function getLibOption(int $option) - { - if (array_key_exists($option, $this->libraryOptions)) { - return $this->libraryOptions[$option]; - } - - return $this->libraryOptions[$option] = $this->getResource()->getOption($option); - } - - public function hasSerializationSupport(PluginCapableInterface $adapter): bool + public function hasSerializationSupport(PluginCapableInterface&StorageInterface $adapter): bool { /** * NOTE: we are not using {@see RedisClusterResourceManager::getLibOption} here @@ -209,7 +150,7 @@ public function hasSerializationSupport(PluginCapableInterface $adapter): bool * resource manager and then apply changes to it. As this is not the common use-case, this is not * considered in this check. */ - $options = $this->options; + $options = $adapter->getOptions(); $serializer = $options->getLibOption( RedisFromExtension::OPT_SERIALIZER, RedisFromExtension::SERIALIZER_NONE @@ -231,29 +172,4 @@ public function hasSerializationSupport(PluginCapableInterface $adapter): bool return false; } - - /** - * @psalm-return RedisClusterInfoType - */ - private function info(RedisClusterFromExtension $resource): array - { - if ($this->options->hasName()) { - $name = $this->options->getName(); - - /** @psalm-var RedisClusterInfoType $info */ - $info = $resource->info($name); - return $info; - } - - $seeds = $this->options->getSeeds(); - if ($seeds === []) { - throw new RuntimeException('Neither the node name nor any seed is configured.'); - } - - $seed = $seeds[0]; - /** @psalm-var RedisClusterInfoType $info */ - $info = $resource->info($seed); - - return $info; - } } diff --git a/src/RedisClusterResourceManagerInterface.php b/src/RedisClusterResourceManagerInterface.php index 438e3db..ea1ce23 100644 --- a/src/RedisClusterResourceManagerInterface.php +++ b/src/RedisClusterResourceManagerInterface.php @@ -5,19 +5,15 @@ namespace Laminas\Cache\Storage\Adapter; use Laminas\Cache\Storage\PluginCapableInterface; +use Laminas\Cache\Storage\StorageInterface; use RedisCluster as RedisClusterFromExtension; interface RedisClusterResourceManagerInterface { - public function getVersion(): string; - public function getResource(): RedisClusterFromExtension; /** - * @psalm-param RedisClusterOptions::OPT_* $option - * @return mixed + * @param StorageInterface&PluginCapableInterface $adapter */ - public function getLibOption(int $option); - - public function hasSerializationSupport(PluginCapableInterface $adapter): bool; + public function hasSerializationSupport(PluginCapableInterface&StorageInterface $adapter): bool; } diff --git a/src/RedisOptions.php b/src/RedisOptions.php index ba9f5bf..fd03019 100644 --- a/src/RedisOptions.php +++ b/src/RedisOptions.php @@ -5,35 +5,100 @@ namespace Laminas\Cache\Storage\Adapter; use Laminas\Cache\Exception; +use Redis; +use function array_is_list; +use function array_key_exists; +use function constant; +use function defined; +use function get_debug_type; +use function is_array; +use function is_int; +use function is_string; +use function parse_url; use function sprintf; +use function str_starts_with; use function strlen; +use function strtoupper; +use function trim; +/** + * @psalm-type ServerArrayShape = array{ + * host: non-empty-string, + * port: int<1,65535>, + * timeout: non-negative-int|null + * } + */ final class RedisOptions extends AdapterOptions { - // @codingStandardsIgnoreStart + public const LIBRARY_OPTIONS = [ + self::OPT_SERIALIZER, + self::OPT_PREFIX, + self::OPT_READ_TIMEOUT, + self::OPT_SCAN, + self::OPT_TCP_KEEPALIVE, + self::OPT_COMPRESSION, + self::OPT_REPLY_LITERAL, + self::OPT_COMPRESSION_LEVEL, + self::OPT_NULL_MULTIBULK_AS_NULL, + self::OPT_MAX_RETRIES, + self::OPT_BACKOFF_ALGORITHM, + self::OPT_BACKOFF_BASE, + self::OPT_BACKOFF_CAP, + ]; + + public const OPT_SERIALIZER = 1; + public const OPT_PREFIX = 2; + public const OPT_READ_TIMEOUT = 3; + public const OPT_SCAN = 4; + public const OPT_TCP_KEEPALIVE = 6; + public const OPT_COMPRESSION = 7; + public const OPT_REPLY_LITERAL = 8; + public const OPT_COMPRESSION_LEVEL = 9; + public const OPT_NULL_MULTIBULK_AS_NULL = 10; + public const OPT_MAX_RETRIES = 11; + public const OPT_BACKOFF_ALGORITHM = 12; + public const OPT_BACKOFF_BASE = 13; + public const OPT_BACKOFF_CAP = 14; + /** * Prioritized properties ordered by prio to be set first * in case a bulk of options sets set at once * * @var string[] */ - protected array $__prioritizedProperties__ = ['resource_manager', 'resource_id', 'server']; + // @codingStandardsIgnoreStart + protected array $__prioritizedProperties__ = ['server', 'persistentId']; // @codingStandardsIgnoreEnd /** * The namespace separator */ - private string $namespaceSeparator = ':'; + protected string $namespaceSeparator = ':'; + protected string|null $persistentId; - /** - * The redis resource manager - */ - private ?RedisResourceManager $resourceManager = null; + protected string $redisVersion = ''; + /** @var non-empty-string|null */ + protected string|null $user; + /** @var array */ + protected array $libOptions = []; + /** @var ServerArrayShape|non-empty-string|null */ + protected array|null|string $server; + protected int $database = 0; + /** @var non-empty-string|null */ + protected string|null $password; + protected bool $persistent = false; /** - * The resource id of the resource manager + * @param iterable|null|AdapterOptions $options */ - private string $resourceId = 'default'; + public function __construct(iterable|null|AdapterOptions $options = null) + { + $this->user = null; + $this->password = null; + $this->server = null; + $this->persistentId = null; + parent::__construct($options); + } /** * {@inheritDoc} @@ -51,16 +116,10 @@ public function setNamespace(string $namespace): self return $this; } - /** - * @return RedisOptions Provides a fluent interface - */ public function setNamespaceSeparator(string $namespaceSeparator): self { - if ($this->namespaceSeparator !== $namespaceSeparator) { - $this->triggerOptionEvent('namespace_separator', $namespaceSeparator); - $this->namespaceSeparator = $namespaceSeparator; - } - + $this->namespaceSeparator = $namespaceSeparator; + $this->triggerOptionEvent('namespace_separator', $namespaceSeparator); return $this; } @@ -69,120 +128,288 @@ public function getNamespaceSeparator(): string return $this->namespaceSeparator; } - public function setResourceManager(?RedisResourceManager $resourceManager = null): self + public function getPersistentId(): string|null + { + return $this->persistentId; + } + + public function setPersistentId(string $persistentId): void { - if ($this->resourceManager !== $resourceManager) { - $this->triggerOptionEvent('resource_manager', $resourceManager); - $this->resourceManager = $resourceManager; + if ($persistentId === '') { + return; } + $this->persistentId = $persistentId; + $this->persistent = true; + $this->triggerOptionEvent('persistent_id', $persistentId); + } + + /** + * @param array $options + */ + public function setLibOptions(array $options): void + { + $this->libOptions = $this->normalizeLibOptions($options); + $this->triggerOptionEvent('lib_option', $options); + } + + /** + * @return array $options + */ + public function getLibOptions(): array + { + return $this->libOptions; + } + + /** + * @param positive-int $option + */ + public function getLibOption(int $option, mixed $default = null): mixed + { + return $this->libOptions[$option] ?? $default; + } + + /** + * Server can be described as follows: + * - URI: /path/to/sock.sock or redis://user:pass@host:port + * - Assoc: array('host' => [, 'port' => [, 'timeout' => ]]) + * - List: array([, , [, ]]) + * + * @param ServerArrayShape|array{0:non-empty-string,1?:int<1,65535>,2?:non-negative-int}|non-empty-string $server + */ + public function setServer(string|array $server): self + { + [$server, $username, $password] = $this->normalizeServer($server); + if ($username !== null) { + $this->setUser($username); + } + + if ($password !== null) { + $this->setPassword($password); + } + + $this->server = $server; return $this; } - public function getResourceManager(): RedisResourceManager + /** + * @return ServerArrayShape|non-empty-string + * @throws Exception\RuntimeException In case there is no server provided by configuration. + */ + public function getServer(): array|string { - if (! $this->resourceManager) { - $this->resourceManager = new RedisResourceManager(); + if ($this->server === null) { + throw new Exception\RuntimeException('Missing `server` option.'); } - return $this->resourceManager; + + return $this->server; + } + + public function setDatabase(int $database): void + { + $this->database = $database; } - public function getResourceId(): string + public function getDatabase(): int { - return $this->resourceId; + return $this->database; } - public function setResourceId(string $resourceId): self + public function setPassword(string $password): void { - if ($this->resourceId !== $resourceId) { - $this->triggerOptionEvent('resource_id', $resourceId); - $this->resourceId = $resourceId; + if ($password === '') { + return; } - return $this; + $this->password = $password; } - public function getPersistentId(): string + public function getPassword(): string|null { - return $this->getResourceManager()->getPersistentId($this->getResourceId()); + return $this->password; } - public function setPersistentId(string $persistentId): self + public function setUser(string $user): void { - $this->triggerOptionEvent('persistent_id', $persistentId); - $this->getResourceManager()->setPersistentId($this->getResourceId(), $persistentId); - return $this; + if ($user === '') { + return; + } + + $this->user = $user; } - public function setLibOptions(array $libOptions): self + public function getUser(): ?string { - $this->triggerOptionEvent('lib_option', $libOptions); - $this->getResourceManager()->setLibOptions($this->getResourceId(), $libOptions); - return $this; + return $this->user; } - public function getLibOptions(): array + public function getRedisVersion(): string { - return $this->getResourceManager()->getLibOptions($this->getResourceId()); + return $this->redisVersion; } /** - * Server can be described as follows: - * - URI: /path/to/sock.sock - * - Assoc: array('host' => [, 'port' => [, 'timeout' => ]]) - * - List: array([, , [, ]]) + * @param non-empty-string $version */ - public function setServer(string|array $server): self + public function setRedisVersion(string $version): void { - $this->getResourceManager()->setServer($this->getResourceId(), $server); - return $this; + $this->redisVersion = trim($version); } /** - * @return array array('host' => [, 'port' => [, 'timeout' => ]]) + * @param ServerArrayShape|array{0:non-empty-string,1?:int<1,65535>,2?:non-negative-int}|non-empty-string $server + * @return array{ + * 0: ServerArrayShape|non-empty-string, + * 1: non-empty-string|null, + * 2: non-empty-string|null + * } */ - public function getServer(): array + private function normalizeServer(array|string $server): array { - return $this->getResourceManager()->getServer($this->getResourceId()); + /** + * @psalm-suppress TypeDoesNotContainType Psalm types do not prevent users from injecting empty strings. + */ + if ($server === [] || $server === '') { + throw new Exception\InvalidArgumentException('Provided `server` configuration must hold any information.'); + } + + if (is_string($server)) { + if (str_starts_with($server, '/')) { + return [$server, null, null]; + } + + $parsedUrl = parse_url($server); + if (! is_array($parsedUrl)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Provided `server` is not a valid URI; "%s" given.', + $server, + )); + } + + $server = $parsedUrl; + unset($parsedUrl); + } + + if (array_is_list($server)) { + return [$this->createServerArrayShape($server[0], $server[1] ?? 6379, $server[2] ?? 0), null, null]; + } + + if (! array_key_exists('host', $server)) { + throw new Exception\InvalidArgumentException('Missing required `host` option in server configuration.'); + } + + $host = $server['host']; + $port = $server['port'] ?? 6379; + $timeout = $server['timeout'] ?? 0; + $user = $server['user'] ?? null; + $password = $server['pass'] ?? null; + + /** + * @psalm-suppress TypeDoesNotContainType Psalm types do not provide that kind of type-safety here and + * thus we should double-check here. + */ + if ($user !== null && (! is_string($user) || $user === '')) { + throw new Exception\InvalidArgumentException(sprintf( + 'Provided `user` option in server configuration must be a non-empty-string; "%s" given.', + get_debug_type($user), + )); + } + + /** + * @psalm-suppress TypeDoesNotContainType Psalm types do not provide that kind of type-safety here and + * thus we should double-check here. + */ + if ($password !== null && (! is_string($password) || $password === '')) { + throw new Exception\InvalidArgumentException(sprintf( + 'Provided `password` option in server configuration must be a non-empty-string; "%s" given.', + get_debug_type($password), + )); + } + + return [ + $this->createServerArrayShape($host, $port, $timeout), + $user, + $password, + ]; } - public function setDatabase(int $database): self + /** + * @return ServerArrayShape + */ + private function createServerArrayShape(mixed $host, mixed $port, mixed $timeout): array { - $this->getResourceManager()->setDatabase($this->getResourceId(), $database); - return $this; + if (! is_string($host) || $host === '') { + throw new Exception\InvalidArgumentException(sprintf( + 'Provided `host` option in server configuration must be a non-empty-string; "%s" given.', + get_debug_type($host), + )); + } + + if (! is_int($port) || $port < 1 || $port > 65535) { + throw new Exception\InvalidArgumentException(sprintf( + 'Provided `port` option in server configuration must be a positive integer between 1 and 65535;' + . ' "%s" given.', + get_debug_type($port), + )); + } + + if (! is_int($timeout) || $timeout < 0) { + throw new Exception\InvalidArgumentException(sprintf( + 'Provided `timeout` option in server configuration must be a non-negative-int; "%s" given.', + get_debug_type($port), + )); + } + + return ['host' => $host, 'port' => $port, 'timeout' => $timeout]; } - public function getDatabase(): int + public function setPersistent(bool $persistent): void { - return $this->getResourceManager()->getDatabase($this->getResourceId()); + $this->persistent = $persistent; + if ($persistent === false) { + $this->persistentId = null; + } } - public function setPassword(string $password): self + /** + * @internal Only providing this method for having tests passed. Please use {@see RedisOptions::isPersistent()}. + * + * @psalm-suppress PossiblyUnusedMethod Method just exists to have tests passing. + */ + public function getPersistent(): bool { - $this->getResourceManager()->setPassword($this->getResourceId(), $password); - return $this; + return $this->isPersistent(); } - public function getPassword(): string|null + public function isPersistent(): bool { - return $this->getResourceManager()->getPassword($this->getResourceId()); + return $this->persistent; } /** - * @param string $user ACL User + * @param array $options + * @return array */ - public function setUser(string $user): RedisOptions + private function normalizeLibOptions(array $options): array { - if ($user === '') { - return $this; - } + $normalized = []; + foreach ($options as $option => $value) { + if (is_string($option)) { + $constant = sprintf('\Redis::OPT_%s', strtoupper($option)); + if (! defined($constant)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Provided redis option `%s` does not exist (anymore).', + $option, + )); + } - $this->getResourceManager()->setUser($this->getResourceId(), $user); - return $this; - } + /** @var value-of $option */ + $option = constant($constant); + } - public function getUser(): ?string - { - return $this->getResourceManager()->getUser($this->getResourceId()); + $normalized[$option] = $value; + } + + return $normalized; } } diff --git a/src/RedisResourceManager.php b/src/RedisResourceManager.php index 7b59355..0000780 100644 --- a/src/RedisResourceManager.php +++ b/src/RedisResourceManager.php @@ -4,735 +4,130 @@ namespace Laminas\Cache\Storage\Adapter; -use Laminas\Cache\Exception; use Laminas\Cache\Storage\Adapter\Exception\RedisRuntimeException; -use Laminas\Stdlib\ArrayUtils; -use Redis as RedisResource; -use RedisException as RedisResourceException; -use ReflectionClass; -use Traversable; - -use function array_replace; -use function assert; -use function constant; -use function defined; +use Laminas\Cache\Storage\Plugin\PluginInterface; +use Laminas\Cache\Storage\Plugin\Serializer; +use Laminas\Cache\Storage\PluginCapableInterface; +use Laminas\Cache\Storage\StorageInterface; +use Redis as RedisFromExtension; +use RedisException as RedisFromExtensionException; + +use function array_filter; use function is_array; -use function is_int; -use function is_string; -use function method_exists; -use function parse_url; -use function str_replace; -use function str_starts_with; -use function strpos; -use function strtoupper; -use function trim; /** - * This is a resource manager for redis - */ -/** - * @psalm-type ResourceArrayShape = array{ - * persistent_id: string, - * lib_options: array, - * server: array{host: string, port: int, timeout: int}, - * user: non-empty-string|null, - * password: non-empty-string|null, - * database: int, - * resource: RedisResource|null, - * initialized: bool, - * version: string - * } + * @uses PluginCapableInterface + * @uses StorageInterface */ -final class RedisResourceManager +final class RedisResourceManager implements RedisResourceManagerInterface { - /** - * Registered resources - * - * @var array $resources - */ - private array $resources = []; - - /** - * Check if a resource exists - * - * @param string $id - * @return bool - */ - public function hasResource($id) - { - return isset($this->resources[$id]); - } - - /** - * Get redis server version - * - * @param string $resourceId - * @return string - * @throws Exception\RuntimeException - */ - public function getVersion($resourceId) - { - // check resource id and initialize the resource - $this->getResource($resourceId); - - return $this->resources[$resourceId]['version']; - } - - /** - * Get redis major server version - * - * @throws Exception\RuntimeException - */ - public function getMajorVersion(string $resourceId): int - { - // check resource id and initialize the resource - $this->getResource($resourceId); - - return (int) $this->resources[$resourceId]['version']; - } - - /** - * Get redis resource database - */ - public function getDatabase(string $id): int - { - if (! $this->hasResource($id)) { - throw new Exception\RuntimeException("No resource with id '{$id}'"); - } - - $resource = &$this->resources[$id]; - return $resource['database']; - } - - public function getPassword(string $id): string|null - { - if (! $this->hasResource($id)) { - throw new Exception\RuntimeException("No resource with id '{$id}'"); - } + private const DEFAULT_REDIS_PORT = 6379; - $resource = $this->resources[$id]; - return $resource['password']; + public function __construct( + private readonly RedisOptions $options, + ) { } - /** - * Get redis resource user - */ - public function getUser(string $id): ?string + public function getResource(): RedisFromExtension { - if (! $this->hasResource($id)) { - throw new Exception\RuntimeException("No resource with id '{$id}'"); - } - - $resource = $this->resources[$id]; - return $resource['user']; - } - - /** - * @throws Exception\RuntimeException - */ - public function getResource(string $id): RedisResource - { - if (! $this->hasResource($id)) { - throw new Exception\RuntimeException("No resource with id '{$id}'"); - } - - $resource = $this->resources[$id]; - if ($resource['resource'] instanceof RedisResource) { - //in case new server was set then connect - if (! $resource['initialized']) { - $this->connect($resource); - } - - if (! $resource['version']) { - $redis = $resource['resource']; - $info = $this->getRedisInfo($redis); - $resource['version'] = $info['redis_version']; - unset($info); - } - - $this->resources[$id] = $resource; - return $resource['resource']; + try { + $resource = $this->createRedisFromExtension($this->options); + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromFailedConnection($exception); } - $redis = new RedisResource(); - - $resource['resource'] = $redis; - $this->connect($resource); + $libraryOptions = $this->options->getLibOptions(); - $this->normalizeLibOptions($resource['lib_options']); - - foreach ($resource['lib_options'] as $k => $v) { - $redis->setOption($k, $v); - } - - $info = $this->getRedisInfo($redis); - $resource['version'] = $info['redis_version']; - $this->resources[$id] = $resource; - return $redis; - } - - /** - * @throws Exception\RuntimeException - * @return array array('host' => [, 'port' => [, 'timeout' => ]]) - */ - public function getServer(string $id): array - { - if (! $this->hasResource($id)) { - throw new Exception\RuntimeException("No resource with id '{$id}'"); + try { + $resource = $this->applyLibraryOptions($resource, $libraryOptions); + } catch (RedisFromExtensionException $exception) { + throw RedisRuntimeException::fromRedisException($exception, $resource); } - $resource = &$this->resources[$id]; - return $resource['server']; + return $resource; } - /** - * Normalize one server into the following format: - * array('host' => [, 'port' => [, 'timeout' => ]]) - * - * @param-out array $server - * @throws Exception\InvalidArgumentException - */ - protected function normalizeServer(string|iterable &$server): void + private function createRedisFromExtension(RedisOptions $options): RedisFromExtension { - $host = null; - $port = null; - $timeout = 0; - - // convert a single server into an array - if ($server instanceof Traversable) { - $server = ArrayUtils::iteratorToArray($server); - } - + $server = $options->getServer(); + $host = $server; + $port = null; if (is_array($server)) { - // array([, [, ]]) - if (isset($server[0])) { - $host = (string) $server[0]; - $port = isset($server[1]) ? (int) $server[1] : $port; - $timeout = isset($server[2]) ? (int) $server[2] : $timeout; - } - - // array('host' => [, 'port' => , ['timeout' => ]]) - if (! isset($server[0]) && isset($server['host'])) { - $host = (string) $server['host']; - $port = isset($server['port']) ? (int) $server['port'] : $port; - $timeout = isset($server['timeout']) ? (int) $server['timeout'] : $timeout; - } - } else { - // parse server from URI host{:?port} - $server = trim($server); - if (! str_starts_with($server, '/')) { - //non unix domain socket connection - $server = parse_url($server); - } else { - $server = ['host' => $server]; - } - - if (! is_array($server)) { - throw new Exception\InvalidArgumentException("Invalid server given"); - } - $host = $server['host']; - $port = isset($server['port']) ? (int) $server['port'] : $port; - } - - if ($host === '' || $host === null) { - throw new Exception\InvalidArgumentException('Missing required server host'); - } - - $server = [ - 'host' => $host, - 'port' => $port, - 'timeout' => $timeout, - ]; - } - - /** - * Extract password to be used on connection - * - * @param ResourceArrayShape $resource - * @return non-empty-string|null - */ - protected function extractPassword(array $resource, mixed $serverUri): ?string - { - if ($resource['password'] !== null) { - return $resource['password']; - } - - if (! is_string($serverUri)) { - return null; + $port = $server['port'] ?? self::DEFAULT_REDIS_PORT; } - // parse server from URI host{:?port} - $server = trim($serverUri); + $authentication = []; + $user = $options->getUser(); + $password = $options->getPassword(); - if (str_starts_with($server, '/')) { - return null; + if ($user !== null) { + $authentication[] = $user; } - //non unix domain socket connection - $server = parse_url($server); - - $password = $server['pass'] ?? null; - if ($password === null || $password === '') { - return null; - } - - return $password; - } - - /** - * Extract password to be used on connection - * - * @param ResourceArrayShape $resource - * @return non-empty-string|null - */ - protected function extractUser(array $resource, array|string $serverUri): ?string - { - if ($resource['user'] !== null) { - return $resource['user']; + if ($password !== null) { + $authentication[] = $password; } - if (! is_string($serverUri)) { - return null; + if ($authentication === []) { + $authentication = null; } - // parse server from URI host{:?port} - $server = trim($serverUri); - - if (str_starts_with($server, '/')) { - return null; - } - - //non unix domain socket connection - $server = parse_url($server); - - $user = $server['user'] ?? null; - if ($user === null || $user === '') { - return null; - } - - return $user; - } - - /** - * Connects to redis server - * - * @param ResourceArrayShape $resource - * @throws Exception\RuntimeException - */ - private function connect(array &$resource): void - { - $server = $resource['server']; - $redis = $resource['resource']; - assert($redis instanceof RedisResource); - - try { - if (($resource['persistent_id'] ?? '') !== '') { - //connect or reuse persistent connection - $success = $redis->pconnect( - $server['host'], - $server['port'], - $server['timeout'], - $resource['persistent_id'] - ); - } elseif ($server['port']) { - $success = $redis->connect($server['host'], $server['port'], $server['timeout']); - } elseif ($server['timeout']) { - //connect through unix domain socket - $success = $redis->connect($server['host'], $server['timeout']); - } else { - $success = $redis->connect($server['host']); - } - - if (! $success) { - throw new Exception\RuntimeException('Could not establish connection with Redis instance'); - } - - if ($resource['user'] !== null && $resource['password'] !== null) { - $redis->auth([$resource['user'], $resource['password']]); - } elseif ($resource['password'] !== null) { - $redis->auth([$resource['password']]); - } - $redis->select($resource['database']); - $resource['initialized'] = true; - } catch (RedisResourceException $exception) { - throw RedisRuntimeException::fromRedisException($exception, $redis); - } - } - - public function setResource(string $id, iterable|RedisResource $resource): self - { - //TODO: how to get back redis connection info from resource? - $defaults = [ - 'persistent_id' => '', - 'lib_options' => [], - 'server' => [], - 'user' => null, - 'password' => null, - 'database' => 0, - 'resource' => null, - 'initialized' => false, - 'version' => '', + $resourceOptions = [ + 'host' => $host, + 'port' => $port, + 'connectTimeout' => $server['timeout'] ?? null, + 'persistent' => $options->getPersistentId() ?? $options->isPersistent(), + 'auth' => $authentication, ]; - if (! $resource instanceof RedisResource) { - if ($resource instanceof Traversable) { - $resource = ArrayUtils::iteratorToArray($resource); - } - - /** - * Lets assume that the resource passed via RedisResourceManager#setResource is already providing - * options in the expected array shape format for now. This is how it worked since 2013 and therefore, - * for BC reasons, we can continue doing that until we refactor this in the next major. - * - * @var ResourceArrayShape $resource - */ - $resource = array_replace($defaults, $resource); - - // normalize and validate params - $this->normalizePersistentId($resource['persistent_id']); - - // #6495 note: order is important here, as `normalizeServer` applies destructive - // transformations on $resource['server'] - $resource['password'] = $this->extractPassword($resource, $resource['server']); - $resource['user'] = $this->extractUser($resource, $resource['server']); - - $this->normalizeServer($resource['server']); - } else { - //there are two ways of determining if redis is already initialized - //with connect function: - //1) pinging server - //2) checking undocumented property socket which is available only - //after successful connect - $resource = array_replace($defaults, [ - 'resource' => $resource, - 'initialized' => isset($resource->socket), - ]); - } - $this->resources[$id] = $resource; - return $this; - } - - /** - * @return RedisResourceManager Fluent interface - */ - public function removeResource(string $id): self - { - unset($this->resources[$id]); - return $this; - } + $resource = new RedisFromExtension(array_filter($resourceOptions, fn (mixed $value) => $value !== null)); + $resource->select($options->getDatabase()); - /** - * @throws Exception\RuntimeException - */ - public function setPersistentId(string $id, string $persistentId): self - { - if (! $this->hasResource($id)) { - return $this->setResource($id, [ - 'persistent_id' => $persistentId, - ]); - } - - $resource = &$this->resources[$id]; - if ($resource['resource'] instanceof RedisResource && $resource['initialized']) { - throw new Exception\RuntimeException( - "Can't change persistent id of resource {$id} after initialization" - ); - } - - $this->normalizePersistentId($persistentId); - $resource['persistent_id'] = $persistentId; - - return $this; + return $resource; } /** - * @throws Exception\RuntimeException + * @param array $options */ - public function getPersistentId(string $id): string + private function applyLibraryOptions(RedisFromExtension $resource, array $options): RedisFromExtension { - if (! $this->hasResource($id)) { - throw new Exception\RuntimeException("No resource with id '{$id}'"); + foreach ($options as $option => $value) { + $resource->setOption($option, $value); } - $resource = &$this->resources[$id]; - - return $resource['persistent_id']; - } - - /** - * Normalize the persistent id - * - * @param-out string $persistentId - */ - private function normalizePersistentId(mixed &$persistentId): void - { - $persistentId = (string) $persistentId; + return $resource; } - /** - * @return RedisResourceManager Fluent interface - */ - public function setLibOptions(string $id, array $libOptions): self + public function hasSerializationSupport(StorageInterface&PluginCapableInterface $adapter): bool { - if (! $this->hasResource($id)) { - return $this->setResource($id, [ - 'lib_options' => $libOptions, - ]); - } - - $resource = &$this->resources[$id]; - - $resource['lib_options'] = $libOptions; - - if (! $resource['resource'] instanceof RedisResource) { - return $this; - } - - $this->normalizeLibOptions($libOptions); - $redis = $resource['resource']; - - if (method_exists($redis, 'setOptions')) { - $redis->setOptions($libOptions); - } else { - foreach ($libOptions as $key => $value) { - $redis->setOption($key, $value); - } - } + /** + * NOTE: we are not using {@see RedisFromExtensionManager::getLibOption} here + * as this would create a connection to redis even tho it won't be needed. + * Theoretically, it would be possible for upstream projects to receive the resource directly from the + * resource manager and then apply changes to it. As this is not the common use-case, this is not + * considered in this check. + */ + $options = $adapter->getOptions(); + $serializer = $options->getLibOption( + RedisFromExtension::OPT_SERIALIZER, + RedisFromExtension::SERIALIZER_NONE + ); - return $this; - } - - /** - * @return array - * @throws Exception\RuntimeException - */ - public function getLibOptions(string $id): array - { - if (! $this->hasResource($id)) { - throw new Exception\RuntimeException("No resource with id '{$id}'"); + if ($serializer !== RedisFromExtension::SERIALIZER_NONE) { + return true; } - $resource = $this->resources[$id]; - - if ($resource['resource'] instanceof RedisResource) { - $libOptions = []; - $reflection = new ReflectionClass('Redis'); - $constants = $reflection->getConstants(); - foreach ($constants as $constName => $constValue) { - if (strpos($constName, 'OPT_') === 0) { - assert( - is_int($constValue), - 'Redis option constants are always pointing to an int-mask.', - ); - $libOptions[$constValue] = $resource['resource']->getOption($constValue); - } + /** @var iterable $plugins */ + $plugins = $adapter->getPluginRegistry(); + foreach ($plugins as $plugin) { + if (! $plugin instanceof Serializer) { + continue; } - return $libOptions; - } - return $resource['lib_options']; - } - - /** - * @return RedisResourceManager Fluent interface - */ - public function setLibOption(string $id, string|int $key, mixed $value): self - { - return $this->setLibOptions($id, [$key => $value]); - } - - /** - * @throws Exception\RuntimeException - */ - public function getLibOption(string $id, string|int $key): mixed - { - if (! $this->hasResource($id)) { - throw new Exception\RuntimeException("No resource with id '{$id}'"); - } - - $this->normalizeLibOptionKey($key); - $resource = $this->resources[$id]; - - if ($resource['resource'] instanceof RedisResource) { - return $resource['resource']->getOption($key); - } - - return $resource['lib_options'][$key] ?? null; - } - - /** - * Normalize Redis options - * - * @param iterable $libOptions - * @throws Exception\InvalidArgumentException - * @param-out array $libOptions - */ - protected function normalizeLibOptions(iterable &$libOptions): void - { - $result = []; - foreach ($libOptions as $key => $value) { - $this->normalizeLibOptionKey($key); - $result[$key] = $value; - } - - $libOptions = $result; - } - - /** - * Convert option name into it's constant value - * - * @throws Exception\InvalidArgumentException - * @param-out int $key - */ - protected function normalizeLibOptionKey(string|int &$key): void - { - if (is_int($key)) { - return; - } - - $const = 'Redis::OPT_' . str_replace([' ', '-'], '_', strtoupper($key)); - if (! defined($const)) { - throw new Exception\InvalidArgumentException("Unknown redis option '{$key}' ({$const})"); - } - $key = constant($const); - assert(is_int($key)); - } - - /** - * Set server - * - * Server can be described as follows: - * - URI: /path/to/sock.sock - * - Assoc: array('host' => [, 'port' => [, 'timeout' => ]]) - * - List: array([, , [, ]]) - */ - public function setServer(string $id, string|array $server): self - { - if (! $this->hasResource($id)) { - return $this->setResource($id, [ - 'server' => $server, - ]); - } - - $this->normalizeServer($server); - - $resource = &$this->resources[$id]; - $resource['password'] = $this->extractPassword($resource, $server); - - $resource['user'] = $this->extractUser($resource, $server); - - if ($resource['resource'] instanceof RedisResource) { - $resourceParams = ['server' => $server]; - - if ($resource['password'] !== null) { - $resourceParams['password'] = $resource['password']; - } - if ($resource['user'] !== null) { - $resourceParams['user'] = $resource['user']; - } - - $this->setResource($id, $resourceParams); - } else { - $resource['server'] = $server; - } - - return $this; - } - - /** - * Set redis password - * - * @param string $id - * @param string $password - * @return RedisResourceManager - */ - public function setPassword($id, $password) - { - if (! $this->hasResource($id)) { - return $this->setResource($id, [ - 'password' => $password, - ]); - } - - $resource = &$this->resources[$id]; - $resource['password'] = $password; - $resource['initialized'] = false; - return $this; - } - - /** - * Set redis database number - * - * @param string $id - * @param int $database - * @return RedisResourceManager - */ - public function setDatabase($id, $database) - { - $database = (int) $database; - - if (! $this->hasResource($id)) { - return $this->setResource($id, [ - 'database' => $database, - ]); - } - - $resource = $this->resources[$id]; - $redis = $resource['resource']; - if ($redis instanceof RedisResource && $resource['initialized']) { - try { - $redis->select($database); - } catch (RedisResourceException $exception) { - throw RedisRuntimeException::fromRedisException($exception, $redis); - } - } - - $resource['database'] = $database; - $this->resources[$id] = $resource; - - return $this; - } - - /** - * @return array{redis_version:string} - */ - private function getRedisInfo(RedisResource $redis): array - { - try { - $info = $redis->info(); - } catch (RedisResourceException $exception) { - throw RedisRuntimeException::fromRedisException($exception, $redis); - } - - if (! is_array($info)) { - return ['redis_version' => '0.0.0-unknown']; - } - - if (! isset($info['redis_version']) || ! is_string($info['redis_version'])) { - return ['redis_version' => '0.0.0-unknown']; - } - - return $info; - } - - /** - * Set redis user - * - * @param non-empty-string $user - */ - public function setUser(string $id, string $user): void - { - if (! $this->hasResource($id)) { - $this->setResource($id, [ - 'user' => $user, - ]); - return; + return true; } - $resource = $this->resources[$id]; - $resource['user'] = $user; - $resource['initialized'] = false; - $this->resources[$id] = $resource; + return false; } } diff --git a/src/RedisResourceManagerInterface.php b/src/RedisResourceManagerInterface.php new file mode 100644 index 0000000..be151ed --- /dev/null +++ b/src/RedisResourceManagerInterface.php @@ -0,0 +1,19 @@ +&PluginCapableInterface $adapter + */ + public function hasSerializationSupport(StorageInterface&PluginCapableInterface $adapter): bool; +} diff --git a/test/integration/Laminas/RedisClusterStorageCreationTrait.php b/test/integration/Laminas/RedisClusterStorageCreationTrait.php index c1637a8..7aaf2c6 100644 --- a/test/integration/Laminas/RedisClusterStorageCreationTrait.php +++ b/test/integration/Laminas/RedisClusterStorageCreationTrait.php @@ -25,6 +25,9 @@ trait RedisClusterStorageCreationTrait /** @var array */ private array $namespaces = []; + /** + * @param RedisFromExtension::SERIALIZER_* $serializerOption + */ private function createRedisClusterStorage(int $serializerOption, bool $serializerPlugin): RedisCluster { $node = $this->getClusterNameFromEnvironment(); diff --git a/test/integration/Laminas/RedisClusterTest.php b/test/integration/Laminas/RedisClusterTest.php index 769cc9f..630da97 100644 --- a/test/integration/Laminas/RedisClusterTest.php +++ b/test/integration/Laminas/RedisClusterTest.php @@ -14,7 +14,7 @@ use Redis as RedisFromExtension; /** - * @template-extends AbstractCommonAdapterTest + * @template-extends AbstractCommonAdapterTest */ final class RedisClusterTest extends AbstractCommonAdapterTest { @@ -26,8 +26,8 @@ public function testWillProperlyFlush(): void self::assertInstanceOf(StorageInterface::class, $storage); $storage->setItem('foo', 'bar'); $flushed = $storage->flush(); - $this->assertTrue($flushed); - $this->assertFalse($storage->hasItem('foo')); + self::assertTrue($flushed); + self::assertFalse($storage->hasItem('foo')); } public function testCanCreateResourceFromSeeds(): void @@ -40,7 +40,7 @@ public function testCanCreateResourceFromSeeds(): void ]); $storage = new RedisCluster($options); - $this->assertTrue($storage->flush()); + self::assertTrue($storage->flush()); } public function testWillHandleIntegratedSerializerInformation(): void @@ -56,7 +56,7 @@ public function testWillHandleIntegratedSerializerInformation(): void $capabilities = $storage->getCapabilities(); $dataTypes = $capabilities->supportedDataTypes; - $this->assertEquals([ + self::assertEquals([ 'NULL' => true, 'boolean' => true, 'integer' => true, @@ -91,7 +91,7 @@ public function testWillHandleNonSupportedSerializerInformation(): void $capabilities = $storage->getCapabilities(); $dataTypes = $capabilities->supportedDataTypes; - $this->assertEquals([ + self::assertEquals([ 'NULL' => 'string', 'boolean' => 'string', 'integer' => 'string', diff --git a/test/integration/Laminas/RedisFromExtensionAsset.php b/test/integration/Laminas/RedisFromExtensionAsset.php deleted file mode 100644 index 73a372a..0000000 --- a/test/integration/Laminas/RedisFromExtensionAsset.php +++ /dev/null @@ -1,18 +0,0 @@ - self::class]; - - $host = $this->host(); - $port = $this->port(); + $host = $this->host(); + $port = $this->port(); + $options = []; if ($host && $port) { $options['server'] = [$host, $port]; @@ -35,6 +37,8 @@ private function createRedisStorage(int $serializerOption, bool $serializerPlugi $options['password'] = $password; } + $options['lib_options'] = [RedisFromExtension::OPT_SERIALIZER => $serializerOption]; + $storage = new Redis(new RedisOptions($options)); if ($serializerOption === RedisFromExtension::SERIALIZER_NONE && $serializerPlugin) { $storage->addPlugin(new Serializer(new AdapterPluginManager(new ServiceManager()))); diff --git a/test/integration/Laminas/RedisTest.php b/test/integration/Laminas/RedisTest.php index 459536c..657847b 100644 --- a/test/integration/Laminas/RedisTest.php +++ b/test/integration/Laminas/RedisTest.php @@ -6,12 +6,10 @@ use Laminas\Cache\Storage\Adapter\Redis; use Laminas\Cache\Storage\Adapter\RedisOptions; -use Laminas\Cache\Storage\Adapter\RedisResourceManager; use Laminas\Cache\Storage\Plugin\Serializer; use Laminas\Serializer\AdapterPluginManager; use Laminas\ServiceManager\ServiceManager; use LaminasTest\Cache\Storage\Adapter\AbstractCommonAdapterTest; -use PHPUnit\Framework\MockObject\MockObject; use Redis as RedisResource; use function count; @@ -19,26 +17,29 @@ /** * @covers Redis - * @template-extends AbstractCommonAdapterTest + * @template-extends AbstractCommonAdapterTest */ final class RedisTest extends AbstractCommonAdapterTest { public function setUp(): void { - $options = ['resource_id' => self::class]; + $options = []; if (getenv('TESTS_LAMINAS_CACHE_REDIS_HOST') && getenv('TESTS_LAMINAS_CACHE_REDIS_PORT')) { - $options['server'] = [getenv('TESTS_LAMINAS_CACHE_REDIS_HOST'), getenv('TESTS_LAMINAS_CACHE_REDIS_PORT')]; + $options['server'] = [ + (string) getenv('TESTS_LAMINAS_CACHE_REDIS_HOST'), + (int) getenv('TESTS_LAMINAS_CACHE_REDIS_PORT'), + ]; } elseif (getenv('TESTS_LAMINAS_CACHE_REDIS_HOST')) { - $options['server'] = [getenv('TESTS_LAMINAS_CACHE_REDIS_HOST')]; + $options['server'] = [(string) getenv('TESTS_LAMINAS_CACHE_REDIS_HOST')]; } if (getenv('TESTS_LAMINAS_CACHE_REDIS_DATABASE')) { - $options['database'] = getenv('TESTS_LAMINAS_CACHE_REDIS_DATABASE'); + $options['database'] = (int) getenv('TESTS_LAMINAS_CACHE_REDIS_DATABASE'); } if (getenv('TESTS_LAMINAS_CACHE_REDIS_PASSWORD')) { - $options['password'] = getenv('TESTS_LAMINAS_CACHE_REDIS_PASSWORD'); + $options['password'] = (string) getenv('TESTS_LAMINAS_CACHE_REDIS_PASSWORD'); } $this->options = new RedisOptions($options); @@ -50,20 +51,22 @@ public function setUp(): void public function testLibOptionsFirst(): void { $options = [ - 'resource_id' => self::class . '2', - 'liboptions' => [ + 'liboptions' => [ RedisResource::OPT_SERIALIZER => RedisResource::SERIALIZER_PHP, ], ]; if (getenv('TESTS_LAMINAS_CACHE_REDIS_HOST') && getenv('TESTS_LAMINAS_CACHE_REDIS_PORT')) { - $options['server'] = [getenv('TESTS_LAMINAS_CACHE_REDIS_HOST'), getenv('TESTS_LAMINAS_CACHE_REDIS_PORT')]; + $options['server'] = [ + getenv('TESTS_LAMINAS_CACHE_REDIS_HOST'), + (int) getenv('TESTS_LAMINAS_CACHE_REDIS_PORT'), + ]; } elseif (getenv('TESTS_LAMINAS_CACHE_REDIS_HOST')) { $options['server'] = [getenv('TESTS_LAMINAS_CACHE_REDIS_HOST')]; } if (getenv('TESTS_LAMINAS_CACHE_REDIS_DATABASE')) { - $options['database'] = getenv('TESTS_LAMINAS_CACHE_REDIS_DATABASE'); + $options['database'] = (int) getenv('TESTS_LAMINAS_CACHE_REDIS_DATABASE'); } if (getenv('TESTS_LAMINAS_CACHE_REDIS_PASSWORD')) { @@ -115,42 +118,6 @@ public function testRedisSetBoolean(): void self::assertEquals('', $this->storage->getItem($key), 'Boolean should be cast to string'); } - public function testGetCapabilitiesTtl(): void - { - $resourceManager = $this->options->getResourceManager(); - $resourceId = $this->options->getResourceId(); - $redis = $resourceManager->getResource($resourceId); - $majorVersion = (int) $redis->info()['redis_version']; - - self::assertEquals($majorVersion, $resourceManager->getMajorVersion($resourceId)); - - $capabilities = $this->storage->getCapabilities(); - self::assertSame( - $majorVersion > 2, - $capabilities->ttlSupported, - 'Only Redis version > 2.0.0 supports key expiration', - ); - } - - public function testGetSetDatabase(): void - { - self::assertTrue($this->storage->setItem('key', 'val')); - self::assertEquals('val', $this->storage->getItem('key')); - - $databaseNumber = 1; - $resourceManager = $this->options->getResourceManager(); - $resourceManager->setDatabase($this->options->getResourceId(), $databaseNumber); - self::assertNull( - $this->storage->getItem('key'), - 'No value should be found because set was done on different database than get' - ); - self::assertEquals( - $databaseNumber, - $resourceManager->getDatabase($this->options->getResourceId()), - 'Incorrect database was returned' - ); - } - public function testGetSetLibOptionsOnExistingRedisResourceInstance(): void { $options = ['serializer' => RedisResource::SERIALIZER_PHP]; @@ -223,48 +190,22 @@ public function testTouchItem(): void self::assertEquals($ttl, $metadata->remainingTimeToLive); } - public function testHasItemReturnsFalseIfRedisExistsReturnsZero(): void - { - $redis = $this->mockInitializedRedisResource(); - $redis->method('exists')->willReturn(0); - $adapter = $this->createAdapterFromResource($redis); - - $hasItem = $adapter->hasItem('does-not-exist'); - - self::assertFalse($hasItem); - } - - public function testHasItemReturnsTrueIfRedisExistsReturnsNonZeroInt(): void + public function testGetVersionFromRedisServer(): void { - $redis = $this->mockInitializedRedisResource(); - $redis->method('exists')->willReturn(23); - $adapter = $this->createAdapterFromResource($redis); + $host = getenv('TESTS_LAMINAS_CACHE_REDIS_HOST') ?: 'localhost'; + $port = (int) (getenv('TESTS_LAMINAS_CACHE_REDIS_PORT') ?: 6379); + $options = new RedisOptions(['server' => ['host' => $host, 'port' => $port]]); + $resourceManager = new Redis($options); - $hasItem = $adapter->hasItem('does-not-exist'); - - self::assertTrue($hasItem); - } - - /** - * @return Redis - */ - private function createAdapterFromResource(RedisResource $redis) - { - $resourceManager = new RedisResourceManager(); - $resourceId = 'my-resource'; - $resourceManager->setResource($resourceId, $redis); - $options = new RedisOptions(['resource_manager' => $resourceManager, 'resource_id' => $resourceId]); - return new Redis($options); + self::assertMatchesRegularExpression( + '#^\d+\.\d+\.\d+#', + $resourceManager->getRedisVersion(), + 'Version from redis is expected to match semver.', + ); } - /** - * @return MockObject&RedisResource - */ - private function mockInitializedRedisResource() + public function testOptionsFluentInterface(): void { - $redis = $this->createMock(RedisFromExtensionAsset::class); - $redis->socket = true; - $redis->method('info')->willReturn(['redis_version' => '0.0.0-unknown']); - return $redis; + self::markTestSkipped('This test does actually use '); } } diff --git a/test/integration/Psr/CacheItemPool/RedisClusterWithPhpIgbinaryTest.php b/test/integration/Psr/CacheItemPool/RedisClusterWithPhpIgbinaryTest.php index 8fb972b..6cf59bb 100644 --- a/test/integration/Psr/CacheItemPool/RedisClusterWithPhpIgbinaryTest.php +++ b/test/integration/Psr/CacheItemPool/RedisClusterWithPhpIgbinaryTest.php @@ -4,28 +4,23 @@ namespace LaminasTest\Cache\Psr\CacheItemPool; +use Laminas\Cache\Storage\Adapter\RedisClusterOptions; +use Laminas\Cache\Storage\FlushableInterface; use Laminas\Cache\Storage\StorageInterface; use LaminasTest\Cache\Storage\Adapter\AbstractCacheItemPoolIntegrationTest; use LaminasTest\Cache\Storage\Adapter\Laminas\RedisClusterStorageCreationTrait; use Redis; -use RedisCluster; - -use function sprintf; +/** + * @uses FlushableInterface + * + * @template-extends AbstractCacheItemPoolIntegrationTest + */ final class RedisClusterWithPhpIgbinaryTest extends AbstractCacheItemPoolIntegrationTest { use RedisClusterStorageCreationTrait; - protected function setUp(): void - { - parent::setUp(); - $this->skippedTests['testHasItemReturnsFalseWhenDeferredItemIsExpired'] = sprintf( - '%s storage doesn\'t support driver deferred', - RedisCluster::class - ); - } - - protected function createStorage(): StorageInterface + protected function createStorage(): StorageInterface&FlushableInterface { return $this->createRedisClusterStorage(Redis::SERIALIZER_IGBINARY, false); } diff --git a/test/integration/Psr/CacheItemPool/RedisClusterWithPhpSerializeTest.php b/test/integration/Psr/CacheItemPool/RedisClusterWithPhpSerializeTest.php index 2fe0bd3..5a3d39f 100644 --- a/test/integration/Psr/CacheItemPool/RedisClusterWithPhpSerializeTest.php +++ b/test/integration/Psr/CacheItemPool/RedisClusterWithPhpSerializeTest.php @@ -4,28 +4,23 @@ namespace LaminasTest\Cache\Psr\CacheItemPool; +use Laminas\Cache\Storage\Adapter\RedisClusterOptions; +use Laminas\Cache\Storage\FlushableInterface; use Laminas\Cache\Storage\StorageInterface; use LaminasTest\Cache\Storage\Adapter\AbstractCacheItemPoolIntegrationTest; use LaminasTest\Cache\Storage\Adapter\Laminas\RedisClusterStorageCreationTrait; use Redis; -use RedisCluster; - -use function sprintf; +/** + * @uses FlushableInterface + * + * @template-extends AbstractCacheItemPoolIntegrationTest + */ final class RedisClusterWithPhpSerializeTest extends AbstractCacheItemPoolIntegrationTest { use RedisClusterStorageCreationTrait; - protected function setUp(): void - { - parent::setUp(); - $this->skippedTests['testHasItemReturnsFalseWhenDeferredItemIsExpired'] = sprintf( - '%s storage doesn\'t support driver deferred', - RedisCluster::class - ); - } - - protected function createStorage(): StorageInterface + protected function createStorage(): StorageInterface&FlushableInterface { return $this->createRedisClusterStorage(Redis::SERIALIZER_PHP, false); } diff --git a/test/integration/Psr/CacheItemPool/RedisClusterWithoutSerializerTest.php b/test/integration/Psr/CacheItemPool/RedisClusterWithoutSerializerTest.php index 40daccf..ca7a43c 100644 --- a/test/integration/Psr/CacheItemPool/RedisClusterWithoutSerializerTest.php +++ b/test/integration/Psr/CacheItemPool/RedisClusterWithoutSerializerTest.php @@ -4,28 +4,23 @@ namespace LaminasTest\Cache\Psr\CacheItemPool; +use Laminas\Cache\Storage\Adapter\RedisClusterOptions; +use Laminas\Cache\Storage\FlushableInterface; use Laminas\Cache\Storage\StorageInterface; use LaminasTest\Cache\Storage\Adapter\AbstractCacheItemPoolIntegrationTest; use LaminasTest\Cache\Storage\Adapter\Laminas\RedisClusterStorageCreationTrait; use Redis; -use RedisCluster; - -use function sprintf; +/** + * @uses FlushableInterface + * + * @template-extends AbstractCacheItemPoolIntegrationTest + */ final class RedisClusterWithoutSerializerTest extends AbstractCacheItemPoolIntegrationTest { use RedisClusterStorageCreationTrait; - protected function setUp(): void - { - parent::setUp(); - $this->skippedTests['testHasItemReturnsFalseWhenDeferredItemIsExpired'] = sprintf( - '%s storage doesn\'t support driver deferred', - RedisCluster::class - ); - } - - protected function createStorage(): StorageInterface + protected function createStorage(): StorageInterface&FlushableInterface { return $this->createRedisClusterStorage(Redis::SERIALIZER_NONE, true); } diff --git a/test/integration/Psr/CacheItemPool/RedisIntegrationTest.php b/test/integration/Psr/CacheItemPool/RedisIntegrationTest.php deleted file mode 100644 index 67c2e2f..0000000 --- a/test/integration/Psr/CacheItemPool/RedisIntegrationTest.php +++ /dev/null @@ -1,31 +0,0 @@ -skippedTests['testHasItemReturnsFalseWhenDeferredItemIsExpired'] - = 'Cache decorator does not support deferred deletion'; - - parent::setUp(); - } - - protected function createStorage(): StorageInterface - { - return $this->createRedisStorage( - Redis::SERIALIZER_NONE, - true - ); - } -} diff --git a/test/integration/Psr/CacheItemPool/RedisWithPhpIgbinaryTest.php b/test/integration/Psr/CacheItemPool/RedisWithPhpIgbinaryTest.php new file mode 100644 index 0000000..8d41291 --- /dev/null +++ b/test/integration/Psr/CacheItemPool/RedisWithPhpIgbinaryTest.php @@ -0,0 +1,27 @@ + + */ +final class RedisWithPhpIgbinaryTest extends AbstractCacheItemPoolIntegrationTest +{ + use RedisStorageCreationTrait; + + protected function createStorage(): StorageInterface&FlushableInterface + { + return $this->createRedisStorage(Redis::SERIALIZER_IGBINARY, false); + } +} diff --git a/test/integration/Psr/CacheItemPool/RedisWithPhpSerializeTest.php b/test/integration/Psr/CacheItemPool/RedisWithPhpSerializeTest.php new file mode 100644 index 0000000..6d12200 --- /dev/null +++ b/test/integration/Psr/CacheItemPool/RedisWithPhpSerializeTest.php @@ -0,0 +1,27 @@ + + */ +final class RedisWithPhpSerializeTest extends AbstractCacheItemPoolIntegrationTest +{ + use RedisStorageCreationTrait; + + protected function createStorage(): StorageInterface&FlushableInterface + { + return $this->createRedisStorage(Redis::SERIALIZER_PHP, false); + } +} diff --git a/test/integration/Psr/CacheItemPool/RedisWithoutSerializerTest.php b/test/integration/Psr/CacheItemPool/RedisWithoutSerializerTest.php new file mode 100644 index 0000000..91771c4 --- /dev/null +++ b/test/integration/Psr/CacheItemPool/RedisWithoutSerializerTest.php @@ -0,0 +1,27 @@ + + */ +final class RedisWithoutSerializerTest extends AbstractCacheItemPoolIntegrationTest +{ + use RedisStorageCreationTrait; + + protected function createStorage(): StorageInterface&FlushableInterface + { + return $this->createRedisStorage(Redis::SERIALIZER_NONE, true); + } +} diff --git a/test/integration/Psr/SimpleCache/RedisClusterWithPhpSerializerTest.php b/test/integration/Psr/SimpleCache/RedisClusterWithPhpSerializeTest.php similarity index 84% rename from test/integration/Psr/SimpleCache/RedisClusterWithPhpSerializerTest.php rename to test/integration/Psr/SimpleCache/RedisClusterWithPhpSerializeTest.php index 8f78ea0..3f59e3e 100644 --- a/test/integration/Psr/SimpleCache/RedisClusterWithPhpSerializerTest.php +++ b/test/integration/Psr/SimpleCache/RedisClusterWithPhpSerializeTest.php @@ -9,7 +9,7 @@ use LaminasTest\Cache\Storage\Adapter\Laminas\RedisClusterStorageCreationTrait; use Redis; -final class RedisClusterWithPhpSerializerTest extends AbstractSimpleCacheIntegrationTest +final class RedisClusterWithPhpSerializeTest extends AbstractSimpleCacheIntegrationTest { use RedisClusterStorageCreationTrait; diff --git a/test/integration/Psr/SimpleCache/RedisIntegrationTest.php b/test/integration/Psr/SimpleCache/RedisWithPhpIgbinaryTest.php similarity index 68% rename from test/integration/Psr/SimpleCache/RedisIntegrationTest.php rename to test/integration/Psr/SimpleCache/RedisWithPhpIgbinaryTest.php index 17bf22a..ae25075 100644 --- a/test/integration/Psr/SimpleCache/RedisIntegrationTest.php +++ b/test/integration/Psr/SimpleCache/RedisWithPhpIgbinaryTest.php @@ -9,15 +9,12 @@ use LaminasTest\Cache\Storage\Adapter\Laminas\RedisStorageCreationTrait; use Redis; -final class RedisIntegrationTest extends AbstractSimpleCacheIntegrationTest +final class RedisWithPhpIgbinaryTest extends AbstractSimpleCacheIntegrationTest { use RedisStorageCreationTrait; protected function createStorage(): StorageInterface { - return $this->createRedisStorage( - Redis::SERIALIZER_NONE, - true - ); + return $this->createRedisStorage(Redis::SERIALIZER_IGBINARY, true); } } diff --git a/test/integration/Psr/SimpleCache/RedisWithPhpSerializeTest.php b/test/integration/Psr/SimpleCache/RedisWithPhpSerializeTest.php new file mode 100644 index 0000000..0838f08 --- /dev/null +++ b/test/integration/Psr/SimpleCache/RedisWithPhpSerializeTest.php @@ -0,0 +1,20 @@ +createRedisStorage(Redis::SERIALIZER_PHP, true); + } +} diff --git a/test/integration/Psr/SimpleCache/RedisWithoutSerializerTest.php b/test/integration/Psr/SimpleCache/RedisWithoutSerializerTest.php new file mode 100644 index 0000000..c6fa278 --- /dev/null +++ b/test/integration/Psr/SimpleCache/RedisWithoutSerializerTest.php @@ -0,0 +1,20 @@ +createRedisStorage(Redis::SERIALIZER_NONE, true); + } +} diff --git a/test/unit/Exception/InvalidConfigurationExceptionTest.php b/test/unit/Exception/InvalidConfigurationExceptionTest.php index 8b9c026..3afeeb2 100644 --- a/test/unit/Exception/InvalidConfigurationExceptionTest.php +++ b/test/unit/Exception/InvalidConfigurationExceptionTest.php @@ -13,6 +13,6 @@ final class InvalidConfigurationExceptionTest extends TestCase public function testInstanceOfLaminasCacheException(): void { $exception = new InvalidRedisClusterConfigurationException(); - $this->assertInstanceOf(ExceptionInterface::class, $exception); + self::assertInstanceOf(ExceptionInterface::class, $exception); } } diff --git a/test/unit/RedisClusterOptionsFromIniTest.php b/test/unit/RedisClusterOptionsFromIniTest.php index 52ab59a..fbdc1a7 100644 --- a/test/unit/RedisClusterOptionsFromIniTest.php +++ b/test/unit/RedisClusterOptionsFromIniTest.php @@ -30,7 +30,7 @@ public function testWillDetectSeedsByName(string $name, string $config, array $e ini_set('redis.clusters.seeds', $config); $options = new RedisClusterOptionsFromIni(); $seeds = $options->getSeeds($name); - $this->assertEquals($expected, $seeds); + self::assertEquals($expected, $seeds); } public function testWillThrowExceptionOnMissingNameInSeeds(): void @@ -73,10 +73,10 @@ public function testCanParseAllConfigurationsForName(): void ini_set('redis.clusters.auth', 'foo=secret'); $options = new RedisClusterOptionsFromIni(); - $this->assertEquals(['bar'], $options->getSeeds('foo')); - $this->assertEquals(1.0, $options->getTimeout('foo', 0.0)); - $this->assertEquals(2.0, $options->getReadTimeout('foo', 0.0)); - $this->assertEquals('secret', $options->getPasswordByName('foo', '')); + self::assertEquals(['bar'], $options->getSeeds('foo')); + self::assertEquals(1.0, $options->getTimeout('foo', 0.0)); + self::assertEquals(2.0, $options->getReadTimeout('foo', 0.0)); + self::assertEquals('secret', $options->getPasswordByName('foo', '')); } protected function setUp(): void diff --git a/test/unit/RedisClusterOptionsTest.php b/test/unit/RedisClusterOptionsTest.php index 58d9bc2..e298bb6 100644 --- a/test/unit/RedisClusterOptionsTest.php +++ b/test/unit/RedisClusterOptionsTest.php @@ -67,12 +67,12 @@ public function testCanHandleOptionsWithNodename(): void 'password' => 'secret', ]); - $this->assertEquals('foo', $options->getName()); - $this->assertEquals(1.0, $options->getTimeout()); - $this->assertEquals(2.0, $options->getReadTimeout()); - $this->assertEquals(false, $options->isPersistent()); - $this->assertEquals('1.0', $options->getRedisVersion()); - $this->assertEquals('secret', $options->getPassword()); + self::assertEquals('foo', $options->getName()); + self::assertEquals(1.0, $options->getTimeout()); + self::assertEquals(2.0, $options->getReadTimeout()); + self::assertEquals(false, $options->isPersistent()); + self::assertEquals('1.0', $options->getRedisVersion()); + self::assertEquals('secret', $options->getPassword()); } public function testCanHandleOptionsWithSeeds(): void @@ -86,12 +86,12 @@ public function testCanHandleOptionsWithSeeds(): void 'password' => 'secret', ]); - $this->assertEquals(['localhost:1234'], $options->getSeeds()); - $this->assertEquals(1.0, $options->getTimeout()); - $this->assertEquals(2.0, $options->getReadTimeout()); - $this->assertEquals(false, $options->isPersistent()); - $this->assertEquals('1.0', $options->getRedisVersion()); - $this->assertEquals('secret', $options->getPassword()); + self::assertEquals(['localhost:1234'], $options->getSeeds()); + self::assertEquals(1.0, $options->getTimeout()); + self::assertEquals(2.0, $options->getReadTimeout()); + self::assertEquals(false, $options->isPersistent()); + self::assertEquals('1.0', $options->getRedisVersion()); + self::assertEquals('secret', $options->getPassword()); } public function testWillDetectSeedsAndNodenameConfiguration(): void diff --git a/test/unit/RedisClusterResourceManagerTest.php b/test/unit/RedisClusterResourceManagerTest.php index dba3326..812d2f8 100644 --- a/test/unit/RedisClusterResourceManagerTest.php +++ b/test/unit/RedisClusterResourceManagerTest.php @@ -25,11 +25,16 @@ public function testCanDetectSerializationSupportFromOptions(RedisClusterOptions { $manager = new RedisClusterResourceManager($options); $adapter = $this->createMock(AbstractAdapter::class); + $adapter + ->expects(self::once()) + ->method('getOptions') + ->willReturn($options); + $adapter ->expects($this->never()) ->method('getPluginRegistry'); - $this->assertTrue($manager->hasSerializationSupport($adapter)); + self::assertTrue($manager->hasSerializationSupport($adapter)); } public function testCanDetectSerializationSupportFromSerializerPlugin(): void @@ -49,23 +54,17 @@ public function testCanDetectSerializationSupportFromSerializerPlugin(): void 'name' => uniqid('', true), ])); $adapter = $this->createMock(AbstractAdapter::class); + $adapter + ->expects(self::once()) + ->method('getOptions') + ->willReturn(new RedisClusterOptions(['name' => 'foo'])); + $adapter ->expects($this->once()) ->method('getPluginRegistry') ->willReturn($registry); - $this->assertTrue($manager->hasSerializationSupport($adapter)); - } - - public function testWillReturnVersionFromOptions(): void - { - $manager = new RedisClusterResourceManager(new RedisClusterOptions([ - 'name' => uniqid('', true), - 'redis_version' => '1.0.0', - ])); - - $version = $manager->getVersion(); - $this->assertEquals('1.0.0', $version); + self::assertTrue($manager->hasSerializationSupport($adapter)); } /** diff --git a/test/unit/RedisClusterTest.php b/test/unit/RedisClusterTest.php index 487ddb7..bbc7eb8 100644 --- a/test/unit/RedisClusterTest.php +++ b/test/unit/RedisClusterTest.php @@ -5,19 +5,23 @@ namespace LaminasTest\Cache\Storage\Adapter; use Laminas\Cache\Storage\Adapter\RedisCluster; +use Laminas\Cache\Storage\Adapter\RedisClusterOptions; use Laminas\Cache\Storage\Adapter\RedisClusterResourceManagerInterface; use PHPUnit\Framework\TestCase; +use function uniqid; + final class RedisClusterTest extends TestCase { public function testCanDetectCapabilitiesWithSerializationSupport(): void { $resourceManager = $this->createMock(RedisClusterResourceManagerInterface::class); - $adapter = new RedisCluster([ - 'name' => 'bar', - ]); - /** @psalm-suppress InternalMethod */ + $adapter = new RedisCluster(new RedisClusterOptions([ + 'name' => 'bar', + 'redis_version' => '5.0.0', + ])); + $adapter->setResourceManager($resourceManager); $resourceManager @@ -26,14 +30,9 @@ public function testCanDetectCapabilitiesWithSerializationSupport(): void ->with($adapter) ->willReturn(true); - $resourceManager - ->expects($this->once()) - ->method('getVersion') - ->willReturn('5.0.0'); - $capabilities = $adapter->getCapabilities(); $datatypes = $capabilities->supportedDataTypes; - $this->assertEquals([ + self::assertEquals([ 'NULL' => true, 'boolean' => true, 'integer' => true, @@ -49,10 +48,11 @@ public function testCanDetectCapabilitiesWithoutSerializationSupport(): void { $resourceManager = $this->createMock(RedisClusterResourceManagerInterface::class); - $adapter = new RedisCluster([ - 'name' => 'bar', - ]); - /** @psalm-suppress InternalMethod */ + $adapter = new RedisCluster(new RedisClusterOptions([ + 'name' => 'bar', + 'redis_version' => '5.0.0', + ])); + $adapter->setResourceManager($resourceManager); $resourceManager @@ -61,14 +61,9 @@ public function testCanDetectCapabilitiesWithoutSerializationSupport(): void ->with($adapter) ->willReturn(false); - $resourceManager - ->expects($this->once()) - ->method('getVersion') - ->willReturn('5.0.0'); - $capabilities = $adapter->getCapabilities(); $datatypes = $capabilities->supportedDataTypes; - $this->assertEquals([ + self::assertEquals([ 'NULL' => 'string', 'boolean' => 'string', 'integer' => 'string', @@ -79,4 +74,15 @@ public function testCanDetectCapabilitiesWithoutSerializationSupport(): void 'resource' => false, ], $datatypes); } + + public function testWillReturnVersionFromOptions(): void + { + $manager = new RedisCluster(new RedisClusterOptions([ + 'name' => uniqid('', true), + 'redis_version' => '1.0.0', + ])); + + $version = $manager->getRedisVersion(); + self::assertEquals('1.0.0', $version); + } } diff --git a/test/unit/RedisOptionsTest.php b/test/unit/RedisOptionsTest.php index 6a43868..f88de84 100644 --- a/test/unit/RedisOptionsTest.php +++ b/test/unit/RedisOptionsTest.php @@ -6,7 +6,6 @@ use Laminas\Cache\Storage\Adapter\AdapterOptions; use Laminas\Cache\Storage\Adapter\RedisOptions; -use Laminas\Cache\Storage\Adapter\RedisResourceManager; use Redis as RedisResource; /** @@ -14,6 +13,11 @@ */ final class RedisOptionsTest extends AbstractAdapterOptionsTest { + public function testOptionsFluentInterface(): void + { + self::markTestSkipped('Redis cluster specific options do not provide fluent interface!'); + } + protected function createAdapterOptions(): AdapterOptions { return new RedisOptions(['server' => ['host' => 'localhost']]); @@ -23,53 +27,37 @@ public function testGetSetNamespace(): void { $namespace = 'testNamespace'; $this->options->setNamespace($namespace); - $this->assertEquals($namespace, $this->options->getNamespace(), 'Namespace was not set correctly'); + self::assertEquals($namespace, $this->options->getNamespace()); } public function testGetSetNamespaceSeparator(): void { $separator = '/'; $this->options->setNamespaceSeparator($separator); - $this->assertEquals($separator, $this->options->getNamespaceSeparator(), 'Separator was not set correctly'); - } - - public function testGetSetResourceManager(): void - { - $resourceManager = new RedisResourceManager(); - $options = new RedisOptions(); - $options->setResourceManager($resourceManager); - $this->assertInstanceOf( - RedisResourceManager::class, - $options->getResourceManager(), - 'Wrong resource manager retuned, it should of type RedisResourceManager' - ); - - $this->assertEquals($resourceManager, $options->getResourceManager()); - } - - public function testGetSetResourceId(): void - { - $resourceId = '1'; - $options = new RedisOptions(); - $options->setResourceId($resourceId); - $this->assertEquals($resourceId, $options->getResourceId(), 'Resource id was not set correctly'); + self::assertEquals($separator, $this->options->getNamespaceSeparator()); } public function testGetSetPersistentId(): void { $persistentId = '1'; $this->options->setPersistentId($persistentId); - $this->assertEquals($persistentId, $this->options->getPersistentId(), 'Persistent id was not set correctly'); + self::assertEquals($persistentId, $this->options->getPersistentId()); + } + + public function testSetPersistentIdImplicitlyEnablesPersistence(): void + { + self::assertFalse($this->options->isPersistent()); + $this->options->setPersistentId('foo'); + self::assertTrue($this->options->isPersistent()); } public function testOptionsGetSetLibOptions(): void { $options = ['serializer' => RedisResource::SERIALIZER_PHP]; $this->options->setLibOptions($options); - $this->assertEquals( - $options, + self::assertEquals( + [RedisResource::OPT_SERIALIZER => RedisResource::SERIALIZER_PHP], $this->options->getLibOptions(), - 'Lib Options were not set correctly through RedisOptions' ); } @@ -81,24 +69,23 @@ public function testGetSetServer(): void 'timeout' => 0, ]; $this->options->setServer($server); - $this->assertEquals($server, $this->options->getServer(), 'Server was not set correctly through RedisOptions'); + self::assertEquals($server, $this->options->getServer()); } public function testOptionsGetSetDatabase(): void { $database = 1; $this->options->setDatabase($database); - $this->assertEquals($database, $this->options->getDatabase(), 'Database not set correctly using RedisOptions'); + self::assertEquals($database, $this->options->getDatabase()); } public function testOptionsGetSetPassword(): void { $password = 'my-secret'; $this->options->setPassword($password); - $this->assertEquals( + self::assertEquals( $password, $this->options->getPassword(), - 'Password was set incorrectly using RedisOptions' ); } @@ -106,10 +93,46 @@ public function testOptionsGetSetUser(): void { $user = 'dummyuser'; $this->options->setUser($user); - $this->assertEquals( + self::assertEquals( $user, $this->options->getUser(), - 'User was set incorrectly using RedisOptions' ); } + + public function testParsesSocketFromServer(): void + { + $socket = '/tmp/redis.sock'; + $this->options->setServer($socket); + self::assertEquals($socket, $this->options->getServer()); + } + + public function testParsesUriFromServer(): void + { + $this->options->setServer('redis://foo:bar@example.org:1234'); + self::assertEquals([ + 'host' => 'example.org', + 'port' => 1234, + 'timeout' => 0, + ], $this->options->getServer()); + self::assertEquals('foo', $this->options->getUser()); + self::assertEquals('bar', $this->options->getPassword()); + } + + /** + * @group 6495 + */ + public function testPasswordFromOptionsOverridesPasswordFromUri(): void + { + $options = new RedisOptions([ + 'server' => 'redis://dummyuser:dummypass@testhost:1234', + 'password' => 'abcd1234', + ]); + + $server = $options->getServer(); + self::assertIsArray($server); + self::assertEquals('testhost', $server['host']); + self::assertEquals(1234, $server['port'] ?? null); + self::assertEquals('abcd1234', $options->getPassword()); + self::assertEquals('dummyuser', $options->getUser()); + } } diff --git a/test/unit/RedisResourceManagerTest.php b/test/unit/RedisResourceManagerTest.php index 6551a2c..8f6c23e 100644 --- a/test/unit/RedisResourceManagerTest.php +++ b/test/unit/RedisResourceManagerTest.php @@ -4,405 +4,87 @@ namespace LaminasTest\Cache\Storage\Adapter; -use Laminas\Cache\Storage\Adapter\Exception\RedisRuntimeException; +use Laminas\Cache\Storage\Adapter\AbstractAdapter; +use Laminas\Cache\Storage\Adapter\RedisOptions; use Laminas\Cache\Storage\Adapter\RedisResourceManager; +use Laminas\Cache\Storage\Plugin\Serializer; +use Laminas\Serializer\AdapterPluginManager; +use Laminas\ServiceManager\ServiceManager; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Redis; -use RedisException; - -use function bin2hex; -use function getenv; -use function random_bytes; - -/** - * PHPUnit test case - */ +use SplObjectStorage; /** - * @group Laminas_Cache * @covers Laminas\Cache\Storage\Adapter\RedisResourceManager */ -class RedisResourceManagerTest extends TestCase +final class RedisResourceManagerTest extends TestCase { - protected RedisResourceManager $resourceManager; - private string $resourceId; - - public function setUp(): void - { - $this->resourceManager = new RedisResourceManager(); - $this->resourceId = bin2hex(random_bytes(5)); - $this->resourceManager->setResource($this->resourceId, [ - 'server' => [ - 'host' => 'localhost', - ], - ]); - parent::setUp(); - } - - /** - * @group 6495 - */ - public function testSetServerWithPasswordInUri(): void - { - $dummyResId = '1234567890'; - $server = 'redis://dummyuser:dummypass@testhost:1234'; - - $this->resourceManager->setServer($dummyResId, $server); - - $server = $this->resourceManager->getServer($dummyResId); - - $this->assertEquals('testhost', $server['host']); - $this->assertEquals(1234, $server['port']); - $this->assertEquals('dummypass', $this->resourceManager->getPassword($dummyResId)); - $this->assertEquals('dummyuser', $this->resourceManager->getUser($dummyResId)); - } - - /** - * @group 6495 - */ - public function testSetServerWithPasswordInParameters(): void + #[DataProvider('serializationSupportOptionsProvider')] + public function testCanDetectSerializationSupportFromOptions(RedisOptions $options): void { - $server = 'redis://dummyuser:dummypass@testhost:1234'; - $dummyResId2 = '12345678901'; - $resource = [ - 'persistent_id' => 'my_connection_name', - 'server' => $server, - 'password' => 'abcd1234', - ]; - - $this->resourceManager->setResource($dummyResId2, $resource); + $manager = new RedisResourceManager($options); + $adapter = $this->createMock(AbstractAdapter::class); + $adapter + ->expects(self::once()) + ->method('getOptions') + ->willReturn($options); - $server = $this->resourceManager->getServer($dummyResId2); + $adapter + ->expects($this->never()) + ->method('getPluginRegistry'); - $this->assertEquals('testhost', $server['host']); - $this->assertEquals(1234, $server['port']); - $this->assertEquals('abcd1234', $this->resourceManager->getPassword($dummyResId2)); - $this->assertEquals('dummyuser', $this->resourceManager->getUser($dummyResId2)); + self::assertTrue($manager->hasSerializationSupport($adapter)); } - public function testSetServerWithPasswordInParametersAndNoUser(): void + public function testCanDetectSerializationSupportFromSerializerPlugin(): void { - $server = 'redis://testhost:1234'; - $dummyResId2 = '12345678901'; - $resource = [ - 'persistent_id' => 'my_connection_name', - 'server' => $server, - 'password' => 'abcd1234', - ]; - - $this->resourceManager->setResource($dummyResId2, $resource); + $registry = $this->createMock(SplObjectStorage::class); + $registry + ->expects($this->any()) + ->method('current') + ->willReturn(new Serializer(new AdapterPluginManager(new ServiceManager()))); - $server = $this->resourceManager->getServer($dummyResId2); - - $this->assertEquals('testhost', $server['host']); - $this->assertEquals(1234, $server['port']); - $this->assertEquals('abcd1234', $this->resourceManager->getPassword($dummyResId2)); - $this->assertEquals('', $this->resourceManager->getUser($dummyResId2)); - } - - public function testSetServerWithPasswordInParametersAndUser(): void - { - $server = 'redis://testhost:1234'; - $dummyResId2 = '12345678901'; - $resource = [ - 'persistent_id' => 'my_connection_name', - 'server' => $server, - 'password' => 'abcd1234', - 'user' => "dummyuser", - ]; - - $this->resourceManager->setResource($dummyResId2, $resource); - - $server = $this->resourceManager->getServer($dummyResId2); - - $this->assertEquals('testhost', $server['host']); - $this->assertEquals(1234, $server['port']); - $this->assertEquals('abcd1234', $this->resourceManager->getPassword($dummyResId2)); - $this->assertEquals('dummyuser', $this->resourceManager->getUser($dummyResId2)); - } - - /** - * @group 6495 - */ - public function testSetServerWithPasswordInUriShouldNotOverridePreviousResource(): void - { - $server = 'redis://dummyuser:dummypass@testhost:1234'; - $server2 = 'redis://dummyuser:dummypass@testhost2:1234'; - $dummyResId2 = '12345678901'; - $resource = [ - 'persistent_id' => 'my_connection_name', - 'server' => $server, - 'password' => 'abcd1234', - ]; + $registry + ->expects($this->once()) + ->method('valid') + ->willReturn(true); - $this->resourceManager->setResource($dummyResId2, $resource); - $this->resourceManager->setServer($dummyResId2, $server2); + $manager = new RedisResourceManager(new RedisOptions([])); + $adapter = $this->createMock(AbstractAdapter::class); + $adapter + ->expects(self::once()) + ->method('getOptions') + ->willReturn(new RedisOptions()); - $server = $this->resourceManager->getServer($dummyResId2); + $adapter + ->expects($this->once()) + ->method('getPluginRegistry') + ->willReturn($registry); - $this->assertEquals('testhost2', $server['host']); - $this->assertEquals(1234, $server['port']); - // Password should not be overridden - $this->assertEquals('abcd1234', $this->resourceManager->getPassword($dummyResId2)); + self::assertTrue($manager->hasSerializationSupport($adapter)); } /** - * Test with 'persistent_id' + * @psalm-return array */ - public function testValidPersistentId(): void - { - $resourceId = 'testValidPersistentId'; - $resource = [ - 'persistent_id' => 'my_connection_name', - 'server' => [ - 'host' => getenv('TESTS_LAMINAS_CACHE_REDIS_HOST') ?: 'localhost', - 'port' => getenv('TESTS_LAMINAS_CACHE_REDIS_PORT') ?: 6379, + public static function serializationSupportOptionsProvider(): array + { + return [ + 'php-serialize' => [ + new RedisOptions([ + 'lib_options' => [ + Redis::OPT_SERIALIZER => Redis::SERIALIZER_PHP, + ], + ]), ], - ]; - $expectedPersistentId = 'my_connection_name'; - $this->resourceManager->setResource($resourceId, $resource); - $this->assertSame($expectedPersistentId, $this->resourceManager->getPersistentId($resourceId)); - $this->assertInstanceOf('Redis', $this->resourceManager->getResource($resourceId)); - } - - /** - * Test with 'persistend_id' instead of 'persistent_id' - */ - public function testNotValidPersistentIdOptionName(): void - { - $resourceId = 'testNotValidPersistentId'; - $resource = [ - 'persistend_id' => 'my_connection_name', - 'server' => [ - 'host' => getenv('TESTS_LAMINAS_CACHE_REDIS_HOST') ?: 'localhost', - 'port' => getenv('TESTS_LAMINAS_CACHE_REDIS_PORT') ?: 6379, + 'igbinary-serialize' => [ + new RedisOptions([ + 'lib_options' => [ + Redis::OPT_SERIALIZER => Redis::SERIALIZER_IGBINARY, + ], + ]), ], ]; - $expectedPersistentId = 'my_connection_name'; - $this->resourceManager->setResource($resourceId, $resource); - - $this->assertNotSame($expectedPersistentId, $this->resourceManager->getPersistentId($resourceId)); - $this->assertEmpty($this->resourceManager->getPersistentId($resourceId)); - $this->assertInstanceOf('Redis', $this->resourceManager->getResource($resourceId)); - } - - public function testGetVersion(): void - { - $resourceId = __FUNCTION__; - $resource = [ - 'server' => [ - 'host' => getenv('TESTS_LAMINAS_CACHE_REDIS_HOST') ?: 'localhost', - 'port' => getenv('TESTS_LAMINAS_CACHE_REDIS_PORT') ?: 6379, - ], - ]; - $this->resourceManager->setResource($resourceId, $resource); - - $this->assertMatchesRegularExpression('/^\d+\.\d+\.\d+/', $this->resourceManager->getVersion($resourceId)); - } - - public function testGetMajorVersion(): void - { - $resourceId = __FUNCTION__; - $resource = [ - 'server' => [ - 'host' => getenv('TESTS_LAMINAS_CACHE_REDIS_HOST') ?: 'localhost', - 'port' => getenv('TESTS_LAMINAS_CACHE_REDIS_PORT') ?: 6379, - ], - ]; - $this->resourceManager->setResource($resourceId, $resource); - - $this->assertGreaterThan(0, $this->resourceManager->getMajorVersion($resourceId)); - } - - public function testWillCatchConnectExceptions(): void - { - $redis = $this->createMock(Redis::class); - $redis - ->expects(self::atLeastOnce()) - ->method('connect') - ->willThrowException(new RedisException('test')); - - $this->resourceManager->setResource('default', ['resource' => $redis, 'server' => 'localhost:6379']); - - $this->expectException(RedisRuntimeException::class); - $this->expectExceptionMessage('test'); - $this->resourceManager->getResource('default'); - } - - public function testWillCatchPConnectExceptions(): void - { - $redis = $this->createMock(Redis::class); - $redis - ->expects(self::atLeastOnce()) - ->method('pconnect') - ->willThrowException(new RedisException('test')); - - $this->expectException(RedisRuntimeException::class); - $this->expectExceptionMessage('test'); - $this->resourceManager->setResource( - 'default', - [ - 'resource' => $redis, - 'server' => 'localhost:6379', - 'persistent_id' => 'test', - ] - ); - $this->resourceManager->getResource('default'); - } - - public function testWillCatchAuthExceptions(): void - { - $redis = $this->createMock(Redis::class); - $redis - ->method('connect') - ->willReturn(true); - - $redis - ->method('info') - ->willReturn(['redis_version' => '1.2.3']); - - $redis - ->expects(self::atLeastOnce()) - ->method('auth') - ->with(['foobar']) - ->willThrowException(new RedisException('test')); - - $this->resourceManager->setResource( - 'default', - [ - 'resource' => $redis, - 'server' => 'whatever:6379', - 'password' => 'foobar', - ] - ); - $this->expectException(RedisRuntimeException::class); - $this->expectExceptionMessage('test'); - $this->resourceManager->getResource('default'); - } - - public function testWillAuthenticateWithUserAndPassword(): void - { - $redis = $this->createMock(Redis::class); - $redis - ->method('connect') - ->willReturn(true); - - $redis - ->method('info') - ->willReturn(['redis_version' => '1.2.3']); - - $redis - ->expects(self::atLeastOnce()) - ->method('auth') - ->with(['default', 'foobar']); - - $this->resourceManager->setResource( - 'default', - [ - 'resource' => $redis, - 'server' => 'whatever:6379', - 'password' => 'foobar', - 'user' => 'default', - ] - ); - - $this->resourceManager->getResource('default'); - } - - public function testWillCatchInfoExceptions(): void - { - $redis = $this->createMock(Redis::class); - $redis - ->method('connect') - ->willReturn(true); - - $redis - ->expects(self::atLeastOnce()) - ->method('info') - ->willThrowException(new RedisException('test')); - - $this->resourceManager->setResource( - 'default', - [ - 'resource' => $redis, - 'initialized' => true, - 'server' => 'somewhere:6379', - ] - ); - - $this->expectException(RedisRuntimeException::class); - $this->expectExceptionMessage('test'); - $this->resourceManager->getResource('default'); - } - - public function testWillCatchAuthDuringConnectException(): void - { - $redis = $this->createMock(Redis::class); - - $redis - ->method('connect') - ->willReturn(true); - - $redis - ->expects(self::atLeastOnce()) - ->method('auth') - ->with(['secret']) - ->willThrowException(new RedisException('test')); - - $this->resourceManager->setResource( - 'default', - [ - 'resource' => $redis, - 'initialized' => false, - 'server' => 'somewhere:6379', - 'password' => 'secret', - ] - ); - - $this->expectException(RedisRuntimeException::class); - $this->expectExceptionMessage('test'); - $this->resourceManager->getResource('default'); - } - - public function testWillCatchSelectDatabaseException(): void - { - $redis = $this->createMock(Redis::class); - - $redis - ->expects(self::atLeastOnce()) - ->method('select') - ->willThrowException(new RedisException('test')); - - $this->resourceManager->setResource( - 'default', - [ - 'resource' => $redis, - 'initialized' => true, - 'server' => 'somewhere:6379', - ] - ); - - $this->expectException(RedisRuntimeException::class); - $this->expectExceptionMessage('test'); - $this->resourceManager->setDatabase('default', 0); - } - - public function testGetSetPassword(): void - { - $pass = 'super secret'; - $this->resourceManager->setPassword($this->resourceId, $pass); - $this->assertEquals( - $pass, - $this->resourceManager->getPassword($this->resourceId), - 'Password was not correctly set' - ); - } - - public function testSocketConnection(): void - { - $socket = '/tmp/redis.sock'; - $this->resourceManager->setServer($this->resourceId, $socket); - $normalized = $this->resourceManager->getServer($this->resourceId); - $this->assertEquals($socket, $normalized['host'], 'Host should equal to socket {$socket}'); } } diff --git a/test/unit/RedisTest.php b/test/unit/RedisTest.php new file mode 100644 index 0000000..dfd6e2b --- /dev/null +++ b/test/unit/RedisTest.php @@ -0,0 +1,83 @@ + '1.0.0', + ])); + + $version = $adapter->getRedisVersion(); + self::assertEquals('1.0.0', $version); + } + + public function testCanDetectCapabilitiesWithSerializationSupport(): void + { + $resourceManager = $this->createMock(RedisResourceManagerInterface::class); + + $adapter = new Redis(new RedisOptions([ + 'redis_version' => '5.0.0', + ])); + + $adapter->setResourceManager($resourceManager); + + $resourceManager + ->expects($this->once()) + ->method('hasSerializationSupport') + ->with($adapter) + ->willReturn(true); + + $capabilities = $adapter->getCapabilities(); + $datatypes = $capabilities->supportedDataTypes; + self::assertEquals([ + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => 'array', + 'object' => 'object', + 'resource' => false, + ], $datatypes); + } + + public function testCanDetectCapabilitiesWithoutSerializationSupport(): void + { + $resourceManager = $this->createMock(RedisResourceManagerInterface::class); + + $adapter = new Redis(new RedisOptions([ + 'redis_version' => '5.0.0', + ])); + + $adapter->setResourceManager($resourceManager); + + $resourceManager + ->expects($this->once()) + ->method('hasSerializationSupport') + ->with($adapter) + ->willReturn(false); + + $capabilities = $adapter->getCapabilities(); + $datatypes = $capabilities->supportedDataTypes; + self::assertEquals([ + 'NULL' => 'string', + 'boolean' => 'string', + 'integer' => 'string', + 'double' => 'string', + 'string' => true, + 'array' => false, + 'object' => false, + 'resource' => false, + ], $datatypes); + } +}