From e58dee8e0ad4552e0ca453b690e2952d8ea72443 Mon Sep 17 00:00:00 2001 From: Kim Sandell Date: Thu, 21 May 2026 09:15:21 +0200 Subject: [PATCH 1/2] fix(cache): round-trip integer values in Redis adapter (#78) Redis get() returned false for any integer stored via set() because Predis always returns strings, so the is_int() fast-path never matched and the raw integer was passed to unserialize(). get() now detects raw integers via filter_var() and uses an explicit null check so falsy stored values are no longer mistaken for cache misses. Adds testValueRoundTrip data-provider regression tests to the Redis and APCu adapter suites. Bumps version to 0.0.41. --- VERSION | 2 +- composer.json | 2 +- package.json | 2 +- src/Cache/Adapters/Redis.php | 24 ++++++++++++++----- tests/Cache/Adapters/ApcuTest.php | 36 +++++++++++++++++++++++++++++ tests/Cache/Adapters/RedisTest.php | 37 ++++++++++++++++++++++++++++++ 6 files changed, 94 insertions(+), 9 deletions(-) diff --git a/VERSION b/VERSION index 4fe2fe8..f9bcd8b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.40 +0.0.41 \ No newline at end of file diff --git a/composer.json b/composer.json index c257b1c..28e25d7 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "celarius/spin-framework", "description": "A super lightweight PHP UI/REST Framework", - "version": "0.0.40", + "version": "0.0.41", "keywords": [ "php8", "php-framework", diff --git a/package.json b/package.json index 897b5cc..5961af8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "spin-framework", "title": "Spin Framework", - "version": "0.0.40", + "version": "0.0.41", "homepage": "https://github.com/Celarius/spin-framework", "description": "A super lightweight PHP UI/REST Framework", "author": { diff --git a/src/Cache/Adapters/Redis.php b/src/Cache/Adapters/Redis.php index 261940e..3b01e84 100644 --- a/src/Cache/Adapters/Redis.php +++ b/src/Cache/Adapters/Redis.php @@ -89,13 +89,25 @@ protected function disconnect(): bool public function get($key, mixed $default = null): mixed { $result = $this->redisClient->get($key); - if ($result) { - if (\is_int($result)) { - return $result; - } - return unserialize($result); + + # Missing key — Predis returns null for a non-existent key + if ($result === null) { + return $default; + } + + # Raw integer — stored un-serialized by set() and by native inc()/dec() + $asInt = \filter_var($result, \FILTER_VALIDATE_INT); + if ($asInt !== false) { + return $asInt; } - return $default; + + # Everything else is serialized; serialize(false) is a valid stored value + $value = @\unserialize($result); + if ($value === false && $result !== \serialize(false)) { + return $default; + } + + return $value; } /** diff --git a/tests/Cache/Adapters/ApcuTest.php b/tests/Cache/Adapters/ApcuTest.php index b1e99b5..f889a67 100644 --- a/tests/Cache/Adapters/ApcuTest.php +++ b/tests/Cache/Adapters/ApcuTest.php @@ -374,6 +374,42 @@ public function testComplexDataTypes(): void $this->assertFalse($this->cacheObj->get('bool_false')); } + /** + * Regression for #78 — every supported value type must survive a + * set()/get() round-trip with its type and value intact. + * + * @dataProvider roundTripValueProvider + */ + public function testValueRoundTrip(string $label, mixed $value): void + { + $key = 'roundtrip_' . $label; + + $this->assertTrue($this->cacheObj->set($key, $value)); + $this->assertSame($value, $this->cacheObj->get($key), "Round-trip failed for: {$label}"); + } + + /** + * @return array + */ + public static function roundTripValueProvider(): array + { + return [ + 'positive_int' => ['positive_int', 12345], + 'zero_int' => ['zero_int', 0], + 'negative_int' => ['negative_int', -7], + 'large_int' => ['large_int', \time()], + 'float' => ['float', 3.14], + 'bool_true' => ['bool_true', true], + 'bool_false' => ['bool_false', false], + 'empty_string' => ['empty_string', ''], + 'zero_string' => ['zero_string', '0'], + 'numeric_string'=> ['numeric_string', '12345'], + 'string' => ['string', 'hello'], + 'null' => ['null', null], + 'array' => ['array', ['a' => 1, 'b' => [2, 3]]], + ]; + } + public function testKeyValidation(): void { // Test with empty key diff --git a/tests/Cache/Adapters/RedisTest.php b/tests/Cache/Adapters/RedisTest.php index a28be02..0389fcd 100644 --- a/tests/Cache/Adapters/RedisTest.php +++ b/tests/Cache/Adapters/RedisTest.php @@ -344,6 +344,43 @@ public function testDriverName(): void $this->assertEquals('Redis', $driver); } + /** + * Regression for #78 — every supported value type must survive a + * set()/get() round-trip with its type and value intact. + * + * @dataProvider roundTripValueProvider + * @throws InvalidArgumentException + */ + public function testValueRoundTrip(string $label, mixed $value): void + { + $key = 'roundtrip_' . $label; + + $this->assertTrue($this->cacheObj->set($key, $value)); + $this->assertSame($value, $this->cacheObj->get($key), "Round-trip failed for: {$label}"); + } + + /** + * @return array + */ + public static function roundTripValueProvider(): array + { + return [ + 'positive_int' => ['positive_int', 12345], + 'zero_int' => ['zero_int', 0], + 'negative_int' => ['negative_int', -7], + 'large_int' => ['large_int', \time()], + 'float' => ['float', 3.14], + 'bool_true' => ['bool_true', true], + 'bool_false' => ['bool_false', false], + 'empty_string' => ['empty_string', ''], + 'zero_string' => ['zero_string', '0'], + 'numeric_string'=> ['numeric_string', '12345'], + 'string' => ['string', 'hello'], + 'null' => ['null', null], + 'array' => ['array', ['a' => 1, 'b' => [2, 3]]], + ]; + } + /** * Test that values persist across different instances * From 9ac32af7ef7e2f90806f4f0c64059c3cf199658c Mon Sep 17 00:00:00 2001 From: Kim Sandell Date: Thu, 21 May 2026 09:26:48 +0200 Subject: [PATCH 2/2] test(cache): use #[DataProvider] attribute for round-trip tests The @dataProvider docblock annotation is not recognized under PHPUnit 12 (PHP 8.3/8.4 CI matrix), causing testValueRoundTrip to run with zero arguments and error. The PHP 8 attribute form works across all PHPUnit versions in the matrix. --- tests/Cache/Adapters/ApcuTest.php | 4 ++-- tests/Cache/Adapters/RedisTest.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Cache/Adapters/ApcuTest.php b/tests/Cache/Adapters/ApcuTest.php index f889a67..fa36789 100644 --- a/tests/Cache/Adapters/ApcuTest.php +++ b/tests/Cache/Adapters/ApcuTest.php @@ -3,6 +3,7 @@ namespace Spin\tests\Core; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; use Psr\SimpleCache\InvalidArgumentException; use Spin\Cache\Adapters\Apcu; use Spin\Exceptions\CacheException; @@ -377,9 +378,8 @@ public function testComplexDataTypes(): void /** * Regression for #78 — every supported value type must survive a * set()/get() round-trip with its type and value intact. - * - * @dataProvider roundTripValueProvider */ + #[DataProvider('roundTripValueProvider')] public function testValueRoundTrip(string $label, mixed $value): void { $key = 'roundtrip_' . $label; diff --git a/tests/Cache/Adapters/RedisTest.php b/tests/Cache/Adapters/RedisTest.php index 0389fcd..38adc3c 100644 --- a/tests/Cache/Adapters/RedisTest.php +++ b/tests/Cache/Adapters/RedisTest.php @@ -3,6 +3,7 @@ namespace Spin\tests\Core; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; use Psr\SimpleCache\InvalidArgumentException; use Spin\Cache\Adapters\Redis; use Spin\Exceptions\CacheException; @@ -348,9 +349,9 @@ public function testDriverName(): void * Regression for #78 — every supported value type must survive a * set()/get() round-trip with its type and value intact. * - * @dataProvider roundTripValueProvider * @throws InvalidArgumentException */ + #[DataProvider('roundTripValueProvider')] public function testValueRoundTrip(string $label, mixed $value): void { $key = 'roundtrip_' . $label;