diff --git a/src/Connection/StreamConnection.php b/src/Connection/StreamConnection.php index 81f52555..620a327c 100644 --- a/src/Connection/StreamConnection.php +++ b/src/Connection/StreamConnection.php @@ -15,6 +15,7 @@ namespace Predis\Connection; use InvalidArgumentException; use Predis\Command\CommandInterface; use Predis\Protocol\Parser\UnexpectedTypeException; +use Predis\Response\Error; use Predis\Response\ErrorInterface as ErrorResponseInterface; /** @@ -301,6 +302,7 @@ class StreamConnection extends AbstractConnection } switch ($parsedData['type']) { + case 'push': case 'array': $data = []; @@ -311,22 +313,38 @@ class StreamConnection extends AbstractConnection return $data; case 'bulkString': - $bulkData = ''; - $size = $parsedData['value']; - $bytesLeft = ($size += 2); - - do { - $chunk = is_resource($socket) ? fread($socket, min($bytesLeft, 4096)) : false; - - if ($chunk === false || $chunk === '') { - $this->onConnectionError('Error while reading bytes from the server.'); - } - - $bulkData .= $chunk; - $bytesLeft = $size - strlen($bulkData); - } while ($bytesLeft > 0); + case 'verbatimString': + $bulkData = $this->readByChunks($socket, $parsedData['value']); return substr($bulkData, 0, -2); + + case 'blobError': + $errorMessage = $this->readByChunks($socket, $parsedData['value']); + + return new Error(substr($errorMessage, 0, -2)); + + case 'map': + $data = []; + + for ($i = 0; $i < $parsedData['value']; ++$i) { + $key = $this->read(); + $data[$key] = $this->read(); + } + + return $data; + + case 'set': + $data = []; + + for ($i = 0; $i < $parsedData['value']; ++$i) { + $element = $this->read(); + + if (!in_array($element, $data, true)) { + $data[] = $element; + } + } + + return $data; } return $parsedData; @@ -352,4 +370,30 @@ class StreamConnection extends AbstractConnection $this->write($buffer); } + + /** + * Reads given resource split on chunks with given size. + * + * @param $resource + * @param int $chunkSize + * @return string + */ + private function readByChunks($resource, int $chunkSize): string + { + $string = ''; + $bytesLeft = ($chunkSize += 2); + + do { + $chunk = is_resource($resource) ? fread($resource, min($bytesLeft, 4096)) : false; + + if ($chunk === false || $chunk === '') { + $this->onConnectionError('Error while reading bytes from the server.'); + } + + $string .= $chunk; + $bytesLeft = $chunkSize - strlen($string); + } while ($bytesLeft > 0); + + return $string; + } } diff --git a/src/Protocol/Parser/Strategy/Resp2Strategy.php b/src/Protocol/Parser/Strategy/Resp2Strategy.php index 65b17b30..40e68fb0 100644 --- a/src/Protocol/Parser/Strategy/Resp2Strategy.php +++ b/src/Protocol/Parser/Strategy/Resp2Strategy.php @@ -22,7 +22,7 @@ class Resp2Strategy implements ParserStrategyInterface /** * Callbacks to process given RESP type. * - * @var callable[] + * @var string[] */ protected $typeCallbacks = [ '+' => 'parseSimpleString', diff --git a/src/Protocol/Parser/Strategy/Resp3Strategy.php b/src/Protocol/Parser/Strategy/Resp3Strategy.php index c244c952..07f1d878 100644 --- a/src/Protocol/Parser/Strategy/Resp3Strategy.php +++ b/src/Protocol/Parser/Strategy/Resp3Strategy.php @@ -14,4 +14,145 @@ namespace Predis\Protocol\Parser\Strategy; class Resp3Strategy extends Resp2Strategy { + /** + * @var string[] + */ + protected $resp3TypeCallbacks = [ + '_' => 'parseNull', + ',' => 'parseDouble', + '#' => 'parseBoolean', + '!' => 'parseBlobError', + '=' => 'parseVerbatimString', + '(' => 'parseBigNumber', + '%' => 'parseMap', + '~' => 'parseSet', + '>' => 'parsePush', + ]; + + public function __construct() + { + $this->typeCallbacks += $this->resp3TypeCallbacks; + } + + /** + * Parse null RESP3 type. + * + * @return null + */ + protected function parseNull(string $string) + { + return null; + } + + /** + * Parse double RESP3 type. + * + * @param string $string + * @return float + */ + protected function parseDouble(string $string): float + { + if ($string === 'inf' || $string === '-inf') { + return INF; + } + + return (float) $string; + } + + /** + * Parse boolean RESP3 type. + * + * @param string $string + * @return bool + */ + protected function parseBoolean(string $string): bool + { + return $string === 't'; + } + + /** + * Parse blob error RESP3 type. + * + * @param string $string + * @return array + */ + protected function parseBlobError(string $string): array + { + return [ + 'type' => 'blobError', + 'value' => (int) $string, + ]; + } + + /** + * Parse verbatim string RESP3 type. + * + * @param string $string + * @return array + */ + protected function parseVerbatimString(string $string): array + { + return [ + 'type' => 'verbatimString', + 'value' => (int) $string, + ]; + } + + /** + * Parse big number RESP3 type. + * Depends on PHP environment returns float on numbers that reaches max integer limit. + * + * @param string $string + * @return int|float + */ + protected function parseBigNumber(string $string) + { + if (bccomp($string, PHP_INT_MAX) === 1) { + return (float) $string; + } + + return $this->parseInteger($string); + } + + /** + * Parse map RESP3 type. + * + * @param string $string + * @return array + */ + protected function parseMap(string $string): array + { + return [ + 'type' => 'map', + 'value' => (int) $string, + ]; + } + + /** + * Parse set RESP3 type. + * + * @param string $string + * @return array + */ + protected function parseSet(string $string): array + { + return [ + 'type' => 'set', + 'value' => (int) $string, + ]; + } + + /** + * Parse push RESP3 type. + * + * @param string $string + * @return array + */ + protected function parsePush(string $string): array + { + return [ + 'type' => 'push', + 'value' => (int) $string, + ]; + } } diff --git a/tests/Predis/Command/Redis/Search/FTINFO_Test.php b/tests/Predis/Command/Redis/Search/FTINFO_Test.php index 276be34e..04a5c9d4 100644 --- a/tests/Predis/Command/Redis/Search/FTINFO_Test.php +++ b/tests/Predis/Command/Redis/Search/FTINFO_Test.php @@ -93,7 +93,7 @@ class FTINFO_Test extends PredisCommandTestCase $redis = $this->getClient(); $this->expectException(ServerException::class); - $this->expectExceptionMessage('Unknown Index name'); + $this->expectExceptionMessage('Unknown index name'); $redis->ftinfo('index'); } diff --git a/tests/Predis/Command/Redis/TimeSeries/TSDECRBY_Test.php b/tests/Predis/Command/Redis/TimeSeries/TSDECRBY_Test.php index b99888a7..4edaeaf7 100644 --- a/tests/Predis/Command/Redis/TimeSeries/TSDECRBY_Test.php +++ b/tests/Predis/Command/Redis/TimeSeries/TSDECRBY_Test.php @@ -143,7 +143,7 @@ class TSDECRBY_Test extends PredisCommandTestCase ); $this->expectException(ServerException::class); - $this->expectExceptionMessage('TSDB: for incrby/decrby, timestamp should be newer than the'); + $this->expectExceptionMessage('TSDB: timestamp must be equal to or higher than the maximum existing timestamp'); $redis->tsdecrby('temperature:2:32', 27, (new DecrByArguments())->timestamp(123123123122)); } diff --git a/tests/Predis/Command/Redis/TimeSeries/TSINCRBY_Test.php b/tests/Predis/Command/Redis/TimeSeries/TSINCRBY_Test.php index 95add6c9..c369ac2d 100644 --- a/tests/Predis/Command/Redis/TimeSeries/TSINCRBY_Test.php +++ b/tests/Predis/Command/Redis/TimeSeries/TSINCRBY_Test.php @@ -143,7 +143,7 @@ class TSINCRBY_Test extends PredisCommandTestCase ); $this->expectException(ServerException::class); - $this->expectExceptionMessage('TSDB: for incrby/decrby, timestamp should be newer than the'); + $this->expectExceptionMessage('TSDB: timestamp must be equal to or higher than the maximum existing timestamp'); $redis->tsincrby('temperature:2:32', 27, (new IncrByArguments())->timestamp(123123123122)); } diff --git a/tests/Predis/Command/Redis/TimeSeries/TSMADD_Test.php b/tests/Predis/Command/Redis/TimeSeries/TSMADD_Test.php index 649139b5..246ca6b7 100644 --- a/tests/Predis/Command/Redis/TimeSeries/TSMADD_Test.php +++ b/tests/Predis/Command/Redis/TimeSeries/TSMADD_Test.php @@ -97,7 +97,7 @@ class TSMADD_Test extends PredisCommandTestCase $redis = $this->getClient(); $this->expectException(ServerException::class); - $this->expectExceptionMessage("ERR wrong number of arguments for 'TS.MADD' command"); + $this->expectExceptionMessage("ERR wrong number of arguments for 'ts.madd' command"); $redis->tsmadd('temperature:2:32', 123123123123, 27, 'temperature:2:33', 123123123124); } diff --git a/tests/Predis/Protocol/Parser/Strategy/Resp3StrategyTest.php b/tests/Predis/Protocol/Parser/Strategy/Resp3StrategyTest.php new file mode 100644 index 00000000..16db9267 --- /dev/null +++ b/tests/Predis/Protocol/Parser/Strategy/Resp3StrategyTest.php @@ -0,0 +1,192 @@ +strategy = new Resp3Strategy(); + } + + /** + * @group disconnected + * @return void + */ + public function testParseDataReturnsNullOnNullType(): void + { + $data = "_\r\n"; + + $actualResponse = $this->strategy->parseData($data); + + $this->assertNull($actualResponse); + } + + /** + * @group disconnected + * @return void + */ + public function testParseDataReturnsFloatOnDoubleType(): void + { + $data = ",1.23\r\n"; + + $actualResponse = $this->strategy->parseData($data); + + $this->assertSame(1.23, $actualResponse); + } + + /** + * @dataProvider infinityProvider + * @group disconnected + * @param string $data + * @return void + */ + public function testParseDataReturnsFloatInfinityOnInfinityOrNegativeInfinity(string $data): void + { + $actualResponse = $this->strategy->parseData($data); + + $this->assertInfinite($actualResponse); + } + + /** + * @dataProvider booleanProvider + * @group disconnected + * @param string $data + * @param bool $expectedValue + * @return void + */ + public function testParseDataReturnsBooleanOnBooleanType(string $data, bool $expectedValue): void + { + $actualResponse = $this->strategy->parseData($data); + + $this->assertSame($expectedValue, $actualResponse); + } + + /** + * @group disconnected + * @return void + */ + public function testParseDataReturnsArrayOnBlobErrorType(): void + { + $data = "!21\r\nSYNTAX invalid syntax\r\n"; + + $actualResponse = $this->strategy->parseData($data); + + $this->assertSame(['type' => 'blobError', 'value' => 21], $actualResponse); + } + + /** + * @group disconnected + * @return void + */ + public function testParseDataReturnsArrayOnVerbatimStringType(): void + { + $data = "=15\r\ntxt:Some string\r\n"; + + $actualResponse = $this->strategy->parseData($data); + + $this->assertSame(['type' => 'verbatimString', 'value' => 15], $actualResponse); + } + + /** + * @dataProvider bigNumberProvider + * @group disconnected + * @param string $data + * @param int|float $expectedValue + * @return void + */ + public function testParseDataReturnsIntegerOrFloatOnBigNumberType(string $data, $expectedValue): void + { + $actualResponse = $this->strategy->parseData($data); + + $this->assertSame($expectedValue, $actualResponse); + } + + /** + * @group disconnected + * @return void + */ + public function testParseDataReturnsArrayOnMapType(): void + { + $data = "%2\r\n+first\r\n:1\r\n+second\r\n:2"; + + $actualResponse = $this->strategy->parseData($data); + + $this->assertSame(['type' => 'map', 'value' => 2], $actualResponse); + } + + /** + * @group disconnected + * @return void + */ + public function testParseDataReturnsArrayOnSetType(): void + { + $data = "~4\r\n+first\r\n:1\r\n+second\r\n:2"; + + $actualResponse = $this->strategy->parseData($data); + + $this->assertSame(['type' => 'set', 'value' => 4], $actualResponse); + } + + /** + * @group disconnected + * @return void + */ + public function testParseDataReturnsArrayOnPushType(): void + { + $data = ">4\r\n+pubsub\r\n+message\r\n+somechannel\r\n+this is the message"; + + $actualResponse = $this->strategy->parseData($data); + + $this->assertSame(['type' => 'push', 'value' => 4], $actualResponse); + } + + public function infinityProvider(): array + { + return [ + 'positive infinity' => [",inf\r\n"], + 'negative infinity' => [",-inf\r\n"], + ]; + } + + public function booleanProvider(): array + { + return [ + 'true' => ["#t\r\n", true], + 'false' => ["#f\r\n", false], + ]; + } + + public function bigNumberProvider(): array + { + return [ + 'greater than integer limit' => [ + "(3492890328409238509324850943850943825024385\r\n", + 3492890328409238509324850943850943825024385, + ], + 'lower than integer limit' => [ + "(34928903\r\n", + 34928903, + ], + ]; + } +}