diff --git a/plugins/cache-redis/LICENSE b/plugins/cache-redis/LICENSE new file mode 100644 index 000000000..9a8cd865b --- /dev/null +++ b/plugins/cache-redis/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2009-2020 Daniele Alessandri (original work) +Copyright (c) 2021-2023 Till Krüss (modified work) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/cache-redis/Predis/Autoloader.php b/plugins/cache-redis/Predis/Autoloader.php new file mode 100644 index 000000000..054f7bbb2 --- /dev/null +++ b/plugins/cache-redis/Predis/Autoloader.php @@ -0,0 +1,64 @@ + + * @author Daniele Alessandri + * @codeCoverageIgnore + */ +class Autoloader +{ + private $directory; + private $prefix; + private $prefixLength; + + /** + * @param string $baseDirectory Base directory where the source files are located. + */ + public function __construct($baseDirectory = __DIR__) + { + $this->directory = $baseDirectory; + $this->prefix = __NAMESPACE__ . '\\'; + $this->prefixLength = strlen($this->prefix); + } + + /** + * Registers the autoloader class with the PHP SPL autoloader. + * + * @param bool $prepend Prepend the autoloader on the stack instead of appending it. + */ + public static function register($prepend = false) + { + spl_autoload_register([new self(), 'autoload'], true, $prepend); + } + + /** + * Loads a class from a file using its fully qualified name. + * + * @param string $className Fully qualified name of a class. + */ + public function autoload($className) + { + if (0 === strpos($className, $this->prefix)) { + $parts = explode('\\', substr($className, $this->prefixLength)); + $filepath = $this->directory . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts) . '.php'; + + if (is_file($filepath)) { + require $filepath; + } + } + } +} diff --git a/plugins/cache-redis/Predis/ClientConfiguration.php b/plugins/cache-redis/Predis/ClientConfiguration.php new file mode 100644 index 000000000..c70cd61cf --- /dev/null +++ b/plugins/cache-redis/Predis/ClientConfiguration.php @@ -0,0 +1,42 @@ + [ + ['name' => 'Json', 'commandPrefix' => 'JSON'], + ['name' => 'BloomFilter', 'commandPrefix' => 'BF'], + ['name' => 'CuckooFilter', 'commandPrefix' => 'CF'], + ['name' => 'CountMinSketch', 'commandPrefix' => 'CMS'], + ['name' => 'TDigest', 'commandPrefix' => 'TDIGEST'], + ['name' => 'TopK', 'commandPrefix' => 'TOPK'], + ['name' => 'Search', 'commandPrefix' => 'FT'], + ['name' => 'TimeSeries', 'commandPrefix' => 'TS'], + ], + ]; + + /** + * Returns available modules with configuration. + * + * @return array|string[][] + */ + public static function getModules(): array + { + return self::$config['modules']; + } +} diff --git a/plugins/cache-redis/Predis/Cluster/Hash/PhpiredisCRC16.php b/plugins/cache-redis/Predis/Cluster/Hash/PhpiredisCRC16.php new file mode 100644 index 000000000..04f58a0ec --- /dev/null +++ b/plugins/cache-redis/Predis/Cluster/Hash/PhpiredisCRC16.php @@ -0,0 +1,42 @@ += 0x0000 && $slot <= 0x3FFF; + } + + /** + * Checks if the given slot range is valid. + * + * @param int $first Initial slot of the range. + * @param int $last Last slot of the range. + * + * @return bool + */ + public static function isValidRange($first, $last) + { + return $first >= 0x0000 && $first <= 0x3FFF && $last >= 0x0000 && $last <= 0x3FFF && $first <= $last; + } + + /** + * Resets the slot map. + */ + public function reset() + { + $this->slots = []; + } + + /** + * Checks if the slot map is empty. + * + * @return bool + */ + public function isEmpty() + { + return empty($this->slots); + } + + /** + * Returns the current slot map as a dictionary of $slot => $node. + * + * The order of the slots in the dictionary is not guaranteed. + * + * @return array + */ + public function toArray() + { + return $this->slots; + } + + /** + * Returns the list of unique nodes in the slot map. + * + * @return array + */ + public function getNodes() + { + return array_keys(array_flip($this->slots)); + } + + /** + * Assigns the specified slot range to a node. + * + * @param int $first Initial slot of the range. + * @param int $last Last slot of the range. + * @param NodeConnectionInterface|string $connection ID or connection instance. + * + * @throws OutOfBoundsException + */ + public function setSlots($first, $last, $connection) + { + if (!static::isValidRange($first, $last)) { + throw new OutOfBoundsException("Invalid slot range $first-$last for `$connection`"); + } + + $this->slots += array_fill($first, $last - $first + 1, (string) $connection); + } + + /** + * Returns the specified slot range. + * + * @param int $first Initial slot of the range. + * @param int $last Last slot of the range. + * + * @return array + */ + public function getSlots($first, $last) + { + if (!static::isValidRange($first, $last)) { + throw new OutOfBoundsException("Invalid slot range $first-$last"); + } + + return array_intersect_key($this->slots, array_fill($first, $last - $first + 1, null)); + } + + /** + * Checks if the specified slot is assigned. + * + * @param int $slot Slot index. + * + * @return bool + */ + #[ReturnTypeWillChange] + public function offsetExists($slot) + { + return isset($this->slots[$slot]); + } + + /** + * Returns the node assigned to the specified slot. + * + * @param int $slot Slot index. + * + * @return string|null + */ + #[ReturnTypeWillChange] + public function offsetGet($slot) + { + return $this->slots[$slot] ?? null; + } + + /** + * Assigns the specified slot to a node. + * + * @param int $slot Slot index. + * @param NodeConnectionInterface|string $connection ID or connection instance. + * + * @return void + */ + #[ReturnTypeWillChange] + public function offsetSet($slot, $connection) + { + if (!static::isValid($slot)) { + throw new OutOfBoundsException("Invalid slot $slot for `$connection`"); + } + + $this->slots[(int) $slot] = (string) $connection; + } + + /** + * Returns the node assigned to the specified slot. + * + * @param int $slot Slot index. + * + * @return void + */ + #[ReturnTypeWillChange] + public function offsetUnset($slot) + { + unset($this->slots[$slot]); + } + + /** + * Returns the current number of assigned slots. + * + * @return int + */ + #[ReturnTypeWillChange] + public function count() + { + return count($this->slots); + } + + /** + * Returns an iterator over the slot map. + * + * @return Traversable + */ + #[ReturnTypeWillChange] + public function getIterator() + { + return new ArrayIterator($this->slots); + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/ArrayableArgument.php b/plugins/cache-redis/Predis/Command/Argument/ArrayableArgument.php new file mode 100644 index 000000000..11073c054 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/ArrayableArgument.php @@ -0,0 +1,26 @@ +unit = $unit; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByBox.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByBox.php new file mode 100644 index 000000000..7dd9f23b7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByBox.php @@ -0,0 +1,43 @@ +width = $width; + $this->height = $height; + $this->setUnit($unit); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [self::KEYWORD, $this->width, $this->height, $this->unit]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByInterface.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByInterface.php new file mode 100644 index 000000000..767886cef --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByInterface.php @@ -0,0 +1,19 @@ +radius = $radius; + $this->setUnit($unit); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [self::KEYWORD, $this->radius, $this->unit]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromInterface.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromInterface.php new file mode 100644 index 000000000..44700bda4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromInterface.php @@ -0,0 +1,19 @@ +longitude = $longitude; + $this->latitude = $latitude; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [self::KEYWORD, $this->longitude, $this->latitude]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromMember.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromMember.php new file mode 100644 index 000000000..9e24b2ba0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromMember.php @@ -0,0 +1,36 @@ +member = $member; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [self::KEYWORD, $this->member]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/AggregateArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/AggregateArguments.php new file mode 100644 index 000000000..950967070 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/AggregateArguments.php @@ -0,0 +1,161 @@ + 'ASC', + 'desc' => 'DESC', + ]; + + /** + * Loads document attributes from the source document. + * + * @param string ...$fields Could be just '*' to load all fields + * @return $this + */ + public function load(string ...$fields): self + { + $arguments = func_get_args(); + + $this->arguments[] = 'LOAD'; + + if ($arguments[0] === '*') { + $this->arguments[] = '*'; + + return $this; + } + + $this->arguments[] = count($arguments); + $this->arguments = array_merge($this->arguments, $arguments); + + return $this; + } + + /** + * Loads document attributes from the source document. + * + * @param string ...$properties + * @return $this + */ + public function groupBy(string ...$properties): self + { + $arguments = func_get_args(); + + array_push($this->arguments, 'GROUPBY', count($arguments)); + $this->arguments = array_merge($this->arguments, $arguments); + + return $this; + } + + /** + * Groups the results in the pipeline based on one or more properties. + * + * If you want to add alias property to your argument just add "true" value in arguments enumeration, + * next value will be considered as alias to previous one. + * + * Example: 'argument', true, 'name' => 'argument' AS 'name' + * + * @param string $function + * @param string|bool ...$argument + * @return $this + */ + public function reduce(string $function, ...$argument): self + { + $arguments = func_get_args(); + $functionValue = array_shift($arguments); + $argumentsCounter = 0; + + for ($i = 0, $iMax = count($arguments); $i < $iMax; $i++) { + if (true === $arguments[$i]) { + $arguments[$i] = 'AS'; + $i++; + continue; + } + + $argumentsCounter++; + } + + array_push($this->arguments, 'REDUCE', $functionValue); + $this->arguments = array_merge($this->arguments, [$argumentsCounter], $arguments); + + return $this; + } + + /** + * Sorts the pipeline up until the point of SORTBY, using a list of properties. + * + * @param int $max + * @param string ...$properties Enumeration of properties, including sorting direction (ASC, DESC) + * @return $this + */ + public function sortBy(int $max = 0, ...$properties): self + { + $arguments = func_get_args(); + $maxValue = array_shift($arguments); + + $this->arguments[] = 'SORTBY'; + $this->arguments = array_merge($this->arguments, [count($arguments)], $arguments); + + if ($maxValue !== 0) { + array_push($this->arguments, 'MAX', $maxValue); + } + + return $this; + } + + /** + * Applies a 1-to-1 transformation on one or more properties and either stores the result + * as a new property down the pipeline or replaces any property using this transformation. + * + * @param string $expression + * @param string $as + * @return $this + */ + public function apply(string $expression, string $as = ''): self + { + array_push($this->arguments, 'APPLY', $expression); + + if ($as !== '') { + array_push($this->arguments, 'AS', $as); + } + + return $this; + } + + /** + * Scan part of the results with a quicker alternative than LIMIT. + * + * @param int $readSize + * @param int $idleTime + * @return $this + */ + public function withCursor(int $readSize = 0, int $idleTime = 0): self + { + $this->arguments[] = 'WITHCURSOR'; + + if ($readSize !== 0) { + array_push($this->arguments, 'COUNT', $readSize); + } + + if ($idleTime !== 0) { + array_push($this->arguments, 'MAXIDLE', $idleTime); + } + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/AlterArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/AlterArguments.php new file mode 100644 index 000000000..5acd2fe1e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/AlterArguments.php @@ -0,0 +1,17 @@ +arguments[] = 'LANGUAGE'; + $this->arguments[] = $defaultLanguage; + + return $this; + } + + /** + * Selects the dialect version under which to execute the query. + * If not specified, the query will execute under the default dialect version + * set during module initial loading or via FT.CONFIG SET command. + * + * @param string $dialect + * @return $this + */ + public function dialect(string $dialect): self + { + $this->arguments[] = 'DIALECT'; + $this->arguments[] = $dialect; + + return $this; + } + + /** + * If set, does not scan and index. + * + * @return $this + */ + public function skipInitialScan(): self + { + $this->arguments[] = 'SKIPINITIALSCAN'; + + return $this; + } + + /** + * Adds an arbitrary, binary safe payload that is exposed to custom scoring functions. + * + * @param string $payload + * @return $this + */ + public function payload(string $payload): self + { + $this->arguments[] = 'PAYLOAD'; + $this->arguments[] = $payload; + + return $this; + } + + /** + * Also returns the relative internal score of each document. + * + * @return $this + */ + public function withScores(): self + { + $this->arguments[] = 'WITHSCORES'; + + return $this; + } + + /** + * Retrieves optional document payloads. + * + * @return $this + */ + public function withPayloads(): self + { + $this->arguments[] = 'WITHPAYLOADS'; + + return $this; + } + + /** + * Does not try to use stemming for query expansion but searches the query terms verbatim. + * + * @return $this + */ + public function verbatim(): self + { + $this->arguments[] = 'VERBATIM'; + + return $this; + } + + /** + * Overrides the timeout parameter of the module. + * + * @param int $timeout + * @return $this + */ + public function timeout(int $timeout): self + { + $this->arguments[] = 'TIMEOUT'; + $this->arguments[] = $timeout; + + return $this; + } + + /** + * Adds an arbitrary, binary safe payload that is exposed to custom scoring functions. + * + * @param int $offset + * @param int $num + * @return $this + */ + public function limit(int $offset, int $num): self + { + array_push($this->arguments, 'LIMIT', $offset, $num); + + return $this; + } + + /** + * Adds filter expression into index. + * + * @param string $filter + * @return $this + */ + public function filter(string $filter): self + { + $this->arguments[] = 'FILTER'; + $this->arguments[] = $filter; + + return $this; + } + + /** + * Defines one or more value parameters. Each parameter has a name and a value. + * + * Example: ['name1', 'value1', 'name2', 'value2'...] + * + * @param array $nameValuesDictionary + * @return $this + */ + public function params(array $nameValuesDictionary): self + { + $this->arguments[] = 'PARAMS'; + $this->arguments[] = count($nameValuesDictionary); + $this->arguments = array_merge($this->arguments, $nameValuesDictionary); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/CreateArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/CreateArguments.php new file mode 100644 index 000000000..b8e0176ba --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/CreateArguments.php @@ -0,0 +1,191 @@ + 'HASH', + 'json' => 'JSON', + ]; + + /** + * Specify data type for given index. To index JSON you must have the RedisJSON module to be installed. + * + * @param string $modifier + * @return $this + */ + public function on(string $modifier = 'HASH'): self + { + if (in_array(strtoupper($modifier), $this->supportedDataTypesEnum)) { + $this->arguments[] = 'ON'; + $this->arguments[] = $this->supportedDataTypesEnum[strtolower($modifier)]; + + return $this; + } + + $enumValues = implode(', ', array_values($this->supportedDataTypesEnum)); + throw new InvalidArgumentException("Wrong modifier value given. Currently supports: {$enumValues}"); + } + + /** + * Adds one or more prefixes into index. + * + * @param array $prefixes + * @return $this + */ + public function prefix(array $prefixes): self + { + $this->arguments[] = 'PREFIX'; + $this->arguments[] = count($prefixes); + $this->arguments = array_merge($this->arguments, $prefixes); + + return $this; + } + + /** + * Document attribute set as document language. + * + * @param string $languageAttribute + * @return $this + */ + public function languageField(string $languageAttribute): self + { + $this->arguments[] = 'LANGUAGE_FIELD'; + $this->arguments[] = $languageAttribute; + + return $this; + } + + /** + * Default score for documents in the index. + * + * @param float $defaultScore + * @return $this + */ + public function score(float $defaultScore = 1.0): self + { + $this->arguments[] = 'SCORE'; + $this->arguments[] = $defaultScore; + + return $this; + } + + /** + * Document attribute that used as the document rank based on the user ranking. + * + * @param string $scoreAttribute + * @return $this + */ + public function scoreField(string $scoreAttribute): self + { + $this->arguments[] = 'SCORE_FIELD'; + $this->arguments[] = $scoreAttribute; + + return $this; + } + + /** + * Forces RediSearch to encode indexes as if there were more than 32 text attributes. + * + * @return $this + */ + public function maxTextFields(): self + { + $this->arguments[] = 'MAXTEXTFIELDS'; + + return $this; + } + + /** + * Does not store term offsets for documents. + * + * @return $this + */ + public function noOffsets(): self + { + $this->arguments[] = 'NOOFFSETS'; + + return $this; + } + + /** + * Creates a lightweight temporary index that expires after a specified period of inactivity, in seconds. + * + * @param int $seconds + * @return $this + */ + public function temporary(int $seconds): self + { + $this->arguments[] = 'TEMPORARY'; + $this->arguments[] = $seconds; + + return $this; + } + + /** + * Conserves storage space and memory by disabling highlighting support. + * + * @return $this + */ + public function noHl(): self + { + $this->arguments[] = 'NOHL'; + + return $this; + } + + /** + * Does not store attribute bits for each term. + * + * @return $this + */ + public function noFields(): self + { + $this->arguments[] = 'NOFIELDS'; + + return $this; + } + + /** + * Avoids saving the term frequencies in the index. + * + * @return $this + */ + public function noFreqs(): self + { + $this->arguments[] = 'NOFREQS'; + + return $this; + } + + /** + * Sets the index with a custom stopword list, to be ignored during indexing and search time. + * + * @param array $stopWords + * @return $this + */ + public function stopWords(array $stopWords): self + { + $this->arguments[] = 'STOPWORDS'; + $this->arguments[] = count($stopWords); + $this->arguments = array_merge($this->arguments, $stopWords); + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/CursorArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/CursorArguments.php new file mode 100644 index 000000000..a8bd6b56f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/CursorArguments.php @@ -0,0 +1,44 @@ +arguments, 'COUNT', $readSize); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/DropArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/DropArguments.php new file mode 100644 index 000000000..0c6313201 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/DropArguments.php @@ -0,0 +1,43 @@ +arguments[] = 'DD'; + + return $this; + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/ExplainArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/ExplainArguments.php new file mode 100644 index 000000000..b4bd235b9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/ExplainArguments.php @@ -0,0 +1,17 @@ +arguments[] = 'SEARCH'; + + return $this; + } + + /** + * Adds aggregate context. + * + * @return $this + */ + public function aggregate(): self + { + $this->arguments[] = 'AGGREGATE'; + + return $this; + } + + /** + * Removes details of reader iterator. + * + * @return $this + */ + public function limited(): self + { + $this->arguments[] = 'LIMITED'; + + return $this; + } + + /** + * Is query string, as if sent to FT.SEARCH. + * + * @param string $query + * @return $this + */ + public function query(string $query): self + { + $this->arguments[] = 'QUERY'; + $this->arguments[] = $query; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/AbstractField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/AbstractField.php new file mode 100644 index 000000000..eb49f0995 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/AbstractField.php @@ -0,0 +1,69 @@ +fieldArguments[] = $identifier; + + if ($alias !== '') { + $this->fieldArguments[] = 'AS'; + $this->fieldArguments[] = $alias; + } + + $this->fieldArguments[] = $fieldType; + + if ($sortable === self::SORTABLE) { + $this->fieldArguments[] = 'SORTABLE'; + } elseif ($sortable === self::SORTABLE_UNF) { + $this->fieldArguments[] = 'SORTABLE'; + $this->fieldArguments[] = 'UNF'; + } + + if ($noIndex) { + $this->fieldArguments[] = 'NOINDEX'; + } + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->fieldArguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/FieldInterface.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/FieldInterface.php new file mode 100644 index 000000000..80e57eba0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/FieldInterface.php @@ -0,0 +1,22 @@ +setCommonOptions('GEO', $identifier, $alias, $sortable, $noIndex); + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/NumericField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/NumericField.php new file mode 100644 index 000000000..758b4e9c6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/NumericField.php @@ -0,0 +1,31 @@ +setCommonOptions('NUMERIC', $identifier, $alias, $sortable, $noIndex); + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TagField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TagField.php new file mode 100644 index 000000000..358b3090e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TagField.php @@ -0,0 +1,44 @@ +setCommonOptions('TAG', $identifier, $alias, $sortable, $noIndex); + + if ($separator !== ',') { + $this->fieldArguments[] = 'SEPARATOR'; + $this->fieldArguments[] = $separator; + } + + if ($caseSensitive) { + $this->fieldArguments[] = 'CASESENSITIVE'; + } + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TextField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TextField.php new file mode 100644 index 000000000..d72c62383 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TextField.php @@ -0,0 +1,57 @@ +setCommonOptions('TEXT', $identifier, $alias, $sortable, $noIndex); + + if ($noStem) { + $this->fieldArguments[] = 'NOSTEM'; + } + + if ($phonetic !== '') { + $this->fieldArguments[] = 'PHONETIC'; + $this->fieldArguments[] = $phonetic; + } + + if ($weight !== 1) { + $this->fieldArguments[] = 'WEIGHT'; + $this->fieldArguments[] = $weight; + } + + if ($withSuffixTrie) { + $this->fieldArguments[] = 'WITHSUFFIXTRIE'; + } + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/VectorField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/VectorField.php new file mode 100644 index 000000000..5228c2250 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/VectorField.php @@ -0,0 +1,47 @@ +setCommonOptions('VECTOR', $fieldName, $alias); + + array_push($this->fieldArguments, $algorithm, count($attributeNameValueDictionary)); + $this->fieldArguments = array_merge($this->fieldArguments, $attributeNameValueDictionary); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->fieldArguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SearchArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SearchArguments.php new file mode 100644 index 000000000..d1eb70580 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SearchArguments.php @@ -0,0 +1,306 @@ + 'ASC', + 'desc' => 'DESC', + ]; + + /** + * Returns the document ids and not the content. + * + * @return $this + */ + public function noContent(): self + { + $this->arguments[] = 'NOCONTENT'; + + return $this; + } + + /** + * Returns the value of the sorting key, right after the id and score and/or payload, if requested. + * + * @return $this + */ + public function withSortKeys(): self + { + $this->arguments[] = 'WITHSORTKEYS'; + + return $this; + } + + /** + * Limits results to those having numeric values ranging between min and max, + * if numeric_attribute is defined as a numeric attribute in FT.CREATE. + * Min and max follow ZRANGE syntax, and can be -inf, +inf, and use( for exclusive ranges. + * Multiple numeric filters for different attributes are supported in one query. + * + * @param array ...$filter Should contain: numeric_field, min and max. Example: ['numeric_field', 1, 10] + * @return $this + */ + public function searchFilter(array ...$filter): self + { + $arguments = func_get_args(); + + foreach ($arguments as $argument) { + array_push($this->arguments, 'FILTER', ...$argument); + } + + return $this; + } + + /** + * Filter the results to a given radius from lon and lat. Radius is given as a number and units. + * + * @param array ...$filter Should contain: geo_field, lon, lat, radius, unit. Example: ['geo_field', 34.1231, 35.1231, 300, km] + * @return $this + */ + public function geoFilter(array ...$filter): self + { + $arguments = func_get_args(); + + foreach ($arguments as $argument) { + array_push($this->arguments, 'GEOFILTER', ...$argument); + } + + return $this; + } + + /** + * Limits the result to a given set of keys specified in the list. + * + * @param array $keys + * @return $this + */ + public function inKeys(array $keys): self + { + $this->arguments[] = 'INKEYS'; + $this->arguments[] = count($keys); + $this->arguments = array_merge($this->arguments, $keys); + + return $this; + } + + /** + * Filters the results to those appearing only in specific attributes of the document, like title or URL. + * + * @param array $fields + * @return $this + */ + public function inFields(array $fields): self + { + $this->arguments[] = 'INFIELDS'; + $this->arguments[] = count($fields); + $this->arguments = array_merge($this->arguments, $fields); + + return $this; + } + + /** + * Limits the attributes returned from the document. + * Num is the number of attributes following the keyword. + * If num is 0, it acts like NOCONTENT. + * Identifier is either an attribute name (for hashes and JSON) or a JSON Path expression (for JSON). + * Property is an optional name used in the result. If not provided, the identifier is used in the result. + * + * If you want to add alias property to your identifier just add "true" value in identifier enumeration, + * next value will be considered as alias to previous one. + * + * Example: 'identifier', true, 'property' => 'identifier' AS 'property' + * + * @param int $count + * @param string|bool ...$identifier + * @return $this + */ + public function addReturn(int $count, ...$identifier): self + { + $arguments = func_get_args(); + + $this->arguments[] = 'RETURN'; + + for ($i = 1, $iMax = count($arguments); $i < $iMax; $i++) { + if (true === $arguments[$i]) { + $arguments[$i] = 'AS'; + } + } + + $this->arguments = array_merge($this->arguments, $arguments); + + return $this; + } + + /** + * Returns only the sections of the attribute that contain the matched text. + * + * @param array $fields + * @param int $frags + * @param int $len + * @param string $separator + * @return $this + */ + public function summarize(array $fields = [], int $frags = 0, int $len = 0, string $separator = ''): self + { + $this->arguments[] = 'SUMMARIZE'; + + if (!empty($fields)) { + $this->arguments[] = 'FIELDS'; + $this->arguments[] = count($fields); + $this->arguments = array_merge($this->arguments, $fields); + } + + if ($frags !== 0) { + $this->arguments[] = 'FRAGS'; + $this->arguments[] = $frags; + } + + if ($len !== 0) { + $this->arguments[] = 'LEN'; + $this->arguments[] = $len; + } + + if ($separator !== '') { + $this->arguments[] = 'SEPARATOR'; + $this->arguments[] = $separator; + } + + return $this; + } + + /** + * Formats occurrences of matched text. + * + * @param array $fields + * @param string $openTag + * @param string $closeTag + * @return $this + */ + public function highlight(array $fields = [], string $openTag = '', string $closeTag = ''): self + { + $this->arguments[] = 'HIGHLIGHT'; + + if (!empty($fields)) { + $this->arguments[] = 'FIELDS'; + $this->arguments[] = count($fields); + $this->arguments = array_merge($this->arguments, $fields); + } + + if ($openTag !== '' && $closeTag !== '') { + array_push($this->arguments, 'TAGS', $openTag, $closeTag); + } + + return $this; + } + + /** + * Allows a maximum of N intervening number of unmatched offsets between phrase terms. + * In other words, the slop for exact phrases is 0. + * + * @param int $slop + * @return $this + */ + public function slop(int $slop): self + { + $this->arguments[] = 'SLOP'; + $this->arguments[] = $slop; + + return $this; + } + + /** + * Puts the query terms in the same order in the document as in the query, regardless of the offsets between them. + * Typically used in conjunction with SLOP. + * + * @return $this + */ + public function inOrder(): self + { + $this->arguments[] = 'INORDER'; + + return $this; + } + + /** + * Uses a custom query expander instead of the stemmer. + * + * @param string $expander + * @return $this + */ + public function expander(string $expander): self + { + $this->arguments[] = 'EXPANDER'; + $this->arguments[] = $expander; + + return $this; + } + + /** + * Uses a custom scoring function you define. + * + * @param string $scorer + * @return $this + */ + public function scorer(string $scorer): self + { + $this->arguments[] = 'SCORER'; + $this->arguments[] = $scorer; + + return $this; + } + + /** + * Returns a textual description of how the scores were calculated. + * Using this options requires the WITHSCORES option. + * + * @return $this + */ + public function explainScore(): self + { + $this->arguments[] = 'EXPLAINSCORE'; + + return $this; + } + + /** + * Orders the results by the value of this attribute. + * This applies to both text and numeric attributes. + * Attributes needed for SORTBY should be declared as SORTABLE in the index, in order to be available with very low latency. + * Note that this adds memory overhead. + * + * @param string $sortAttribute + * @param string $orderBy + * @return $this + */ + public function sortBy(string $sortAttribute, string $orderBy = 'asc'): self + { + $this->arguments[] = 'SORTBY'; + $this->arguments[] = $sortAttribute; + + if (in_array(strtoupper($orderBy), $this->sortingEnum)) { + $this->arguments[] = $this->sortingEnum[strtolower($orderBy)]; + } else { + $enumValues = implode(', ', array_values($this->sortingEnum)); + throw new InvalidArgumentException("Wrong order direction value given. Currently supports: {$enumValues}"); + } + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SpellcheckArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SpellcheckArguments.php new file mode 100644 index 000000000..7a6c24891 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SpellcheckArguments.php @@ -0,0 +1,59 @@ + 'INCLUDE', + 'exclude' => 'EXCLUDE', + ]; + + /** + * Is maximum Levenshtein distance for spelling suggestions (default: 1, max: 4). + * + * @return $this + */ + public function distance(int $distance): self + { + $this->arguments[] = 'DISTANCE'; + $this->arguments[] = $distance; + + return $this; + } + + /** + * Specifies an inclusion (INCLUDE) or exclusion (EXCLUDE) of a custom dictionary named {dict}. + * + * @param string $dictionary + * @param string $modifier + * @param string ...$terms + * @return $this + */ + public function terms(string $dictionary, string $modifier = 'INCLUDE', string ...$terms): self + { + if (!in_array(strtoupper($modifier), $this->termsEnum)) { + $enumValues = implode(', ', array_values($this->termsEnum)); + throw new InvalidArgumentException("Wrong modifier value given. Currently supports: {$enumValues}"); + } + + array_push($this->arguments, 'TERMS', $this->termsEnum[strtolower($modifier)], $dictionary, ...$terms); + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SugAddArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SugAddArguments.php new file mode 100644 index 000000000..c8b976978 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SugAddArguments.php @@ -0,0 +1,28 @@ +arguments[] = 'INCR'; + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SugGetArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SugGetArguments.php new file mode 100644 index 000000000..1176c7736 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SugGetArguments.php @@ -0,0 +1,41 @@ +arguments[] = 'FUZZY'; + + return $this; + } + + /** + * Limits the results to a maximum of num (default: 5). + * + * @param int $num + * @return $this + */ + public function max(int $num): self + { + array_push($this->arguments, 'MAX', $num); + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SynUpdateArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SynUpdateArguments.php new file mode 100644 index 000000000..a6b286a48 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SynUpdateArguments.php @@ -0,0 +1,17 @@ +offset = $offset; + $this->count = $count; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [self::KEYWORD, $this->offset, $this->count]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Server/To.php b/plugins/cache-redis/Predis/Command/Argument/Server/To.php new file mode 100644 index 000000000..1d77ef681 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Server/To.php @@ -0,0 +1,57 @@ +host = $host; + $this->port = $port; + $this->isForce = $isForce; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + $arguments = [self::KEYWORD, $this->host, $this->port]; + + if ($this->isForce) { + $arguments[] = self::FORCE_KEYWORD; + } + + return $arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AddArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AddArguments.php new file mode 100644 index 000000000..a9fe6f79f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AddArguments.php @@ -0,0 +1,30 @@ +arguments, 'ON_DUPLICATE', $policy); + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AlterArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AlterArguments.php new file mode 100644 index 000000000..238ebc36d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AlterArguments.php @@ -0,0 +1,17 @@ +arguments, 'RETENTION', $retentionPeriod); + + return $this; + } + + /** + * Is initial allocation size, in bytes, for the data part of each new chunk. + * + * @param int $size + * @return $this + */ + public function chunkSize(int $size): self + { + array_push($this->arguments, 'CHUNK_SIZE', $size); + + return $this; + } + + /** + * Is policy for handling insertion of multiple samples with identical timestamps. + * + * @param string $policy + * @return $this + */ + public function duplicatePolicy(string $policy = self::POLICY_BLOCK): self + { + array_push($this->arguments, 'DUPLICATE_POLICY', $policy); + + return $this; + } + + /** + * Is set of label-value pairs that represent metadata labels of the key and serve as a secondary index. + * + * @param mixed ...$labelValuePair + * @return $this + */ + public function labels(...$labelValuePair): self + { + array_push($this->arguments, 'LABELS', ...$labelValuePair); + + return $this; + } + + /** + * Specifies the series samples encoding format. + * + * @param string $encoding + * @return $this + */ + public function encoding(string $encoding = self::ENCODING_COMPRESSED): self + { + array_push($this->arguments, 'ENCODING', $encoding); + + return $this; + } + + /** + * Is used when a time series is a compaction. + * With LATEST, TS.GET reports the compacted value of the latest, possibly partial, bucket. + * + * @return $this + */ + public function latest(): self + { + $this->arguments[] = 'LATEST'; + + return $this; + } + + /** + * Includes in the reply all label-value pairs representing metadata labels of the time series. + * + * @return $this + */ + public function withLabels(): self + { + $this->arguments[] = 'WITHLABELS'; + + return $this; + } + + /** + * Returns a subset of the label-value pairs that represent metadata labels of the time series. + * + * @return $this + */ + public function selectedLabels(string ...$labels): self + { + array_push($this->arguments, 'SELECTED_LABELS', ...$labels); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CreateArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CreateArguments.php new file mode 100644 index 000000000..e47d8ef72 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CreateArguments.php @@ -0,0 +1,17 @@ +arguments, 'TIMESTAMP', $timeStamp); + + return $this; + } + + /** + * Changes data storage from compressed (default) to uncompressed. + * + * @return $this + */ + public function uncompressed(): self + { + $this->arguments[] = 'UNCOMPRESSED'; + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/InfoArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/InfoArguments.php new file mode 100644 index 000000000..1b2cec664 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/InfoArguments.php @@ -0,0 +1,43 @@ +arguments[] = 'DEBUG'; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MGetArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MGetArguments.php new file mode 100644 index 000000000..585f574e6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MGetArguments.php @@ -0,0 +1,17 @@ +arguments, 'FILTER', ...$filterExpressions); + + return $this; + } + + /** + * Splits time series into groups, each group contains time series that share the same + * value for the provided label name, then aggregates results in each group. + * + * @param string $label + * @param string $reducer + * @return $this + */ + public function groupBy(string $label, string $reducer): self + { + array_push($this->arguments, 'GROUPBY', $label, 'REDUCE', $reducer); + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/RangeArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/RangeArguments.php new file mode 100644 index 000000000..00d3772d7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/RangeArguments.php @@ -0,0 +1,85 @@ +arguments, 'FILTER_BY_TS', ...$ts); + + return $this; + } + + /** + * Filters samples by minimum and maximum values. + * + * @param int $min + * @param int $max + * @return $this + */ + public function filterByValue(int $min, int $max): self + { + array_push($this->arguments, 'FILTER_BY_VALUE', $min, $max); + + return $this; + } + + /** + * Limits the number of returned samples. + * + * @param int $count + * @return $this + */ + public function count(int $count): self + { + array_push($this->arguments, 'COUNT', $count); + + return $this; + } + + /** + * Aggregates samples into time buckets. + * + * @param string $aggregator + * @param int $bucketDuration Is duration of each bucket, in milliseconds. + * @param int $align It controls the time bucket timestamps by changing the reference timestamp on which a bucket is defined. + * @param int $bucketTimestamp Controls how bucket timestamps are reported. + * @param bool $empty Is a flag, which, when specified, reports aggregations also for empty buckets. + * @return $this + */ + public function aggregation(string $aggregator, int $bucketDuration, int $align = 0, int $bucketTimestamp = 0, bool $empty = false): self + { + if ($align > 0) { + array_push($this->arguments, 'ALIGN', $align); + } + + array_push($this->arguments, 'AGGREGATION', $aggregator, $bucketDuration); + + if ($bucketTimestamp > 0) { + array_push($this->arguments, 'BUCKETTIMESTAMP', $bucketTimestamp); + } + + if (true === $empty) { + $this->arguments[] = 'EMPTY'; + } + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Factory.php b/plugins/cache-redis/Predis/Command/Factory.php new file mode 100644 index 000000000..ed94dc572 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Factory.php @@ -0,0 +1,143 @@ +getCommandClass($commandID) === null) { + return false; + } + } + + return true; + } + + /** + * Returns the FQCN of a class that represents the specified command ID. + * + * @codeCoverageIgnore + * + * @param string $commandID Command ID + * + * @return string|null + */ + public function getCommandClass(string $commandID): ?string + { + return $this->commands[strtoupper($commandID)] ?? null; + } + + /** + * {@inheritdoc} + */ + public function create(string $commandID, array $arguments = []): CommandInterface + { + if (!$commandClass = $this->getCommandClass($commandID)) { + $commandID = strtoupper($commandID); + + throw new ClientException("Command `$commandID` is not a registered Redis command."); + } + + $command = new $commandClass(); + $command->setArguments($arguments); + + if (isset($this->processor)) { + $this->processor->process($command); + } + + return $command; + } + + /** + * Defines a command in the factory. + * + * Only classes implementing Predis\Command\CommandInterface are allowed to + * handle a command. If the command specified by its ID is already handled + * by the factory, the underlying command class is replaced by the new one. + * + * @param string $commandID Command ID + * @param string $commandClass FQCN of a class implementing Predis\Command\CommandInterface + * + * @throws InvalidArgumentException + */ + public function define(string $commandID, string $commandClass): void + { + if (!is_a($commandClass, 'Predis\Command\CommandInterface', true)) { + throw new InvalidArgumentException( + "Class $commandClass must implement Predis\Command\CommandInterface" + ); + } + + $this->commands[strtoupper($commandID)] = $commandClass; + } + + /** + * Undefines a command in the factory. + * + * When the factory already has a class handler associated to the specified + * command ID it is removed from the map of known commands. Nothing happens + * when the command is not handled by the factory. + * + * @param string $commandID Command ID + */ + public function undefine(string $commandID): void + { + unset($this->commands[strtoupper($commandID)]); + } + + /** + * Sets a command processor for processing command arguments. + * + * Command processors are used to process and transform arguments of Redis + * commands before their newly created instances are returned to the caller + * of "create()". + * + * A NULL value can be used to effectively unset any processor if previously + * set for the command factory. + * + * @param ProcessorInterface|null $processor Command processor or NULL value. + */ + public function setProcessor(?ProcessorInterface $processor): void + { + $this->processor = $processor; + } + + /** + * Returns the current command processor. + * + * @return ProcessorInterface|null + */ + public function getProcessor(): ?ProcessorInterface + { + return $this->processor; + } +} diff --git a/plugins/cache-redis/Predis/Command/FactoryInterface.php b/plugins/cache-redis/Predis/Command/FactoryInterface.php new file mode 100644 index 000000000..e81951086 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/FactoryInterface.php @@ -0,0 +1,42 @@ +setKeys($arguments, false); + } + + public function parseResponse($data) + { + $key = array_shift($data); + + if (null === $key) { + return [$key]; + } + + return array_combine([$key], [[$data[0] => $data[1]]]); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BGREWRITEAOF.php b/plugins/cache-redis/Predis/Command/Redis/BGREWRITEAOF.php new file mode 100644 index 000000000..97d443d90 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BGREWRITEAOF.php @@ -0,0 +1,37 @@ + 'CAPACITY', + 'size' => 'SIZE', + 'filters' => 'FILTERS', + 'items' => 'ITEMS', + 'expansion' => 'EXPANSION', + ]; + + public function getId() + { + return 'BF.INFO'; + } + + public function setArguments(array $arguments) + { + if (isset($arguments[1])) { + $modifier = array_pop($arguments); + + if ($modifier === '') { + parent::setArguments($arguments); + + return; + } + + if (!in_array(strtoupper($modifier), $this->modifierEnum)) { + $enumValues = implode(', ', array_keys($this->modifierEnum)); + throw new UnexpectedValueException("Argument accepts only: {$enumValues} values"); + } + + $arguments[] = $this->modifierEnum[strtolower($modifier)]; + } + + parent::setArguments($arguments); + } + + public function parseResponse($data) + { + if (count($data) > 1) { + $result = []; + + for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) { + if (array_key_exists($i + 1, $data)) { + $result[(string) $data[$i]] = $data[++$i]; + } + } + + return $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINSERT.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINSERT.php new file mode 100644 index 000000000..50e23eeaf --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINSERT.php @@ -0,0 +1,72 @@ +setNoCreate($arguments); + $arguments = $this->getArguments(); + + if (array_key_exists(5, $arguments) && $arguments[5]) { + $arguments[5] = 'NONSCALING'; + } + + $this->setItems($arguments); + $arguments = $this->getArguments(); + + $this->setExpansion($arguments); + $arguments = $this->getArguments(); + + $this->setErrorRate($arguments); + $arguments = $this->getArguments(); + + $this->setCapacity($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFLOADCHUNK.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFLOADCHUNK.php new file mode 100644 index 000000000..76a78e997 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFLOADCHUNK.php @@ -0,0 +1,28 @@ +setExpansion($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFSCANDUMP.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFSCANDUMP.php new file mode 100644 index 000000000..f8576d2d0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFSCANDUMP.php @@ -0,0 +1,29 @@ +getArguments(), CASE_UPPER); + + switch (strtoupper($args[0])) { + case 'LIST': + return $this->parseClientList($data); + case 'KILL': + case 'GETNAME': + case 'SETNAME': + default: + return $data; + } // @codeCoverageIgnore + } + + /** + * Parses the response to CLIENT LIST and returns a structured list. + * + * @param string $data Response buffer. + * + * @return array + */ + protected function parseClientList($data) + { + $clients = []; + + foreach (explode("\n", $data, -1) as $clientData) { + $client = []; + + foreach (explode(' ', $clientData) as $kv) { + @[$k, $v] = explode('=', $kv); + $client[$k] = $v; + } + + $clients[] = $client; + } + + return $clients; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CLUSTER.php b/plugins/cache-redis/Predis/Command/Redis/CLUSTER.php new file mode 100644 index 000000000..3bbb77cc8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CLUSTER.php @@ -0,0 +1,26 @@ +setDB($arguments); + $arguments = $this->getArguments(); + + $this->setReplace($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/ACL.php b/plugins/cache-redis/Predis/Command/Redis/Container/ACL.php new file mode 100644 index 000000000..2699d37e8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/ACL.php @@ -0,0 +1,28 @@ +client = $client; + } + + /** + * {@inheritDoc} + */ + public function __call(string $subcommandID, array $arguments) + { + array_unshift($arguments, strtoupper($subcommandID)); + + return $this->client->executeCommand( + $this->client->createCommand($this->getContainerCommandId(), $arguments) + ); + } + + abstract public function getContainerCommandId(): string; +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/CLUSTER.php b/plugins/cache-redis/Predis/Command/Redis/Container/CLUSTER.php new file mode 100644 index 000000000..b2925b19b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/CLUSTER.php @@ -0,0 +1,29 @@ + FunctionContainer::class, + ]; + + /** + * Creates container command. + * + * @param ClientInterface $client + * @param string $containerCommandID + * @return ContainerInterface + */ + public static function create(ClientInterface $client, string $containerCommandID): ContainerInterface + { + $containerCommandID = strtoupper($containerCommandID); + $commandModule = self::resolveCommandModuleByPrefix($containerCommandID); + + if (null !== $commandModule) { + if (class_exists($containerClass = self::CONTAINER_NAMESPACE . '\\' . $commandModule . '\\' . $containerCommandID)) { + return new $containerClass($client); + } + + throw new UnexpectedValueException('Given module container command is not supported.'); + } + + if (class_exists($containerClass = self::CONTAINER_NAMESPACE . '\\' . $containerCommandID)) { + return new $containerClass($client); + } + + if (array_key_exists($containerCommandID, self::$specialMappings)) { + $containerClass = self::$specialMappings[$containerCommandID]; + + return new $containerClass($client); + } + + throw new UnexpectedValueException('Given container command is not supported.'); + } + + /** + * @param string $commandID + * @return string|null + */ + private static function resolveCommandModuleByPrefix(string $commandID): ?string + { + $modules = ClientConfiguration::getModules(); + + foreach ($modules as $module) { + if (preg_match("/^{$module['commandPrefix']}/", $commandID)) { + return $module['name']; + } + } + + return null; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/ContainerInterface.php b/plugins/cache-redis/Predis/Command/Redis/Container/ContainerInterface.php new file mode 100644 index 000000000..e30c539e3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/ContainerInterface.php @@ -0,0 +1,33 @@ + 1) { + $result = []; + + for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) { + if (array_key_exists($i + 1, $data)) { + $result[(string) $data[$i]] = $data[++$i]; + } + } + + return $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM.php b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM.php new file mode 100644 index 000000000..8f1f23ff3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM.php @@ -0,0 +1,28 @@ + 1) { + $result = []; + + for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) { + if (array_key_exists($i + 1, $data)) { + $result[(string) $data[$i]] = $data[++$i]; + } + } + + return $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERT.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERT.php new file mode 100644 index 000000000..3a49a38d7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERT.php @@ -0,0 +1,52 @@ +setNoCreate($arguments); + $arguments = $this->getArguments(); + + $this->setItems($arguments); + $arguments = $this->getArguments(); + + $this->setCapacity($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERTNX.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERTNX.php new file mode 100644 index 000000000..629327b4f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERTNX.php @@ -0,0 +1,27 @@ +setExpansion($arguments); + $arguments = $this->getArguments(); + + $this->setMaxIterations($arguments); + $arguments = $this->getArguments(); + + $this->setBucketSize($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFSCANDUMP.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFSCANDUMP.php new file mode 100644 index 000000000..59caa651c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFSCANDUMP.php @@ -0,0 +1,29 @@ +getArgument(0); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/EVALSHA_RO.php b/plugins/cache-redis/Predis/Command/Redis/EVALSHA_RO.php new file mode 100644 index 000000000..809a08757 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EVALSHA_RO.php @@ -0,0 +1,27 @@ +getArgument(0)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/EVAL_RO.php b/plugins/cache-redis/Predis/Command/Redis/EVAL_RO.php new file mode 100644 index 000000000..cef8bd35e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EVAL_RO.php @@ -0,0 +1,34 @@ +setTimeout($arguments); + $arguments = $this->getArguments(); + + $this->setTo($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/FCALL.php b/plugins/cache-redis/Predis/Command/Redis/FCALL.php new file mode 100644 index 000000000..cc5639321 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/FCALL.php @@ -0,0 +1,33 @@ + 2) { + for ($i = 2, $iMax = count($arguments); $i < $iMax; $i++) { + $processedArguments[] = $arguments[$i]; + } + } + + parent::setArguments($processedArguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/FLUSHALL.php b/plugins/cache-redis/Predis/Command/Redis/FLUSHALL.php new file mode 100644 index 000000000..a03a3133d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/FLUSHALL.php @@ -0,0 +1,29 @@ +strategyResolver = new SubcommandStrategyResolver(); + } + + public function getId() + { + return 'FUNCTION'; + } + + public function setArguments(array $arguments) + { + $strategy = $this->strategyResolver->resolve('functions', strtolower($arguments[0])); + $arguments = $strategy->processArguments($arguments); + + parent::setArguments($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GEOADD.php b/plugins/cache-redis/Predis/Command/Redis/GEOADD.php new file mode 100644 index 000000000..56156b72b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GEOADD.php @@ -0,0 +1,43 @@ +setSorting($arguments); + $arguments = $this->getArguments(); + + $this->setWithCoord($arguments); + $arguments = $this->getArguments(); + + $this->setWithDist($arguments); + $arguments = $this->getArguments(); + + $this->setWithHash($arguments); + $arguments = $this->getArguments(); + + $this->setCount($arguments, $arguments[5] ?? false); + $arguments = $this->getArguments(); + + $this->setFrom($arguments); + $arguments = $this->getArguments(); + + $this->setBy($arguments); + $this->filterArguments(); + } + + public function parseResponse($data) + { + $parsedData = []; + $itemKey = ''; + + foreach ($data as $item) { + if (!is_array($item)) { + $parsedData[] = $item; + continue; + } + + foreach ($item as $key => $itemRow) { + if ($key === 0) { + $itemKey = $itemRow; + continue; + } + + if (is_string($itemRow)) { + $parsedData[$itemKey]['dist'] = round((float) $itemRow, 5); + } elseif (is_int($itemRow)) { + $parsedData[$itemKey]['hash'] = $itemRow; + } else { + $parsedData[$itemKey]['lng'] = round($itemRow[0], 5); + $parsedData[$itemKey]['lat'] = round($itemRow[1], 5); + } + } + } + + return $parsedData; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GEOSEARCHSTORE.php b/plugins/cache-redis/Predis/Command/Redis/GEOSEARCHSTORE.php new file mode 100644 index 000000000..6798db7ed --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GEOSEARCHSTORE.php @@ -0,0 +1,71 @@ +setStoreDist($arguments); + $arguments = $this->getArguments(); + + $this->setCount($arguments, $arguments[6] ?? false); + $arguments = $this->getArguments(); + + $this->setSorting($arguments); + $arguments = $this->getArguments(); + + $this->setFrom($arguments); + $arguments = $this->getArguments(); + + $this->setBy($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GET.php b/plugins/cache-redis/Predis/Command/Redis/GET.php new file mode 100644 index 000000000..e9177ab28 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GET.php @@ -0,0 +1,29 @@ + 'EX', + 'px' => 'PX', + 'exat' => 'EXAT', + 'pxat' => 'PXAT', + 'persist' => 'PERSIST', + ]; + + public function getId() + { + return 'GETEX'; + } + + public function setArguments(array $arguments) + { + if (!array_key_exists(1, $arguments) || $arguments[1] === '') { + parent::setArguments([$arguments[0]]); + + return; + } + + if (!in_array(strtoupper($arguments[1]), self::$modifierEnum)) { + $enumValues = implode(', ', array_keys(self::$modifierEnum)); + throw new UnexpectedValueException("Modifier argument accepts only: {$enumValues} values"); + } + + if ($arguments[1] === 'persist') { + parent::setArguments([$arguments[0], self::$modifierEnum[$arguments[1]]]); + + return; + } + + $arguments[1] = self::$modifierEnum[$arguments[1]]; + + if (!array_key_exists(2, $arguments)) { + throw new UnexpectedValueException('You should provide value for current modifier'); + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GETRANGE.php b/plugins/cache-redis/Predis/Command/Redis/GETRANGE.php new file mode 100644 index 000000000..c6feb408b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GETRANGE.php @@ -0,0 +1,29 @@ + $v) { + $flattenedKVs[] = $k; + $flattenedKVs[] = $v; + } + + $arguments = $flattenedKVs; + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HRANDFIELD.php b/plugins/cache-redis/Predis/Command/Redis/HRANDFIELD.php new file mode 100644 index 000000000..62ce7dbe9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HRANDFIELD.php @@ -0,0 +1,53 @@ +prepareOptions(array_pop($arguments)); + $arguments = array_merge($arguments, $options); + } + + parent::setArguments($arguments); + } + + /** + * Returns a list of options and modifiers compatible with Redis. + * + * @param array $options List of options. + * + * @return array + */ + protected function prepareOptions($options) + { + $options = array_change_key_case($options, CASE_UPPER); + $normalized = []; + + if (!empty($options['MATCH'])) { + $normalized[] = 'MATCH'; + $normalized[] = $options['MATCH']; + } + + if (!empty($options['COUNT'])) { + $normalized[] = 'COUNT'; + $normalized[] = $options['COUNT']; + } + + return $normalized; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (is_array($data)) { + $fields = $data[1]; + $result = []; + + for ($i = 0; $i < count($fields); ++$i) { + $result[$fields[$i]] = $fields[++$i]; + } + + $data[1] = $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/HSET.php b/plugins/cache-redis/Predis/Command/Redis/HSET.php new file mode 100644 index 000000000..662094a3c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HSET.php @@ -0,0 +1,29 @@ +parseNewResponseFormat($lines); + } else { + return $this->parseOldResponseFormat($lines); + } + } + + /** + * {@inheritdoc} + */ + public function parseNewResponseFormat($lines) + { + $info = []; + $current = null; + + foreach ($lines as $row) { + if ($row === '') { + continue; + } + + if (preg_match('/^# (\w+)$/', $row, $matches)) { + $info[$matches[1]] = []; + $current = &$info[$matches[1]]; + continue; + } + + [$k, $v] = $this->parseRow($row); + $current[$k] = $v; + } + + return $info; + } + + /** + * {@inheritdoc} + */ + public function parseOldResponseFormat($lines) + { + $info = []; + + foreach ($lines as $row) { + if (strpos($row, ':') === false) { + continue; + } + + [$k, $v] = $this->parseRow($row); + $info[$k] = $v; + } + + return $info; + } + + /** + * Parses a single row of the response and returns the key-value pair. + * + * @param string $row Single row of the response. + * + * @return array + */ + protected function parseRow($row) + { + if (preg_match('/^module:name/', $row)) { + return $this->parseModuleRow($row); + } + + [$k, $v] = explode(':', $row, 2); + + if (preg_match('/^db\d+$/', $k)) { + $v = $this->parseDatabaseStats($v); + } + + return [$k, $v]; + } + + /** + * Extracts the statistics of each logical DB from the string buffer. + * + * @param string $str Response buffer. + * + * @return array + */ + protected function parseDatabaseStats($str) + { + $db = []; + + foreach (explode(',', $str) as $dbvar) { + [$dbvk, $dbvv] = explode('=', $dbvar); + $db[trim($dbvk)] = $dbvv; + } + + return $db; + } + + /** + * Parsing module rows because of different format. + * + * @param string $row + * @return array + */ + protected function parseModuleRow(string $row): array + { + [$moduleKeyword, $moduleData] = explode(':', $row); + $explodedData = explode(',', $moduleData); + $parsedData = []; + + foreach ($explodedData as $moduleDataRow) { + [$k, $v] = explode('=', $moduleDataRow); + + if ($k === 'name') { + $parsedData[0] = $v; + continue; + } + + $parsedData[1][$k] = $v; + } + + return $parsedData; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRAPPEND.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRAPPEND.php new file mode 100644 index 000000000..1b2a92036 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRAPPEND.php @@ -0,0 +1,28 @@ +setSpace($arguments); + $arguments = $this->getArguments(); + + $this->setNewline($arguments); + $arguments = $this->getArguments(); + + $this->setIndent($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONMERGE.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONMERGE.php new file mode 100644 index 000000000..a13222833 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONMERGE.php @@ -0,0 +1,29 @@ +setSubcommand($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRAPPEND.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRAPPEND.php new file mode 100644 index 000000000..9b11458e4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRAPPEND.php @@ -0,0 +1,28 @@ +filterArguments(); + } + + public function parseResponse($data) + { + if (is_array($data)) { + if ($data !== array_values($data)) { + return $data; // Relay + } + + return [$data[0] => $data[1], $data[2] => $data[3]]; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/LINDEX.php b/plugins/cache-redis/Predis/Command/Redis/LINDEX.php new file mode 100644 index 000000000..80510e1e4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LINDEX.php @@ -0,0 +1,29 @@ +setCount($arguments); + $arguments = $this->getArguments(); + + $this->setLeftRight($arguments); + $arguments = $this->getArguments(); + + $this->setKeys($arguments); + $this->filterArguments(); + } + + public function parseResponse($data) + { + if (null === $data) { + return null; + } + + return [$data[0] => $data[1]]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/LPOP.php b/plugins/cache-redis/Predis/Command/Redis/LPOP.php new file mode 100644 index 000000000..d375bacaf --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LPOP.php @@ -0,0 +1,29 @@ + $value) { + $modifier = strtoupper($modifier); + + if ($modifier === 'COPY' && $value == true) { + $arguments[] = $modifier; + } + + if ($modifier === 'REPLACE' && $value == true) { + $arguments[] = $modifier; + } + } + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/MONITOR.php b/plugins/cache-redis/Predis/Command/Redis/MONITOR.php new file mode 100644 index 000000000..06e9e59b6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/MONITOR.php @@ -0,0 +1,29 @@ + $v) { + $flattenedKVs[] = $k; + $flattenedKVs[] = $v; + } + + $arguments = $flattenedKVs; + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/MSETNX.php b/plugins/cache-redis/Predis/Command/Redis/MSETNX.php new file mode 100644 index 000000000..94d9ce269 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/MSETNX.php @@ -0,0 +1,27 @@ +getArgument(0))) { + case 'numsub': + return self::processNumsub($data); + + default: + return $data; + } + } + + /** + * Returns the processed response to PUBSUB NUMSUB. + * + * @param array $channels List of channels + * + * @return array + */ + protected static function processNumsub(array $channels) + { + $processed = []; + $count = count($channels); + + for ($i = 0; $i < $count; ++$i) { + $processed[$channels[$i]] = $channels[++$i]; + } + + return $processed; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/PUNSUBSCRIBE.php b/plugins/cache-redis/Predis/Command/Redis/PUNSUBSCRIBE.php new file mode 100644 index 000000000..f15a39fbb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PUNSUBSCRIBE.php @@ -0,0 +1,39 @@ +prepareOptions(array_pop($arguments)); + $arguments = array_merge($arguments, $options); + } + + parent::setArguments($arguments); + } + + /** + * Returns a list of options and modifiers compatible with Redis. + * + * @param array $options List of options. + * + * @return array + */ + protected function prepareOptions($options) + { + $options = array_change_key_case($options, CASE_UPPER); + $normalized = []; + + if (!empty($options['MATCH'])) { + $normalized[] = 'MATCH'; + $normalized[] = $options['MATCH']; + } + + if (!empty($options['COUNT'])) { + $normalized[] = 'COUNT'; + $normalized[] = $options['COUNT']; + } + + return $normalized; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SCARD.php b/plugins/cache-redis/Predis/Command/Redis/SCARD.php new file mode 100644 index 000000000..daf1393da --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SCARD.php @@ -0,0 +1,29 @@ +getArgument(0); + $argument = is_null($argument) ? null : strtolower($argument); + + switch ($argument) { + case 'masters': + case 'slaves': + return self::processMastersOrSlaves($data); + + default: + return $data; + } + } + + /** + * Returns a processed response to SENTINEL MASTERS or SENTINEL SLAVES. + * + * @param array $servers List of Redis servers. + * + * @return array + */ + protected static function processMastersOrSlaves(array $servers) + { + foreach ($servers as $idx => $node) { + $processed = []; + $count = count($node); + + for ($i = 0; $i < $count; ++$i) { + $processed[$node[$i]] = $node[++$i]; + } + + $servers[$idx] = $processed; + } + + return $servers; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SET.php b/plugins/cache-redis/Predis/Command/Redis/SET.php new file mode 100644 index 000000000..f8956f40b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SET.php @@ -0,0 +1,29 @@ +setLimit($arguments); + $arguments = $this->getArguments(); + + $this->setKeys($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SINTERSTORE.php b/plugins/cache-redis/Predis/Command/Redis/SINTERSTORE.php new file mode 100644 index 000000000..144335a1f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SINTERSTORE.php @@ -0,0 +1,41 @@ + $entry) { + $log[$index] = [ + 'id' => $entry[0], + 'timestamp' => $entry[1], + 'duration' => $entry[2], + 'command' => $entry[3], + ]; + } + + return $log; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SMEMBERS.php b/plugins/cache-redis/Predis/Command/Redis/SMEMBERS.php new file mode 100644 index 000000000..8f32be40e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SMEMBERS.php @@ -0,0 +1,29 @@ +setSorting($arguments); + $arguments = $this->getArguments(); + + $this->setGetArgument($arguments); + $arguments = $this->getArguments(); + + $this->setLimit($arguments); + $arguments = $this->getArguments(); + + $this->setBy($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/SPOP.php b/plugins/cache-redis/Predis/Command/Redis/SPOP.php new file mode 100644 index 000000000..e09a3d55a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SPOP.php @@ -0,0 +1,29 @@ +prepareOptions(array_pop($arguments)); + $arguments = array_merge($arguments, $options); + } + + parent::setArguments($arguments); + } + + /** + * Returns a list of options and modifiers compatible with Redis. + * + * @param array $options List of options. + * + * @return array + */ + protected function prepareOptions($options) + { + $options = array_change_key_case($options, CASE_UPPER); + $normalized = []; + + if (!empty($options['MATCH'])) { + $normalized[] = 'MATCH'; + $normalized[] = $options['MATCH']; + } + + if (!empty($options['COUNT'])) { + $normalized[] = 'COUNT'; + $normalized[] = $options['COUNT']; + } + + return $normalized; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/STRLEN.php b/plugins/cache-redis/Predis/Command/Redis/STRLEN.php new file mode 100644 index 000000000..f51775662 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/STRLEN.php @@ -0,0 +1,29 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$index, $query], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASADD.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASADD.php new file mode 100644 index 000000000..a2511ea32 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASADD.php @@ -0,0 +1,28 @@ +toArray() : []; + + $schema = array_reduce($schema, static function (array $carry, FieldInterface $field) { + return array_merge($carry, $field->toArray()); + }, []); + + array_unshift($schema, 'SCHEMA', 'ADD'); + + parent::setArguments(array_merge( + [$index], + $commandArguments, + $schema + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTCONFIG.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTCONFIG.php new file mode 100644 index 000000000..9cba60e05 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTCONFIG.php @@ -0,0 +1,30 @@ +toArray() : []; + + $schema = array_reduce($schema, static function (array $carry, FieldInterface $field) { + return array_merge($carry, $field->toArray()); + }, []); + + array_unshift($schema, 'SCHEMA'); + + parent::setArguments(array_merge( + [$index], + $commandArguments, + $schema + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTCURSOR.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTCURSOR.php new file mode 100644 index 000000000..14a01948a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTCURSOR.php @@ -0,0 +1,34 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$subcommand, $index, $cursorId], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTADD.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTADD.php new file mode 100644 index 000000000..c0bc3da72 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTADD.php @@ -0,0 +1,28 @@ +toArray(); + } + + parent::setArguments(array_merge( + [$index], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTEXPLAIN.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTEXPLAIN.php new file mode 100644 index 000000000..e2aa35d47 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTEXPLAIN.php @@ -0,0 +1,43 @@ +toArray(); + } + + parent::setArguments(array_merge( + [$index, $query], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTINFO.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTINFO.php new file mode 100644 index 000000000..02fa95974 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTINFO.php @@ -0,0 +1,28 @@ +toArray() + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSEARCH.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSEARCH.php new file mode 100644 index 000000000..9fbe8a563 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSEARCH.php @@ -0,0 +1,39 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$index, $query], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSPELLCHECK.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSPELLCHECK.php new file mode 100644 index 000000000..ac0232bb2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSPELLCHECK.php @@ -0,0 +1,38 @@ +toArray(); + } + + parent::setArguments(array_merge( + [$index, $query], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGADD.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGADD.php new file mode 100644 index 000000000..71c42a4a3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGADD.php @@ -0,0 +1,39 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $string, $score], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGDEL.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGDEL.php new file mode 100644 index 000000000..15457d10b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGDEL.php @@ -0,0 +1,28 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $prefix], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGLEN.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGLEN.php new file mode 100644 index 000000000..1e11d8d30 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGLEN.php @@ -0,0 +1,28 @@ +toArray(); + } + + $terms = array_slice($arguments, 3); + + parent::setArguments(array_merge( + [$index, $synonymGroupId], + $commandArguments, + $terms + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTTAGVALS.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTTAGVALS.php new file mode 100644 index 000000000..b323949fe --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTTAGVALS.php @@ -0,0 +1,28 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $timestamp, $value], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSALTER.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSALTER.php new file mode 100644 index 000000000..8797d68a3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSALTER.php @@ -0,0 +1,39 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATE.php new file mode 100644 index 000000000..0d88072c0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATE.php @@ -0,0 +1,39 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATERULE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATERULE.php new file mode 100644 index 000000000..c8bd62bcb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATERULE.php @@ -0,0 +1,40 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $value], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDEL.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDEL.php new file mode 100644 index 000000000..747c5a629 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDEL.php @@ -0,0 +1,28 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINCRBY.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINCRBY.php new file mode 100644 index 000000000..3141e5ded --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINCRBY.php @@ -0,0 +1,41 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $value], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINFO.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINFO.php new file mode 100644 index 000000000..d4941d407 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINFO.php @@ -0,0 +1,39 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMADD.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMADD.php new file mode 100644 index 000000000..085224693 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMADD.php @@ -0,0 +1,28 @@ +toArray(); + + array_push($processedArguments, 'FILTER', ...$arguments); + + parent::setArguments(array_merge( + $commandArguments, + $processedArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMRANGE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMRANGE.php new file mode 100644 index 000000000..3d68cdd99 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMRANGE.php @@ -0,0 +1,39 @@ +toArray(); + + parent::setArguments(array_merge( + [$fromTimestamp, $toTimestamp], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMREVRANGE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMREVRANGE.php new file mode 100644 index 000000000..987fd8201 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMREVRANGE.php @@ -0,0 +1,26 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $fromTimestamp, $toTimestamp], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSREVRANGE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSREVRANGE.php new file mode 100644 index 000000000..1e8768e96 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSREVRANGE.php @@ -0,0 +1,26 @@ +filterArguments(); + } + + public function parseResponse($data) + { + if ($this->isWithCountModifier()) { + $result = []; + + for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) { + if (array_key_exists($i + 1, $data)) { + $result[(string) $data[$i]] = $data[++$i]; + } + } + + return $result; + } + + return $data; + } + + /** + * Checks for the presence of the WITHCOUNT modifier. + * + * @return bool + */ + private function isWithCountModifier(): bool + { + $arguments = $this->getArguments(); + $lastArgument = (!empty($arguments)) ? $arguments[count($arguments) - 1] : null; + + return is_string($lastArgument) && strtoupper($lastArgument) === 'WITHCOUNT'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKQUERY.php b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKQUERY.php new file mode 100644 index 000000000..128902194 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKQUERY.php @@ -0,0 +1,29 @@ + $val) { + $args[] = $key; + $args[] = $val; + } + } + + parent::setArguments($args); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/XDEL.php b/plugins/cache-redis/Predis/Command/Redis/XDEL.php new file mode 100644 index 000000000..f1c509e35 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/XDEL.php @@ -0,0 +1,39 @@ + $score) { + $arguments[] = $score; + $arguments[] = $member; + } + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZCARD.php b/plugins/cache-redis/Predis/Command/Redis/ZCARD.php new file mode 100644 index 000000000..7f6cfd4d0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZCARD.php @@ -0,0 +1,29 @@ +setKeys($arguments); + $arguments = $this->getArguments(); + + $this->setWithScore($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZDIFFSTORE.php b/plugins/cache-redis/Predis/Command/Redis/ZDIFFSTORE.php new file mode 100644 index 000000000..729988372 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZDIFFSTORE.php @@ -0,0 +1,40 @@ +setLimit($arguments); + $arguments = $this->getArguments(); + + $this->setKeys($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZINTERSTORE.php b/plugins/cache-redis/Predis/Command/Redis/ZINTERSTORE.php new file mode 100644 index 000000000..7f0aa4498 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZINTERSTORE.php @@ -0,0 +1,27 @@ +setCount($arguments); + $arguments = $this->getArguments(); + + $this->resolveModifier(static::$modifierArgumentPositionOffset, $arguments); + + $this->setKeys($arguments); + $arguments = $this->getArguments(); + + parent::setArguments($arguments); + } + + public function parseResponse($data) + { + $key = array_shift($data); + + if (null === $key) { + return [$key]; + } + + $data = $data[0]; + $parsedData = []; + + for ($i = 0, $iMax = count($data); $i < $iMax; $i++) { + for ($j = 0, $jMax = count($data[$i]); $j < $jMax; ++$j) { + if ($data[$i][$j + 1] ?? false) { + $parsedData[$data[$i][$j]] = $data[$i][++$j]; + } + } + } + + return array_combine([$key], [$parsedData]); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZMSCORE.php b/plugins/cache-redis/Predis/Command/Redis/ZMSCORE.php new file mode 100644 index 000000000..2dd76fd65 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZMSCORE.php @@ -0,0 +1,34 @@ + true]; + $lastType = 'array'; + } + + if ($lastType === 'array') { + $options = $this->prepareOptions(array_pop($arguments)); + $arguments = array_merge($arguments, $options); + } + } + + parent::setArguments($arguments); + } + + /** + * Returns a list of options and modifiers compatible with Redis. + * + * @param array $options List of options. + * + * @return array + */ + protected function prepareOptions($options) + { + $opts = array_change_key_case($options, CASE_UPPER); + $finalizedOpts = []; + + if (!empty($opts['WITHSCORES'])) { + $finalizedOpts[] = 'WITHSCORES'; + } + + return $finalizedOpts; + } + + /** + * Checks for the presence of the WITHSCORES modifier. + * + * @return bool + */ + protected function withScores() + { + $arguments = $this->getArguments(); + + if (count($arguments) < 4) { + return false; + } + + return strtoupper($arguments[3]) === 'WITHSCORES'; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if ($this->withScores()) { + $result = []; + + for ($i = 0; $i < count($data); ++$i) { + if (is_array($data[$i])) { + $result[$data[$i][0]] = $data[$i][1]; // Relay + } else { + $result[$data[$i]] = $data[++$i]; + } + } + + return $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYLEX.php b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYLEX.php new file mode 100644 index 000000000..18b4a6d30 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYLEX.php @@ -0,0 +1,54 @@ +getArguments(); + + for ($i = 3; $i < count($arguments); ++$i) { + switch (strtoupper($arguments[$i])) { + case 'WITHSCORES': + return true; + + case 'LIMIT': + $i += 2; + break; + } + } + + return false; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZRANGESTORE.php b/plugins/cache-redis/Predis/Command/Redis/ZRANGESTORE.php new file mode 100644 index 000000000..4f820b06c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANGESTORE.php @@ -0,0 +1,57 @@ +setByLexByScoreArgument($arguments); + $arguments = $this->getArguments(); + + $this->setReversedArgument($arguments); + $arguments = $this->getArguments(); + + $this->setLimitArguments($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZRANK.php b/plugins/cache-redis/Predis/Command/Redis/ZRANK.php new file mode 100644 index 000000000..3c6ac2a6e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANK.php @@ -0,0 +1,29 @@ +prepareOptions(array_pop($arguments)); + $arguments = array_merge($arguments, $options); + } + + parent::setArguments($arguments); + } + + /** + * Returns a list of options and modifiers compatible with Redis. + * + * @param array $options List of options. + * + * @return array + */ + protected function prepareOptions($options) + { + $options = array_change_key_case($options, CASE_UPPER); + $normalized = []; + + if (!empty($options['MATCH'])) { + $normalized[] = 'MATCH'; + $normalized[] = $options['MATCH']; + } + + if (!empty($options['COUNT'])) { + $normalized[] = 'COUNT'; + $normalized[] = $options['COUNT']; + } + + return $normalized; + } + + /** + * {@inheritdoc} + */ + public function parseResponse($data) + { + if (is_array($data)) { + $members = $data[1]; + $result = []; + + for ($i = 0; $i < count($members); ++$i) { + $result[$members[$i]] = (float) $members[++$i]; + } + + $data[1] = $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZSCORE.php b/plugins/cache-redis/Predis/Command/Redis/ZSCORE.php new file mode 100644 index 000000000..978a17292 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZSCORE.php @@ -0,0 +1,29 @@ +setAggregate($arguments); + $arguments = $this->getArguments(); + + $this->setWeights($arguments); + $arguments = $this->getArguments(); + + $this->setKeys($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/RedisFactory.php b/plugins/cache-redis/Predis/Command/RedisFactory.php new file mode 100644 index 000000000..10c4541d5 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/RedisFactory.php @@ -0,0 +1,112 @@ +commands = [ + 'ECHO' => 'Predis\Command\Redis\ECHO_', + 'EVAL' => 'Predis\Command\Redis\EVAL_', + 'OBJECT' => 'Predis\Command\Redis\OBJECT_', + // Class name corresponds to PHP reserved word "function", added mapping to bypass restrictions + 'FUNCTION' => FUNCTIONS::class, + ]; + } + + /** + * {@inheritdoc} + */ + public function getCommandClass(string $commandID): ?string + { + $commandID = strtoupper($commandID); + + if (isset($this->commands[$commandID]) || array_key_exists($commandID, $this->commands)) { + return $this->commands[$commandID]; + } + + $commandClass = $this->resolve($commandID); + + if (null === $commandClass) { + return null; + } + + $this->commands[$commandID] = $commandClass; + + return $commandClass; + } + + /** + * {@inheritdoc} + */ + public function undefine(string $commandID): void + { + // NOTE: we explicitly associate `NULL` to the command ID in the map + // instead of the parent's `unset()` because our subclass tries to load + // a predefined class from the Predis\Command\Redis namespace when no + // explicit mapping is defined, see RedisFactory::getCommandClass() for + // details of the implementation of this mechanism. + $this->commands[strtoupper($commandID)] = null; + } + + /** + * Resolves command object from given command ID. + * + * @param string $commandID Command ID of virtual method call + * @return string|null FQDN of corresponding command object + */ + private function resolve(string $commandID): ?string + { + if (class_exists($commandClass = self::COMMANDS_NAMESPACE . '\\' . $commandID)) { + return $commandClass; + } + + $commandModule = $this->resolveCommandModuleByPrefix($commandID); + + if (null === $commandModule) { + return null; + } + + if (class_exists($commandClass = self::COMMANDS_NAMESPACE . '\\' . $commandModule . '\\' . $commandID)) { + return $commandClass; + } + + return null; + } + + private function resolveCommandModuleByPrefix(string $commandID): ?string + { + foreach (ClientConfiguration::getModules() as $module) { + if (preg_match("/^{$module['commandPrefix']}/", $commandID)) { + return $module['name']; + } + } + + return null; + } +} diff --git a/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DeleteStrategy.php b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DeleteStrategy.php new file mode 100644 index 000000000..250ae744e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DeleteStrategy.php @@ -0,0 +1,26 @@ +separator = $separator; + } + + /** + * {@inheritDoc} + */ + public function resolve(string $commandId, string $subcommandId): SubcommandStrategyInterface + { + $subcommandStrategyClass = ucwords($subcommandId) . 'Strategy'; + $commandDirectoryName = ucwords($commandId); + + if (!is_null($this->separator)) { + $subcommandStrategyClass = str_replace($this->separator, '', $subcommandStrategyClass); + $commandDirectoryName = str_replace($this->separator, '', $commandDirectoryName); + } + + if (class_exists( + $containerCommandClass = self::CONTAINER_COMMANDS_NAMESPACE . '\\' . $commandDirectoryName . '\\' . $subcommandStrategyClass + )) { + return new $containerCommandClass(); + } + + throw new InvalidArgumentException('Non-existing container command given'); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Aggregate.php b/plugins/cache-redis/Predis/Command/Traits/Aggregate.php new file mode 100644 index 000000000..c49c31085 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Aggregate.php @@ -0,0 +1,66 @@ + 'MIN', + 'max' => 'MAX', + 'sum' => 'SUM', + ]; + + /** + * @var string + */ + private static $aggregateModifier = 'AGGREGATE'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$aggregateArgumentPositionOffset >= $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$aggregateArgumentPositionOffset]; + + if (is_string($argument) && in_array(strtoupper($argument), self::$aggregateValuesEnum)) { + $argument = self::$aggregateValuesEnum[$argument]; + } else { + $enumValues = implode(', ', array_keys(self::$aggregateValuesEnum)); + throw new UnexpectedValueException("Aggregate argument accepts only: {$enumValues} values"); + } + + $argumentsBefore = array_slice($arguments, 0, static::$aggregateArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$aggregateArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$aggregateModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BitByte.php b/plugins/cache-redis/Predis/Command/Traits/BitByte.php new file mode 100644 index 000000000..067302d22 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BitByte.php @@ -0,0 +1,40 @@ + 'BIT', + 'byte' => 'BYTE', + ]; + + public function setArguments(array $arguments) + { + $value = array_pop($arguments); + + if (null === $value) { + parent::setArguments($arguments); + + return; + } + + if (in_array(strtoupper($value), self::$argumentEnum, true)) { + $arguments[] = self::$argumentEnum[$value]; + } else { + $arguments[] = $value; + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/BucketSize.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/BucketSize.php new file mode 100644 index 000000000..99e22be05 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/BucketSize.php @@ -0,0 +1,57 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$bucketSizeArgumentPositionOffset] === -1) { + array_splice($arguments, static::$bucketSizeArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$bucketSizeArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong bucket size argument value or position offset'); + } + + $argument = $arguments[static::$bucketSizeArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$bucketSizeArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$bucketSizeArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$bucketSizeModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Capacity.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Capacity.php new file mode 100644 index 000000000..c0dccc8a4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Capacity.php @@ -0,0 +1,57 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$capacityArgumentPositionOffset] === -1) { + array_splice($arguments, static::$capacityArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$capacityArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong capacity argument value or position offset'); + } + + $argument = $arguments[static::$capacityArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$capacityArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$capacityArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$capacityModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Error.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Error.php new file mode 100644 index 000000000..661def80f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Error.php @@ -0,0 +1,57 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$errorArgumentPositionOffset] === -1) { + array_splice($arguments, static::$errorArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$errorArgumentPositionOffset] < 0) { + throw new UnexpectedValueException('Wrong error argument value or position offset'); + } + + $argument = $arguments[static::$errorArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$errorArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$errorArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$errorModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Expansion.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Expansion.php new file mode 100644 index 000000000..74d916f4c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Expansion.php @@ -0,0 +1,53 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$expansionArgumentPositionOffset] === -1) { + array_splice($arguments, static::$expansionArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$expansionArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong expansion argument value or position offset'); + } + + $argument = $arguments[static::$expansionArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$expansionArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$expansionArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$expansionModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Items.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Items.php new file mode 100644 index 000000000..9d2e4dcfe --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Items.php @@ -0,0 +1,45 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$itemsArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$itemsArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$itemsArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$itemsModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/MaxIterations.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/MaxIterations.php new file mode 100644 index 000000000..fb307e6d9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/MaxIterations.php @@ -0,0 +1,57 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$maxIterationsArgumentPositionOffset] === -1) { + array_splice($arguments, static::$maxIterationsArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$maxIterationsArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong max iterations argument value or position offset'); + } + + $argument = $arguments[static::$maxIterationsArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$maxIterationsArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$maxIterationsArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$maxIterationsModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/NoCreate.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/NoCreate.php new file mode 100644 index 000000000..7fc084ec8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/NoCreate.php @@ -0,0 +1,49 @@ += $argumentsLength + || false === $arguments[static::$noCreateArgumentPositionOffset] + ) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$noCreateArgumentPositionOffset]; + + if (true === $argument) { + $argument = 'NOCREATE'; + } else { + throw new UnexpectedValueException('Wrong NOCREATE argument type'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$noCreateArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$noCreateArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/By/ByArgument.php b/plugins/cache-redis/Predis/Command/Traits/By/ByArgument.php new file mode 100644 index 000000000..99bd1722f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/By/ByArgument.php @@ -0,0 +1,40 @@ += $argumentsLength || null === $arguments[static::$byArgumentPositionOffset]) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$byArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$byArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$byArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$this->byModifier, $argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/By/ByLexByScore.php b/plugins/cache-redis/Predis/Command/Traits/By/ByLexByScore.php new file mode 100644 index 000000000..66c531584 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/By/ByLexByScore.php @@ -0,0 +1,49 @@ + 'BYLEX', + 'byscore' => 'BYSCORE', + ]; + + public function setArguments(array $arguments) + { + $argument = $arguments[static::$byLexByScoreArgumentPositionOffset]; + + if (false === $argument) { + parent::setArguments($arguments); + + return; + } + + if (is_string($argument) && in_array(strtoupper($argument), self::$argumentsEnum)) { + $argument = self::$argumentsEnum[$argument]; + } else { + throw new UnexpectedValueException('By argument accepts only "bylex" and "byscore" values'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$byLexByScoreArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$byLexByScoreArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/By/GeoBy.php b/plugins/cache-redis/Predis/Command/Traits/By/GeoBy.php new file mode 100644 index 000000000..c5ee2b7d1 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/By/GeoBy.php @@ -0,0 +1,49 @@ +getByArgumentPositionOffset($arguments); + + if (null === $argumentPositionOffset) { + throw new InvalidArgumentException('Invalid BY argument value given'); + } + + $byArgumentObject = $arguments[$argumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, $argumentPositionOffset); + $argumentsAfter = array_slice($arguments, $argumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + $byArgumentObject->toArray(), + $argumentsAfter + )); + } + + private function getByArgumentPositionOffset(array $arguments): ?int + { + foreach ($arguments as $i => $value) { + if ($value instanceof ByInterface) { + return $i; + } + } + + return null; + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Count.php b/plugins/cache-redis/Predis/Command/Traits/Count.php new file mode 100644 index 000000000..46ae489d6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Count.php @@ -0,0 +1,71 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$countArgumentPositionOffset] === -1) { + array_splice($arguments, static::$countArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$countArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong count argument value or position offset'); + } + + $countArgument = $arguments[static::$countArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$countArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$countArgumentPositionOffset + 2); + + if (!$any) { + $argumentsAfter = array_slice($arguments, static::$countArgumentPositionOffset + 1); + parent::setArguments(array_merge( + $argumentsBefore, + [$this->countModifier], + [$countArgument], + $argumentsAfter + )); + + return; + } + + parent::setArguments(array_merge( + $argumentsBefore, + [$this->countModifier], + [$countArgument], + [$this->anyModifier], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/DB.php b/plugins/cache-redis/Predis/Command/Traits/DB.php new file mode 100644 index 000000000..cf494f96a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/DB.php @@ -0,0 +1,53 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if (!is_numeric($arguments[static::$dbArgumentPositionOffset])) { + throw new UnexpectedValueException('DB argument should be a valid numeric value'); + } + + if ($arguments[static::$dbArgumentPositionOffset] < 0) { + array_splice($arguments, static::$dbArgumentPositionOffset, 1); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$dbArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$dbArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$dbArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [$this->dbModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Expire/ExpireOptions.php b/plugins/cache-redis/Predis/Command/Traits/Expire/ExpireOptions.php new file mode 100644 index 000000000..4f683f3ca --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Expire/ExpireOptions.php @@ -0,0 +1,42 @@ + 'NX', + 'xx' => 'XX', + 'gt' => 'GT', + 'lt' => 'LT', + ]; + + public function setArguments(array $arguments) + { + $value = array_pop($arguments); + + if (null === $value) { + parent::setArguments($arguments); + + return; + } + + if (in_array(strtoupper($value), self::$argumentEnum, true)) { + $arguments[] = self::$argumentEnum[strtolower($value)]; + } else { + $arguments[] = $value; + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/From/GeoFrom.php b/plugins/cache-redis/Predis/Command/Traits/From/GeoFrom.php new file mode 100644 index 000000000..22688adba --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/From/GeoFrom.php @@ -0,0 +1,49 @@ +getFromArgumentPositionOffset($arguments); + + if (null === $argumentPositionOffset) { + throw new InvalidArgumentException('Invalid FROM argument value given'); + } + + $fromArgumentObject = $arguments[$argumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, $argumentPositionOffset); + $argumentsAfter = array_slice($arguments, $argumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + $fromArgumentObject->toArray(), + $argumentsAfter + )); + } + + private function getFromArgumentPositionOffset(array $arguments): ?int + { + foreach ($arguments as $i => $value) { + if ($value instanceof FromInterface) { + return $i; + } + } + + return null; + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Get/Get.php b/plugins/cache-redis/Predis/Command/Traits/Get/Get.php new file mode 100644 index 000000000..256676e9e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Get/Get.php @@ -0,0 +1,47 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if (!is_array($arguments[static::$getArgumentPositionOffset])) { + throw new UnexpectedValueException('Wrong get argument type'); + } + + $patterns = []; + + foreach ($arguments[static::$getArgumentPositionOffset] as $pattern) { + $patterns[] = self::$getModifier; + $patterns[] = $pattern; + } + + $argumentsBeforeKeys = array_slice($arguments, 0, static::$getArgumentPositionOffset); + $argumentsAfterKeys = array_slice($arguments, static::$getArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBeforeKeys, $patterns, $argumentsAfterKeys)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Json/Indent.php b/plugins/cache-redis/Predis/Command/Traits/Json/Indent.php new file mode 100644 index 000000000..3e0dfb133 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Json/Indent.php @@ -0,0 +1,54 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$indentArgumentPositionOffset] === '') { + array_splice($arguments, static::$indentArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$indentArgumentPositionOffset]; + + if (!is_string($argument)) { + throw new UnexpectedValueException('Indent argument value should be a string'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$indentArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$indentArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$indentModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Json/Newline.php b/plugins/cache-redis/Predis/Command/Traits/Json/Newline.php new file mode 100644 index 000000000..7bab8205b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Json/Newline.php @@ -0,0 +1,54 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$newlineArgumentPositionOffset] === '') { + array_splice($arguments, static::$newlineArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$newlineArgumentPositionOffset]; + + if (!is_string($argument)) { + throw new UnexpectedValueException('Newline argument value should be a string'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$newlineArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$newlineArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$newlineModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Json/NxXxArgument.php b/plugins/cache-redis/Predis/Command/Traits/Json/NxXxArgument.php new file mode 100644 index 000000000..39d3cb234 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Json/NxXxArgument.php @@ -0,0 +1,64 @@ + 'NX', + 'xx' => 'XX', + ]; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$nxXxArgumentPositionOffset >= $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if (null === $arguments[static::$nxXxArgumentPositionOffset]) { + array_splice($arguments, static::$nxXxArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$nxXxArgumentPositionOffset]; + + if (!in_array(strtoupper($argument), self::$argumentEnum, true)) { + $enumValues = implode(', ', array_keys(self::$argumentEnum)); + throw new UnexpectedValueException("Argument accepts only: {$enumValues} values"); + } + + $argumentsBefore = array_slice($arguments, 0, static::$nxXxArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$nxXxArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$argumentEnum[strtolower($argument)]], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Json/Space.php b/plugins/cache-redis/Predis/Command/Traits/Json/Space.php new file mode 100644 index 000000000..5c99828f4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Json/Space.php @@ -0,0 +1,54 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$spaceArgumentPositionOffset] === '') { + array_splice($arguments, static::$spaceArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$spaceArgumentPositionOffset]; + + if (!is_string($argument)) { + throw new UnexpectedValueException('Space argument value should be a string'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$spaceArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$spaceArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$spaceModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Keys.php b/plugins/cache-redis/Predis/Command/Traits/Keys.php new file mode 100644 index 000000000..5dc86ad7d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Keys.php @@ -0,0 +1,47 @@ + $argumentsLength + || !is_array($arguments[static::$keysArgumentPositionOffset]) + ) { + throw new UnexpectedValueException('Wrong keys argument type or position offset'); + } + + $keysArgument = $arguments[static::$keysArgumentPositionOffset]; + $argumentsBeforeKeys = array_slice($arguments, 0, static::$keysArgumentPositionOffset); + $argumentsAfterKeys = array_slice($arguments, static::$keysArgumentPositionOffset + 1); + + if ($withNumkeys) { + $numkeys = count($keysArgument); + parent::setArguments(array_merge($argumentsBeforeKeys, [$numkeys], $keysArgument, $argumentsAfterKeys)); + + return; + } + + parent::setArguments(array_merge($argumentsBeforeKeys, $keysArgument, $argumentsAfterKeys)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/LeftRight.php b/plugins/cache-redis/Predis/Command/Traits/LeftRight.php new file mode 100644 index 000000000..181fbd143 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/LeftRight.php @@ -0,0 +1,60 @@ + 'LEFT', + 'right' => 'RIGHT', + ]; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$leftRightArgumentPositionOffset >= $argumentsLength) { + $arguments[] = 'LEFT'; + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$leftRightArgumentPositionOffset]; + + if (is_string($argument) && in_array(strtoupper($argument), self::$leftRightEnum, true)) { + $argument = self::$leftRightEnum[$argument]; + } else { + $enumValues = implode(', ', array_keys(self::$leftRightEnum)); + throw new UnexpectedValueException("Left/Right argument accepts only: {$enumValues} values"); + } + + $argumentsBefore = array_slice($arguments, 0, static::$leftRightArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$leftRightArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Limit/Limit.php b/plugins/cache-redis/Predis/Command/Traits/Limit/Limit.php new file mode 100644 index 000000000..e24499472 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Limit/Limit.php @@ -0,0 +1,54 @@ += $argumentsLength + || false === $arguments[static::$limitArgumentPositionOffset] + ) { + parent::setArguments($argumentsBefore); + + return; + } + + $argument = $arguments[static::$limitArgumentPositionOffset]; + $argumentsAfter = array_slice($arguments, static::$limitArgumentPositionOffset + 1); + + if (true === $argument) { + parent::setArguments(array_merge($argumentsBefore, [self::$limitModifier], $argumentsAfter)); + + return; + } + + if (!is_int($argument)) { + throw new UnexpectedValueException('Wrong limit argument type'); + } + + parent::setArguments(array_merge($argumentsBefore, [self::$limitModifier], [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Limit/LimitObject.php b/plugins/cache-redis/Predis/Command/Traits/Limit/LimitObject.php new file mode 100644 index 000000000..3e47de9ab --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Limit/LimitObject.php @@ -0,0 +1,50 @@ +getLimitArgumentPositionOffset($arguments); + + if (null === $argumentPositionOffset) { + parent::setArguments($arguments); + + return; + } + + $limitObject = $arguments[$argumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, $argumentPositionOffset); + $argumentsAfter = array_slice($arguments, $argumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + $limitObject->toArray(), + $argumentsAfter + )); + } + + private function getLimitArgumentPositionOffset(array $arguments): ?int + { + foreach ($arguments as $i => $value) { + if ($value instanceof LimitInterface) { + return $i; + } + } + + return null; + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/MinMaxModifier.php b/plugins/cache-redis/Predis/Command/Traits/MinMaxModifier.php new file mode 100644 index 000000000..f48f4c1e8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/MinMaxModifier.php @@ -0,0 +1,45 @@ + 'MIN', + 'max' => 'MAX', + ]; + + public function resolveModifier(int $offset, array &$arguments): void + { + if ($offset >= count($arguments)) { + $arguments[$offset] = $this->modifierEnum['min']; + + return; + } + + if (!is_string($arguments[$offset]) || !array_key_exists($arguments[$offset], $this->modifierEnum)) { + throw new UnexpectedValueException('Wrong type of modifier given'); + } + + $arguments[$offset] = $this->modifierEnum[$arguments[$offset]]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Replace.php b/plugins/cache-redis/Predis/Command/Traits/Replace.php new file mode 100644 index 000000000..d193d66d3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Replace.php @@ -0,0 +1,34 @@ + 'ASC', + 'desc' => 'DESC', + ]; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$sortArgumentPositionOffset >= $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$sortArgumentPositionOffset]; + + if (null === $argument) { + array_splice($arguments, static::$sortArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if (!in_array(strtoupper($argument), self::$sortingEnum, true)) { + $enumValues = implode(', ', array_keys(self::$sortingEnum)); + throw new UnexpectedValueException("Sorting argument accepts only: {$enumValues} values"); + } + + $argumentsBefore = array_slice($arguments, 0, static::$sortArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$sortArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$sortingEnum[$argument]], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Storedist.php b/plugins/cache-redis/Predis/Command/Traits/Storedist.php new file mode 100644 index 000000000..6feed1e8c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Storedist.php @@ -0,0 +1,49 @@ += $argumentsLength + || false === $arguments[static::$storeDistArgumentPositionOffset] + ) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$storeDistArgumentPositionOffset]; + + if (true === $argument) { + $argument = 'STOREDIST'; + } else { + throw new UnexpectedValueException('Wrong STOREDIST argument type'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$storeDistArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$storeDistArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Timeout.php b/plugins/cache-redis/Predis/Command/Traits/Timeout.php new file mode 100644 index 000000000..fd33ea9cf --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Timeout.php @@ -0,0 +1,53 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$timeoutArgumentPositionOffset] === -1) { + array_splice($arguments, static::$timeoutArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$timeoutArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong timeout argument value or position offset'); + } + + $argument = $arguments[static::$timeoutArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$timeoutArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$timeoutArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$timeoutModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/To/ServerTo.php b/plugins/cache-redis/Predis/Command/Traits/To/ServerTo.php new file mode 100644 index 000000000..1ab13eca9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/To/ServerTo.php @@ -0,0 +1,48 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + /** @var To|null $toArgument */ + $toArgument = $arguments[static::$toArgumentPositionOffset]; + + if (null === $toArgument) { + array_splice($arguments, static::$toArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argumentsBefore = array_slice($arguments, 0, static::$toArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$toArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + $toArgument->toArray(), + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Weights.php b/plugins/cache-redis/Predis/Command/Traits/Weights.php new file mode 100644 index 000000000..1f175ed8b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Weights.php @@ -0,0 +1,61 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if (!is_array($arguments[static::$weightsArgumentPositionOffset])) { + throw new UnexpectedValueException('Wrong weights argument type'); + } + + $weightsArray = $arguments[static::$weightsArgumentPositionOffset]; + + if (empty($weightsArray)) { + unset($arguments[static::$weightsArgumentPositionOffset]); + parent::setArguments($arguments); + + return; + } + + $argumentsBefore = array_slice($arguments, 0, static::$weightsArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$weightsArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$weightsModifier], + $weightsArray, + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithCoord.php b/plugins/cache-redis/Predis/Command/Traits/With/WithCoord.php new file mode 100644 index 000000000..797ae2e52 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithCoord.php @@ -0,0 +1,49 @@ += $argumentsLength + || false === $arguments[static::$withCoordArgumentPositionOffset] + ) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$withCoordArgumentPositionOffset]; + + if (true === $argument) { + $argument = 'WITHCOORD'; + } else { + throw new UnexpectedValueException('Wrong WITHCOORD argument type'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$withCoordArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$withCoordArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithDist.php b/plugins/cache-redis/Predis/Command/Traits/With/WithDist.php new file mode 100644 index 000000000..479606dca --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithDist.php @@ -0,0 +1,45 @@ += $argumentsLength + || false === $arguments[static::$withDistArgumentPositionOffset] + ) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$withDistArgumentPositionOffset]; + + if (true === $argument) { + $argument = 'WITHDIST'; + } else { + throw new UnexpectedValueException('Wrong WITHDIST argument type'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$withDistArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$withDistArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithHash.php b/plugins/cache-redis/Predis/Command/Traits/With/WithHash.php new file mode 100644 index 000000000..c00f680b2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithHash.php @@ -0,0 +1,45 @@ += $argumentsLength + || false === $arguments[static::$withHashArgumentPositionOffset] + ) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$withHashArgumentPositionOffset]; + + if (true === $argument) { + $argument = 'WITHHASH'; + } else { + throw new UnexpectedValueException('Wrong WITHHASH argument type'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$withHashArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$withHashArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithScores.php b/plugins/cache-redis/Predis/Command/Traits/With/WithScores.php new file mode 100644 index 000000000..bc81d36c9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithScores.php @@ -0,0 +1,68 @@ +isWithScoreModifier()) { + $result = []; + + for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) { + if (is_array($data[$i])) { + $result[$data[$i][0]] = $data[$i][1]; // Relay + } elseif (array_key_exists($i + 1, $data)) { + $result[$data[$i]] = $data[++$i]; + } + } + + return $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithValues.php b/plugins/cache-redis/Predis/Command/Traits/With/WithValues.php new file mode 100644 index 000000000..4efb06584 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithValues.php @@ -0,0 +1,34 @@ +getConnectionInitializer($options, $value); + } + + /** + * Wraps a user-supplied callable used to create a new aggregate connection. + * + * When the original callable acting as a connection initializer is executed + * by the client to create a new aggregate connection, it will receive the + * following arguments: + * + * - $parameters (same as passed to Predis\Client::__construct()) + * - $options (options container, Predis\Configuration\OptionsInterface) + * - $option (current option, Predis\Configuration\OptionInterface) + * + * The original callable must return a valid aggregation connection instance + * of type Predis\Connection\AggregateConnectionInterface, this is enforced + * by the wrapper returned by this method and an exception is thrown when + * invalid values are returned. + * + * @param OptionsInterface $options Client options + * @param callable $callable Callable initializer + * + * @return callable + * @throws InvalidArgumentException + */ + protected function getConnectionInitializer(OptionsInterface $options, callable $callable) + { + return function ($parameters = null, $autoaggregate = false) use ($callable, $options) { + $connection = call_user_func_array($callable, [&$parameters, $options, $this]); + + if (!$connection instanceof AggregateConnectionInterface) { + throw new InvalidArgumentException(sprintf( + '%s expects the supplied callable to return an instance of %s, but %s was returned', + static::class, + AggregateConnectionInterface::class, + is_object($connection) ? get_class($connection) : gettype($connection) + )); + } + + if ($parameters && $autoaggregate) { + static::aggregate($options, $connection, $parameters); + } + + return $connection; + }; + } + + /** + * Adds single connections to an aggregate connection instance. + * + * @param OptionsInterface $options Client options + * @param AggregateConnectionInterface $connection Target aggregate connection + * @param array $nodes List of nodes to be added to the target aggregate connection + */ + public static function aggregate(OptionsInterface $options, AggregateConnectionInterface $connection, array $nodes) + { + $connections = $options->connections; + + foreach ($nodes as $node) { + $connection->add($node instanceof NodeConnectionInterface ? $node : $connections->create($node)); + } + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + return; + } +} diff --git a/plugins/cache-redis/Predis/Configuration/Option/CRC16.php b/plugins/cache-redis/Predis/Configuration/Option/CRC16.php new file mode 100644 index 000000000..b17144922 --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/CRC16.php @@ -0,0 +1,74 @@ +getHashGeneratorByDescription($options, $value); + } elseif ($value instanceof Hash\HashGeneratorInterface) { + return $value; + } else { + $class = get_class($this); + throw new InvalidArgumentException("$class expects a valid hash generator"); + } + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + return function_exists('phpiredis_utils_crc16') + ? new Hash\PhpiredisCRC16() + : new Hash\CRC16(); + } +} diff --git a/plugins/cache-redis/Predis/Configuration/Option/Cluster.php b/plugins/cache-redis/Predis/Configuration/Option/Cluster.php new file mode 100644 index 000000000..34b33de47 --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Cluster.php @@ -0,0 +1,99 @@ +getConnectionInitializerByString($options, $value); + } + + if (is_callable($value)) { + return $this->getConnectionInitializer($options, $value); + } else { + throw new InvalidArgumentException(sprintf( + '%s expects either a string or a callable value, %s given', + static::class, + is_object($value) ? get_class($value) : gettype($value) + )); + } + } + + /** + * Returns a connection initializer from a descriptive name. + * + * @param OptionsInterface $options Client options + * @param string $description Identifier of a replication backend (`predis`, `sentinel`) + * + * @return callable + */ + protected function getConnectionInitializerByString(OptionsInterface $options, string $description) + { + switch ($description) { + case 'redis': + case 'redis-cluster': + return function ($parameters, $options, $option) { + return new RedisCluster($options->connections, new RedisStrategy($options->crc16)); + }; + + case 'predis': + return $this->getDefaultConnectionInitializer(); + + default: + throw new InvalidArgumentException(sprintf( + '%s expects either `predis`, `redis` or `redis-cluster` as valid string values, `%s` given', + static::class, + $description + )); + } + } + + /** + * Returns the default connection initializer. + * + * @return callable + */ + protected function getDefaultConnectionInitializer() + { + return function ($parameters, $options, $option) { + return new PredisCluster(); + }; + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + return $this->getConnectionInitializer( + $options, + $this->getDefaultConnectionInitializer() + ); + } +} diff --git a/plugins/cache-redis/Predis/Configuration/Option/Commands.php b/plugins/cache-redis/Predis/Configuration/Option/Commands.php new file mode 100644 index 000000000..2fbe00e74 --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Commands.php @@ -0,0 +1,146 @@ +createFactoryByArray($options, $value); + } elseif (is_string($value)) { + return $this->createFactoryByString($options, $value); + } else { + throw new InvalidArgumentException(sprintf( + '%s expects a valid command factory', + static::class + )); + } + } + + /** + * Creates a new default command factory from a named array. + * + * The factory instance is configured according to the supplied named array + * mapping command IDs (passed as keys) to the FCQN of classes implementing + * Predis\Command\CommandInterface. + * + * @param OptionsInterface $options Client options container + * @param array $value Named array mapping command IDs to classes + * + * @return FactoryInterface + */ + protected function createFactoryByArray(OptionsInterface $options, array $value) + { + /** + * @var FactoryInterface + */ + $commands = $this->getDefault($options); + + foreach ($value as $commandID => $commandClass) { + if ($commandClass === null) { + $commands->undefine($commandID); + } else { + $commands->define($commandID, $commandClass); + } + } + + return $commands; + } + + /** + * Creates a new command factory from a descriptive string. + * + * The factory instance is configured according to the supplied descriptive + * string that identifies specific configurations of schemes and connection + * classes. Supported configuration values are: + * + * - "predis" returns the default command factory used by Predis + * - "raw" returns a command factory that creates only raw commands + * - "default" is simply an alias of "predis" + * + * @param OptionsInterface $options Client options container + * @param string $value Descriptive string identifying the desired configuration + * + * @return FactoryInterface + */ + protected function createFactoryByString(OptionsInterface $options, string $value) + { + switch (strtolower($value)) { + case 'default': + case 'predis': + return $this->getDefault($options); + + case 'raw': + return $this->createRawFactory($options); + + default: + throw new InvalidArgumentException(sprintf( + '%s does not recognize `%s` as a supported configuration string', + static::class, + $value + )); + } + } + + /** + * Creates a new raw command factory instance. + * + * @param OptionsInterface $options Client options container + */ + protected function createRawFactory(OptionsInterface $options): FactoryInterface + { + $commands = new RawFactory(); + + if (isset($options->prefix)) { + throw new InvalidArgumentException(sprintf( + '%s does not support key prefixing', RawFactory::class + )); + } + + return $commands; + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + $commands = new RedisFactory(); + + if (isset($options->prefix)) { + $commands->setProcessor($options->prefix); + } + + return $commands; + } +} diff --git a/plugins/cache-redis/Predis/Configuration/Option/Connections.php b/plugins/cache-redis/Predis/Configuration/Option/Connections.php new file mode 100644 index 000000000..e37de4cad --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Connections.php @@ -0,0 +1,152 @@ +createFactoryByArray($options, $value); + } elseif (is_string($value)) { + return $this->createFactoryByString($options, $value); + } else { + throw new InvalidArgumentException(sprintf( + '%s expects a valid connection factory', static::class + )); + } + } + + /** + * Creates a new connection factory from a named array. + * + * The factory instance is configured according to the supplied named array + * mapping URI schemes (passed as keys) to the FCQN of classes implementing + * Predis\Connection\NodeConnectionInterface, or callable objects acting as + * lazy initializers and returning new instances of classes implementing + * Predis\Connection\NodeConnectionInterface. + * + * @param OptionsInterface $options Client options + * @param array $value Named array mapping URI schemes to classes or callables + * + * @return FactoryInterface + */ + protected function createFactoryByArray(OptionsInterface $options, array $value) + { + /** + * @var FactoryInterface + */ + $factory = $this->getDefault($options); + + foreach ($value as $scheme => $initializer) { + $factory->define($scheme, $initializer); + } + + return $factory; + } + + /** + * Creates a new connection factory from a descriptive string. + * + * The factory instance is configured according to the supplied descriptive + * string that identifies specific configurations of schemes and connection + * classes. Supported configuration values are: + * + * - "phpiredis-stream" maps tcp, redis, unix to PhpiredisStreamConnection + * - "phpiredis-socket" maps tcp, redis, unix to PhpiredisSocketConnection + * - "phpiredis" is an alias of "phpiredis-stream" + * - "relay" maps tcp, redis, unix, tls, rediss to RelayConnection + * + * @param OptionsInterface $options Client options + * @param string $value Descriptive string identifying the desired configuration + * + * @return FactoryInterface + */ + protected function createFactoryByString(OptionsInterface $options, string $value) + { + /** + * @var FactoryInterface + */ + $factory = $this->getDefault($options); + + switch (strtolower($value)) { + case 'phpiredis': + case 'phpiredis-stream': + $factory->define('tcp', PhpiredisStreamConnection::class); + $factory->define('redis', PhpiredisStreamConnection::class); + $factory->define('unix', PhpiredisStreamConnection::class); + break; + + case 'phpiredis-socket': + $factory->define('tcp', PhpiredisSocketConnection::class); + $factory->define('redis', PhpiredisSocketConnection::class); + $factory->define('unix', PhpiredisSocketConnection::class); + break; + + case 'relay': + $factory->define('tcp', RelayConnection::class); + $factory->define('redis', RelayConnection::class); + $factory->define('unix', RelayConnection::class); + break; + + case 'default': + return $factory; + + default: + throw new InvalidArgumentException(sprintf( + '%s does not recognize `%s` as a supported configuration string', static::class, $value + )); + } + + return $factory; + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + $factory = new Factory(); + + if ($options->defined('parameters')) { + $factory->setDefaultParameters($options->parameters); + } + + return $factory; + } +} diff --git a/plugins/cache-redis/Predis/Configuration/Option/Exceptions.php b/plugins/cache-redis/Predis/Configuration/Option/Exceptions.php new file mode 100644 index 000000000..6834272f0 --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Exceptions.php @@ -0,0 +1,39 @@ +getConnectionInitializerByString($options, $value); + } + + if (is_callable($value)) { + return $this->getConnectionInitializer($options, $value); + } else { + throw new InvalidArgumentException(sprintf( + '%s expects either a string or a callable value, %s given', + static::class, + is_object($value) ? get_class($value) : gettype($value) + )); + } + } + + /** + * Returns a connection initializer (callable) from a descriptive string. + * + * Each connection initializer is specialized for the specified replication + * backend so that all the necessary steps for the configuration of the new + * aggregate connection are performed inside the initializer and the client + * receives a ready-to-use connection. + * + * Supported configuration values are: + * + * - `predis` for unmanaged replication setups + * - `redis-sentinel` for replication setups managed by redis-sentinel + * - `sentinel` is an alias of `redis-sentinel` + * + * @param OptionsInterface $options Client options + * @param string $description Identifier of a replication backend + * + * @return callable + */ + protected function getConnectionInitializerByString(OptionsInterface $options, string $description) + { + switch ($description) { + case 'sentinel': + case 'redis-sentinel': + return function ($parameters, $options) { + return new SentinelReplication($options->service, $parameters, $options->connections); + }; + + case 'predis': + return $this->getDefaultConnectionInitializer(); + + default: + throw new InvalidArgumentException(sprintf( + '%s expects either `predis`, `sentinel` or `redis-sentinel` as valid string values, `%s` given', + static::class, + $description + )); + } + } + + /** + * Returns the default connection initializer. + * + * @return callable + */ + protected function getDefaultConnectionInitializer() + { + return function ($parameters, $options) { + $connection = new MasterSlaveReplication(); + + if ($options->autodiscovery) { + $connection->setConnectionFactory($options->connections); + $connection->setAutoDiscovery(true); + } + + return $connection; + }; + } + + /** + * {@inheritdoc} + */ + public static function aggregate(OptionsInterface $options, AggregateConnectionInterface $connection, array $nodes) + { + if (!$connection instanceof SentinelReplication) { + parent::aggregate($options, $connection, $nodes); + } + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + return $this->getConnectionInitializer( + $options, + $this->getDefaultConnectionInitializer() + ); + } +} diff --git a/plugins/cache-redis/Predis/Connection/Cluster/ClusterInterface.php b/plugins/cache-redis/Predis/Connection/Cluster/ClusterInterface.php new file mode 100644 index 000000000..79c6f96e9 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Cluster/ClusterInterface.php @@ -0,0 +1,23 @@ +strategy = $strategy ?: new PredisStrategy(); + $this->distributor = $this->strategy->getDistributor(); + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + foreach ($this->pool as $connection) { + if ($connection->isConnected()) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function connect() + { + foreach ($this->pool as $connection) { + $connection->connect(); + } + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + foreach ($this->pool as $connection) { + $connection->disconnect(); + } + } + + /** + * {@inheritdoc} + */ + public function add(NodeConnectionInterface $connection) + { + $parameters = $connection->getParameters(); + + $this->pool[(string) $connection] = $connection; + + if (isset($parameters->alias)) { + $this->aliases[$parameters->alias] = $connection; + } + + $this->distributor->add($connection, $parameters->weight); + } + + /** + * {@inheritdoc} + */ + public function remove(NodeConnectionInterface $connection) + { + if (false !== $id = array_search($connection, $this->pool, true)) { + unset($this->pool[$id]); + $this->distributor->remove($connection); + + if ($this->aliases && $alias = $connection->getParameters()->alias) { + unset($this->aliases[$alias]); + } + + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getConnectionByCommand(CommandInterface $command) + { + $slot = $this->strategy->getSlot($command); + + if (!isset($slot)) { + throw new NotSupportedException( + "Cannot use '{$command->getId()}' over clusters of connections." + ); + } + + return $this->distributor->getBySlot($slot); + } + + /** + * {@inheritdoc} + */ + public function getConnectionById($id) + { + return $this->pool[$id] ?? null; + } + + /** + * Returns a connection instance by its alias. + * + * @param string $alias Connection alias. + * + * @return NodeConnectionInterface|null + */ + public function getConnectionByAlias($alias) + { + return $this->aliases[$alias] ?? null; + } + + /** + * Retrieves a connection instance by slot. + * + * @param string $slot Slot name. + * + * @return NodeConnectionInterface|null + */ + public function getConnectionBySlot($slot) + { + return $this->distributor->getBySlot($slot); + } + + /** + * Retrieves a connection instance from the cluster using a key. + * + * @param string $key Key string. + * + * @return NodeConnectionInterface + */ + public function getConnectionByKey($key) + { + $hash = $this->strategy->getSlotByKey($key); + + return $this->distributor->getBySlot($hash); + } + + /** + * Returns the underlying command hash strategy used to hash commands by + * using keys found in their arguments. + * + * @return StrategyInterface + */ + public function getClusterStrategy() + { + return $this->strategy; + } + + /** + * @return int + */ + #[ReturnTypeWillChange] + public function count() + { + return count($this->pool); + } + + /** + * @return Traversable + */ + #[ReturnTypeWillChange] + public function getIterator() + { + return new ArrayIterator($this->pool); + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + $this->getConnectionByCommand($command)->writeRequest($command); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + return $this->getConnectionByCommand($command)->readResponse($command); + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + return $this->getConnectionByCommand($command)->executeCommand($command); + } +} diff --git a/plugins/cache-redis/Predis/Connection/Cluster/RedisCluster.php b/plugins/cache-redis/Predis/Connection/Cluster/RedisCluster.php new file mode 100644 index 000000000..7f3013c17 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Cluster/RedisCluster.php @@ -0,0 +1,673 @@ += 3.0.0). + * + * This connection backend offers smart support for redis-cluster by handling + * automatic slots map (re)generation upon -MOVED or -ASK responses returned by + * Redis when redirecting a client to a different node. + * + * The cluster can be pre-initialized using only a subset of the actual nodes in + * the cluster, Predis will do the rest by adjusting the slots map and creating + * the missing underlying connection instances on the fly. + * + * It is possible to pre-associate connections to a slots range with the "slots" + * parameter in the form "$first-$last". This can greatly reduce runtime node + * guessing and redirections. + * + * It is also possible to ask for the full and updated slots map directly to one + * of the nodes and optionally enable such a behaviour upon -MOVED redirections. + * Asking for the cluster configuration to Redis is actually done by issuing a + * CLUSTER SLOTS command to a random node in the pool. + */ +class RedisCluster implements ClusterInterface, IteratorAggregate, Countable +{ + private $useClusterSlots = true; + private $pool = []; + private $slots = []; + private $slotmap; + private $strategy; + private $connections; + private $retryLimit = 5; + private $retryInterval = 10; + + /** + * @param FactoryInterface $connections Optional connection factory. + * @param StrategyInterface $strategy Optional cluster strategy. + */ + public function __construct( + FactoryInterface $connections, + StrategyInterface $strategy = null + ) { + $this->connections = $connections; + $this->strategy = $strategy ?: new RedisClusterStrategy(); + $this->slotmap = new SlotMap(); + } + + /** + * Sets the maximum number of retries for commands upon server failure. + * + * -1 = unlimited retry attempts + * 0 = no retry attempts (fails immediately) + * n = fail only after n retry attempts + * + * @param int $retry Number of retry attempts. + */ + public function setRetryLimit($retry) + { + $this->retryLimit = (int) $retry; + } + + /** + * Sets the initial retry interval (milliseconds). + * + * @param int $retryInterval Milliseconds between retries. + */ + public function setRetryInterval($retryInterval) + { + $this->retryInterval = (int) $retryInterval; + } + + /** + * Returns the retry interval (milliseconds). + * + * @return int Milliseconds between retries. + */ + public function getRetryInterval() + { + return (int) $this->retryInterval; + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + foreach ($this->pool as $connection) { + if ($connection->isConnected()) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function connect() + { + if ($connection = $this->getRandomConnection()) { + $connection->connect(); + } + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + foreach ($this->pool as $connection) { + $connection->disconnect(); + } + } + + /** + * {@inheritdoc} + */ + public function add(NodeConnectionInterface $connection) + { + $this->pool[(string) $connection] = $connection; + $this->slotmap->reset(); + } + + /** + * {@inheritdoc} + */ + public function remove(NodeConnectionInterface $connection) + { + if (false !== $id = array_search($connection, $this->pool, true)) { + $this->slotmap->reset(); + $this->slots = array_diff($this->slots, [$connection]); + unset($this->pool[$id]); + + return true; + } + + return false; + } + + /** + * Removes a connection instance by using its identifier. + * + * @param string $connectionID Connection identifier. + * + * @return bool True if the connection was in the pool. + */ + public function removeById($connectionID) + { + if (isset($this->pool[$connectionID])) { + $this->slotmap->reset(); + $this->slots = array_diff($this->slots, [$connectionID]); + unset($this->pool[$connectionID]); + + return true; + } + + return false; + } + + /** + * Generates the current slots map by guessing the cluster configuration out + * of the connection parameters of the connections in the pool. + * + * Generation is based on the same algorithm used by Redis to generate the + * cluster, so it is most effective when all of the connections supplied on + * initialization have the "slots" parameter properly set accordingly to the + * current cluster configuration. + */ + public function buildSlotMap() + { + $this->slotmap->reset(); + + foreach ($this->pool as $connectionID => $connection) { + $parameters = $connection->getParameters(); + + if (!isset($parameters->slots)) { + continue; + } + + foreach (explode(',', $parameters->slots) as $slotRange) { + $slots = explode('-', $slotRange, 2); + + if (!isset($slots[1])) { + $slots[1] = $slots[0]; + } + + $this->slotmap->setSlots($slots[0], $slots[1], $connectionID); + } + } + } + + /** + * Queries the specified node of the cluster to fetch the updated slots map. + * + * When the connection fails, this method tries to execute the same command + * on a different connection picked at random from the pool of known nodes, + * up until the retry limit is reached. + * + * @param NodeConnectionInterface $connection Connection to a node of the cluster. + * + * @return mixed + */ + private function queryClusterNodeForSlotMap(NodeConnectionInterface $connection) + { + $retries = 0; + $retryAfter = $this->retryInterval; + $command = RawCommand::create('CLUSTER', 'SLOTS'); + + while ($retries <= $this->retryLimit) { + try { + $response = $connection->executeCommand($command); + break; + } catch (ConnectionException $exception) { + $connection = $exception->getConnection(); + $connection->disconnect(); + + $this->remove($connection); + + if ($retries === $this->retryLimit) { + throw $exception; + } + + if (!$connection = $this->getRandomConnection()) { + throw new ClientException('No connections left in the pool for `CLUSTER SLOTS`'); + } + + usleep($retryAfter * 1000); + $retryAfter = $retryAfter * 2; + ++$retries; + } + } + + return $response; + } + + /** + * Generates an updated slots map fetching the cluster configuration using + * the CLUSTER SLOTS command against the specified node or a random one from + * the pool. + * + * @param NodeConnectionInterface $connection Optional connection instance. + */ + public function askSlotMap(NodeConnectionInterface $connection = null) + { + if (!$connection && !$connection = $this->getRandomConnection()) { + return; + } + + $this->slotmap->reset(); + + $response = $this->queryClusterNodeForSlotMap($connection); + + foreach ($response as $slots) { + // We only support master servers for now, so we ignore subsequent + // elements in the $slots array identifying slaves. + [$start, $end, $master] = $slots; + + if ($master[0] === '') { + $this->slotmap->setSlots($start, $end, (string) $connection); + } else { + $this->slotmap->setSlots($start, $end, "{$master[0]}:{$master[1]}"); + } + } + } + + /** + * Guesses the correct node associated to a given slot using a precalculated + * slots map, falling back to the same logic used by Redis to initialize a + * cluster (best-effort). + * + * @param int $slot Slot index. + * + * @return string Connection ID. + */ + protected function guessNode($slot) + { + if (!$this->pool) { + throw new ClientException('No connections available in the pool'); + } + + if ($this->slotmap->isEmpty()) { + $this->buildSlotMap(); + } + + if ($node = $this->slotmap[$slot]) { + return $node; + } + + $count = count($this->pool); + $index = min((int) ($slot / (int) (16384 / $count)), $count - 1); + $nodes = array_keys($this->pool); + + return $nodes[$index]; + } + + /** + * Creates a new connection instance from the given connection ID. + * + * @param string $connectionID Identifier for the connection. + * + * @return NodeConnectionInterface + */ + protected function createConnection($connectionID) + { + $separator = strrpos($connectionID, ':'); + + return $this->connections->create([ + 'host' => substr($connectionID, 0, $separator), + 'port' => substr($connectionID, $separator + 1), + ]); + } + + /** + * {@inheritdoc} + */ + public function getConnectionByCommand(CommandInterface $command) + { + $slot = $this->strategy->getSlot($command); + + if (!isset($slot)) { + throw new NotSupportedException( + "Cannot use '{$command->getId()}' with redis-cluster." + ); + } + + if (isset($this->slots[$slot])) { + return $this->slots[$slot]; + } else { + return $this->getConnectionBySlot($slot); + } + } + + /** + * Returns the connection currently associated to a given slot. + * + * @param int $slot Slot index. + * + * @return NodeConnectionInterface + * @throws OutOfBoundsException + */ + public function getConnectionBySlot($slot) + { + if (!SlotMap::isValid($slot)) { + throw new OutOfBoundsException("Invalid slot [$slot]."); + } + + if (isset($this->slots[$slot])) { + return $this->slots[$slot]; + } + + $connectionID = $this->guessNode($slot); + + if (!$connection = $this->getConnectionById($connectionID)) { + $connection = $this->createConnection($connectionID); + $this->pool[$connectionID] = $connection; + } + + return $this->slots[$slot] = $connection; + } + + /** + * {@inheritdoc} + */ + public function getConnectionById($connectionID) + { + return $this->pool[$connectionID] ?? null; + } + + /** + * Returns a random connection from the pool. + * + * @return NodeConnectionInterface|null + */ + protected function getRandomConnection() + { + if (!$this->pool) { + return null; + } + + return $this->pool[array_rand($this->pool)]; + } + + /** + * Permanently associates the connection instance to a new slot. + * The connection is added to the connections pool if not yet included. + * + * @param NodeConnectionInterface $connection Connection instance. + * @param int $slot Target slot index. + */ + protected function move(NodeConnectionInterface $connection, $slot) + { + $this->pool[(string) $connection] = $connection; + $this->slots[(int) $slot] = $connection; + $this->slotmap[(int) $slot] = $connection; + } + + /** + * Handles -ERR responses returned by Redis. + * + * @param CommandInterface $command Command that generated the -ERR response. + * @param ErrorResponseInterface $error Redis error response object. + * + * @return mixed + */ + protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $error) + { + $details = explode(' ', $error->getMessage(), 2); + + switch ($details[0]) { + case 'MOVED': + return $this->onMovedResponse($command, $details[1]); + + case 'ASK': + return $this->onAskResponse($command, $details[1]); + + default: + return $error; + } + } + + /** + * Handles -MOVED responses by executing again the command against the node + * indicated by the Redis response. + * + * @param CommandInterface $command Command that generated the -MOVED response. + * @param string $details Parameters of the -MOVED response. + * + * @return mixed + */ + protected function onMovedResponse(CommandInterface $command, $details) + { + [$slot, $connectionID] = explode(' ', $details, 2); + + if (!$connection = $this->getConnectionById($connectionID)) { + $connection = $this->createConnection($connectionID); + } + + if ($this->useClusterSlots) { + $this->askSlotMap($connection); + } + + $this->move($connection, $slot); + + return $this->executeCommand($command); + } + + /** + * Handles -ASK responses by executing again the command against the node + * indicated by the Redis response. + * + * @param CommandInterface $command Command that generated the -ASK response. + * @param string $details Parameters of the -ASK response. + * + * @return mixed + */ + protected function onAskResponse(CommandInterface $command, $details) + { + [$slot, $connectionID] = explode(' ', $details, 2); + + if (!$connection = $this->getConnectionById($connectionID)) { + $connection = $this->createConnection($connectionID); + } + + $connection->executeCommand(RawCommand::create('ASKING')); + + return $connection->executeCommand($command); + } + + /** + * Ensures that a command is executed one more time on connection failure. + * + * The connection to the node that generated the error is evicted from the + * pool before trying to fetch an updated slots map from another node. If + * the new slots map points to an unreachable server the client gives up and + * throws the exception as the nodes participating in the cluster may still + * have to agree that something changed in the configuration of the cluster. + * + * @param CommandInterface $command Command instance. + * @param string $method Actual method. + * + * @return mixed + */ + private function retryCommandOnFailure(CommandInterface $command, $method) + { + $retries = 0; + $retryAfter = $this->retryInterval; + + while ($retries <= $this->retryLimit) { + try { + $response = $this->getConnectionByCommand($command)->$method($command); + + if ($response instanceof ErrorResponse) { + $message = $response->getMessage(); + + if (strpos($message, 'CLUSTERDOWN') !== false) { + throw new ServerException($message); + } + } + + break; + } catch (Throwable $exception) { + usleep($retryAfter * 1000); + $retryAfter = $retryAfter * 2; + + if ($exception instanceof ConnectionException) { + $connection = $exception->getConnection(); + + if ($connection) { + $connection->disconnect(); + $this->remove($connection); + } + } + + if ($retries === $this->retryLimit) { + throw $exception; + } + + if ($this->useClusterSlots) { + $this->askSlotMap(); + } + + ++$retries; + } + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + $response = $this->retryCommandOnFailure($command, __FUNCTION__); + + if ($response instanceof ErrorResponseInterface) { + return $this->onErrorResponse($command, $response); + } + + return $response; + } + + /** + * @return int + */ + #[ReturnTypeWillChange] + public function count() + { + return count($this->pool); + } + + /** + * @return Traversable + */ + #[ReturnTypeWillChange] + public function getIterator() + { + if ($this->slotmap->isEmpty()) { + $this->useClusterSlots ? $this->askSlotMap() : $this->buildSlotMap(); + } + + $connections = []; + + foreach ($this->slotmap->getNodes() as $node) { + if (!$connection = $this->getConnectionById($node)) { + $this->add($connection = $this->createConnection($node)); + } + + $connections[] = $connection; + } + + return new ArrayIterator($connections); + } + + /** + * Returns the underlying slot map. + * + * @return SlotMap + */ + public function getSlotMap() + { + return $this->slotmap; + } + + /** + * Returns the underlying command hash strategy used to hash commands by + * using keys found in their arguments. + * + * @return StrategyInterface + */ + public function getClusterStrategy() + { + return $this->strategy; + } + + /** + * Returns the underlying connection factory used to create new connection + * instances to Redis nodes indicated by redis-cluster. + * + * @return FactoryInterface + */ + public function getConnectionFactory() + { + return $this->connections; + } + + /** + * Enables automatic fetching of the current slots map from one of the nodes + * using the CLUSTER SLOTS command. This option is enabled by default as + * asking the current slots map to Redis upon -MOVED responses may reduce + * overhead by eliminating the trial-and-error nature of the node guessing + * procedure, mostly when targeting many keys that would end up in a lot of + * redirections. + * + * The slots map can still be manually fetched using the askSlotMap() + * method whether or not this option is enabled. + * + * @param bool $value Enable or disable the use of CLUSTER SLOTS. + */ + public function useClusterSlots($value) + { + $this->useClusterSlots = (bool) $value; + } +} diff --git a/plugins/cache-redis/Predis/Connection/RelayConnection.php b/plugins/cache-redis/Predis/Connection/RelayConnection.php new file mode 100644 index 000000000..4ff674f5f --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/RelayConnection.php @@ -0,0 +1,337 @@ +assertExtensions(); + + $this->parameters = $this->assertParameters($parameters); + $this->client = $this->createClient(); + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + return $this->client->isConnected(); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + if ($this->client->isConnected()) { + $this->client->close(); + } + } + + /** + * Checks if the Relay extension is loaded in PHP. + */ + private function assertExtensions() + { + if (!extension_loaded('relay')) { + throw new NotSupportedException( + 'The "relay" extension is required by this connection backend.' + ); + } + } + + /** + * {@inheritdoc} + */ + protected function assertParameters(ParametersInterface $parameters) + { + if (!in_array($parameters->scheme, ['tcp', 'tls', 'unix', 'redis', 'rediss'])) { + throw new InvalidArgumentException("Invalid scheme: '{$parameters->scheme}'."); + } + + if (!in_array($parameters->serializer, [null, 'php', 'igbinary', 'msgpack', 'json'])) { + throw new InvalidArgumentException("Invalid serializer: '{$parameters->serializer}'."); + } + + if (!in_array($parameters->compression, [null, 'lzf', 'lz4', 'zstd'])) { + throw new InvalidArgumentException("Invalid compression algorithm: '{$parameters->compression}'."); + } + + return $parameters; + } + + /** + * Creates a new instance of the client. + * + * @return \Relay\Relay + */ + private function createClient() + { + $client = new Relay(); + + // throw when errors occur and return `null` for non-existent keys + $client->setOption(Relay::OPT_PHPREDIS_COMPATIBILITY, false); + + // use reply literals + $client->setOption(Relay::OPT_REPLY_LITERAL, true); + + // disable Relay's command/connection retry + $client->setOption(Relay::OPT_MAX_RETRIES, 0); + + // whether to use in-memory caching + $client->setOption(Relay::OPT_USE_CACHE, $this->parameters->cache ?? true); + + // set data serializer + $client->setOption(Relay::OPT_SERIALIZER, constant(sprintf( + '%s::SERIALIZER_%s', + Relay::class, + strtoupper($this->parameters->serializer ?? 'none') + ))); + + // set data compression algorithm + $client->setOption(Relay::OPT_COMPRESSION, constant(sprintf( + '%s::COMPRESSION_%s', + Relay::class, + strtoupper($this->parameters->compression ?? 'none') + ))); + + return $client; + } + + /** + * Returns the underlying client. + * + * @return \Relay\Relay + */ + public function getClient() + { + return $this->client; + } + + /** + * {@inheritdoc} + */ + protected function getIdentifier() + { + return $this->client->endpointId(); + } + + /** + * {@inheritdoc} + */ + protected function createStreamSocket(ParametersInterface $parameters, $address, $flags) + { + $timeout = isset($parameters->timeout) ? (float) $parameters->timeout : 5.0; + + $retry_interval = 0; + $read_timeout = 5.0; + + if (isset($parameters->read_write_timeout)) { + $read_timeout = (float) $parameters->read_write_timeout; + $read_timeout = $read_timeout > 0 ? $read_timeout : 0; + } + + try { + $this->client->connect( + $parameters->path ?? $parameters->host, + isset($parameters->path) ? 0 : $parameters->port, + $timeout, + null, + $retry_interval, + $read_timeout + ); + } catch (RelayException $ex) { + $this->onConnectionError($ex->getMessage(), $ex->getCode()); + } + + return $this->client; + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + if (!$this->client->isConnected()) { + $this->getResource(); + } + + try { + $name = $command->getId(); + + // When using compression or a serializer, we'll need a dedicated + // handler for `Predis\Command\RawCommand` calls, currently both + // parameters are unsupported until a future Relay release + return in_array($name, $this->atypicalCommands) + ? $this->client->{$name}(...$command->getArguments()) + : $this->client->rawCommand($name, ...$command->getArguments()); + } catch (RelayException $ex) { + throw $this->onCommandError($ex, $command); + } + } + + /** + * {@inheritdoc} + */ + public function onCommandError(RelayException $exception, CommandInterface $command) + { + $code = $exception->getCode(); + $message = $exception->getMessage(); + + if (strpos($message, 'RELAY_ERR_IO')) { + return new ConnectionException($this, $message, $code, $exception); + } + + if (strpos($message, 'RELAY_ERR_REDIS')) { + return new ServerException($message, $code, $exception); + } + + if (strpos($message, 'RELAY_ERR_WRONGTYPE') && strpos($message, "Got reply-type 'status'")) { + $message = 'Operation against a key holding the wrong kind of value'; + } + + return new ClientException($message, $code, $exception); + } + + /** + * Applies the configured serializer and compression to given value. + * + * @param mixed $value + * @return string + */ + public function pack($value) + { + return $this->client->_pack($value); + } + + /** + * Deserializes and decompresses to given value. + * + * @param mixed $value + * @return string + */ + public function unpack($value) + { + return $this->client->_unpack($value); + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + throw new NotSupportedException('The "relay" extension does not support writing requests.'); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + throw new NotSupportedException('The "relay" extension does not support reading responses.'); + } + + /** + * {@inheritdoc} + */ + public function __destruct() + { + $this->disconnect(); + } + + /** + * {@inheritdoc} + */ + public function __wakeup() + { + $this->assertExtensions(); + $this->client = $this->createClient(); + } +} diff --git a/plugins/cache-redis/Predis/Connection/RelayMethods.php b/plugins/cache-redis/Predis/Connection/RelayMethods.php new file mode 100644 index 000000000..a52c4a035 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/RelayMethods.php @@ -0,0 +1,136 @@ +client->onFlushed($callback); + } + + /** + * Registers a new `invalidated` event listener. + * + * @param callable $callback + * @param string $pattern + * @return bool + */ + public function onInvalidated(?callable $callback, string $pattern = null) + { + return $this->client->onInvalidated($callback, $pattern); + } + + /** + * Dispatches all pending events. + * + * @return int|false + */ + public function dispatchEvents() + { + return $this->client->dispatchEvents(); + } + + /** + * Adds ignore pattern(s). Matching keys will not be cached in memory. + * + * @param string $pattern,... + * @return int + */ + public function addIgnorePatterns(string ...$pattern) + { + return $this->client->addIgnorePatterns(...$pattern); + } + + /** + * Adds allow pattern(s). Only matching keys will be cached in memory. + * + * @param string $pattern,... + * @return int + */ + public function addAllowPatterns(string ...$pattern) + { + return $this->client->addAllowPatterns(...$pattern); + } + + /** + * Returns the connection's endpoint identifier. + * + * @return string|false + */ + public function endpointId() + { + return $this->client->endpointId(); + } + + /** + * Returns a unique representation of the underlying socket connection identifier. + * + * @return string|false + */ + public function socketId() + { + return $this->client->socketId(); + } + + /** + * Returns information about the license. + * + * @return array + */ + public function license() + { + return $this->client->license(); + } + + /** + * Returns statistics about Relay. + * + * @return array> + */ + public function stats() + { + return $this->client->stats(); + } + + /** + * Returns the number of bytes allocated, or `0` in client-only mode. + * + * @return int + */ + public function maxMemory() + { + return $this->client->maxMemory(); + } + + /** + * Flushes Relay's in-memory cache of all databases. + * When given an endpoint, only that connection will be flushed. + * When given an endpoint and database index, only that database + * for that connection will be flushed. + * + * @param ?string $endpointId + * @param ?int $db + * @return bool + */ + public function flushMemory(string $endpointId = null, int $db = null) + { + return $this->client->flushMemory($endpointId, $db); + } +} diff --git a/plugins/cache-redis/Predis/Connection/Replication/MasterSlaveReplication.php b/plugins/cache-redis/Predis/Connection/Replication/MasterSlaveReplication.php new file mode 100644 index 000000000..9d57a5ac3 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Replication/MasterSlaveReplication.php @@ -0,0 +1,553 @@ +strategy = $strategy ?: new ReplicationStrategy(); + } + + /** + * Configures the automatic discovery of the replication configuration on failure. + * + * @param bool $value Enable or disable auto discovery. + */ + public function setAutoDiscovery($value) + { + if (!$this->connectionFactory) { + throw new ClientException('Automatic discovery requires a connection factory'); + } + + $this->autoDiscovery = (bool) $value; + } + + /** + * Sets the connection factory used to create the connections by the auto + * discovery procedure. + * + * @param FactoryInterface $connectionFactory Connection factory instance. + */ + public function setConnectionFactory(FactoryInterface $connectionFactory) + { + $this->connectionFactory = $connectionFactory; + } + + /** + * Resets the connection state. + */ + protected function reset() + { + $this->current = null; + } + + /** + * {@inheritdoc} + */ + public function add(NodeConnectionInterface $connection) + { + $parameters = $connection->getParameters(); + + if ('master' === $parameters->role) { + $this->master = $connection; + } else { + // everything else is considered a slvave. + $this->slaves[] = $connection; + } + + if (isset($parameters->alias)) { + $this->aliases[$parameters->alias] = $connection; + } + + $this->pool[(string) $connection] = $connection; + + $this->reset(); + } + + /** + * {@inheritdoc} + */ + public function remove(NodeConnectionInterface $connection) + { + if ($connection === $this->master) { + $this->master = null; + } elseif (false !== $id = array_search($connection, $this->slaves, true)) { + unset($this->slaves[$id]); + } else { + return false; + } + + unset($this->pool[(string) $connection]); + + if ($this->aliases && $alias = $connection->getParameters()->alias) { + unset($this->aliases[$alias]); + } + + $this->reset(); + + return true; + } + + /** + * {@inheritdoc} + */ + public function getConnectionByCommand(CommandInterface $command) + { + if (!$this->current) { + if ($this->strategy->isReadOperation($command) && $slave = $this->pickSlave()) { + $this->current = $slave; + } else { + $this->current = $this->getMasterOrDie(); + } + + return $this->current; + } + + if ($this->current === $master = $this->getMasterOrDie()) { + return $master; + } + + if (!$this->strategy->isReadOperation($command) || !$this->slaves) { + $this->current = $master; + } + + return $this->current; + } + + /** + * {@inheritdoc} + */ + public function getConnectionById($id) + { + return $this->pool[$id] ?? null; + } + + /** + * Returns a connection instance by its alias. + * + * @param string $alias Connection alias. + * + * @return NodeConnectionInterface|null + */ + public function getConnectionByAlias($alias) + { + return $this->aliases[$alias] ?? null; + } + + /** + * Returns a connection by its role. + * + * @param string $role Connection role (`master` or `slave`) + * + * @return NodeConnectionInterface|null + */ + public function getConnectionByRole($role) + { + if ($role === 'master') { + return $this->getMaster(); + } elseif ($role === 'slave') { + return $this->pickSlave(); + } + + return null; + } + + /** + * Switches the internal connection in use by the backend. + * + * @param NodeConnectionInterface $connection Connection instance in the pool. + */ + public function switchTo(NodeConnectionInterface $connection) + { + if ($connection && $connection === $this->current) { + return; + } + + if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) { + throw new InvalidArgumentException('Invalid connection or connection not found.'); + } + + $this->current = $connection; + } + + /** + * {@inheritdoc} + */ + public function switchToMaster() + { + if (!$connection = $this->getConnectionByRole('master')) { + throw new InvalidArgumentException('Invalid connection or connection not found.'); + } + + $this->switchTo($connection); + } + + /** + * {@inheritdoc} + */ + public function switchToSlave() + { + if (!$connection = $this->getConnectionByRole('slave')) { + throw new InvalidArgumentException('Invalid connection or connection not found.'); + } + + $this->switchTo($connection); + } + + /** + * {@inheritdoc} + */ + public function getCurrent() + { + return $this->current; + } + + /** + * {@inheritdoc} + */ + public function getMaster() + { + return $this->master; + } + + /** + * Returns the connection associated to the master server. + * + * @return NodeConnectionInterface + */ + private function getMasterOrDie() + { + if (!$connection = $this->getMaster()) { + throw new MissingMasterException('No master server available for replication'); + } + + return $connection; + } + + /** + * {@inheritdoc} + */ + public function getSlaves() + { + return $this->slaves; + } + + /** + * Returns the underlying replication strategy. + * + * @return ReplicationStrategy + */ + public function getReplicationStrategy() + { + return $this->strategy; + } + + /** + * Returns a random slave. + * + * @return NodeConnectionInterface|null + */ + protected function pickSlave() + { + if (!$this->slaves) { + return null; + } + + return $this->slaves[array_rand($this->slaves)]; + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + return $this->current ? $this->current->isConnected() : false; + } + + /** + * {@inheritdoc} + */ + public function connect() + { + if (!$this->current) { + if (!$this->current = $this->pickSlave()) { + if (!$this->current = $this->getMaster()) { + throw new ClientException('No available connection for replication'); + } + } + } + + $this->current->connect(); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + foreach ($this->pool as $connection) { + $connection->disconnect(); + } + } + + /** + * Handles response from INFO. + * + * @param string $response + * + * @return array + */ + private function handleInfoResponse($response) + { + $info = []; + + foreach (preg_split('/\r?\n/', $response) as $row) { + if (strpos($row, ':') === false) { + continue; + } + + [$k, $v] = explode(':', $row, 2); + $info[$k] = $v; + } + + return $info; + } + + /** + * Fetches the replication configuration from one of the servers. + */ + public function discover() + { + if (!$this->connectionFactory) { + throw new ClientException('Discovery requires a connection factory'); + } + + while (true) { + try { + if ($connection = $this->getMaster()) { + $this->discoverFromMaster($connection, $this->connectionFactory); + break; + } elseif ($connection = $this->pickSlave()) { + $this->discoverFromSlave($connection, $this->connectionFactory); + break; + } else { + throw new ClientException('No connection available for discovery'); + } + } catch (ConnectionException $exception) { + $this->remove($connection); + } + } + } + + /** + * Discovers the replication configuration by contacting the master node. + * + * @param NodeConnectionInterface $connection Connection to the master node. + * @param FactoryInterface $connectionFactory Connection factory instance. + */ + protected function discoverFromMaster(NodeConnectionInterface $connection, FactoryInterface $connectionFactory) + { + $response = $connection->executeCommand(RawCommand::create('INFO', 'REPLICATION')); + $replication = $this->handleInfoResponse($response); + + if ($replication['role'] !== 'master') { + throw new ClientException("Role mismatch (expected master, got slave) [$connection]"); + } + + $this->slaves = []; + + foreach ($replication as $k => $v) { + $parameters = null; + + if (strpos($k, 'slave') === 0 && preg_match('/ip=(?P.*),port=(?P\d+)/', $v, $parameters)) { + $slaveConnection = $connectionFactory->create([ + 'host' => $parameters['host'], + 'port' => $parameters['port'], + 'role' => 'slave', + ]); + + $this->add($slaveConnection); + } + } + } + + /** + * Discovers the replication configuration by contacting one of the slaves. + * + * @param NodeConnectionInterface $connection Connection to one of the slaves. + * @param FactoryInterface $connectionFactory Connection factory instance. + */ + protected function discoverFromSlave(NodeConnectionInterface $connection, FactoryInterface $connectionFactory) + { + $response = $connection->executeCommand(RawCommand::create('INFO', 'REPLICATION')); + $replication = $this->handleInfoResponse($response); + + if ($replication['role'] !== 'slave') { + throw new ClientException("Role mismatch (expected slave, got master) [$connection]"); + } + + $masterConnection = $connectionFactory->create([ + 'host' => $replication['master_host'], + 'port' => $replication['master_port'], + 'role' => 'master', + ]); + + $this->add($masterConnection); + + $this->discoverFromMaster($masterConnection, $connectionFactory); + } + + /** + * Retries the execution of a command upon slave failure. + * + * @param CommandInterface $command Command instance. + * @param string $method Actual method. + * + * @return mixed + */ + private function retryCommandOnFailure(CommandInterface $command, $method) + { + while (true) { + try { + $connection = $this->getConnectionByCommand($command); + $response = $connection->$method($command); + + if ($response instanceof ResponseErrorInterface && $response->getErrorType() === 'LOADING') { + throw new ConnectionException($connection, "Redis is loading the dataset in memory [$connection]"); + } + + break; + } catch (ConnectionException $exception) { + $connection = $exception->getConnection(); + $connection->disconnect(); + + if ($connection === $this->master && !$this->autoDiscovery) { + // Throw immediately when master connection is failing, even + // when the command represents a read-only operation, unless + // automatic discovery has been enabled. + throw $exception; + } else { + // Otherwise remove the failing slave and attempt to execute + // the command again on one of the remaining slaves... + $this->remove($connection); + } + + // ... that is, unless we have no more connections to use. + if (!$this->slaves && !$this->master) { + throw $exception; + } elseif ($this->autoDiscovery) { + $this->discover(); + } + } catch (MissingMasterException $exception) { + if ($this->autoDiscovery) { + $this->discover(); + } else { + throw $exception; + } + } + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function __sleep() + { + return ['master', 'slaves', 'pool', 'aliases', 'strategy']; + } +} diff --git a/plugins/cache-redis/Predis/Connection/Replication/ReplicationInterface.php b/plugins/cache-redis/Predis/Connection/Replication/ReplicationInterface.php new file mode 100644 index 000000000..14fd2499e --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Replication/ReplicationInterface.php @@ -0,0 +1,53 @@ + + * @author Ville Mattila + */ +class SentinelReplication implements ReplicationInterface +{ + /** + * @var NodeConnectionInterface + */ + protected $master; + + /** + * @var NodeConnectionInterface[] + */ + protected $slaves = []; + + /** + * @var NodeConnectionInterface[] + */ + protected $pool = []; + + /** + * @var NodeConnectionInterface + */ + protected $current; + + /** + * @var string + */ + protected $service; + + /** + * @var ConnectionFactoryInterface + */ + protected $connectionFactory; + + /** + * @var ReplicationStrategy + */ + protected $strategy; + + /** + * @var NodeConnectionInterface[] + */ + protected $sentinels = []; + + /** + * @var int + */ + protected $sentinelIndex = 0; + + /** + * @var NodeConnectionInterface + */ + protected $sentinelConnection; + + /** + * @var float + */ + protected $sentinelTimeout = 0.100; + + /** + * Max number of automatic retries of commands upon server failure. + * + * -1 = unlimited retry attempts + * 0 = no retry attempts (fails immediately) + * n = fail only after n retry attempts + * + * @var int + */ + protected $retryLimit = 20; + + /** + * Time to wait in milliseconds before fetching a new configuration from one + * of the sentinel servers. + * + * @var int + */ + protected $retryWait = 1000; + + /** + * Flag for automatic fetching of available sentinels. + * + * @var bool + */ + protected $updateSentinels = false; + + /** + * @param string $service Name of the service for autodiscovery. + * @param array $sentinels Sentinel servers connection parameters. + * @param ConnectionFactoryInterface $connectionFactory Connection factory instance. + * @param ReplicationStrategy $strategy Replication strategy instance. + */ + public function __construct( + $service, + array $sentinels, + ConnectionFactoryInterface $connectionFactory, + ReplicationStrategy $strategy = null + ) { + $this->sentinels = $sentinels; + $this->service = $service; + $this->connectionFactory = $connectionFactory; + $this->strategy = $strategy ?: new ReplicationStrategy(); + } + + /** + * Sets a default timeout for connections to sentinels. + * + * When "timeout" is present in the connection parameters of sentinels, its + * value overrides the default sentinel timeout. + * + * @param float $timeout Timeout value. + */ + public function setSentinelTimeout($timeout) + { + $this->sentinelTimeout = (float) $timeout; + } + + /** + * Sets the maximum number of retries for commands upon server failure. + * + * -1 = unlimited retry attempts + * 0 = no retry attempts (fails immediately) + * n = fail only after n retry attempts + * + * @param int $retry Number of retry attempts. + */ + public function setRetryLimit($retry) + { + $this->retryLimit = (int) $retry; + } + + /** + * Sets the time to wait (in milliseconds) before fetching a new configuration + * from one of the sentinels. + * + * @param float $milliseconds Time to wait before the next attempt. + */ + public function setRetryWait($milliseconds) + { + $this->retryWait = (float) $milliseconds; + } + + /** + * Set automatic fetching of available sentinels. + * + * @param bool $update Enable or disable automatic updates. + */ + public function setUpdateSentinels($update) + { + $this->updateSentinels = (bool) $update; + } + + /** + * Resets the current connection. + */ + protected function reset() + { + $this->current = null; + } + + /** + * Wipes the current list of master and slaves nodes. + */ + protected function wipeServerList() + { + $this->reset(); + + $this->master = null; + $this->slaves = []; + $this->pool = []; + } + + /** + * {@inheritdoc} + */ + public function add(NodeConnectionInterface $connection) + { + $parameters = $connection->getParameters(); + $role = $parameters->role; + + if ('master' === $role) { + $this->master = $connection; + } elseif ('sentinel' === $role) { + $this->sentinels[] = $connection; + + // sentinels are not considered part of the pool. + return; + } else { + // everything else is considered a slave. + $this->slaves[] = $connection; + } + + $this->pool[(string) $connection] = $connection; + + $this->reset(); + } + + /** + * {@inheritdoc} + */ + public function remove(NodeConnectionInterface $connection) + { + if ($connection === $this->master) { + $this->master = null; + } elseif (false !== $id = array_search($connection, $this->slaves, true)) { + unset($this->slaves[$id]); + } elseif (false !== $id = array_search($connection, $this->sentinels, true)) { + unset($this->sentinels[$id]); + + return true; + } else { + return false; + } + + unset($this->pool[(string) $connection]); + + $this->reset(); + + return true; + } + + /** + * Creates a new connection to a sentinel server. + * + * @return NodeConnectionInterface + */ + protected function createSentinelConnection($parameters) + { + if ($parameters instanceof NodeConnectionInterface) { + return $parameters; + } + + if (is_string($parameters)) { + $parameters = Parameters::parse($parameters); + } + + if (is_array($parameters)) { + // NOTE: sentinels do not accept AUTH and SELECT commands so we must + // explicitly set them to NULL to avoid problems when using default + // parameters set via client options. Actually AUTH is supported for + // sentinels starting with Redis 5 but we have to differentiate from + // sentinels passwords and nodes passwords, this will be implemented + // in a later release. + $parameters['database'] = null; + $parameters['username'] = null; + + // don't leak password from between configurations + // https://github.com/predis/predis/pull/807/#discussion_r985764770 + if (!isset($parameters['password'])) { + $parameters['password'] = null; + } + + if (!isset($parameters['timeout'])) { + $parameters['timeout'] = $this->sentinelTimeout; + } + } + + return $this->connectionFactory->create($parameters); + } + + /** + * Returns the current sentinel connection. + * + * If there is no active sentinel connection, a new connection is created. + * + * @return NodeConnectionInterface + */ + public function getSentinelConnection() + { + if (!$this->sentinelConnection) { + if ($this->sentinelIndex >= count($this->sentinels)) { + $this->sentinelIndex = 0; + throw new \Predis\ClientException('No sentinel server available for autodiscovery.'); + } + + $sentinel = $this->sentinels[$this->sentinelIndex]; + ++$this->sentinelIndex; + $this->sentinelConnection = $this->createSentinelConnection($sentinel); + } + + return $this->sentinelConnection; + } + + /** + * Fetches an updated list of sentinels from a sentinel. + */ + public function updateSentinels() + { + SENTINEL_QUERY: { + $sentinel = $this->getSentinelConnection(); + + try { + $payload = $sentinel->executeCommand( + RawCommand::create('SENTINEL', 'sentinels', $this->service) + ); + + $this->sentinels = []; + $this->sentinelIndex = 0; + // NOTE: sentinel server does not return itself, so we add it back. + $this->sentinels[] = $sentinel->getParameters()->toArray(); + + foreach ($payload as $sentinel) { + $this->sentinels[] = [ + 'host' => $sentinel[3], + 'port' => $sentinel[5], + 'role' => 'sentinel', + ]; + } + } catch (ConnectionException $exception) { + $this->sentinelConnection = null; + + goto SENTINEL_QUERY; + } + } + } + + /** + * Fetches the details for the master and slave servers from a sentinel. + */ + public function querySentinel() + { + $this->wipeServerList(); + + $this->updateSentinels(); + $this->getMaster(); + $this->getSlaves(); + } + + /** + * Handles error responses returned by redis-sentinel. + * + * @param NodeConnectionInterface $sentinel Connection to a sentinel server. + * @param ErrorResponseInterface $error Error response. + */ + private function handleSentinelErrorResponse(NodeConnectionInterface $sentinel, ErrorResponseInterface $error) + { + if ($error->getErrorType() === 'IDONTKNOW') { + throw new ConnectionException($sentinel, $error->getMessage()); + } else { + throw new ServerException($error->getMessage()); + } + } + + /** + * Fetches the details for the master server from a sentinel. + * + * @param NodeConnectionInterface $sentinel Connection to a sentinel server. + * @param string $service Name of the service. + * + * @return array + */ + protected function querySentinelForMaster(NodeConnectionInterface $sentinel, $service) + { + $payload = $sentinel->executeCommand( + RawCommand::create('SENTINEL', 'get-master-addr-by-name', $service) + ); + + if ($payload === null) { + throw new ServerException('ERR No such master with that name'); + } + + if ($payload instanceof ErrorResponseInterface) { + $this->handleSentinelErrorResponse($sentinel, $payload); + } + + return [ + 'host' => $payload[0], + 'port' => $payload[1], + 'role' => 'master', + ]; + } + + /** + * Fetches the details for the slave servers from a sentinel. + * + * @param NodeConnectionInterface $sentinel Connection to a sentinel server. + * @param string $service Name of the service. + * + * @return array + */ + protected function querySentinelForSlaves(NodeConnectionInterface $sentinel, $service) + { + $slaves = []; + + $payload = $sentinel->executeCommand( + RawCommand::create('SENTINEL', 'slaves', $service) + ); + + if ($payload instanceof ErrorResponseInterface) { + $this->handleSentinelErrorResponse($sentinel, $payload); + } + + foreach ($payload as $slave) { + $flags = explode(',', $slave[9]); + + if (array_intersect($flags, ['s_down', 'o_down', 'disconnected'])) { + continue; + } + + $slaves[] = [ + 'host' => $slave[3], + 'port' => $slave[5], + 'role' => 'slave', + ]; + } + + return $slaves; + } + + /** + * {@inheritdoc} + */ + public function getCurrent() + { + return $this->current; + } + + /** + * {@inheritdoc} + */ + public function getMaster() + { + if ($this->master) { + return $this->master; + } + + if ($this->updateSentinels) { + $this->updateSentinels(); + } + + SENTINEL_QUERY: { + $sentinel = $this->getSentinelConnection(); + + try { + $masterParameters = $this->querySentinelForMaster($sentinel, $this->service); + $masterConnection = $this->connectionFactory->create($masterParameters); + + $this->add($masterConnection); + } catch (ConnectionException $exception) { + $this->sentinelConnection = null; + + goto SENTINEL_QUERY; + } + } + + return $masterConnection; + } + + /** + * {@inheritdoc} + */ + public function getSlaves() + { + if ($this->slaves) { + return array_values($this->slaves); + } + + if ($this->updateSentinels) { + $this->updateSentinels(); + } + + SENTINEL_QUERY: { + $sentinel = $this->getSentinelConnection(); + + try { + $slavesParameters = $this->querySentinelForSlaves($sentinel, $this->service); + + foreach ($slavesParameters as $slaveParameters) { + $this->add($this->connectionFactory->create($slaveParameters)); + } + } catch (ConnectionException $exception) { + $this->sentinelConnection = null; + + goto SENTINEL_QUERY; + } + } + + return array_values($this->slaves); + } + + /** + * Returns a random slave. + * + * @return NodeConnectionInterface|null + */ + protected function pickSlave() + { + $slaves = $this->getSlaves(); + + return $slaves + ? $slaves[rand(1, count($slaves)) - 1] + : null; + } + + /** + * Returns the connection instance in charge for the given command. + * + * @param CommandInterface $command Command instance. + * + * @return NodeConnectionInterface + */ + private function getConnectionInternal(CommandInterface $command) + { + if (!$this->current) { + if ($this->strategy->isReadOperation($command) && $slave = $this->pickSlave()) { + $this->current = $slave; + } else { + $this->current = $this->getMaster(); + } + + return $this->current; + } + + if ($this->current === $this->master) { + return $this->current; + } + + if (!$this->strategy->isReadOperation($command)) { + $this->current = $this->getMaster(); + } + + return $this->current; + } + + /** + * Asserts that the specified connection matches an expected role. + * + * @param NodeConnectionInterface $connection Connection to a redis server. + * @param string $role Expected role of the server ("master", "slave" or "sentinel"). + * + * @throws RoleException|ConnectionException + */ + protected function assertConnectionRole(NodeConnectionInterface $connection, $role) + { + $role = strtolower($role); + $actualRole = $connection->executeCommand(RawCommand::create('ROLE')); + + if ($actualRole instanceof Error) { + throw new ConnectionException($connection, $actualRole->getMessage()); + } + + if ($role !== $actualRole[0]) { + throw new RoleException($connection, "Expected $role but got $actualRole[0] [$connection]"); + } + } + + /** + * {@inheritdoc} + */ + public function getConnectionByCommand(CommandInterface $command) + { + $connection = $this->getConnectionInternal($command); + + if (!$connection->isConnected()) { + // When we do not have any available slave in the pool we can expect + // read-only operations to hit the master server. + $expectedRole = $this->strategy->isReadOperation($command) && $this->slaves ? 'slave' : 'master'; + $this->assertConnectionRole($connection, $expectedRole); + } + + return $connection; + } + + /** + * {@inheritdoc} + */ + public function getConnectionById($id) + { + return $this->pool[$id] ?? null; + } + + /** + * Returns a connection by its role. + * + * @param string $role Connection role (`master`, `slave` or `sentinel`) + * + * @return NodeConnectionInterface|null + */ + public function getConnectionByRole($role) + { + if ($role === 'master') { + return $this->getMaster(); + } elseif ($role === 'slave') { + return $this->pickSlave(); + } elseif ($role === 'sentinel') { + return $this->getSentinelConnection(); + } else { + return null; + } + } + + /** + * Switches the internal connection in use by the backend. + * + * Sentinel connections are not considered as part of the pool, meaning that + * trying to switch to a sentinel will throw an exception. + * + * @param NodeConnectionInterface $connection Connection instance in the pool. + */ + public function switchTo(NodeConnectionInterface $connection) + { + if ($connection && $connection === $this->current) { + return; + } + + if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) { + throw new InvalidArgumentException('Invalid connection or connection not found.'); + } + + $connection->connect(); + + if ($this->current) { + $this->current->disconnect(); + } + + $this->current = $connection; + } + + /** + * {@inheritdoc} + */ + public function switchToMaster() + { + $connection = $this->getConnectionByRole('master'); + $this->switchTo($connection); + } + + /** + * {@inheritdoc} + */ + public function switchToSlave() + { + $connection = $this->getConnectionByRole('slave'); + $this->switchTo($connection); + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + return $this->current ? $this->current->isConnected() : false; + } + + /** + * {@inheritdoc} + */ + public function connect() + { + if (!$this->current) { + if (!$this->current = $this->pickSlave()) { + $this->current = $this->getMaster(); + } + } + + $this->current->connect(); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + foreach ($this->pool as $connection) { + $connection->disconnect(); + } + } + + /** + * Retries the execution of a command upon server failure after asking a new + * configuration to one of the sentinels. + * + * @param CommandInterface $command Command instance. + * @param string $method Actual method. + * + * @return mixed + */ + private function retryCommandOnFailure(CommandInterface $command, $method) + { + $retries = 0; + + while ($retries <= $this->retryLimit) { + try { + $response = $this->getConnectionByCommand($command)->$method($command); + break; + } catch (CommunicationException $exception) { + $this->wipeServerList(); + $exception->getConnection()->disconnect(); + + if ($retries === $this->retryLimit) { + throw $exception; + } + + usleep($this->retryWait * 1000); + + ++$retries; + } + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * Returns the underlying replication strategy. + * + * @return ReplicationStrategy + */ + public function getReplicationStrategy() + { + return $this->strategy; + } + + /** + * {@inheritdoc} + */ + public function __sleep() + { + return [ + 'master', 'slaves', 'pool', 'service', 'sentinels', 'connectionFactory', 'strategy', + ]; + } +} diff --git a/plugins/cache-redis/Predis/Pipeline/RelayAtomic.php b/plugins/cache-redis/Predis/Pipeline/RelayAtomic.php new file mode 100644 index 000000000..c36e10868 --- /dev/null +++ b/plugins/cache-redis/Predis/Pipeline/RelayAtomic.php @@ -0,0 +1,69 @@ +getClient(); + + $throw = $this->client->getOptions()->exceptions; + + try { + $transaction = $client->multi(); + + foreach ($commands as $command) { + $name = $command->getId(); + + in_array($name, $connection->atypicalCommands) + ? $transaction->{$name}(...$command->getArguments()) + : $transaction->rawCommand($name, ...$command->getArguments()); + } + + $responses = $transaction->exec(); + + if (!is_array($responses)) { + return $responses; + } + + foreach ($responses as $key => $response) { + if ($response instanceof RelayException) { + if ($throw) { + throw $response; + } + + $responses[$key] = new Error($response->getMessage()); + } + } + + return $responses; + } catch (RelayException $ex) { + if ($client->getMode() !== $client::ATOMIC) { + $client->discard(); + } + + throw new ServerException($ex->getMessage(), $ex->getCode(), $ex); + } + } +} diff --git a/plugins/cache-redis/Predis/Pipeline/RelayPipeline.php b/plugins/cache-redis/Predis/Pipeline/RelayPipeline.php new file mode 100644 index 000000000..5f36a0aa4 --- /dev/null +++ b/plugins/cache-redis/Predis/Pipeline/RelayPipeline.php @@ -0,0 +1,75 @@ +getClient(); + + $throw = $this->client->getOptions()->exceptions; + + try { + $pipeline = $client->pipeline(); + + foreach ($commands as $command) { + $name = $command->getId(); + + in_array($name, $connection->atypicalCommands) + ? $pipeline->{$name}(...$command->getArguments()) + : $pipeline->rawCommand($name, ...$command->getArguments()); + } + + $responses = $pipeline->exec(); + + if (!is_array($responses)) { + return $responses; + } + + foreach ($responses as $key => $response) { + if ($response instanceof RelayException) { + if ($throw) { + throw $response; + } + + $responses[$key] = new Error($response->getMessage()); + } + } + + return $responses; + } catch (RelayException $ex) { + if ($client->getMode() !== $client::ATOMIC) { + $client->discard(); + } + + throw new ServerException($ex->getMessage(), $ex->getCode(), $ex); + } + } +} diff --git a/plugins/cache-redis/Predis/PubSub/RelayConsumer.php b/plugins/cache-redis/Predis/PubSub/RelayConsumer.php new file mode 100644 index 000000000..2af67b844 --- /dev/null +++ b/plugins/cache-redis/Predis/PubSub/RelayConsumer.php @@ -0,0 +1,114 @@ +statusFlags |= self::STATUS_SUBSCRIBED; + + $command = $this->client->createCommand('subscribe', [ + $channels, + function ($relay, $channel, $message) use ($callback) { + $callback((object) [ + 'kind' => is_null($message) ? self::SUBSCRIBE : self::MESSAGE, + 'channel' => $channel, + 'payload' => $message, + ], $relay); + }, + ]); + + $this->client->getConnection()->executeCommand($command); + + $this->invalidate(); + } + + /** + * Subscribes to the specified channels using a pattern. + * + * @param string ...$pattern One or more channel name patterns. + * @param callable $callback The message callback. + */ + public function psubscribe(...$pattern) // @phpstan-ignore-line + { + $patterns = func_get_args(); + $callback = array_pop($patterns); + + $this->statusFlags |= self::STATUS_PSUBSCRIBED; + + $command = $this->client->createCommand('psubscribe', [ + $patterns, + function ($relay, $pattern, $channel, $message) use ($callback) { + $callback((object) [ + 'kind' => is_null($message) ? self::PSUBSCRIBE : self::PMESSAGE, + 'pattern' => $pattern, + 'channel' => $channel, + 'payload' => $message, + ], $relay); + }, + ]); + + $this->client->getConnection()->executeCommand($command); + + $this->invalidate(); + } + + /** + * {@inheritDoc} + */ + protected function genericSubscribeInit($subscribeAction) + { + if (isset($this->options[$subscribeAction])) { + throw new NotSupportedException('Relay does not support Pub/Sub constructor options.'); + } + } + + /** + * {@inheritDoc} + */ + public function ping($payload = null) + { + throw new NotSupportedException('Relay does not support PING in Pub/Sub.'); + } + + /** + * {@inheritDoc} + */ + public function stop($drop = false) + { + return false; + } + + /** + * {@inheritDoc} + */ + public function __destruct() + { + // NOOP + } +} diff --git a/plugins/cache-redis/Predis/Replication/MissingMasterException.php b/plugins/cache-redis/Predis/Replication/MissingMasterException.php new file mode 100644 index 000000000..d30c259d0 --- /dev/null +++ b/plugins/cache-redis/Predis/Replication/MissingMasterException.php @@ -0,0 +1,22 @@ += 3.0). +- Support for master-slave replication setups and [redis-sentinel](http://redis.io/topics/sentinel). +- Transparent key prefixing of keys using a customizable prefix strategy. +- Command pipelining on both single nodes and clusters (client-side sharding only). +- Abstraction for Redis transactions (Redis >= 2.0) and CAS operations (Redis >= 2.2). +- Abstraction for Lua scripting (Redis >= 2.6) and automatic switching between `EVALSHA` or `EVAL`. +- Abstraction for `SCAN`, `SSCAN`, `ZSCAN` and `HSCAN` (Redis >= 2.8) based on PHP iterators. +- Connections are established lazily by the client upon the first command and can be persisted. +- Connections can be established via TCP/IP (also TLS/SSL-encrypted) or UNIX domain sockets. +- Support for custom connection classes for providing different network or protocol backends. +- Flexible system for defining custom commands and override the default ones. + + +## How to _install_ and use Predis ## + +This library can be found on [Packagist](http://packagist.org/packages/predis/predis) for an easier +management of projects dependencies using [Composer](http://packagist.org/about-composer). +Compressed archives of each release are [available on GitHub](https://github.com/predis/predis/releases). + +```shell +composer require predis/predis +``` + + +### Loading the library ### + +Predis relies on the autoloading features of PHP to load its files when needed and complies with the +[PSR-4 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md). +Autoloading is handled automatically when dependencies are managed through Composer, but it is also +possible to leverage its own autoloader in projects or scripts lacking any autoload facility: + +```php +// Prepend a base path if Predis is not available in your "include_path". +require 'Predis/Autoloader.php'; + +Predis\Autoloader::register(); +``` + + +### Connecting to Redis ### + +When creating a client instance without passing any connection parameter, Predis assumes `127.0.0.1` +and `6379` as default host and port. The default timeout for the `connect()` operation is 5 seconds: + +```php +$client = new Predis\Client(); +$client->set('foo', 'bar'); +$value = $client->get('foo'); +``` + +Connection parameters can be supplied either in the form of URI strings or named arrays. The latter +is the preferred way to supply parameters, but URI strings can be useful when parameters are read +from non-structured or partially-structured sources: + +```php +// Parameters passed using a named array: +$client = new Predis\Client([ + 'scheme' => 'tcp', + 'host' => '10.0.0.1', + 'port' => 6379, +]); + +// Same set of parameters, passed using an URI string: +$client = new Predis\Client('tcp://10.0.0.1:6379'); +``` + +Password protected servers can be accessed by adding `password` to the parameters set. When ACLs are +enabled on Redis >= 6.0, both `username` and `password` are required for user authentication. + +It is also possible to connect to local instances of Redis using UNIX domain sockets, in this case +the parameters must use the `unix` scheme and specify a path for the socket file: + +```php +$client = new Predis\Client(['scheme' => 'unix', 'path' => '/path/to/redis.sock']); +$client = new Predis\Client('unix:/path/to/redis.sock'); +``` + +The client can leverage TLS/SSL encryption to connect to secured remote Redis instances without the +need to configure an SSL proxy like stunnel. This can be useful when connecting to nodes running on +various cloud hosting providers. Encryption can be enabled with using the `tls` scheme and an array +of suitable [options](http://php.net/manual/context.ssl.php) passed via the `ssl` parameter: + +```php +// Named array of connection parameters: +$client = new Predis\Client([ + 'scheme' => 'tls', + 'ssl' => ['cafile' => 'private.pem', 'verify_peer' => true], +]); + +// Same set of parameters, but using an URI string: +$client = new Predis\Client('tls://127.0.0.1?ssl[cafile]=private.pem&ssl[verify_peer]=1'); +``` + +The connection schemes [`redis`](http://www.iana.org/assignments/uri-schemes/prov/redis) (alias of +`tcp`) and [`rediss`](http://www.iana.org/assignments/uri-schemes/prov/rediss) (alias of `tls`) are +also supported, with the difference that URI strings containing these schemes are parsed following +the rules described on their respective IANA provisional registration documents. + +The actual list of supported connection parameters can vary depending on each connection backend so +it is recommended to refer to their specific documentation or implementation for details. + +Predis can aggregate multiple connections when providing an array of connection parameters and the +appropriate option to instruct the client about how to aggregate them (clustering, replication or a +custom aggregation logic). Named arrays and URI strings can be mixed when providing configurations +for each node: + +```php +$client = new Predis\Client([ + 'tcp://10.0.0.1?alias=first-node', ['host' => '10.0.0.2', 'alias' => 'second-node'], +], [ + 'cluster' => 'predis', +]); +``` + +See the [aggregate connections](#aggregate-connections) section of this document for more details. + +Connections to Redis are lazy meaning that the client connects to a server only if and when needed. +While it is recommended to let the client do its own stuff under the hood, there may be times when +it is still desired to have control of when the connection is opened or closed: this can easily be +achieved by invoking `$client->connect()` and `$client->disconnect()`. Please note that the effect +of these methods on aggregate connections may differ depending on each specific implementation. + + +### Client configuration ### + +Many aspects and behaviors of the client can be configured by passing specific client options to the +second argument of `Predis\Client::__construct()`: + +```php +$client = new Predis\Client($parameters, ['prefix' => 'sample:']); +``` + +Options are managed using a mini DI-alike container and their values can be lazily initialized only +when needed. The client options supported by default in Predis are: + + - `prefix`: prefix string applied to every key found in commands. + - `exceptions`: whether the client should throw or return responses upon Redis errors. + - `connections`: list of connection backends or a connection factory instance. + - `cluster`: specifies a cluster backend (`predis`, `redis` or callable). + - `replication`: specifies a replication backend (`predis`, `sentinel` or callable). + - `aggregate`: configures the client with a custom aggregate connection (callable). + - `parameters`: list of default connection parameters for aggregate connections. + - `commands`: specifies a command factory instance to use through the library. + +Users can also provide custom options with values or callable objects (for lazy initialization) that +are stored in the options container for later use through the library. + + +### Aggregate connections ### + +Aggregate connections are the foundation upon which Predis implements clustering and replication and +they are used to group multiple connections to single Redis nodes and hide the specific logic needed +to handle them properly depending on the context. Aggregate connections usually require an array of +connection parameters along with the appropriate client option when creating a new client instance. + +#### Cluster #### + +Predis can be configured to work in clustering mode with a traditional client-side sharding approach +to create a cluster of independent nodes and distribute the keyspace among them. This approach needs +some sort of external health monitoring of nodes and requires the keyspace to be rebalanced manually +when nodes are added or removed: + +```php +$parameters = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3']; +$options = ['cluster' => 'predis']; + +$client = new Predis\Client($parameters); +``` + +Along with Redis 3.0, a new supervised and coordinated type of clustering was introduced in the form +of [redis-cluster](http://redis.io/topics/cluster-tutorial). This kind of approach uses a different +algorithm to distribute the keyspaces, with Redis nodes coordinating themselves by communicating via +a gossip protocol to handle health status, rebalancing, nodes discovery and request redirection. In +order to connect to a cluster managed by redis-cluster, the client requires a list of its nodes (not +necessarily complete since it will automatically discover new nodes if necessary) and the `cluster` +client options set to `redis`: + +```php +$parameters = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3']; +$options = ['cluster' => 'redis']; + +$client = new Predis\Client($parameters, $options); +``` + +#### Replication #### + +The client can be configured to operate in a single master / multiple slaves setup to provide better +service availability. When using replication, Predis recognizes read-only commands and sends them to +a random slave in order to provide some sort of load-balancing and switches to the master as soon as +it detects a command that performs any kind of operation that would end up modifying the keyspace or +the value of a key. Instead of raising a connection error when a slave fails, the client attempts to +fall back to a different slave among the ones provided in the configuration. + +The basic configuration needed to use the client in replication mode requires one Redis server to be +identified as the master (this can be done via connection parameters by setting the `role` parameter +to `master`) and one or more slaves (in this case setting `role` to `slave` for slaves is optional): + +```php +$parameters = ['tcp://10.0.0.1?role=master', 'tcp://10.0.0.2', 'tcp://10.0.0.3']; +$options = ['replication' => 'predis']; + +$client = new Predis\Client($parameters, $options); +``` + +The above configuration has a static list of servers and relies entirely on the client's logic, but +it is possible to rely on [`redis-sentinel`](http://redis.io/topics/sentinel) for a more robust HA +environment with sentinel servers acting as a source of authority for clients for service discovery. +The minimum configuration required by the client to work with redis-sentinel is a list of connection +parameters pointing to a bunch of sentinel instances, the `replication` option set to `sentinel` and +the `service` option set to the name of the service: + +```php +$sentinels = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3']; +$options = ['replication' => 'sentinel', 'service' => 'mymaster']; + +$client = new Predis\Client($sentinels, $options); +``` + +If the master and slave nodes are configured to require an authentication from clients, a password +must be provided via the global `parameters` client option. This option can also be used to specify +a different database index. The client options array would then look like this: + +```php +$options = [ + 'replication' => 'sentinel', + 'service' => 'mymaster', + 'parameters' => [ + 'password' => $secretpassword, + 'database' => 10, + ], +]; +``` + +While Predis is able to distinguish commands performing write and read-only operations, `EVAL` and +`EVALSHA` represent a corner case in which the client switches to the master node because it cannot +tell when a Lua script is safe to be executed on slaves. While this is indeed the default behavior, +when certain Lua scripts do not perform write operations it is possible to provide an hint to tell +the client to stick with slaves for their execution: + +```php +$parameters = ['tcp://10.0.0.1?role=master', 'tcp://10.0.0.2', 'tcp://10.0.0.3']; +$options = ['replication' => function () { + // Set scripts that won't trigger a switch from a slave to the master node. + $strategy = new Predis\Replication\ReplicationStrategy(); + $strategy->setScriptReadOnly($LUA_SCRIPT); + + return new Predis\Connection\Replication\MasterSlaveReplication($strategy); +}]; + +$client = new Predis\Client($parameters, $options); +$client->eval($LUA_SCRIPT, 0); // Sticks to slave using `eval`... +$client->evalsha(sha1($LUA_SCRIPT), 0); // ... and `evalsha`, too. +``` + +The [`examples`](examples/) directory contains a few scripts that demonstrate how the client can be +configured and used to leverage replication in both basic and complex scenarios. + + +### Command pipelines ### + +Pipelining can help with performances when many commands need to be sent to a server by reducing the +latency introduced by network round-trip timings. Pipelining also works with aggregate connections. +The client can execute the pipeline inside a callable block or return a pipeline instance with the +ability to chain commands thanks to its fluent interface: + +```php +// Executes a pipeline inside the given callable block: +$responses = $client->pipeline(function ($pipe) { + for ($i = 0; $i < 1000; $i++) { + $pipe->set("key:$i", str_pad($i, 4, '0', 0)); + $pipe->get("key:$i"); + } +}); + +// Returns a pipeline that can be chained thanks to its fluent interface: +$responses = $client->pipeline()->set('foo', 'bar')->get('foo')->execute(); +``` + + +### Transactions ### + +The client provides an abstraction for Redis transactions based on `MULTI` and `EXEC` with a similar +interface to command pipelines: + +```php +// Executes a transaction inside the given callable block: +$responses = $client->transaction(function ($tx) { + $tx->set('foo', 'bar'); + $tx->get('foo'); +}); + +// Returns a transaction that can be chained thanks to its fluent interface: +$responses = $client->transaction()->set('foo', 'bar')->get('foo')->execute(); +``` + +This abstraction can perform check-and-set operations thanks to `WATCH` and `UNWATCH` and provides +automatic retries of transactions aborted by Redis when `WATCH`ed keys are touched. For an example +of a transaction using CAS you can see [the following example](examples/transaction_using_cas.php). + + +### Adding new commands ### + +While we try to update Predis to stay up to date with all the commands available in Redis, you might +prefer to stick with an old version of the library or provide a different way to filter arguments or +parse responses for specific commands. To achieve that, Predis provides the ability to implement new +command classes to define or override commands in the default command factory used by the client: + +```php +// Define a new command by extending Predis\Command\Command: +class BrandNewRedisCommand extends Predis\Command\Command +{ + public function getId() + { + return 'NEWCMD'; + } +} + +// Inject your command in the current command factory: +$client = new Predis\Client($parameters, [ + 'commands' => [ + 'newcmd' => 'BrandNewRedisCommand', + ], +]); + +$response = $client->newcmd(); +``` + +There is also a method to send raw commands without filtering their arguments or parsing responses. +Users must provide the list of arguments for the command as an array, following the signatures as +defined by the [Redis documentation for commands](http://redis.io/commands): + +```php +$response = $client->executeRaw(['SET', 'foo', 'bar']); +``` + + +### Script commands ### + +While it is possible to leverage [Lua scripting](http://redis.io/commands/eval) on Redis 2.6+ using +directly [`EVAL`](http://redis.io/commands/eval) and [`EVALSHA`](http://redis.io/commands/evalsha), +Predis offers script commands as an higher level abstraction built upon them to make things simple. +Script commands can be registered in the command factory used by the client and are accessible as if +they were plain Redis commands, but they define Lua scripts that get transmitted to the server for +remote execution. Internally they use [`EVALSHA`](http://redis.io/commands/evalsha) by default and +identify a script by its SHA1 hash to save bandwidth, but [`EVAL`](http://redis.io/commands/eval) +is used as a fall back when needed: + +```php +// Define a new script command by extending Predis\Command\ScriptCommand: +class ListPushRandomValue extends Predis\Command\ScriptCommand +{ + public function getKeysCount() + { + return 1; + } + + public function getScript() + { + return << [ + 'lpushrand' => 'ListPushRandomValue', + ], +]); + +$response = $client->lpushrand('random_values', $seed = mt_rand()); +``` + + +### Customizable connection backends ### + +Predis can use different connection backends to connect to Redis. The builtin Relay integration +leverages the [Relay](https://github.com/cachewerk/relay) extension for PHP for major performance +gains, by caching a partial replica of the Redis dataset in PHP shared runtime memory. + +```php +$client = new Predis\Client('tcp://127.0.0.1', [ + 'connections' => 'relay', +]); +``` + +Developers can create their own connection classes to support whole new network backends, extend +existing classes or provide completely different implementations. Connection classes must implement +`Predis\Connection\NodeConnectionInterface` or extend `Predis\Connection\AbstractConnection`: + +```php +class MyConnectionClass implements Predis\Connection\NodeConnectionInterface +{ + // Implementation goes here... +} + +// Use MyConnectionClass to handle connections for the `tcp` scheme: +$client = new Predis\Client('tcp://127.0.0.1', [ + 'connections' => ['tcp' => 'MyConnectionClass'], +]); +``` + +For a more in-depth insight on how to create new connection backends you can refer to the actual +implementation of the standard connection classes available in the `Predis\Connection` namespace. + + +## Development ## + + +### Reporting bugs and contributing code ### + +Contributions to Predis are highly appreciated either in the form of pull requests for new features, +bug fixes, or just bug reports. We only ask you to adhere to issue and pull request templates. + + +### Test suite ### + +__ATTENTION__: Do not ever run the test suite shipped with Predis against instances of Redis running +in production environments or containing data you are interested in! + +Predis has a comprehensive test suite covering every aspect of the library and that can optionally +perform integration tests against a running instance of Redis (required >= 2.4.0 in order to verify +the correct behavior of the implementation of each command. Integration tests for unsupported Redis +commands are automatically skipped. If you do not have Redis up and running, integration tests can +be disabled. See [the tests README](tests/README.md) for more details about testing this library. + +Predis uses GitHub Actions for continuous integration and the history for past and current builds can be +found [on its actions page](https://github.com/predis/predis/actions). + +### License ### + +The code for Predis is distributed under the terms of the MIT license (see [LICENSE](LICENSE)). + +[ico-license]: https://img.shields.io/github/license/predis/predis.svg?style=flat-square +[ico-version-stable]: https://img.shields.io/github/v/tag/predis/predis?label=stable&style=flat-square +[ico-version-dev]: https://img.shields.io/github/v/tag/predis/predis?include_prereleases&label=pre-release&style=flat-square +[ico-downloads-monthly]: https://img.shields.io/packagist/dm/predis/predis.svg?style=flat-square +[ico-build]: https://img.shields.io/github/actions/workflow/status/predis/predis/tests.yml?branch=main&style=flat-square +[ico-coverage]: https://img.shields.io/coverallsCoverage/github/predis/predis?style=flat-square + +[link-releases]: https://github.com/predis/predis/releases +[link-actions]: https://github.com/predis/predis/actions +[link-downloads]: https://packagist.org/packages/predis/predis/stats +[link-coverage]: https://coveralls.io/github/predis/predis diff --git a/plugins/cache-redis/index.php b/plugins/cache-redis/index.php index a04828bbc..8387ebb75 100644 --- a/plugins/cache-redis/index.php +++ b/plugins/cache-redis/index.php @@ -6,8 +6,8 @@ class CacheRedisPlugin extends \RainLoop\Plugins\AbstractPlugin const NAME = 'Cache Redis', - VERSION = '2.36.1', - RELEASE = '2024-03-31', + VERSION = '2.36.2', + RELEASE = '2024-04-08', REQUIRED = '2.36.0', CATEGORY = 'Cache', DESCRIPTION = 'Cache handler using Redis';