diff --git a/src/SerializationTrait.php b/src/SerializationTrait.php index 0bebc6c..331e5bf 100644 --- a/src/SerializationTrait.php +++ b/src/SerializationTrait.php @@ -50,12 +50,21 @@ protected function serializeValue(mixed $value): string /** * Deserialize a value retrieved from a string-only backend. * - * @param string $stored The raw stored string. + * Accepts mixed so co-tenant writers (legacy Horde_HashTable_Memcache, + * prior releases of this package, or unrelated components sharing the + * same memcached pool) that stored bare non-string scalars are passed + * through instead of tripping a strict-type error. String values still + * go through the s:/p: prefix protocol; anything else is returned as-is. + * + * @param mixed $stored The raw stored value as returned by the backend. * * @return mixed The original value. */ - protected function deserializeValue(string $stored): mixed + protected function deserializeValue(mixed $stored): mixed { + if (!is_string($stored)) { + return $stored; + } if (str_starts_with($stored, 's:')) { return substr($stored, 2); } diff --git a/test/src/Unit/SerializationTraitTest.php b/test/src/Unit/SerializationTraitTest.php new file mode 100644 index 0000000..190cc37 --- /dev/null +++ b/test/src/Unit/SerializationTraitTest.php @@ -0,0 +1,91 @@ +serializeValue($value); + } + + public function decode(mixed $stored): mixed + { + return $this->deserializeValue($stored); + } +} + +#[CoversTrait(SerializationTrait::class)] +final class SerializationTraitTest extends TestCase +{ + private SerializationTraitConsumer $sut; + + protected function setUp(): void + { + $this->sut = new SerializationTraitConsumer(); + } + + #[Test] + public function stringRoundTrip(): void + { + $this->assertSame('hello', $this->sut->decode($this->sut->encode('hello'))); + } + + #[Test] + public function intRoundTrip(): void + { + $this->assertSame(42, $this->sut->decode($this->sut->encode(42))); + } + + #[Test] + public function arrayRoundTrip(): void + { + $value = ['a' => 1, 'b' => [2, 3]]; + $this->assertSame($value, $this->sut->decode($this->sut->encode($value))); + } + + /** + * Values retrieved from the backend that never went through the trait's + * writer (co-tenant writer, legacy driver, prior release) must pass + * through instead of tripping a strict-type error. + */ + #[Test] + public function nonStringInputPassesThroughUnchanged(): void + { + $this->assertSame(1735689600, $this->sut->decode(1735689600)); + $this->assertSame(true, $this->sut->decode(true)); + $this->assertSame(null, $this->sut->decode(null)); + $this->assertSame(['x' => 1], $this->sut->decode(['x' => 1])); + } + + /** + * Bare (unprefixed) strings — the "legacy data" case documented in the + * trait — must round-trip as a raw string. + */ + #[Test] + public function unprefixedStringTreatedAsRaw(): void + { + $this->assertSame('legacy', $this->sut->decode('legacy')); + } + + #[Test] + public function stringPrefixShortForm(): void + { + // Bytes that happen to look like the p: prefix don't get confused + // with prefixed content when they came from the s: writer. + $encoded = $this->sut->encode('p:not-really-serialized'); + $this->assertSame('p:not-really-serialized', $this->sut->decode($encoded)); + } +}