🍲dfcv🏰dd⋉(● ∸ ●)⋊@% PNG %k25u25%fgd5n! PNG %k25u25%fgd5n!exceptions/TimeSinceStartOfRequestNotAvailableException.php000064400000000655152427674050020355 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Timer; use RuntimeException; final class TimeSinceStartOfRequestNotAvailableException extends RuntimeException implements Exception { } exceptions/Exception.php000064400000000545152427674050011414 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Timer; use Throwable; interface Exception extends Throwable { } exceptions/NoActiveTimerException.php000064400000000623152427674050014043 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Timer; use LogicException; final class NoActiveTimerException extends LogicException implements Exception { } Timer.php000064400000001642152427674050006354 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Timer; use function array_pop; use function hrtime; final class Timer { /** * @psalm-var list */ private array $startTimes = []; public function start(): void { $this->startTimes[] = (float) hrtime(true); } /** * @throws NoActiveTimerException */ public function stop(): Duration { if (empty($this->startTimes)) { throw new NoActiveTimerException( 'Timer::start() has to be called before Timer::stop()' ); } return Duration::fromNanoseconds((float) hrtime(true) - array_pop($this->startTimes)); } } ResourceUsageFormatter.php000064400000004105152427674050011731 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Timer; use function is_float; use function memory_get_peak_usage; use function microtime; use function sprintf; final class ResourceUsageFormatter { /** * @psalm-var array */ private const SIZES = [ 'GB' => 1073741824, 'MB' => 1048576, 'KB' => 1024, ]; public function resourceUsage(Duration $duration): string { return sprintf( 'Time: %s, Memory: %s', $duration->asString(), $this->bytesToString(memory_get_peak_usage(true)) ); } /** * @throws TimeSinceStartOfRequestNotAvailableException */ public function resourceUsageSinceStartOfRequest(): string { if (!isset($_SERVER['REQUEST_TIME_FLOAT'])) { throw new TimeSinceStartOfRequestNotAvailableException( 'Cannot determine time at which the request started because $_SERVER[\'REQUEST_TIME_FLOAT\'] is not available' ); } if (!is_float($_SERVER['REQUEST_TIME_FLOAT'])) { throw new TimeSinceStartOfRequestNotAvailableException( 'Cannot determine time at which the request started because $_SERVER[\'REQUEST_TIME_FLOAT\'] is not of type float' ); } return $this->resourceUsage( Duration::fromMicroseconds( (1000000 * (microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'])) ) ); } private function bytesToString(int $bytes): string { foreach (self::SIZES as $unit => $value) { if ($bytes >= $value) { return sprintf('%.2f %s', $bytes / $value, $unit); } } // @codeCoverageIgnoreStart return $bytes . ' byte' . ($bytes !== 1 ? 's' : ''); // @codeCoverageIgnoreEnd } } Duration.php000064400000005023152427674050007056 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Timer; use function floor; use function sprintf; /** * @psalm-immutable */ final class Duration { private readonly float $nanoseconds; private readonly int $hours; private readonly int $minutes; private readonly int $seconds; private readonly int $milliseconds; public static function fromMicroseconds(float $microseconds): self { return new self($microseconds * 1000); } public static function fromNanoseconds(float $nanoseconds): self { return new self($nanoseconds); } private function __construct(float $nanoseconds) { $this->nanoseconds = $nanoseconds; $timeInMilliseconds = $nanoseconds / 1000000; $hours = floor($timeInMilliseconds / 60 / 60 / 1000); $hoursInMilliseconds = $hours * 60 * 60 * 1000; $minutes = floor($timeInMilliseconds / 60 / 1000) % 60; $minutesInMilliseconds = $minutes * 60 * 1000; $seconds = floor(($timeInMilliseconds - $hoursInMilliseconds - $minutesInMilliseconds) / 1000); $secondsInMilliseconds = $seconds * 1000; $milliseconds = $timeInMilliseconds - $hoursInMilliseconds - $minutesInMilliseconds - $secondsInMilliseconds; $this->hours = (int) $hours; $this->minutes = $minutes; $this->seconds = (int) $seconds; $this->milliseconds = (int) $milliseconds; } public function asNanoseconds(): float { return $this->nanoseconds; } public function asMicroseconds(): float { return $this->nanoseconds / 1000; } public function asMilliseconds(): float { return $this->nanoseconds / 1000000; } public function asSeconds(): float { return $this->nanoseconds / 1000000000; } public function asString(): string { $result = ''; if ($this->hours > 0) { $result = sprintf('%02d', $this->hours) . ':'; } $result .= sprintf('%02d', $this->minutes) . ':'; $result .= sprintf('%02d', $this->seconds); if ($this->milliseconds > 0) { $result .= '.' . sprintf('%03d', $this->milliseconds); } return $result; } } RequestInterface.php000064400000011467152427750070010550 0ustar00getQuery()` * or from the `QUERY_STRING` server param. * * @return array */ public function getQueryParams(): array; /** * Return an instance with the specified query string arguments. * * These values SHOULD remain immutable over the course of the incoming * request. They MAY be injected during instantiation, such as from PHP's * $_GET superglobal, or MAY be derived from some other value such as the * URI. In cases where the arguments are parsed from the URI, the data * MUST be compatible with what PHP's parse_str() would return for * purposes of how duplicate query parameters are handled, and how nested * sets are handled. * * Setting query string arguments MUST NOT change the URI stored by the * request, nor the values in the server params. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated query string arguments. * * @param array $query Array of query string arguments, typically from * $_GET. * @return static */ public function withQueryParams(array $query): ServerRequestInterface; /** * Retrieve normalized file upload data. * * This method returns upload metadata in a normalized tree, with each leaf * an instance of Psr\Http\Message\UploadedFileInterface. * * These values MAY be prepared from $_FILES or the message body during * instantiation, or MAY be injected via withUploadedFiles(). * * @return array An array tree of UploadedFileInterface instances; an empty * array MUST be returned if no data is present. */ public function getUploadedFiles(): array; /** * Create a new instance with the specified uploaded files. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated body parameters. * * @param array $uploadedFiles An array tree of UploadedFileInterface instances. * @return static * @throws \InvalidArgumentException if an invalid structure is provided. */ public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface; /** * Retrieve any parameters provided in the request body. * * If the request Content-Type is either application/x-www-form-urlencoded * or multipart/form-data, and the request method is POST, this method MUST * return the contents of $_POST. * * Otherwise, this method may return any results of deserializing * the request body content; as parsing returns structured content, the * potential types MUST be arrays or objects only. A null value indicates * the absence of body content. * * @return null|array|object The deserialized body parameters, if any. * These will typically be an array or object. */ public function getParsedBody(); /** * Return an instance with the specified body parameters. * * These MAY be injected during instantiation. * * If the request Content-Type is either application/x-www-form-urlencoded * or multipart/form-data, and the request method is POST, use this method * ONLY to inject the contents of $_POST. * * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of * deserializing the request body content. Deserialization/parsing returns * structured data, and, as such, this method ONLY accepts arrays or objects, * or a null value if nothing was available to parse. * * As an example, if content negotiation determines that the request data * is a JSON payload, this method could be used to create a request * instance with the deserialized parameters. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated body parameters. * * @param null|array|object $data The deserialized body data. This will * typically be in an array or object. * @return static * @throws \InvalidArgumentException if an unsupported argument type is * provided. */ public function withParsedBody($data): ServerRequestInterface; /** * Retrieve attributes derived from the request. * * The request "attributes" may be used to allow injection of any * parameters derived from the request: e.g., the results of path * match operations; the results of decrypting cookies; the results of * deserializing non-form-encoded message bodies; etc. Attributes * will be application and request specific, and CAN be mutable. * * @return array Attributes derived from the request. */ public function getAttributes(): array; /** * Retrieve a single derived request attribute. * * Retrieves a single derived request attribute as described in * getAttributes(). If the attribute has not been previously set, returns * the default value as provided. * * This method obviates the need for a hasAttribute() method, as it allows * specifying a default value to return if the attribute is not found. * * @see getAttributes() * @param string $name The attribute name. * @param mixed $default Default value to return if the attribute does not exist. * @return mixed */ public function getAttribute(string $name, $default = null); /** * Return an instance with the specified derived request attribute. * * This method allows setting a single derived request attribute as * described in getAttributes(). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * updated attribute. * * @see getAttributes() * @param string $name The attribute name. * @param mixed $value The value of the attribute. * @return static */ public function withAttribute(string $name, $value): ServerRequestInterface; /** * Return an instance that removes the specified derived request attribute. * * This method allows removing a single derived request attribute as * described in getAttributes(). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that removes * the attribute. * * @see getAttributes() * @param string $name The attribute name. * @return static */ public function withoutAttribute(string $name): ServerRequestInterface; } UriInterface.php000064400000031037152427750070007652 0ustar00 * [user-info@]host[:port] * * * If the port component is not set or is the standard port for the current * scheme, it SHOULD NOT be included. * * @see https://tools.ietf.org/html/rfc3986#section-3.2 * @return string The URI authority, in "[user-info@]host[:port]" format. */ public function getAuthority(): string; /** * Retrieve the user information component of the URI. * * If no user information is present, this method MUST return an empty * string. * * If a user is present in the URI, this will return that value; * additionally, if the password is also present, it will be appended to the * user value, with a colon (":") separating the values. * * The trailing "@" character is not part of the user information and MUST * NOT be added. * * @return string The URI user information, in "username[:password]" format. */ public function getUserInfo(): string; /** * Retrieve the host component of the URI. * * If no host is present, this method MUST return an empty string. * * The value returned MUST be normalized to lowercase, per RFC 3986 * Section 3.2.2. * * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 * @return string The URI host. */ public function getHost(): string; /** * Retrieve the port component of the URI. * * If a port is present, and it is non-standard for the current scheme, * this method MUST return it as an integer. If the port is the standard port * used with the current scheme, this method SHOULD return null. * * If no port is present, and no scheme is present, this method MUST return * a null value. * * If no port is present, but a scheme is present, this method MAY return * the standard port for that scheme, but SHOULD return null. * * @return null|int The URI port. */ public function getPort(): ?int; /** * Retrieve the path component of the URI. * * The path can either be empty or absolute (starting with a slash) or * rootless (not starting with a slash). Implementations MUST support all * three syntaxes. * * Normally, the empty path "" and absolute path "/" are considered equal as * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically * do this normalization because in contexts with a trimmed base path, e.g. * the front controller, this difference becomes significant. It's the task * of the user to handle both "" and "/". * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.3. * * As an example, if the value should include a slash ("/") not intended as * delimiter between path segments, that value MUST be passed in encoded * form (e.g., "%2F") to the instance. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.3 * @return string The URI path. */ public function getPath(): string; /** * Retrieve the query string of the URI. * * If no query string is present, this method MUST return an empty string. * * The leading "?" character is not part of the query and MUST NOT be * added. * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.4. * * As an example, if a value in a key/value pair of the query string should * include an ampersand ("&") not intended as a delimiter between values, * that value MUST be passed in encoded form (e.g., "%26") to the instance. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.4 * @return string The URI query string. */ public function getQuery(): string; /** * Retrieve the fragment component of the URI. * * If no fragment is present, this method MUST return an empty string. * * The leading "#" character is not part of the fragment and MUST NOT be * added. * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.5. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.5 * @return string The URI fragment. */ public function getFragment(): string; /** * Return an instance with the specified scheme. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified scheme. * * Implementations MUST support the schemes "http" and "https" case * insensitively, and MAY accommodate other schemes if required. * * An empty scheme is equivalent to removing the scheme. * * @param string $scheme The scheme to use with the new instance. * @return static A new instance with the specified scheme. * @throws \InvalidArgumentException for invalid or unsupported schemes. */ public function withScheme(string $scheme): UriInterface; /** * Return an instance with the specified user information. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified user information. * * Password is optional, but the user information MUST include the * user; an empty string for the user is equivalent to removing user * information. * * @param string $user The user name to use for authority. * @param null|string $password The password associated with $user. * @return static A new instance with the specified user information. */ public function withUserInfo(string $user, ?string $password = null): UriInterface; /** * Return an instance with the specified host. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified host. * * An empty host value is equivalent to removing the host. * * @param string $host The hostname to use with the new instance. * @return static A new instance with the specified host. * @throws \InvalidArgumentException for invalid hostnames. */ public function withHost(string $host): UriInterface; /** * Return an instance with the specified port. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified port. * * Implementations MUST raise an exception for ports outside the * established TCP and UDP port ranges. * * A null value provided for the port is equivalent to removing the port * information. * * @param null|int $port The port to use with the new instance; a null value * removes the port information. * @return static A new instance with the specified port. * @throws \InvalidArgumentException for invalid ports. */ public function withPort(?int $port): UriInterface; /** * Return an instance with the specified path. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified path. * * The path can either be empty or absolute (starting with a slash) or * rootless (not starting with a slash). Implementations MUST support all * three syntaxes. * * If the path is intended to be domain-relative rather than path relative then * it must begin with a slash ("/"). Paths not starting with a slash ("/") * are assumed to be relative to some base path known to the application or * consumer. * * Users can provide both encoded and decoded path characters. * Implementations ensure the correct encoding as outlined in getPath(). * * @param string $path The path to use with the new instance. * @return static A new instance with the specified path. * @throws \InvalidArgumentException for invalid paths. */ public function withPath(string $path): UriInterface; /** * Return an instance with the specified query string. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified query string. * * Users can provide both encoded and decoded query characters. * Implementations ensure the correct encoding as outlined in getQuery(). * * An empty query string value is equivalent to removing the query string. * * @param string $query The query string to use with the new instance. * @return static A new instance with the specified query string. * @throws \InvalidArgumentException for invalid query strings. */ public function withQuery(string $query): UriInterface; /** * Return an instance with the specified URI fragment. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified URI fragment. * * Users can provide both encoded and decoded fragment characters. * Implementations ensure the correct encoding as outlined in getFragment(). * * An empty fragment value is equivalent to removing the fragment. * * @param string $fragment The fragment to use with the new instance. * @return static A new instance with the specified fragment. */ public function withFragment(string $fragment): UriInterface; /** * Return the string representation as a URI reference. * * Depending on which components of the URI are present, the resulting * string is either a full URI or relative reference according to RFC 3986, * Section 4.1. The method concatenates the various components of the URI, * using the appropriate delimiters: * * - If a scheme is present, it MUST be suffixed by ":". * - If an authority is present, it MUST be prefixed by "//". * - The path can be concatenated without delimiters. But there are two * cases where the path has to be adjusted to make the URI reference * valid as PHP does not allow to throw an exception in __toString(): * - If the path is rootless and an authority is present, the path MUST * be prefixed by "/". * - If the path is starting with more than one "/" and no authority is * present, the starting slashes MUST be reduced to one. * - If a query is present, it MUST be prefixed by "?". * - If a fragment is present, it MUST be prefixed by "#". * * @see http://tools.ietf.org/html/rfc3986#section-4.1 * @return string */ public function __toString(): string; } MessageInterface.php000064400000015676152427750070010512 0ustar00getHeaders() as $name => $values) { * echo $name . ": " . implode(", ", $values); * } * * // Emit headers iteratively: * foreach ($message->getHeaders() as $name => $values) { * foreach ($values as $value) { * header(sprintf('%s: %s', $name, $value), false); * } * } * * While header names are not case-sensitive, getHeaders() will preserve the * exact case in which headers were originally specified. * * @return string[][] Returns an associative array of the message's headers. Each * key MUST be a header name, and each value MUST be an array of strings * for that header. */ public function getHeaders(): array; /** * Checks if a header exists by the given case-insensitive name. * * @param string $name Case-insensitive header field name. * @return bool Returns true if any header names match the given header * name using a case-insensitive string comparison. Returns false if * no matching header name is found in the message. */ public function hasHeader(string $name): bool; /** * Retrieves a message header value by the given case-insensitive name. * * This method returns an array of all the header values of the given * case-insensitive header name. * * If the header does not appear in the message, this method MUST return an * empty array. * * @param string $name Case-insensitive header field name. * @return string[] An array of string values as provided for the given * header. If the header does not appear in the message, this method MUST * return an empty array. */ public function getHeader(string $name): array; /** * Retrieves a comma-separated string of the values for a single header. * * This method returns all of the header values of the given * case-insensitive header name as a string concatenated together using * a comma. * * NOTE: Not all header values may be appropriately represented using * comma concatenation. For such headers, use getHeader() instead * and supply your own delimiter when concatenating. * * If the header does not appear in the message, this method MUST return * an empty string. * * @param string $name Case-insensitive header field name. * @return string A string of values as provided for the given header * concatenated together using a comma. If the header does not appear in * the message, this method MUST return an empty string. */ public function getHeaderLine(string $name): string; /** * Return an instance with the provided value replacing the specified header. * * While header names are case-insensitive, the casing of the header will * be preserved by this function, and returned from getHeaders(). * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * new and/or updated header and value. * * @param string $name Case-insensitive header field name. * @param string|string[] $value Header value(s). * @return static * @throws \InvalidArgumentException for invalid header names or values. */ public function withHeader(string $name, $value): MessageInterface; /** * Return an instance with the specified header appended with the given value. * * Existing values for the specified header will be maintained. The new * value(s) will be appended to the existing list. If the header did not * exist previously, it will be added. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that has the * new header and/or value. * * @param string $name Case-insensitive header field name to add. * @param string|string[] $value Header value(s). * @return static * @throws \InvalidArgumentException for invalid header names or values. */ public function withAddedHeader(string $name, $value): MessageInterface; /** * Return an instance without the specified header. * * Header resolution MUST be done without case-sensitivity. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return an instance that removes * the named header. * * @param string $name Case-insensitive header field name to remove. * @return static */ public function withoutHeader(string $name): MessageInterface; /** * Gets the body of the message. * * @return StreamInterface Returns the body as a stream. */ public function getBody(): StreamInterface; /** * Return an instance with the specified message body. * * The body MUST be a StreamInterface object. * * This method MUST be implemented in such a way as to retain the * immutability of the message, and MUST return a new instance that has the * new body stream. * * @param StreamInterface $body Body. * @return static * @throws \InvalidArgumentException When the body is not valid. */ public function withBody(StreamInterface $body): MessageInterface; } Transaction/MultiExecState.php000064400000005735152427751160012466 0ustar00flags = 0; } /** * Sets the internal state flags. * * @param int $flags Set of flags */ public function set($flags) { $this->flags = $flags; } /** * Gets the internal state flags. * * @return int */ public function get() { return $this->flags; } /** * Sets one or more flags. * * @param int $flags Set of flags */ public function flag($flags) { $this->flags |= $flags; } /** * Resets one or more flags. * * @param int $flags Set of flags */ public function unflag($flags) { $this->flags &= ~$flags; } /** * Returns if the specified flag or set of flags is set. * * @param int $flags Flag * * @return bool */ public function check($flags) { return ($this->flags & $flags) === $flags; } /** * Resets the state of a transaction. */ public function reset() { $this->flags = 0; } /** * Returns the state of the RESET flag. * * @return bool */ public function isReset() { return $this->flags === 0; } /** * Returns the state of the INITIALIZED flag. * * @return bool */ public function isInitialized() { return $this->check(self::INITIALIZED); } /** * Returns the state of the INSIDEBLOCK flag. * * @return bool */ public function isExecuting() { return $this->check(self::INSIDEBLOCK); } /** * Returns the state of the CAS flag. * * @return bool */ public function isCAS() { return $this->check(self::CAS); } /** * Returns if WATCH is allowed in the current state. * * @return bool */ public function isWatchAllowed() { return $this->check(self::INITIALIZED) && !$this->check(self::CAS); } /** * Returns the state of the WATCH flag. * * @return bool */ public function isWatching() { return $this->check(self::WATCH); } /** * Returns the state of the DISCARDED flag. * * @return bool */ public function isDiscarded() { return $this->check(self::DISCARDED); } } Transaction/AbortedMultiExecException.php000064400000002072152427751160014634 0ustar00transaction = $transaction; } /** * Returns the transaction that generated the exception. * * @return MultiExec */ public function getTransaction() { return $this->transaction; } } Transaction/MultiExec.php000064400000033174152427751160011463 0ustar00assertClient($client); $this->client = $client; $this->state = new MultiExecState(); $this->configure($client, $options ?: []); $this->reset(); } /** * Checks if the passed client instance satisfies the required conditions * needed to initialize the transaction object. * * @param ClientInterface $client Client instance used by the transaction object. * * @throws NotSupportedException */ private function assertClient(ClientInterface $client) { if ($client->getConnection() instanceof ClusterInterface) { throw new NotSupportedException( 'Cannot initialize a MULTI/EXEC transaction over cluster connections.' ); } if (!$client->getCommandFactory()->supports('MULTI', 'EXEC', 'DISCARD')) { throw new NotSupportedException( 'MULTI, EXEC and DISCARD are not supported by the current command factory.' ); } } /** * Configures the transaction using the provided options. * * @param ClientInterface $client Underlying client instance. * @param array $options Array of options for the transaction. **/ protected function configure(ClientInterface $client, array $options) { if (isset($options['exceptions'])) { $this->exceptions = (bool) $options['exceptions']; } else { $this->exceptions = $client->getOptions()->exceptions; } if (isset($options['cas'])) { $this->modeCAS = (bool) $options['cas']; } if (isset($options['watch']) && $keys = $options['watch']) { $this->watchKeys = $keys; } if (isset($options['retry'])) { $this->attempts = (int) $options['retry']; } } /** * Resets the state of the transaction. */ protected function reset() { $this->state->reset(); $this->commands = new SplQueue(); } /** * Initializes the transaction context. */ protected function initialize() { if ($this->state->isInitialized()) { return; } if ($this->modeCAS) { $this->state->flag(MultiExecState::CAS); } if ($this->watchKeys) { $this->watch($this->watchKeys); } $cas = $this->state->isCAS(); $discarded = $this->state->isDiscarded(); if (!$cas || ($cas && $discarded)) { $this->call('MULTI'); if ($discarded) { $this->state->unflag(MultiExecState::CAS); } } $this->state->unflag(MultiExecState::DISCARDED); $this->state->flag(MultiExecState::INITIALIZED); } /** * Dynamically invokes a Redis command with the specified arguments. * * @param string $method Command ID. * @param array $arguments Arguments for the command. * * @return mixed */ public function __call($method, $arguments) { return $this->executeCommand( $this->client->createCommand($method, $arguments) ); } /** * Executes a Redis command bypassing the transaction logic. * * @param string $commandID Command ID. * @param array $arguments Arguments for the command. * * @return mixed * @throws ServerException */ protected function call($commandID, array $arguments = []) { try { $response = $this->client->executeCommand( $this->client->createCommand($commandID, $arguments) ); } catch (ServerException $exception) { if (!$this->client->getConnection() instanceof RelayConnection) { throw $exception; } if (strcasecmp($commandID, 'EXEC') != 0) { throw $exception; } if (!strpos($exception->getMessage(), 'RELAY_ERR_REDIS')) { throw $exception; } return null; } if ($response instanceof ErrorResponseInterface) { throw new ServerException($response->getMessage()); } return $response; } /** * Executes the specified Redis command. * * @param CommandInterface $command Command instance. * * @return $this|mixed * @throws AbortedMultiExecException * @throws CommunicationException */ public function executeCommand(CommandInterface $command) { $this->initialize(); if ($this->state->isCAS()) { return $this->client->executeCommand($command); } $response = $this->client->getConnection()->executeCommand($command); if ($response instanceof StatusResponse && $response == 'QUEUED') { $this->commands->enqueue($command); } elseif ($response instanceof Relay) { $this->commands->enqueue($command); } elseif ($response instanceof ErrorResponseInterface) { throw new AbortedMultiExecException($this, $response->getMessage()); } else { $this->onProtocolError('The server did not return a +QUEUED status response.'); } return $this; } /** * Executes WATCH against one or more keys. * * @param string|array $keys One or more keys. * * @return mixed * @throws NotSupportedException * @throws ClientException */ public function watch($keys) { if (!$this->client->getCommandFactory()->supports('WATCH')) { throw new NotSupportedException('WATCH is not supported by the current command factory.'); } if ($this->state->isWatchAllowed()) { throw new ClientException('Sending WATCH after MULTI is not allowed.'); } $response = $this->call('WATCH', is_array($keys) ? $keys : [$keys]); $this->state->flag(MultiExecState::WATCH); return $response; } /** * Finalizes the transaction by executing MULTI on the server. * * @return MultiExec */ public function multi() { if ($this->state->check(MultiExecState::INITIALIZED | MultiExecState::CAS)) { $this->state->unflag(MultiExecState::CAS); $this->call('MULTI'); } else { $this->initialize(); } return $this; } /** * Executes UNWATCH. * * @return MultiExec * @throws NotSupportedException */ public function unwatch() { if (!$this->client->getCommandFactory()->supports('UNWATCH')) { throw new NotSupportedException( 'UNWATCH is not supported by the current command factory.' ); } $this->state->unflag(MultiExecState::WATCH); $this->__call('UNWATCH', []); return $this; } /** * Resets the transaction by UNWATCH-ing the keys that are being WATCHed and * DISCARD-ing pending commands that have been already sent to the server. * * @return MultiExec */ public function discard() { if ($this->state->isInitialized()) { $this->call($this->state->isCAS() ? 'UNWATCH' : 'DISCARD'); $this->reset(); $this->state->flag(MultiExecState::DISCARDED); } return $this; } /** * Executes the whole transaction. * * @return mixed */ public function exec() { return $this->execute(); } /** * Checks the state of the transaction before execution. * * @param mixed $callable Callback for execution. * * @throws InvalidArgumentException * @throws ClientException */ private function checkBeforeExecution($callable) { if ($this->state->isExecuting()) { throw new ClientException( 'Cannot invoke "execute" or "exec" inside an active transaction context.' ); } if ($callable) { if (!is_callable($callable)) { throw new InvalidArgumentException('The argument must be a callable object.'); } if (!$this->commands->isEmpty()) { $this->discard(); throw new ClientException( 'Cannot execute a transaction block after using fluent interface.' ); } } elseif ($this->attempts) { $this->discard(); throw new ClientException( 'Automatic retries are supported only when a callable block is provided.' ); } } /** * Handles the actual execution of the whole transaction. * * @param mixed $callable Optional callback for execution. * * @return array * @throws CommunicationException * @throws AbortedMultiExecException * @throws ServerException */ public function execute($callable = null) { $this->checkBeforeExecution($callable); $execResponse = null; $attempts = $this->attempts; do { if ($callable) { $this->executeTransactionBlock($callable); } if ($this->commands->isEmpty()) { if ($this->state->isWatching()) { $this->discard(); } return; } $execResponse = $this->call('EXEC'); // The additional `false` check is needed for Relay, // let's hope it won't break anything if ($execResponse === null || $execResponse === false) { if ($attempts === 0) { throw new AbortedMultiExecException( $this, 'The current transaction has been aborted by the server.' ); } $this->reset(); continue; } break; } while ($attempts-- > 0); $response = []; $commands = $this->commands; $size = count($execResponse); if ($size !== count($commands)) { $this->onProtocolError('EXEC returned an unexpected number of response items.'); } for ($i = 0; $i < $size; ++$i) { $cmdResponse = $execResponse[$i]; if ($this->exceptions && $cmdResponse instanceof ErrorResponseInterface) { throw new ServerException($cmdResponse->getMessage()); } if ($cmdResponse instanceof RelayException) { if ($this->exceptions) { throw new ServerException($cmdResponse->getMessage(), $cmdResponse->getCode(), $cmdResponse); } $commands->dequeue(); $response[$i] = new Error($cmdResponse->getMessage()); continue; } $response[$i] = $commands->dequeue()->parseResponse($cmdResponse); } return $response; } /** * Passes the current transaction object to a callable block for execution. * * @param mixed $callable Callback. * * @throws CommunicationException * @throws ServerException */ protected function executeTransactionBlock($callable) { $exception = null; $this->state->flag(MultiExecState::INSIDEBLOCK); try { call_user_func($callable, $this); } catch (CommunicationException $exception) { // NOOP } catch (ServerException $exception) { // NOOP } catch (Exception $exception) { $this->discard(); } $this->state->unflag(MultiExecState::INSIDEBLOCK); if ($exception) { throw $exception; } } /** * Helper method for protocol errors encountered inside the transaction. * * @param string $message Error message. */ private function onProtocolError($message) { // Since a MULTI/EXEC block cannot be initialized when using aggregate // connections we can safely assume that Predis\Client::getConnection() // will return a Predis\Connection\NodeConnectionInterface instance. CommunicationException::handle(new ProtocolException( $this->client->getConnection(), $message )); } } Collection/Iterator/ListKey.php000064400000010741152427751160012542 0ustar00requiredCommand($client, 'LRANGE'); if ((false === $count = filter_var($count, FILTER_VALIDATE_INT)) || $count < 0) { throw new InvalidArgumentException('The $count argument must be a positive integer.'); } $this->client = $client; $this->key = $key; $this->count = $count; $this->reset(); } /** * Ensures that the client instance supports the specified Redis command * required to fetch elements from the server to perform the iteration. * * @param ClientInterface $client Client connected to Redis. * @param string $commandID Command ID. * * @throws NotSupportedException */ protected function requiredCommand(ClientInterface $client, $commandID) { if (!$client->getCommandFactory()->supports($commandID)) { throw new NotSupportedException("'$commandID' is not supported by the current command factory."); } } /** * Resets the inner state of the iterator. */ protected function reset() { $this->valid = true; $this->fetchmore = true; $this->elements = []; $this->position = -1; $this->current = null; } /** * Fetches a new set of elements from the remote collection, effectively * advancing the iteration process. * * @return array */ protected function executeCommand() { return $this->client->lrange($this->key, $this->position + 1, $this->position + $this->count); } /** * Populates the local buffer of elements fetched from the server during the * iteration. */ protected function fetch() { $elements = $this->executeCommand(); if (count($elements) < $this->count) { $this->fetchmore = false; } $this->elements = $elements; } /** * Extracts next values for key() and current(). */ protected function extractNext() { ++$this->position; $this->current = array_shift($this->elements); } /** * @return void */ #[ReturnTypeWillChange] public function rewind() { $this->reset(); $this->next(); } /** * @return mixed */ #[ReturnTypeWillChange] public function current() { return $this->current; } /** * @return int|null */ #[ReturnTypeWillChange] public function key() { return $this->position; } /** * @return void */ #[ReturnTypeWillChange] public function next() { if (!$this->elements && $this->fetchmore) { $this->fetch(); } if ($this->elements) { $this->extractNext(); } else { $this->valid = false; } } /** * @return bool */ #[ReturnTypeWillChange] public function valid() { return $this->valid; } } Collection/Iterator/SortedSetKey.php000064400000002413152427751160013540 0ustar00= 2.8) wrapped in a fully-rewindable PHP iterator. * * @see http://redis.io/commands/scan */ class SortedSetKey extends CursorBasedIterator { protected $key; /** * {@inheritdoc} */ public function __construct(ClientInterface $client, $key, $match = null, $count = null) { $this->requiredCommand($client, 'ZSCAN'); parent::__construct($client, $match, $count); $this->key = $key; } /** * {@inheritdoc} */ protected function executeCommand() { return $this->client->zscan($this->key, $this->cursor, $this->getScanOptions()); } /** * {@inheritdoc} */ protected function extractNext() { $this->position = key($this->elements); $this->current = current($this->elements); unset($this->elements[$this->position]); } } Collection/Iterator/Keyspace.php000064400000001727152427751160012726 0ustar00= 2.8) wrapped in a fully-rewindable PHP iterator. * * @see http://redis.io/commands/scan */ class Keyspace extends CursorBasedIterator { /** * {@inheritdoc} */ public function __construct(ClientInterface $client, $match = null, $count = null) { $this->requiredCommand($client, 'SCAN'); parent::__construct($client, $match, $count); } /** * {@inheritdoc} */ protected function executeCommand() { return $this->client->scan($this->cursor, $this->getScanOptions()); } } Collection/Iterator/SetKey.php000064400000002022152427751160012353 0ustar00= 2.8) wrapped in a fully-rewindable PHP iterator. * * @see http://redis.io/commands/scan */ class SetKey extends CursorBasedIterator { protected $key; /** * {@inheritdoc} */ public function __construct(ClientInterface $client, $key, $match = null, $count = null) { $this->requiredCommand($client, 'SSCAN'); parent::__construct($client, $match, $count); $this->key = $key; } /** * {@inheritdoc} */ protected function executeCommand() { return $this->client->sscan($this->key, $this->cursor, $this->getScanOptions()); } } Collection/Iterator/HashKey.php000064400000002404152427751160012507 0ustar00= 2.8) wrapped in a fully-rewindable PHP iterator. * * @see http://redis.io/commands/scan */ class HashKey extends CursorBasedIterator { protected $key; /** * {@inheritdoc} */ public function __construct(ClientInterface $client, $key, $match = null, $count = null) { $this->requiredCommand($client, 'HSCAN'); parent::__construct($client, $match, $count); $this->key = $key; } /** * {@inheritdoc} */ protected function executeCommand() { return $this->client->hscan($this->key, $this->cursor, $this->getScanOptions()); } /** * {@inheritdoc} */ protected function extractNext() { $this->position = key($this->elements); $this->current = current($this->elements); unset($this->elements[$this->position]); } } Collection/Iterator/CursorBasedIterator.php000064400000011104152427751160015076 0ustar00client = $client; $this->match = $match; $this->count = $count; $this->reset(); } /** * Ensures that the client supports the specified Redis command required to * fetch elements from the server to perform the iteration. * * @param ClientInterface $client Client connected to Redis. * @param string $commandID Command ID. * * @throws NotSupportedException */ protected function requiredCommand(ClientInterface $client, $commandID) { if (!$client->getCommandFactory()->supports($commandID)) { throw new NotSupportedException("'$commandID' is not supported by the current command factory."); } } /** * Resets the inner state of the iterator. */ protected function reset() { $this->valid = true; $this->fetchmore = true; $this->elements = []; $this->cursor = 0; $this->position = -1; $this->current = null; } /** * Returns an array of options for the `SCAN` command. * * @return array */ protected function getScanOptions() { $options = []; if (strlen(strval($this->match)) > 0) { $options['MATCH'] = $this->match; } if ($this->count > 0) { $options['COUNT'] = $this->count; } return $options; } /** * Fetches a new set of elements from the remote collection, effectively * advancing the iteration process. * * @return array */ abstract protected function executeCommand(); /** * Populates the local buffer of elements fetched from the server during * the iteration. */ protected function fetch() { [$cursor, $elements] = $this->executeCommand(); if (!$cursor) { $this->fetchmore = false; } $this->cursor = $cursor; $this->elements = $elements; } /** * Extracts next values for key() and current(). */ protected function extractNext() { ++$this->position; $this->current = array_shift($this->elements); } /** * @return void */ #[ReturnTypeWillChange] public function rewind() { $this->reset(); $this->next(); } /** * @return mixed */ #[ReturnTypeWillChange] public function current() { return $this->current; } /** * @return int|null */ #[ReturnTypeWillChange] public function key() { return $this->position; } /** * @return void */ #[ReturnTypeWillChange] public function next() { tryFetch: if (!$this->elements && $this->fetchmore) { $this->fetch(); } if ($this->elements) { $this->extractNext(); } elseif ($this->cursor) { goto tryFetch; } else { $this->valid = false; } } /** * @return bool */ #[ReturnTypeWillChange] public function valid() { return $this->valid; } } PubSub/AbstractConsumer.php000064400000013067152427751160011755 0ustar00stop(true); } /** * Checks if the specified flag is valid based on the state of the consumer. * * @param int $value Flag. * * @return bool */ protected function isFlagSet($value) { return ($this->statusFlags & $value) === $value; } /** * Subscribes to the specified channels. * * @param string ...$channel One or more channel names. */ public function subscribe($channel /* , ... */) { $this->writeRequest(self::SUBSCRIBE, func_get_args()); $this->statusFlags |= self::STATUS_SUBSCRIBED; } /** * Unsubscribes from the specified channels. * * @param string ...$channel One or more channel names. */ public function unsubscribe(...$channel) { $this->writeRequest(self::UNSUBSCRIBE, func_get_args()); } /** * Subscribes to the specified channels using a pattern. * * @param string ...$pattern One or more channel name patterns. */ public function psubscribe(...$pattern) { $this->writeRequest(self::PSUBSCRIBE, func_get_args()); $this->statusFlags |= self::STATUS_PSUBSCRIBED; } /** * Unsubscribes from the specified channels using a pattern. * * @param string ...$pattern One or more channel name patterns. */ public function punsubscribe(...$pattern) { $this->writeRequest(self::PUNSUBSCRIBE, func_get_args()); } /** * PING the server with an optional payload that will be echoed as a * PONG message in the pub/sub loop. * * @param string $payload Optional PING payload. */ public function ping($payload = null) { $this->writeRequest('PING', [$payload]); } /** * Closes the context by unsubscribing from all the subscribed channels. The * context can be forcefully closed by dropping the underlying connection. * * @param bool $drop Indicates if the context should be closed by dropping the connection. * * @return bool Returns false when there are no pending messages. */ public function stop($drop = false) { if (!$this->valid()) { return false; } if ($drop) { $this->invalidate(); $this->disconnect(); } else { if ($this->isFlagSet(self::STATUS_SUBSCRIBED)) { $this->unsubscribe(); } if ($this->isFlagSet(self::STATUS_PSUBSCRIBED)) { $this->punsubscribe(); } } return !$drop; } /** * Closes the underlying connection when forcing a disconnection. */ abstract protected function disconnect(); /** * Writes a Redis command on the underlying connection. * * @param string $method Command ID. * @param array $arguments Arguments for the command. */ abstract protected function writeRequest($method, $arguments); /** * @return void */ #[ReturnTypeWillChange] public function rewind() { // NOOP } /** * Returns the last message payload retrieved from the server and generated * by one of the active subscriptions. * * @return array */ #[ReturnTypeWillChange] public function current() { return $this->getValue(); } /** * @return int|null */ #[ReturnTypeWillChange] public function key() { return $this->position; } /** * @return int|null */ #[ReturnTypeWillChange] public function next() { if ($this->valid()) { ++$this->position; } return $this->position; } /** * Checks if the the consumer is still in a valid state to continue. * * @return bool */ #[ReturnTypeWillChange] public function valid() { $isValid = $this->isFlagSet(self::STATUS_VALID); $subscriptionFlags = self::STATUS_SUBSCRIBED | self::STATUS_PSUBSCRIBED; $hasSubscriptions = ($this->statusFlags & $subscriptionFlags) > 0; return $isValid && $hasSubscriptions; } /** * Resets the state of the consumer. */ protected function invalidate() { $this->statusFlags = 0; // 0b0000; } /** * Waits for a new message from the server generated by one of the active * subscriptions and returns it when available. * * @return array */ abstract protected function getValue(); } PubSub/DispatcherLoop.php000064400000010233152427751160011406 0ustar00callbacks = []; $this->pubsub = $pubsub; } /** * Checks if the passed argument is a valid callback. * * @param mixed $callable A callback. * * @throws InvalidArgumentException */ protected function assertCallback($callable) { if (!is_callable($callable)) { throw new InvalidArgumentException('The given argument must be a callable object.'); } } /** * Returns the underlying PUB / SUB context. * * @return Consumer */ public function getPubSubConsumer() { return $this->pubsub; } /** * Sets a callback that gets invoked upon new subscriptions. * * @param mixed $callable A callback. */ public function subscriptionCallback($callable = null) { if (isset($callable)) { $this->assertCallback($callable); } $this->subscriptionCallback = $callable; } /** * Sets a callback that gets invoked when a message is received on a * channel that does not have an associated callback. * * @param mixed $callable A callback. */ public function defaultCallback($callable = null) { if (isset($callable)) { $this->assertCallback($callable); } $this->subscriptionCallback = $callable; } /** * Binds a callback to a channel. * * @param string $channel Channel name. * @param callable $callback A callback. */ public function attachCallback($channel, $callback) { $callbackName = $this->getPrefixKeys() . $channel; $this->assertCallback($callback); $this->callbacks[$callbackName] = $callback; $this->pubsub->subscribe($channel); } /** * Stops listening to a channel and removes the associated callback. * * @param string $channel Redis channel. */ public function detachCallback($channel) { $callbackName = $this->getPrefixKeys() . $channel; if (isset($this->callbacks[$callbackName])) { unset($this->callbacks[$callbackName]); $this->pubsub->unsubscribe($channel); } } /** * Starts the dispatcher loop. */ public function run() { foreach ($this->pubsub as $message) { $kind = $message->kind; if ($kind !== Consumer::MESSAGE && $kind !== Consumer::PMESSAGE) { if (isset($this->subscriptionCallback)) { $callback = $this->subscriptionCallback; call_user_func($callback, $message, $this); } continue; } if (isset($this->callbacks[$message->channel])) { $callback = $this->callbacks[$message->channel]; call_user_func($callback, $message->payload, $this); } elseif (isset($this->defaultCallback)) { $callback = $this->defaultCallback; call_user_func($callback, $message, $this); } } } /** * Terminates the dispatcher loop. */ public function stop() { $this->pubsub->stop(); } /** * Return the prefix used for keys. * * @return string */ protected function getPrefixKeys() { $options = $this->pubsub->getClient()->getOptions(); if (isset($options->prefix)) { return $options->prefix->getPrefix(); } return ''; } } PubSub/RelayConsumer.php000064400000005626152427751160011270 0ustar00statusFlags |= 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 } } PubSub/Consumer.php000064400000010435152427751160010265 0ustar00checkCapabilities($client); $this->options = $options ?: []; $this->client = $client; $this->genericSubscribeInit('subscribe'); $this->genericSubscribeInit('psubscribe'); } /** * Returns the underlying client instance used by the pub/sub iterator. * * @return ClientInterface */ public function getClient() { return $this->client; } /** * Checks if the client instance satisfies the required conditions needed to * initialize a PUB/SUB consumer. * * @param ClientInterface $client Client instance used by the consumer. * * @throws NotSupportedException */ protected function checkCapabilities(ClientInterface $client) { if ($client->getConnection() instanceof ClusterInterface) { throw new NotSupportedException( 'Cannot initialize a PUB/SUB consumer over cluster connections.' ); } $commands = ['publish', 'subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe']; if (!$client->getCommandFactory()->supports(...$commands)) { throw new NotSupportedException( 'PUB/SUB commands are not supported by the current command factory.' ); } } /** * This method shares the logic to handle both SUBSCRIBE and PSUBSCRIBE. * * @param string $subscribeAction Type of subscription. */ protected function genericSubscribeInit($subscribeAction) { if (isset($this->options[$subscribeAction])) { $this->$subscribeAction($this->options[$subscribeAction]); } } /** * {@inheritdoc} */ protected function writeRequest($method, $arguments) { $this->client->getConnection()->writeRequest( $this->client->createCommand($method, Command::normalizeArguments($arguments) ) ); } /** * {@inheritdoc} */ protected function disconnect() { $this->client->disconnect(); } /** * {@inheritdoc} */ protected function getValue() { $response = $this->client->getConnection()->read(); switch ($response[0]) { case self::SUBSCRIBE: case self::UNSUBSCRIBE: case self::PSUBSCRIBE: case self::PUNSUBSCRIBE: if ($response[2] === 0) { $this->invalidate(); } // The missing break here is intentional as we must process // subscriptions and unsubscriptions as standard messages. // no break case self::MESSAGE: return (object) [ 'kind' => $response[0], 'channel' => $response[1], 'payload' => $response[2], ]; case self::PMESSAGE: return (object) [ 'kind' => $response[0], 'pattern' => $response[1], 'channel' => $response[2], 'payload' => $response[3], ]; case self::PONG: return (object) [ 'kind' => $response[0], 'payload' => $response[1], ]; default: throw new ClientException( "Unknown message type '{$response[0]}' received in the PUB/SUB context." ); } } } Connection/Cluster/RedisCluster.php000064400000047201152427751160013423 0ustar00= 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|null $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 *= 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|null $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); // Handle connection ID in the form of "IP:port (details about exception)" // by trimming everything after first space (including the space) $startPositionOfExtraDetails = strpos($connectionID, ' '); if ($startPositionOfExtraDetails !== false) { $connectionID = substr($connectionID, 0, $startPositionOfExtraDetails); } 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 *= 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; } } Connection/Cluster/ClusterInterface.php000064400000001034152427751160014247 0ustar00strategy = $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); } } Connection/Replication/SentinelReplication.php000064400000051164152427751160015621 0ustar00 * @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|null $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; } // ensure `master-link-status` is ok if (isset($slave[31]) && $slave[31] === 'err') { 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', ]; } } Connection/Replication/MasterSlaveReplication.php000064400000034626152427751160016272 0ustar00strategy = $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']; } } Connection/Replication/ReplicationInterface.php000064400000002361152427751160015733 0ustar00parameters->persistent) && $this->parameters->persistent) { return; } $this->disconnect(); } /** * {@inheritdoc} */ protected function assertParameters(ParametersInterface $parameters) { switch ($parameters->scheme) { case 'tcp': case 'redis': case 'unix': case 'tls': case 'rediss': break; default: throw new InvalidArgumentException("Invalid scheme: '$parameters->scheme'."); } return $parameters; } /** * {@inheritdoc} */ protected function createResource() { switch ($this->parameters->scheme) { case 'tcp': case 'redis': return $this->tcpStreamInitializer($this->parameters); case 'unix': return $this->unixStreamInitializer($this->parameters); case 'tls': case 'rediss': return $this->tlsStreamInitializer($this->parameters); default: throw new InvalidArgumentException("Invalid scheme: '{$this->parameters->scheme}'."); } } /** * Creates a connected stream socket resource. * * @param ParametersInterface $parameters Connection parameters. * @param string $address Address for stream_socket_client(). * @param int $flags Flags for stream_socket_client(). * * @return resource */ protected function createStreamSocket(ParametersInterface $parameters, $address, $flags) { $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0); $context = stream_context_create(['socket' => ['tcp_nodelay' => (bool) $parameters->tcp_nodelay]]); if (!$resource = @stream_socket_client($address, $errno, $errstr, $timeout, $flags, $context)) { $this->onConnectionError(trim($errstr), $errno); } if (isset($parameters->read_write_timeout)) { $rwtimeout = (float) $parameters->read_write_timeout; $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1; $timeoutSeconds = floor($rwtimeout); $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000; stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds); } return $resource; } /** * Initializes a TCP stream resource. * * @param ParametersInterface $parameters Initialization parameters for the connection. * * @return resource */ protected function tcpStreamInitializer(ParametersInterface $parameters) { if (!filter_var($parameters->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $address = "tcp://$parameters->host:$parameters->port"; } else { $address = "tcp://[$parameters->host]:$parameters->port"; } $flags = STREAM_CLIENT_CONNECT; if (isset($parameters->async_connect) && $parameters->async_connect) { $flags |= STREAM_CLIENT_ASYNC_CONNECT; } if (isset($parameters->persistent)) { if (false !== $persistent = filter_var($parameters->persistent, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) { $flags |= STREAM_CLIENT_PERSISTENT; if ($persistent === null) { $address = "{$address}/{$parameters->persistent}"; } } } return $this->createStreamSocket($parameters, $address, $flags); } /** * Initializes a UNIX stream resource. * * @param ParametersInterface $parameters Initialization parameters for the connection. * * @return resource */ protected function unixStreamInitializer(ParametersInterface $parameters) { if (!isset($parameters->path)) { throw new InvalidArgumentException('Missing UNIX domain socket path.'); } $flags = STREAM_CLIENT_CONNECT; if (isset($parameters->persistent)) { if (false !== $persistent = filter_var($parameters->persistent, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) { $flags |= STREAM_CLIENT_PERSISTENT; if ($persistent === null) { throw new InvalidArgumentException( 'Persistent connection IDs are not supported when using UNIX domain sockets.' ); } } } return $this->createStreamSocket($parameters, "unix://{$parameters->path}", $flags); } /** * Initializes a SSL-encrypted TCP stream resource. * * @param ParametersInterface $parameters Initialization parameters for the connection. * * @return resource */ protected function tlsStreamInitializer(ParametersInterface $parameters) { $resource = $this->tcpStreamInitializer($parameters); $metadata = stream_get_meta_data($resource); // Detect if crypto mode is already enabled for this stream (PHP >= 7.0.0). if (isset($metadata['crypto'])) { return $resource; } if (isset($parameters->ssl) && is_array($parameters->ssl)) { $options = $parameters->ssl; } else { $options = []; } if (!isset($options['crypto_type'])) { $options['crypto_type'] = STREAM_CRYPTO_METHOD_TLS_CLIENT; } if (!stream_context_set_option($resource, ['ssl' => $options])) { $this->onConnectionError('Error while setting SSL context options'); } if (!stream_socket_enable_crypto($resource, true, $options['crypto_type'])) { $this->onConnectionError('Error while switching to encrypted communication'); } return $resource; } /** * {@inheritdoc} */ public function connect() { if (parent::connect() && $this->initCommands) { foreach ($this->initCommands as $command) { $response = $this->executeCommand($command); if ($response instanceof ErrorResponseInterface && $command->getId() === 'CLIENT') { // Do nothing on CLIENT SETINFO command failure } elseif ($response instanceof ErrorResponseInterface) { $this->onConnectionError("`{$command->getId()}` failed: {$response->getMessage()}", 0); } } } } /** * {@inheritdoc} */ public function disconnect() { if ($this->isConnected()) { $resource = $this->getResource(); if (is_resource($resource)) { fclose($resource); } parent::disconnect(); } } /** * Performs a write operation over the stream of the buffer containing a * command serialized with the Redis wire protocol. * * @param string $buffer Representation of a command in the Redis wire protocol. */ protected function write($buffer) { $socket = $this->getResource(); while (($length = strlen($buffer)) > 0) { $written = is_resource($socket) ? @fwrite($socket, $buffer) : false; if ($length === $written) { return; } if ($written === false || $written === 0) { $this->onConnectionError('Error while writing bytes to the server.'); } $buffer = substr($buffer, $written); } } /** * {@inheritdoc} */ public function read() { $socket = $this->getResource(); $chunk = fgets($socket); if ($chunk === false || $chunk === '') { $this->onConnectionError('Error while reading line from the server.'); } $prefix = $chunk[0]; $payload = substr($chunk, 1, -2); switch ($prefix) { case '+': return StatusResponse::get($payload); case '$': $size = (int) $payload; if ($size === -1) { return; } $bulkData = ''; $bytesLeft = ($size += 2); do { $chunk = is_resource($socket) ? fread($socket, min($bytesLeft, 4096)) : false; if ($chunk === false || $chunk === '') { $this->onConnectionError('Error while reading bytes from the server.'); } $bulkData .= $chunk; $bytesLeft = $size - strlen($bulkData); } while ($bytesLeft > 0); return substr($bulkData, 0, -2); case '*': $count = (int) $payload; if ($count === -1) { return; } $multibulk = []; for ($i = 0; $i < $count; ++$i) { $multibulk[$i] = $this->read(); } return $multibulk; case ':': $integer = (int) $payload; return $integer == $payload ? $integer : $payload; case '-': return new ErrorResponse($payload); default: $this->onProtocolError("Unknown response prefix: '$prefix'."); return; } } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $commandID = $command->getId(); $arguments = $command->getArguments(); $cmdlen = strlen($commandID); $reqlen = count($arguments) + 1; $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n"; foreach ($arguments as $argument) { $arglen = strlen(strval($argument)); $buffer .= "\${$arglen}\r\n{$argument}\r\n"; } $this->write($buffer); } } Connection/PhpiredisStreamConnection.php000064400000016470152427751160014521 0ustar00assertExtensions(); parent::__construct($parameters); $this->reader = $this->createReader(); } /** * {@inheritdoc} */ public function __destruct() { parent::__destruct(); phpiredis_reader_destroy($this->reader); } /** * {@inheritdoc} */ public function disconnect() { phpiredis_reader_reset($this->reader); parent::disconnect(); } /** * Checks if the phpiredis extension is loaded in PHP. */ private function assertExtensions() { if (!extension_loaded('phpiredis')) { throw new NotSupportedException( 'The "phpiredis" extension is required by this connection backend.' ); } } /** * {@inheritdoc} */ protected function assertParameters(ParametersInterface $parameters) { switch ($parameters->scheme) { case 'tcp': case 'redis': case 'unix': break; case 'tls': case 'rediss': throw new InvalidArgumentException('SSL encryption is not supported by this connection backend.'); default: throw new InvalidArgumentException("Invalid scheme: '$parameters->scheme'."); } return $parameters; } /** * {@inheritdoc} */ protected function createStreamSocket(ParametersInterface $parameters, $address, $flags) { $socket = null; $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0); $context = stream_context_create(['socket' => ['tcp_nodelay' => (bool) $parameters->tcp_nodelay]]); if (!$resource = @stream_socket_client($address, $errno, $errstr, $timeout, $flags, $context)) { $this->onConnectionError(trim($errstr), $errno); } if (isset($parameters->read_write_timeout) && function_exists('socket_import_stream')) { $rwtimeout = (float) $parameters->read_write_timeout; $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1; $timeout = [ 'sec' => $timeoutSeconds = floor($rwtimeout), 'usec' => ($rwtimeout - $timeoutSeconds) * 1000000, ]; $socket = $socket ?: socket_import_stream($resource); @socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout); @socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, $timeout); } if (isset($parameters->tcp_nodelay) && function_exists('socket_import_stream')) { $socket = $socket ?: socket_import_stream($resource); socket_set_option($socket, SOL_TCP, TCP_NODELAY, (int) $parameters->tcp_nodelay); } return $resource; } /** * Creates a new instance of the protocol reader resource. * * @return resource */ private function createReader() { $reader = phpiredis_reader_create(); phpiredis_reader_set_status_handler($reader, $this->getStatusHandler()); phpiredis_reader_set_error_handler($reader, $this->getErrorHandler()); return $reader; } /** * Returns the underlying protocol reader resource. * * @return resource */ protected function getReader() { return $this->reader; } /** * Returns the handler used by the protocol reader for inline responses. * * @return Closure */ protected function getStatusHandler() { static $statusHandler; if (!$statusHandler) { $statusHandler = function ($payload) { return StatusResponse::get($payload); }; } return $statusHandler; } /** * Returns the handler used by the protocol reader for error responses. * * @return Closure */ protected function getErrorHandler() { static $errorHandler; if (!$errorHandler) { $errorHandler = function ($errorMessage) { return new ErrorResponse($errorMessage); }; } return $errorHandler; } /** * {@inheritdoc} */ public function read() { $socket = $this->getResource(); $reader = $this->reader; while (PHPIREDIS_READER_STATE_INCOMPLETE === $state = phpiredis_reader_get_state($reader)) { $buffer = stream_socket_recvfrom($socket, 4096); if ($buffer === false || $buffer === '') { $this->onConnectionError('Error while reading bytes from the server.'); } phpiredis_reader_feed($reader, $buffer); } if ($state === PHPIREDIS_READER_STATE_COMPLETE) { return phpiredis_reader_get_reply($reader); } else { $this->onProtocolError(phpiredis_reader_get_error($reader)); return; } } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $arguments = $command->getArguments(); array_unshift($arguments, $command->getId()); $this->write(phpiredis_format_command($arguments)); } /** * {@inheritdoc} */ public function __wakeup() { $this->assertExtensions(); $this->reader = $this->createReader(); } } Connection/ConnectionException.php000064400000000706152427751160013347 0ustar00client->onFlushed($callback); } /** * Registers a new `invalidated` event listener. * * @param callable $callback * @param string|null $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); } } Connection/PhpiredisSocketConnection.php000064400000027072152427751160014516 0ustar00assertExtensions(); parent::__construct($parameters); $this->reader = $this->createReader(); } /** * Disconnects from the server and destroys the underlying resource and the * protocol reader resource when PHP's garbage collector kicks in. */ public function __destruct() { parent::__destruct(); phpiredis_reader_destroy($this->reader); } /** * Checks if the socket and phpiredis extensions are loaded in PHP. */ protected function assertExtensions() { if (!extension_loaded('sockets')) { throw new NotSupportedException( 'The "sockets" extension is required by this connection backend.' ); } if (!extension_loaded('phpiredis')) { throw new NotSupportedException( 'The "phpiredis" extension is required by this connection backend.' ); } } /** * {@inheritdoc} */ protected function assertParameters(ParametersInterface $parameters) { switch ($parameters->scheme) { case 'tcp': case 'redis': case 'unix': break; default: throw new InvalidArgumentException("Invalid scheme: '$parameters->scheme'."); } if (isset($parameters->persistent)) { throw new NotSupportedException( 'Persistent connections are not supported by this connection backend.' ); } return $parameters; } /** * Creates a new instance of the protocol reader resource. * * @return resource */ private function createReader() { $reader = phpiredis_reader_create(); phpiredis_reader_set_status_handler($reader, $this->getStatusHandler()); phpiredis_reader_set_error_handler($reader, $this->getErrorHandler()); return $reader; } /** * Returns the underlying protocol reader resource. * * @return resource */ protected function getReader() { return $this->reader; } /** * Returns the handler used by the protocol reader for inline responses. * * @return Closure */ protected function getStatusHandler() { static $statusHandler; if (!$statusHandler) { $statusHandler = function ($payload) { return StatusResponse::get($payload); }; } return $statusHandler; } /** * Returns the handler used by the protocol reader for error responses. * * @return Closure */ protected function getErrorHandler() { static $errorHandler; if (!$errorHandler) { $errorHandler = function ($errorMessage) { return new ErrorResponse($errorMessage); }; } return $errorHandler; } /** * Helper method used to throw exceptions on socket errors. */ private function emitSocketError() { $errno = socket_last_error(); $errstr = socket_strerror($errno); $this->disconnect(); $this->onConnectionError(trim($errstr), $errno); } /** * Gets the address of an host from connection parameters. * * @param ParametersInterface $parameters Parameters used to initialize the connection. * * @return string */ protected static function getAddress(ParametersInterface $parameters) { if (filter_var($host = $parameters->host, FILTER_VALIDATE_IP)) { return $host; } if ($host === $address = gethostbyname($host)) { return false; } return $address; } /** * {@inheritdoc} */ protected function createResource() { $parameters = $this->parameters; if ($parameters->scheme === 'unix') { $address = $parameters->path; $domain = AF_UNIX; $protocol = 0; } else { if (false === $address = self::getAddress($parameters)) { $this->onConnectionError("Cannot resolve the address of '$parameters->host'."); } $domain = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? AF_INET6 : AF_INET; $protocol = SOL_TCP; } if (false === $socket = @socket_create($domain, SOCK_STREAM, $protocol)) { $this->emitSocketError(); } $this->setSocketOptions($socket, $parameters); $this->connectWithTimeout($socket, $address, $parameters); return $socket; } /** * Sets options on the socket resource from the connection parameters. * * @param resource $socket Socket resource. * @param ParametersInterface $parameters Parameters used to initialize the connection. */ private function setSocketOptions($socket, ParametersInterface $parameters) { if ($parameters->scheme !== 'unix') { if (!socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1)) { $this->emitSocketError(); } if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) { $this->emitSocketError(); } } if (isset($parameters->read_write_timeout)) { $rwtimeout = (float) $parameters->read_write_timeout; $timeoutSec = floor($rwtimeout); $timeoutUsec = ($rwtimeout - $timeoutSec) * 1000000; $timeout = [ 'sec' => $timeoutSec, 'usec' => $timeoutUsec, ]; if (!socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout)) { $this->emitSocketError(); } if (!socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, $timeout)) { $this->emitSocketError(); } } } /** * Opens the actual connection to the server with a timeout. * * @param resource $socket Socket resource. * @param string $address IP address (DNS-resolved from hostname) * @param ParametersInterface $parameters Parameters used to initialize the connection. * * @return void */ private function connectWithTimeout($socket, $address, ParametersInterface $parameters) { socket_set_nonblock($socket); if (@socket_connect($socket, $address, (int) $parameters->port) === false) { $error = socket_last_error(); if ($error != SOCKET_EINPROGRESS && $error != SOCKET_EALREADY) { $this->emitSocketError(); } } socket_set_block($socket); $null = null; $selectable = [$socket]; $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0); $timeoutSecs = floor($timeout); $timeoutUSecs = ($timeout - $timeoutSecs) * 1000000; $selected = socket_select($selectable, $selectable, $null, $timeoutSecs, $timeoutUSecs); if ($selected === 2) { $this->onConnectionError('Connection refused.', SOCKET_ECONNREFUSED); } if ($selected === 0) { $this->onConnectionError('Connection timed out.', SOCKET_ETIMEDOUT); } if ($selected === false) { $this->emitSocketError(); } } /** * {@inheritdoc} */ public function connect() { if (parent::connect() && $this->initCommands) { foreach ($this->initCommands as $command) { $response = $this->executeCommand($command); if ($response instanceof ErrorResponseInterface) { $this->onConnectionError("`{$command->getId()}` failed: {$response->getMessage()}", 0); } } } } /** * {@inheritdoc} */ public function disconnect() { if ($this->isConnected()) { phpiredis_reader_reset($this->reader); socket_close($this->getResource()); parent::disconnect(); } } /** * {@inheritdoc} */ protected function write($buffer) { $socket = $this->getResource(); while (($length = strlen($buffer)) > 0) { $written = socket_write($socket, $buffer, $length); if ($length === $written) { return; } if ($written === false) { $this->onConnectionError('Error while writing bytes to the server.'); } $buffer = substr($buffer, $written); } } /** * {@inheritdoc} */ public function read() { $socket = $this->getResource(); $reader = $this->reader; while (PHPIREDIS_READER_STATE_INCOMPLETE === $state = phpiredis_reader_get_state($reader)) { if (@socket_recv($socket, $buffer, 4096, 0) === false || $buffer === '' || $buffer === null) { $this->emitSocketError(); } phpiredis_reader_feed($reader, $buffer); } if ($state === PHPIREDIS_READER_STATE_COMPLETE) { return phpiredis_reader_get_reply($reader); } else { $this->onProtocolError(phpiredis_reader_get_error($reader)); return; } } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $arguments = $command->getArguments(); array_unshift($arguments, $command->getId()); $this->write(phpiredis_format_command($arguments)); } /** * {@inheritdoc} */ public function __wakeup() { $this->assertExtensions(); $this->reader = $this->createReader(); } } Connection/CompositeConnectionInterface.php000064400000002233152427751160015171 0ustar00assertExtensions(); if ($parameters->scheme !== 'http') { throw new InvalidArgumentException("Invalid scheme: '{$parameters->scheme}'."); } $this->parameters = $parameters; $this->resource = $this->createCurl(); $this->reader = $this->createReader(); } /** * Frees the underlying cURL and protocol reader resources when the garbage * collector kicks in. */ public function __destruct() { curl_close($this->resource); phpiredis_reader_destroy($this->reader); } /** * Helper method used to throw on unsupported methods. * * @param string $method Name of the unsupported method. * * @throws NotSupportedException */ private function throwNotSupportedException($method) { $class = __CLASS__; throw new NotSupportedException("The method $class::$method() is not supported."); } /** * Checks if the cURL and phpiredis extensions are loaded in PHP. */ private function assertExtensions() { if (!extension_loaded('curl')) { throw new NotSupportedException( 'The "curl" extension is required by this connection backend.' ); } if (!extension_loaded('phpiredis')) { throw new NotSupportedException( 'The "phpiredis" extension is required by this connection backend.' ); } } /** * Initializes cURL. * * @return resource */ private function createCurl() { $parameters = $this->getParameters(); $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0) * 1000; if (filter_var($host = $parameters->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $host = "[$host]"; } $options = [ CURLOPT_FAILONERROR => true, CURLOPT_CONNECTTIMEOUT_MS => $timeout, CURLOPT_URL => "$parameters->scheme://$host:$parameters->port", CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_POST => true, CURLOPT_WRITEFUNCTION => [$this, 'feedReader'], ]; if (isset($parameters->user, $parameters->pass)) { $options[CURLOPT_USERPWD] = "{$parameters->user}:{$parameters->pass}"; } curl_setopt_array($resource = curl_init(), $options); return $resource; } /** * Initializes the phpiredis protocol reader. * * @return resource */ private function createReader() { $reader = phpiredis_reader_create(); phpiredis_reader_set_status_handler($reader, $this->getStatusHandler()); phpiredis_reader_set_error_handler($reader, $this->getErrorHandler()); return $reader; } /** * Returns the handler used by the protocol reader for inline responses. * * @return Closure */ protected function getStatusHandler() { static $statusHandler; if (!$statusHandler) { $statusHandler = function ($payload) { return StatusResponse::get($payload); }; } return $statusHandler; } /** * Returns the handler used by the protocol reader for error responses. * * @return Closure */ protected function getErrorHandler() { static $errorHandler; if (!$errorHandler) { $errorHandler = function ($errorMessage) { return new ErrorResponse($errorMessage); }; } return $errorHandler; } /** * Feeds the phpredis reader resource with the data read from the network. * * @param resource $resource Reader resource. * @param string $buffer Buffer of data read from a connection. * * @return int */ protected function feedReader($resource, $buffer) { phpiredis_reader_feed($this->reader, $buffer); return strlen($buffer); } /** * {@inheritdoc} */ public function connect() { // NOOP } /** * {@inheritdoc} */ public function disconnect() { // NOOP } /** * {@inheritdoc} */ public function isConnected() { return true; } /** * Checks if the specified command is supported by this connection class. * * @param CommandInterface $command Command instance. * * @return string * @throws NotSupportedException */ protected function getCommandId(CommandInterface $command) { switch ($commandID = $command->getId()) { case 'AUTH': case 'SELECT': case 'MULTI': case 'EXEC': case 'WATCH': case 'UNWATCH': case 'DISCARD': case 'MONITOR': throw new NotSupportedException("Command '$commandID' is not allowed by Webdis."); default: return $commandID; } } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $this->throwNotSupportedException(__FUNCTION__); } /** * {@inheritdoc} */ public function readResponse(CommandInterface $command) { $this->throwNotSupportedException(__FUNCTION__); } /** * {@inheritdoc} */ public function executeCommand(CommandInterface $command) { $resource = $this->resource; $commandId = $this->getCommandId($command); if ($arguments = $command->getArguments()) { $arguments = implode('/', array_map('urlencode', $arguments)); $serializedCommand = "$commandId/$arguments.raw"; } else { $serializedCommand = "$commandId.raw"; } curl_setopt($resource, CURLOPT_POSTFIELDS, $serializedCommand); if (curl_exec($resource) === false) { $error = trim(curl_error($resource)); $errno = curl_errno($resource); throw new ConnectionException($this, "$error{$this->getParameters()}]", $errno); } if (phpiredis_reader_get_state($this->reader) !== PHPIREDIS_READER_STATE_COMPLETE) { throw new ProtocolException($this, phpiredis_reader_get_error($this->reader)); } return phpiredis_reader_get_reply($this->reader); } /** * {@inheritdoc} */ public function getResource() { return $this->resource; } /** * {@inheritdoc} */ public function getParameters() { return $this->parameters; } /** * {@inheritdoc} */ public function addConnectCommand(CommandInterface $command) { $this->throwNotSupportedException(__FUNCTION__); } /** * {@inheritdoc} */ public function read() { $this->throwNotSupportedException(__FUNCTION__); } /** * {@inheritdoc} */ public function __toString() { return "{$this->parameters->host}:{$this->parameters->port}"; } /** * {@inheritdoc} */ public function __sleep() { return ['parameters']; } /** * {@inheritdoc} */ public function __wakeup() { $this->assertExtensions(); $this->resource = $this->createCurl(); $this->reader = $this->createReader(); } } Connection/CompositeStreamConnection.php000064400000005632152427751160014532 0ustar00parameters = $this->assertParameters($parameters); $this->protocol = $protocol ?: new TextProtocolProcessor(); } /** * {@inheritdoc} */ public function getProtocol() { return $this->protocol; } /** * {@inheritdoc} */ public function writeBuffer($buffer) { $this->write($buffer); } /** * {@inheritdoc} */ public function readBuffer($length) { if ($length <= 0) { throw new InvalidArgumentException('Length parameter must be greater than 0.'); } $value = ''; $socket = $this->getResource(); do { $chunk = fread($socket, $length); if ($chunk === false || $chunk === '') { $this->onConnectionError('Error while reading bytes from the server.'); } $value .= $chunk; } while (($length -= strlen($chunk)) > 0); return $value; } /** * {@inheritdoc} */ public function readLine() { $value = ''; $socket = $this->getResource(); do { $chunk = fgets($socket); if ($chunk === false || $chunk === '') { $this->onConnectionError('Error while reading line from the server.'); } $value .= $chunk; } while (substr($value, -2) !== "\r\n"); return substr($value, 0, -2); } /** * {@inheritdoc} */ public function writeRequest(CommandInterface $command) { $this->protocol->write($this, $command); } /** * {@inheritdoc} */ public function read() { return $this->protocol->read($this); } /** * {@inheritdoc} */ public function __sleep() { return array_merge(parent::__sleep(), ['protocol']); } } Connection/Factory.php000064400000013034152427751160010776 0ustar00 'Predis\Connection\StreamConnection', 'unix' => 'Predis\Connection\StreamConnection', 'tls' => 'Predis\Connection\StreamConnection', 'redis' => 'Predis\Connection\StreamConnection', 'rediss' => 'Predis\Connection\StreamConnection', 'http' => 'Predis\Connection\WebdisConnection', ]; /** * Checks if the provided argument represents a valid connection class * implementing Predis\Connection\NodeConnectionInterface. Optionally, * callable objects are used for lazy initialization of connection objects. * * @param mixed $initializer FQN of a connection class or a callable for lazy initialization. * * @return mixed * @throws InvalidArgumentException */ protected function checkInitializer($initializer) { if (is_callable($initializer)) { return $initializer; } $class = new ReflectionClass($initializer); if (!$class->isSubclassOf('Predis\Connection\NodeConnectionInterface')) { throw new InvalidArgumentException( 'A connection initializer must be a valid connection class or a callable object.' ); } return $initializer; } /** * {@inheritdoc} */ public function define($scheme, $initializer) { $this->schemes[$scheme] = $this->checkInitializer($initializer); } /** * {@inheritdoc} */ public function undefine($scheme) { unset($this->schemes[$scheme]); } /** * {@inheritdoc} */ public function create($parameters) { if (!$parameters instanceof ParametersInterface) { $parameters = $this->createParameters($parameters); } $scheme = $parameters->scheme; if (!isset($this->schemes[$scheme])) { throw new InvalidArgumentException("Unknown connection scheme: '$scheme'."); } $initializer = $this->schemes[$scheme]; if (is_callable($initializer)) { $connection = call_user_func($initializer, $parameters, $this); } else { $connection = new $initializer($parameters); $this->prepareConnection($connection); } if (!$connection instanceof NodeConnectionInterface) { throw new UnexpectedValueException( 'Objects returned by connection initializers must implement ' . "'Predis\Connection\NodeConnectionInterface'." ); } return $connection; } /** * Assigns a default set of parameters applied to new connections. * * The set of parameters passed to create a new connection have precedence * over the default values set for the connection factory. * * @param array $parameters Set of connection parameters. */ public function setDefaultParameters(array $parameters) { $this->defaults = $parameters; } /** * Returns the default set of parameters applied to new connections. * * @return array */ public function getDefaultParameters() { return $this->defaults; } /** * Creates a connection parameters instance from the supplied argument. * * @param mixed $parameters Original connection parameters. * * @return ParametersInterface */ protected function createParameters($parameters) { if (is_string($parameters)) { $parameters = Parameters::parse($parameters); } else { $parameters = $parameters ?: []; } if ($this->defaults) { $parameters += $this->defaults; } return new Parameters($parameters); } /** * Prepares a connection instance after its initialization. * * @param NodeConnectionInterface $connection Connection instance. */ protected function prepareConnection(NodeConnectionInterface $connection) { $parameters = $connection->getParameters(); if (isset($parameters->password) && strlen($parameters->password)) { $cmdAuthArgs = isset($parameters->username) && strlen($parameters->username) ? [$parameters->username, $parameters->password] : [$parameters->password]; $connection->addConnectCommand( new RawCommand('AUTH', $cmdAuthArgs) ); } if (($parameters->client_info ?? false) && !$connection instanceof RelayConnection) { $connection->addConnectCommand( new RawCommand('CLIENT', ['SETINFO', 'LIB-NAME', 'predis']) ); $connection->addConnectCommand( new RawCommand('CLIENT', ['SETINFO', 'LIB-VER', Client::VERSION]) ); } if (isset($parameters->database) && strlen($parameters->database)) { $connection->addConnectCommand( new RawCommand('SELECT', [$parameters->database]) ); } } } Connection/NodeConnectionInterface.php000064400000002442152427751160014116 0ustar00assertExtensions(); $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 */ 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 */ public function getClient() { return $this->client; } /** * {@inheritdoc} */ public function getIdentifier() { try { return $this->client->endpointId(); } catch (RelayException $ex) { return parent::getIdentifier(); } } /** * {@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) { $exception = $this->onCommandError($ex, $command); if ($exception instanceof ErrorResponseInterface) { return $exception; } throw $exception; } } /** * {@inheritdoc} */ public function onCommandError(RelayException $exception, CommandInterface $command) { $code = $exception->getCode(); $message = $exception->getMessage(); if (strpos($message, 'RELAY_ERR_IO') !== false) { return new ConnectionException($this, $message, $code, $exception); } if (strpos($message, 'RELAY_ERR_REDIS') !== false) { return new ServerException($message, $code, $exception); } if (strpos($message, 'RELAY_ERR_WRONGTYPE') !== false && strpos($message, "Got reply-type 'status'") !== false) { $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(); } } Connection/Parameters.php000064400000012266152427751160011500 0ustar00 'tcp', 'host' => '127.0.0.1', 'port' => 6379, ]; /** * Set of connection parameters already filtered * for NULL or 0-length string values. * * @var array */ protected $parameters; /** * @param array $parameters Named array of connection parameters. */ public function __construct(array $parameters = []) { $this->parameters = $this->filter($parameters + static::$defaults); } /** * Filters parameters removing entries with NULL or 0-length string values. * * @params array $parameters Array of parameters to be filtered * * @return array */ protected function filter(array $parameters) { return array_filter($parameters, function ($value) { return $value !== null && $value !== ''; }); } /** * Creates a new instance by supplying the initial parameters either in the * form of an URI string or a named array. * * @param array|string $parameters Set of connection parameters. * * @return Parameters */ public static function create($parameters) { if (is_string($parameters)) { $parameters = static::parse($parameters); } return new static($parameters ?: []); } /** * Parses an URI string returning an array of connection parameters. * * When using the "redis" and "rediss" schemes the URI is parsed according * to the rules defined by the provisional registration documents approved * by IANA. If the URI has a password in its "user-information" part or a * database number in the "path" part these values override the values of * "password" and "database" if they are present in the "query" part. * * @see http://www.iana.org/assignments/uri-schemes/prov/redis * @see http://www.iana.org/assignments/uri-schemes/prov/rediss * * @param string $uri URI string. * * @return array * @throws InvalidArgumentException */ public static function parse($uri) { if (stripos($uri, 'unix://') === 0) { // parse_url() can parse unix:/path/to/sock so we do not need the // unix:///path/to/sock hack, we will support it anyway until 2.0. $uri = str_ireplace('unix://', 'unix:', $uri); } if (!$parsed = parse_url($uri)) { throw new InvalidArgumentException("Invalid parameters URI: $uri"); } if ( isset($parsed['host']) && false !== strpos($parsed['host'], '[') && false !== strpos($parsed['host'], ']') ) { $parsed['host'] = substr($parsed['host'], 1, -1); } if (isset($parsed['query'])) { parse_str($parsed['query'], $queryarray); unset($parsed['query']); $parsed = array_merge($parsed, $queryarray); } if (stripos($uri, 'redis') === 0) { if (isset($parsed['user'])) { if (strlen($parsed['user'])) { $parsed['username'] = $parsed['user']; } unset($parsed['user']); } if (isset($parsed['pass'])) { if (strlen($parsed['pass'])) { $parsed['password'] = $parsed['pass']; } unset($parsed['pass']); } if (isset($parsed['path']) && preg_match('/^\/(\d+)(\/.*)?/', $parsed['path'], $path)) { $parsed['database'] = $path[1]; if (isset($path[2])) { $parsed['path'] = $path[2]; } else { unset($parsed['path']); } } } return $parsed; } /** * {@inheritdoc} */ public function toArray() { return $this->parameters; } /** * {@inheritdoc} */ public function __get($parameter) { if (isset($this->parameters[$parameter])) { return $this->parameters[$parameter]; } } /** * {@inheritdoc} */ public function __isset($parameter) { return isset($this->parameters[$parameter]); } /** * {@inheritdoc} */ public function __toString() { if ($this->scheme === 'unix') { return "$this->scheme:$this->path"; } if (filter_var($this->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { return "$this->scheme://[$this->host]:$this->port"; } return "$this->scheme://$this->host:$this->port"; } /** * {@inheritdoc} */ public function __sleep() { return ['parameters']; } } Connection/AbstractConnection.php000064400000010715152427751160013155 0ustar00parameters = $this->assertParameters($parameters); } /** * Disconnects from the server and destroys the underlying resource when * PHP's garbage collector kicks in. */ public function __destruct() { $this->disconnect(); } /** * Checks some of the parameters used to initialize the connection. * * @param ParametersInterface $parameters Initialization parameters for the connection. * * @return ParametersInterface * @throws InvalidArgumentException */ abstract protected function assertParameters(ParametersInterface $parameters); /** * Creates the underlying resource used to communicate with Redis. * * @return mixed */ abstract protected function createResource(); /** * {@inheritdoc} */ public function isConnected() { return isset($this->resource); } /** * {@inheritdoc} */ public function connect() { if (!$this->isConnected()) { $this->resource = $this->createResource(); return true; } return false; } /** * {@inheritdoc} */ public function disconnect() { unset($this->resource); } /** * {@inheritdoc} */ public function addConnectCommand(CommandInterface $command) { $this->initCommands[] = $command; } /** * {@inheritdoc} */ public function getInitCommands(): array { return $this->initCommands; } /** * {@inheritdoc} */ public function executeCommand(CommandInterface $command) { $this->writeRequest($command); return $this->readResponse($command); } /** * {@inheritdoc} */ public function readResponse(CommandInterface $command) { return $this->read(); } /** * Helper method to handle connection errors. * * @param string $message Error message. * @param int $code Error code. */ protected function onConnectionError($message, $code = 0) { CommunicationException::handle( new ConnectionException($this, "$message [{$this->getParameters()}]", $code) ); } /** * Helper method to handle protocol errors. * * @param string $message Error message. */ protected function onProtocolError($message) { CommunicationException::handle( new ProtocolException($this, "$message [{$this->getParameters()}]") ); } /** * {@inheritdoc} */ public function getResource() { if (isset($this->resource)) { return $this->resource; } $this->connect(); return $this->resource; } /** * {@inheritdoc} */ public function getParameters() { return $this->parameters; } /** * Gets an identifier for the connection. * * @return string */ protected function getIdentifier() { if ($this->parameters->scheme === 'unix') { return $this->parameters->path; } return "{$this->parameters->host}:{$this->parameters->port}"; } /** * {@inheritdoc} */ public function __toString() { if (!isset($this->cachedId)) { $this->cachedId = $this->getIdentifier(); } return $this->cachedId; } /** * {@inheritdoc} */ public function __sleep() { return ['parameters', 'initCommands']; } } Connection/AggregateConnectionInterface.php000064400000003044152427751160015116 0ustar00> 8) ^ ord($value[$i])]) & 0xFFFF; } return $crc; } } Cluster/Hash/PhpiredisCRC16.php000064400000001701152427751160012220 0ustar00 */ class HashRing implements DistributorInterface, HashGeneratorInterface { public const DEFAULT_REPLICAS = 128; public const DEFAULT_WEIGHT = 100; private $ring; private $ringKeys; private $ringKeysCount; private $replicas; private $nodeHashCallback; private $nodes = []; /** * @param int $replicas Number of replicas in the ring. * @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes. */ public function __construct($replicas = self::DEFAULT_REPLICAS, $nodeHashCallback = null) { $this->replicas = $replicas; $this->nodeHashCallback = $nodeHashCallback; } /** * Adds a node to the ring with an optional weight. * * @param mixed $node Node object. * @param int $weight Weight for the node. */ public function add($node, $weight = null) { // In case of collisions in the hashes of the nodes, the node added // last wins, thus the order in which nodes are added is significant. $this->nodes[] = [ 'object' => $node, 'weight' => (int) $weight ?: $this::DEFAULT_WEIGHT, ]; $this->reset(); } /** * {@inheritdoc} */ public function remove($node) { // A node is removed by resetting the ring so that it's recreated from // scratch, in order to reassign possible hashes with collisions to the // right node according to the order in which they were added in the // first place. for ($i = 0; $i < count($this->nodes); ++$i) { if ($this->nodes[$i]['object'] === $node) { array_splice($this->nodes, $i, 1); $this->reset(); break; } } } /** * Resets the distributor. */ private function reset() { unset( $this->ring, $this->ringKeys, $this->ringKeysCount ); } /** * Returns the initialization status of the distributor. * * @return bool */ private function isInitialized() { return isset($this->ringKeys); } /** * Calculates the total weight of all the nodes in the distributor. * * @return int */ private function computeTotalWeight() { $totalWeight = 0; foreach ($this->nodes as $node) { $totalWeight += $node['weight']; } return $totalWeight; } /** * Initializes the distributor. */ private function initialize() { if ($this->isInitialized()) { return; } if (!$this->nodes) { throw new EmptyRingException('Cannot initialize an empty hashring.'); } $this->ring = []; $totalWeight = $this->computeTotalWeight(); $nodesCount = count($this->nodes); foreach ($this->nodes as $node) { $weightRatio = $node['weight'] / $totalWeight; $this->addNodeToRing($this->ring, $node, $nodesCount, $this->replicas, $weightRatio); } ksort($this->ring, SORT_NUMERIC); $this->ringKeys = array_keys($this->ring); $this->ringKeysCount = count($this->ringKeys); } /** * Implements the logic needed to add a node to the hashring. * * @param array $ring Source hashring. * @param mixed $node Node object to be added. * @param int $totalNodes Total number of nodes. * @param int $replicas Number of replicas in the ring. * @param float $weightRatio Weight ratio for the node. */ protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio) { $nodeObject = $node['object']; $nodeHash = $this->getNodeHash($nodeObject); $replicas = (int) round($weightRatio * $totalNodes * $replicas); for ($i = 0; $i < $replicas; ++$i) { $key = $this->hash("$nodeHash:$i"); $ring[$key] = $nodeObject; } } /** * {@inheritdoc} */ protected function getNodeHash($nodeObject) { if (!isset($this->nodeHashCallback)) { return (string) $nodeObject; } return call_user_func($this->nodeHashCallback, $nodeObject); } /** * {@inheritdoc} */ public function hash($value) { return crc32($value); } /** * {@inheritdoc} */ public function getByHash($hash) { return $this->ring[$this->getSlot($hash)]; } /** * {@inheritdoc} */ public function getBySlot($slot) { $this->initialize(); if (isset($this->ring[$slot])) { return $this->ring[$slot]; } } /** * {@inheritdoc} */ public function getSlot($hash) { $this->initialize(); $ringKeys = $this->ringKeys; $upper = $this->ringKeysCount - 1; $lower = 0; while ($lower <= $upper) { $index = ($lower + $upper) >> 1; $item = $ringKeys[$index]; if ($item > $hash) { $upper = $index - 1; } elseif ($item < $hash) { $lower = $index + 1; } else { return $item; } } return $ringKeys[$this->wrapAroundStrategy($upper, $lower, $this->ringKeysCount)]; } /** * {@inheritdoc} */ public function get($value) { $hash = $this->hash($value); return $this->getByHash($hash); } /** * Implements a strategy to deal with wrap-around errors during binary searches. * * @param int $upper * @param int $lower * @param int $ringKeysCount * * @return int */ protected function wrapAroundStrategy($upper, $lower, $ringKeysCount) { // Binary search for the last item in ringkeys with a value less or // equal to the key. If no such item exists, return the last item. return $upper >= 0 ? $upper : $ringKeysCount - 1; } /** * {@inheritdoc} */ public function getHashGenerator() { return $this; } } Cluster/Distributor/DistributorInterface.php000064400000003444152427751160015362 0ustar00 */ class KetamaRing extends HashRing { public const DEFAULT_REPLICAS = 160; /** * @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes. */ public function __construct($nodeHashCallback = null) { parent::__construct($this::DEFAULT_REPLICAS, $nodeHashCallback); } /** * {@inheritdoc} */ protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio) { $nodeObject = $node['object']; $nodeHash = $this->getNodeHash($nodeObject); $replicas = (int) floor($weightRatio * $totalNodes * ($replicas / 4)); for ($i = 0; $i < $replicas; ++$i) { $unpackedDigest = unpack('V4', md5("$nodeHash-$i", true)); foreach ($unpackedDigest as $key) { $ring[$key] = $nodeObject; } } } /** * {@inheritdoc} */ public function hash($value) { $hash = unpack('V', md5($value, true)); return $hash[1]; } /** * {@inheritdoc} */ protected function wrapAroundStrategy($upper, $lower, $ringKeysCount) { // Binary search for the first item in ringkeys with a value greater // or equal to the key. If no such item exists, return the first item. return $lower < $ringKeysCount ? $lower : 0; } } Cluster/Distributor/EmptyRingException.php000064400000000637152427751160015025 0ustar00distributor = $distributor ?: new HashRing(); } /** * {@inheritdoc} */ public function getSlotByKey($key) { $key = $this->extractKeyTag($key); $hash = $this->distributor->hash($key); return $this->distributor->getSlot($hash); } /** * {@inheritdoc} */ protected function checkSameSlotForKeys(array $keys) { if (!$count = count($keys)) { return false; } $currentKey = $this->extractKeyTag($keys[0]); for ($i = 1; $i < $count; ++$i) { $nextKey = $this->extractKeyTag($keys[$i]); if ($currentKey !== $nextKey) { return false; } $currentKey = $nextKey; } return true; } /** * {@inheritdoc} */ public function getDistributor() { return $this->distributor; } } Cluster/ClusterStrategy.php000064400000036440152427751160012063 0ustar00commands = $this->getDefaultCommands(); } /** * Returns the default map of supported commands with their handlers. * * @return array */ protected function getDefaultCommands() { $getKeyFromFirstArgument = [$this, 'getKeyFromFirstArgument']; $getKeyFromAllArguments = [$this, 'getKeyFromAllArguments']; return [ /* commands operating on the key space */ 'EXISTS' => $getKeyFromAllArguments, 'DEL' => $getKeyFromAllArguments, 'TYPE' => $getKeyFromFirstArgument, 'EXPIRE' => $getKeyFromFirstArgument, 'EXPIREAT' => $getKeyFromFirstArgument, 'PERSIST' => $getKeyFromFirstArgument, 'PEXPIRE' => $getKeyFromFirstArgument, 'PEXPIREAT' => $getKeyFromFirstArgument, 'TTL' => $getKeyFromFirstArgument, 'PTTL' => $getKeyFromFirstArgument, 'SORT' => [$this, 'getKeyFromSortCommand'], 'DUMP' => $getKeyFromFirstArgument, 'RESTORE' => $getKeyFromFirstArgument, 'FLUSHDB' => [$this, 'getFakeKey'], /* commands operating on string values */ 'APPEND' => $getKeyFromFirstArgument, 'DECR' => $getKeyFromFirstArgument, 'DECRBY' => $getKeyFromFirstArgument, 'GET' => $getKeyFromFirstArgument, 'GETBIT' => $getKeyFromFirstArgument, 'MGET' => $getKeyFromAllArguments, 'SET' => $getKeyFromFirstArgument, 'GETRANGE' => $getKeyFromFirstArgument, 'GETSET' => $getKeyFromFirstArgument, 'INCR' => $getKeyFromFirstArgument, 'INCRBY' => $getKeyFromFirstArgument, 'INCRBYFLOAT' => $getKeyFromFirstArgument, 'SETBIT' => $getKeyFromFirstArgument, 'SETEX' => $getKeyFromFirstArgument, 'MSET' => [$this, 'getKeyFromInterleavedArguments'], 'MSETNX' => [$this, 'getKeyFromInterleavedArguments'], 'SETNX' => $getKeyFromFirstArgument, 'SETRANGE' => $getKeyFromFirstArgument, 'STRLEN' => $getKeyFromFirstArgument, 'SUBSTR' => $getKeyFromFirstArgument, 'BITOP' => [$this, 'getKeyFromBitOp'], 'BITCOUNT' => $getKeyFromFirstArgument, 'BITFIELD' => $getKeyFromFirstArgument, /* commands operating on lists */ 'LINSERT' => $getKeyFromFirstArgument, 'LINDEX' => $getKeyFromFirstArgument, 'LLEN' => $getKeyFromFirstArgument, 'LPOP' => $getKeyFromFirstArgument, 'RPOP' => $getKeyFromFirstArgument, 'RPOPLPUSH' => $getKeyFromAllArguments, 'BLPOP' => [$this, 'getKeyFromBlockingListCommands'], 'BRPOP' => [$this, 'getKeyFromBlockingListCommands'], 'BRPOPLPUSH' => [$this, 'getKeyFromBlockingListCommands'], 'LPUSH' => $getKeyFromFirstArgument, 'LPUSHX' => $getKeyFromFirstArgument, 'RPUSH' => $getKeyFromFirstArgument, 'RPUSHX' => $getKeyFromFirstArgument, 'LRANGE' => $getKeyFromFirstArgument, 'LREM' => $getKeyFromFirstArgument, 'LSET' => $getKeyFromFirstArgument, 'LTRIM' => $getKeyFromFirstArgument, /* commands operating on sets */ 'SADD' => $getKeyFromFirstArgument, 'SCARD' => $getKeyFromFirstArgument, 'SDIFF' => $getKeyFromAllArguments, 'SDIFFSTORE' => $getKeyFromAllArguments, 'SINTER' => $getKeyFromAllArguments, 'SINTERSTORE' => $getKeyFromAllArguments, 'SUNION' => $getKeyFromAllArguments, 'SUNIONSTORE' => $getKeyFromAllArguments, 'SISMEMBER' => $getKeyFromFirstArgument, 'SMEMBERS' => $getKeyFromFirstArgument, 'SSCAN' => $getKeyFromFirstArgument, 'SPOP' => $getKeyFromFirstArgument, 'SRANDMEMBER' => $getKeyFromFirstArgument, 'SREM' => $getKeyFromFirstArgument, /* commands operating on sorted sets */ 'ZADD' => $getKeyFromFirstArgument, 'ZCARD' => $getKeyFromFirstArgument, 'ZCOUNT' => $getKeyFromFirstArgument, 'ZINCRBY' => $getKeyFromFirstArgument, 'ZINTERSTORE' => [$this, 'getKeyFromZsetAggregationCommands'], 'ZRANGE' => $getKeyFromFirstArgument, 'ZRANGEBYSCORE' => $getKeyFromFirstArgument, 'ZRANK' => $getKeyFromFirstArgument, 'ZREM' => $getKeyFromFirstArgument, 'ZREMRANGEBYRANK' => $getKeyFromFirstArgument, 'ZREMRANGEBYSCORE' => $getKeyFromFirstArgument, 'ZREVRANGE' => $getKeyFromFirstArgument, 'ZREVRANGEBYSCORE' => $getKeyFromFirstArgument, 'ZREVRANK' => $getKeyFromFirstArgument, 'ZSCORE' => $getKeyFromFirstArgument, 'ZUNIONSTORE' => [$this, 'getKeyFromZsetAggregationCommands'], 'ZSCAN' => $getKeyFromFirstArgument, 'ZLEXCOUNT' => $getKeyFromFirstArgument, 'ZRANGEBYLEX' => $getKeyFromFirstArgument, 'ZREMRANGEBYLEX' => $getKeyFromFirstArgument, 'ZREVRANGEBYLEX' => $getKeyFromFirstArgument, /* commands operating on hashes */ 'HDEL' => $getKeyFromFirstArgument, 'HEXISTS' => $getKeyFromFirstArgument, 'HGET' => $getKeyFromFirstArgument, 'HGETALL' => $getKeyFromFirstArgument, 'HMGET' => $getKeyFromFirstArgument, 'HMSET' => $getKeyFromFirstArgument, 'HINCRBY' => $getKeyFromFirstArgument, 'HINCRBYFLOAT' => $getKeyFromFirstArgument, 'HKEYS' => $getKeyFromFirstArgument, 'HLEN' => $getKeyFromFirstArgument, 'HSET' => $getKeyFromFirstArgument, 'HSETNX' => $getKeyFromFirstArgument, 'HVALS' => $getKeyFromFirstArgument, 'HSCAN' => $getKeyFromFirstArgument, 'HSTRLEN' => $getKeyFromFirstArgument, /* commands operating on HyperLogLog */ 'PFADD' => $getKeyFromFirstArgument, 'PFCOUNT' => $getKeyFromAllArguments, 'PFMERGE' => $getKeyFromAllArguments, /* scripting */ 'EVAL' => [$this, 'getKeyFromScriptingCommands'], 'EVALSHA' => [$this, 'getKeyFromScriptingCommands'], /* server */ 'INFO' => [$this, 'getFakeKey'], /* commands performing geospatial operations */ 'GEOADD' => $getKeyFromFirstArgument, 'GEOHASH' => $getKeyFromFirstArgument, 'GEOPOS' => $getKeyFromFirstArgument, 'GEODIST' => $getKeyFromFirstArgument, 'GEORADIUS' => [$this, 'getKeyFromGeoradiusCommands'], 'GEORADIUSBYMEMBER' => [$this, 'getKeyFromGeoradiusCommands'], /* cluster */ 'CLUSTER' => [$this, 'getFakeKey'], ]; } /** * Returns the list of IDs for the supported commands. * * @return array */ public function getSupportedCommands() { return array_keys($this->commands); } /** * Sets an handler for the specified command ID. * * The signature of the callback must have a single parameter of type * Predis\Command\CommandInterface. * * When the callback argument is omitted or NULL, the previously associated * handler for the specified command ID is removed. * * @param string $commandID Command ID. * @param mixed $callback A valid callable object, or NULL to unset the handler. * * @throws InvalidArgumentException */ public function setCommandHandler($commandID, $callback = null) { $commandID = strtoupper($commandID); if (!isset($callback)) { unset($this->commands[$commandID]); return; } if (!is_callable($callback)) { throw new InvalidArgumentException( 'The argument must be a callable object or NULL.' ); } $this->commands[$commandID] = $callback; } /** * Get fake key for commands with no key argument. * * @return string */ protected function getFakeKey(): string { return 'key'; } /** * Extracts the key from the first argument of a command instance. * * @param CommandInterface $command Command instance. * * @return string */ protected function getKeyFromFirstArgument(CommandInterface $command) { return $command->getArgument(0); } /** * Extracts the key from a command with multiple keys only when all keys in * the arguments array produce the same hash. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromAllArguments(CommandInterface $command) { $arguments = $command->getArguments(); if (!$this->checkSameSlotForKeys($arguments)) { return null; } return $arguments[0]; } /** * Extracts the key from a command with multiple keys only when all keys in * the arguments array produce the same hash. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromInterleavedArguments(CommandInterface $command) { $arguments = $command->getArguments(); $keys = []; for ($i = 0; $i < count($arguments); $i += 2) { $keys[] = $arguments[$i]; } if (!$this->checkSameSlotForKeys($keys)) { return null; } return $arguments[0]; } /** * Extracts the key from SORT command. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromSortCommand(CommandInterface $command) { $arguments = $command->getArguments(); $firstKey = $arguments[0]; if (1 === $argc = count($arguments)) { return $firstKey; } $keys = [$firstKey]; for ($i = 1; $i < $argc; ++$i) { if (strtoupper($arguments[$i]) === 'STORE') { $keys[] = $arguments[++$i]; } } if (!$this->checkSameSlotForKeys($keys)) { return null; } return $firstKey; } /** * Extracts the key from BLPOP and BRPOP commands. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromBlockingListCommands(CommandInterface $command) { $arguments = $command->getArguments(); if (!$this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) { return null; } return $arguments[0]; } /** * Extracts the key from BITOP command. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromBitOp(CommandInterface $command) { $arguments = $command->getArguments(); if (!$this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) { return null; } return $arguments[1]; } /** * Extracts the key from GEORADIUS and GEORADIUSBYMEMBER commands. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromGeoradiusCommands(CommandInterface $command) { $arguments = $command->getArguments(); $argc = count($arguments); $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; if ($argc > $startIndex) { $keys = [$arguments[0]]; for ($i = $startIndex; $i < $argc; ++$i) { $argument = strtoupper($arguments[$i]); if ($argument === 'STORE' || $argument === 'STOREDIST') { $keys[] = $arguments[++$i]; } } if (!$this->checkSameSlotForKeys($keys)) { return null; } } return $arguments[0]; } /** * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromZsetAggregationCommands(CommandInterface $command) { $arguments = $command->getArguments(); $keys = array_merge([$arguments[0]], array_slice($arguments, 2, $arguments[1])); if (!$this->checkSameSlotForKeys($keys)) { return null; } return $arguments[0]; } /** * Extracts the key from EVAL and EVALSHA commands. * * @param CommandInterface $command Command instance. * * @return string|null */ protected function getKeyFromScriptingCommands(CommandInterface $command) { $keys = $command instanceof ScriptCommand ? $command->getKeys() : array_slice($args = $command->getArguments(), 2, $args[1]); if (!$keys || !$this->checkSameSlotForKeys($keys)) { return null; } return $keys[0]; } /** * {@inheritdoc} */ public function getSlot(CommandInterface $command) { $slot = $command->getSlot(); if (!isset($slot) && isset($this->commands[$cmdID = $command->getId()])) { $key = call_user_func($this->commands[$cmdID], $command); if (isset($key)) { $slot = $this->getSlotByKey($key); $command->setSlot($slot); } } return $slot; } /** * Checks if the specified array of keys will generate the same hash. * * @param array $keys Array of keys. * * @return bool */ protected function checkSameSlotForKeys(array $keys) { if (!$count = count($keys)) { return false; } $currentSlot = $this->getSlotByKey($keys[0]); for ($i = 1; $i < $count; ++$i) { $nextSlot = $this->getSlotByKey($keys[$i]); if ($currentSlot !== $nextSlot) { return false; } $currentSlot = $nextSlot; } return true; } /** * Returns only the hashable part of a key (delimited by "{...}"), or the * whole key if a key tag is not found in the string. * * @param string $key A key. * * @return string */ protected function extractKeyTag($key) { if (false !== $start = strpos($key, '{')) { if (false !== ($end = strpos($key, '}', $start)) && $end !== ++$start) { $key = substr($key, $start, $end - $start); } } return $key; } } Cluster/SlotMap.php000064400000011413152427751160010267 0ustar00= 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); } } Cluster/RedisStrategy.php000064400000002400152427751160011475 0ustar00hashGenerator = $hashGenerator ?: new CRC16(); } /** * {@inheritdoc} */ public function getSlotByKey($key) { $key = $this->extractKeyTag($key); return $this->hashGenerator->hash($key) & 0x3FFF; } /** * {@inheritdoc} */ public function getDistributor() { $class = get_class($this); throw new NotSupportedException("$class does not provide an external distributor"); } } Cluster/StrategyInterface.php000064400000002440152427751160012333 0ustar00separator = $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'); } } Command/Traits/To/ServerTo.php000064400000002340152427751160012245 0ustar00= $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 )); } } Command/Traits/From/GeoFrom.php000064400000002466152427751160012364 0ustar00getFromArgumentPositionOffset($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; } } Command/Traits/Expire/ExpireOptions.php000064400000001620152427751160014156 0ustar00 '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); } } Command/Traits/Limit/LimitObject.php000064400000002375152427751160013405 0ustar00getLimitArgumentPositionOffset($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; } } Command/Traits/Limit/Limit.php000064400000002700152427751160012246 0ustar00= $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)); } } Command/Traits/Get/Get.php000064400000002404152427751160011351 0ustar00= $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)); } } Command/Traits/With/WithDist.php000064400000002334152427751160012567 0ustar00= $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)); } } Command/Traits/With/WithCoord.php000064400000002432152427751160012731 0ustar00= $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)); } } Command/Traits/With/WithValues.php000064400000001305152427751160013120 0ustar00= $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)); } } Command/Traits/With/WithScores.php000064400000003205152427751160013120 0ustar00isWithScoreModifier()) { $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; } } Command/Traits/Json/NxXxArgument.php000064400000003252152427751160013436 0ustar00 '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 )); } } Command/Traits/Json/Space.php000064400000002660152427751160012063 0ustar00= $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 )); } } Command/Traits/Json/Newline.php000064400000002706152427751160012432 0ustar00= $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 )); } } Command/Traits/Json/Indent.php000064400000002673152427751160012255 0ustar00= $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 )); } } Command/Traits/By/GeoBy.php000064400000002444152427751160011476 0ustar00getByArgumentPositionOffset($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; } } Command/Traits/By/ByLexByScore.php000064400000002471152427751160013003 0ustar00 '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)); } } Command/Traits/By/ByArgument.php000064400000002064152427751160012544 0ustar00= $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)); } } Command/Traits/BloomFilters/BucketSize.php000064400000003117152427751160014566 0ustar00= $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 )); } } Command/Traits/BloomFilters/Expansion.php000064400000003014152427751160014456 0ustar00= $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 )); } } Command/Traits/BloomFilters/MaxIterations.php000064400000003163152427751160015306 0ustar00= $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 )); } } Command/Traits/BloomFilters/Error.php000064400000003022152427751160013602 0ustar00= $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 )); } } Command/Traits/BloomFilters/Items.php000064400000002127152427751160013577 0ustar00= $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 )); } } Command/Traits/BloomFilters/NoCreate.php000064400000002432152427751160014215 0ustar00= $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)); } } Command/Traits/BloomFilters/Capacity.php000064400000003066152427751160014256 0ustar00= $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 )); } } Command/Traits/Sorting.php000064400000003035152427751160011541 0ustar00 '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 )); } } Command/Traits/MinMaxModifier.php000064400000002012152427751160012756 0ustar00 '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]]; } } Command/Traits/BitByte.php000064400000001521152427751160011454 0ustar00 '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); } } Command/Traits/Count.php000064400000003677152427751160011220 0ustar00= $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 )); } } Command/Traits/Storedist.php000064400000002425152427751160012076 0ustar00= $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)); } } Command/Traits/Rev.php000064400000002065152427751160010652 0ustar00= $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 )); } } Command/Traits/Keys.php000064400000002554152427751160011034 0ustar00 $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)); } } Command/Traits/Replace.php000064400000001253152427751160011467 0ustar00= $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 )); } } Command/Traits/LeftRight.php000064400000003121152427751160012000 0ustar00 '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 )); } } Command/Traits/Aggregate.php000064400000003305152427751160012002 0ustar00 '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 )); } } Command/Traits/Timeout.php000064400000002747152427751160011553 0ustar00= $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 )); } } Command/Redis/LMPOP.php000064400000002527152427751160010610 0ustar00setCount($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]]; } } Command/Redis/GEORADIUSBYMEMBER.php000064400000001317152427751160012362 0ustar00setSorting($arguments); $arguments = $this->getArguments(); $this->setGetArgument($arguments); $arguments = $this->getArguments(); $this->setLimit($arguments); $arguments = $this->getArguments(); $this->setBy($arguments); $this->filterArguments(); } } Command/Redis/MULTI.php000064400000001010152427751160010575 0ustar00 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; } } Command/Redis/GEOHASH.php000064400000001524152427751160010773 0ustar00toArray(); } parent::setArguments(array_merge( [$index, $query], $commandArguments )); } } Command/Redis/Search/FTTAGVALS.php000064400000001070152427751160012451 0ustar00toArray(); } $terms = array_slice($arguments, 3); parent::setArguments(array_merge( [$index, $synonymGroupId], $commandArguments, $terms )); } } Command/Redis/Search/FTCREATE.php000064400000002176152427751160012323 0ustar00toArray() : []; $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 )); } } Command/Redis/Search/FTPROFILE.php000064400000001454152427751160012456 0ustar00toArray() )); } } Command/Redis/Search/FTALIASADD.php000064400000001035152427751160012513 0ustar00toArray() : []; parent::setArguments(array_merge( [$index, $query], $commandArguments )); } } Command/Redis/Search/FTSUGADD.php000064400000001575152427751160012331 0ustar00toArray() : []; parent::setArguments(array_merge( [$key, $string, $score], $commandArguments )); } } Command/Redis/Search/FTAGGREGATE.php000064400000001663152427751160012646 0ustar00toArray() : []; parent::setArguments(array_merge( [$index, $query], $commandArguments )); } } Command/Redis/Search/FTSUGLEN.php000064400000001065152427751160012351 0ustar00toArray() : []; parent::setArguments(array_merge( [$key, $prefix], $commandArguments )); } } Command/Redis/Search/FTCURSOR.php000064400000001421152427751160012365 0ustar00toArray() : []; parent::setArguments(array_merge( [$subcommand, $index, $cursorId], $commandArguments )); } } Command/Redis/Search/FTALIASDEL.php000064400000001042152427751160012525 0ustar00toArray(); } parent::setArguments(array_merge( [$index, $query], $commandArguments )); } } Command/Redis/Search/FTALTER.php000064400000002033152427751160012217 0ustar00toArray() : []; $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 )); } } Command/Redis/Search/FTSYNDUMP.php000064400000001046152427751160012512 0ustar00toArray(); } parent::setArguments(array_merge( [$index], $commandArguments )); } } Command/Redis/Search/FTDICTADD.php000064400000001033152427751160012403 0ustar00setByLexByScoreArgument($arguments); $arguments = $this->getArguments(); $this->setReversedArgument($arguments); $arguments = $this->getArguments(); $this->setLimitArguments($arguments); $this->filterArguments(); } } Command/Redis/HEXPIRE.php000064400000002472152427751160011024 0ustar00flagsEnum, true)) { $processedArguments[] = strtoupper($arguments[3]); } else { throw new UnexpectedValueException('Unsupported flag value'); } } if (array_key_exists(2, $arguments) && null !== $arguments[2]) { array_push($processedArguments, 'FIELDS', count($arguments[2])); $processedArguments = array_merge($processedArguments, $arguments[2]); } parent::setArguments($processedArguments); } } Command/Redis/QUIT.php000064400000001005152427751160010471 0ustar00toArray() : []; parent::setArguments(array_merge( [$key], $commandArguments )); } } Command/Redis/TimeSeries/TSGET.php000064400000001527152427751160012657 0ustar00toArray() : []; parent::setArguments(array_merge( [$key], $commandArguments )); } } Command/Redis/TimeSeries/TSQUERYINDEX.php000064400000001076152427751160013734 0ustar00toArray() : []; parent::setArguments(array_merge( [$key], $commandArguments )); } } Command/Redis/TimeSeries/TSDELETERULE.php000064400000001047152427751160013667 0ustar00toArray() : []; parent::setArguments(array_merge( [$key, $fromTimestamp, $toTimestamp], $commandArguments )); } } Command/Redis/TimeSeries/TSINCRBY.php000064400000001770152427751160013226 0ustar00toArray() : []; parent::setArguments(array_merge( [$key, $value], $commandArguments )); } } Command/Redis/TimeSeries/TSDECRBY.php000064400000001771152427751160013211 0ustar00toArray() : []; parent::setArguments(array_merge( [$key, $value], $commandArguments )); } } Command/Redis/TimeSeries/TSADD.php000064400000001535152427751160012627 0ustar00toArray() : []; parent::setArguments(array_merge( [$key, $timestamp, $value], $commandArguments )); } } Command/Redis/TimeSeries/TSCREATE.php000064400000001466152427751160013205 0ustar00toArray() : []; parent::setArguments(array_merge( [$key], $commandArguments )); } } Command/Redis/TimeSeries/TSMGET.php000064400000001512152427751160012766 0ustar00toArray(); array_push($processedArguments, 'FILTER', ...$arguments); parent::setArguments(array_merge( $commandArguments, $processedArguments )); } } Command/Redis/TimeSeries/TSREVRANGE.php000064400000000771152427751160013451 0ustar00toArray(); parent::setArguments(array_merge( [$fromTimestamp, $toTimestamp], $commandArguments )); } } Command/Redis/EXISTS.php000064400000001013152427751160010725 0ustar00prepareOptions(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; } } Command/Redis/DUMP.php000064400000001005152427751160010454 0ustar00 $score) { $arguments[] = $score; $arguments[] = $member; } } parent::setArguments($arguments); } } Command/Redis/ZUNIONSTORE.php000064400000003233152427751160011553 0ustar00setAggregate($arguments); $arguments = $this->getArguments(); $this->setWeights($arguments); $arguments = $this->getArguments(); $this->setKeys($arguments); } } Command/Redis/BITOP.php000064400000001602152427751160010567 0ustar00 $entry) { $log[$index] = [ 'id' => $entry[0], 'timestamp' => $entry[1], 'duration' => $entry[2], 'command' => $entry[3], ]; } return $log; } return $data; } } Command/Redis/HGETALL.php000064400000001534152427751160010776 0ustar00parseNewResponseFormat($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; } } Command/Redis/EVAL_RO.php000064400000001271152427751160011043 0ustar00 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; } } Command/Redis/CountMinSketch/CMSMERGE.php000064400000002064152427751160014015 0ustar00getArguments(), 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; } } Command/Redis/SMISMEMBER.php000064400000001074152427751160011360 0ustar00setDB($arguments); $arguments = $this->getArguments(); $this->setReplace($arguments); } } Command/Redis/LASTSAVE.php000064400000001021152427751160011127 0ustar00 $value) { if ($index < 2) { continue; } if (false === $value || null === $value) { unset($arguments[$index]); } } parent::setArguments($arguments); } } Command/Redis/ZPOPMAX.php000064400000001616152427751160011055 0ustar00getArgument(0); } } Command/Redis/RENAME.php000064400000001013152427751160010655 0ustar00filterArguments(); } 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'; } } Command/Redis/TopK/TOPKQUERY.php000064400000001126152427751160012173 0ustar00getArguments(); for ($i = 3; $i < count($arguments); ++$i) { switch (strtoupper($arguments[$i])) { case 'WITHSCORES': return true; case 'LIMIT': $i += 2; break; } } return false; } } Command/Redis/PEXPIREAT.php000064400000001024152427751160011251 0ustar00setLimit($arguments); $arguments = $this->getArguments(); $this->setKeys($arguments); } } Command/Redis/ZMPOP.php000064400000003643152427751160010626 0ustar00setCount($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]); } } Command/Redis/PEXPIRETIME.php000064400000001210152427751160011500 0ustar00setKeys($arguments, false); } public function parseResponse($data) { $key = array_shift($data); if (null === $key) { return [$key]; } return array_combine([$key], [[$data[0] => $data[1]]]); } } Command/Redis/INCR.php000064400000001005152427751160010442 0ustar00setTimeout($arguments); $arguments = $this->getArguments(); $this->setTo($arguments); $this->filterArguments(); } } Command/Redis/ZINCRBY.php000064400000001016152427751160011031 0ustar00getArgument(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; } } Command/Redis/HPEXPIRE.php000064400000000610152427751160011134 0ustar00getArgument(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; } } Command/Redis/HINCRBY.php000064400000001016152427751160011007 0ustar00setStoreDist($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(); } } Command/Redis/LINDEX.php000064400000001013152427751160010671 0ustar00 2) { for ($i = 2, $iMax = count($arguments); $i < $iMax; $i++) { $processedArguments[] = $arguments[$i]; } } parent::setArguments($processedArguments); } } Command/Redis/MOVE.php000064400000001005152427751160010455 0ustar00setLimit($arguments); $arguments = $this->getArguments(); $this->setKeys($arguments); } } Command/Redis/XADD.php000064400000002631152427751160010435 0ustar00 $val) { $args[] = $key; $args[] = $val; } } parent::setArguments($args); } } Command/Redis/GEOSEARCH.php000064400000006520152427751160011216 0ustar00setSorting($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; } } Command/Redis/DBSIZE.php000064400000001013152427751160010666 0ustar00 '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); } } Command/Redis/RPOPLPUSH.php000064400000001024152427751160011304 0ustar00setExpansion($arguments); $arguments = $this->getArguments(); $this->setMaxIterations($arguments); $arguments = $this->getArguments(); $this->setBucketSize($arguments); $this->filterArguments(); } } Command/Redis/CuckooFilter/CFINSERT.php000064400000002405152427751160013522 0ustar00setNoCreate($arguments); $arguments = $this->getArguments(); $this->setItems($arguments); $arguments = $this->getArguments(); $this->setCapacity($arguments); $this->filterArguments(); } } Command/Redis/CuckooFilter/CFINFO.php000064400000001642152427751160013253 0ustar00 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; } } Command/Redis/CuckooFilter/CFEXISTS.php000064400000001063152427751160013534 0ustar00prepareOptions(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; } } Command/Redis/ZREVRANGEBYSCORE.php000064400000000775152427751160012316 0ustar00setExpansion($arguments); $this->filterArguments(); } } Command/Redis/BloomFilter/BFSCANDUMP.php000064400000001233152427751160013552 0ustar00 '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; } } Command/Redis/BloomFilter/BFEXISTS.php000064400000001103152427751160013353 0ustar00setNoCreate($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(); } } Command/Redis/Container/Search/FTCURSOR.php000064400000001327152427751160014314 0ustar00client = $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; } Command/Redis/Container/ContainerFactory.php000064400000004541152427751160015113 0ustar00 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; } } Command/Redis/Container/CLUSTER.php000064400000001172152427751160012757 0ustar00 $value) { $modifier = strtoupper($modifier); if ($modifier === 'COPY' && $value == true) { $arguments[] = $modifier; } if ($modifier === 'REPLACE' && $value == true) { $arguments[] = $modifier; } } } parent::setArguments($arguments); } } Command/Redis/HRANDFIELD.php000064400000002273152427751160011317 0ustar00strategyResolver = 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(); } } Command/Redis/Json/JSONMSET.php000064400000001125152427751160012065 0ustar00setSpace($arguments); $arguments = $this->getArguments(); $this->setNewline($arguments); $arguments = $this->getArguments(); $this->setIndent($arguments); $this->filterArguments(); } } Command/Redis/Json/JSONCLEAR.php000064400000001102152427751160012136 0ustar00setSubcommand($arguments); $this->filterArguments(); } } Command/Redis/Json/JSONMGET.php000064400000001341152427751160012051 0ustar00prepareOptions(array_pop($arguments)); $arguments = array_merge($arguments, $options); } $this->arguments = $arguments; 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']; } if (!empty($options['NOVALUES']) && true === $options['NOVALUES']) { $normalized[] = 'NOVALUES'; } return $normalized; } /** * {@inheritdoc} */ public function parseResponse($data) { if (!in_array('NOVALUES', $this->arguments, true)) { 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; } } Command/Redis/SUBSTR.php000064400000001013152427751160010730 0ustar00filterArguments(); } 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; } } Command/Redis/SRANDMEMBER.php000064400000001032152427751160011446 0ustar00 $v) { $flattenedKVs[] = $k; $flattenedKVs[] = $v; } $arguments = $flattenedKVs; } parent::setArguments($arguments); } } Command/Redis/EXPIREAT.php000064400000001420152427751160011131 0ustar00setKeys($arguments); $arguments = $this->getArguments(); $this->setWithScore($arguments); } } Command/Redis/UNSUBSCRIBE.php000064400000001345152427751160011502 0ustar00getArgument(0)); } } Command/Redis/ZSCAN.php000064400000003515152427751160010575 0ustar00prepareOptions(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; } } Command/Redis/GETRANGE.php000064400000001021152427751160011101 0ustar00 $v) { $flattenedKVs[] = $k; $flattenedKVs[] = $v; } $arguments = $flattenedKVs; } parent::setArguments($arguments); } } Command/Redis/SELECT.php000064400000001013152427751160010665 0ustar00setCommonOptions('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; } } Command/Argument/Search/SchemaFields/GeoShapeField.php000064400000003003152427751160016616 0ustar00fieldArguments[] = $identifier; if ($alias !== '') { $this->fieldArguments[] = 'AS'; $this->fieldArguments[] = $alias; } $this->fieldArguments[] = 'GEOSHAPE'; if (null !== $coordSystem) { $this->fieldArguments[] = $coordSystem; } if ($sortable === self::SORTABLE) { $this->fieldArguments[] = 'SORTABLE'; } elseif ($sortable === self::SORTABLE_UNF) { $this->fieldArguments[] = 'SORTABLE'; $this->fieldArguments[] = 'UNF'; } if ($noIndex) { $this->fieldArguments[] = 'NOINDEX'; } } } Command/Argument/Search/SchemaFields/TagField.php000064400000002537152427751160015651 0ustar00setCommonOptions('TAG', $identifier, $alias, $sortable, $noIndex, $allowsMissing); if ($separator !== ',') { $this->fieldArguments[] = 'SEPARATOR'; $this->fieldArguments[] = $separator; } if ($caseSensitive) { $this->fieldArguments[] = 'CASESENSITIVE'; } if ($allowsEmpty) { $this->fieldArguments[] = 'INDEXEMPTY'; } } } Command/Argument/Search/SchemaFields/FieldInterface.php000064400000000716152427751160017033 0ustar00fieldArguments[] = $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'; } if ($allowsMissing) { $this->fieldArguments[] = 'INDEXMISSING'; } } /** * {@inheritDoc} */ public function toArray(): array { return $this->fieldArguments; } } Command/Argument/Search/SchemaFields/GeoField.php000064400000001535152427751160015645 0ustar00setCommonOptions('GEO', $identifier, $alias, $sortable, $noIndex, $allowsMissing); } } Command/Argument/Search/SchemaFields/TextField.php000064400000003333152427751160016055 0ustar00setCommonOptions('TEXT', $identifier, $alias, $sortable, $noIndex, $allowsMissing); 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'; } if ($allowsEmpty) { $this->fieldArguments[] = 'INDEXEMPTY'; } } } Command/Argument/Search/SchemaFields/NumericField.php000064400000001545152427751160016536 0ustar00setCommonOptions('NUMERIC', $identifier, $alias, $sortable, $noIndex, $allowsMissing); } } Command/Argument/Search/SugGetArguments.php000064400000001543152427751160014723 0ustar00arguments[] = '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; } } Command/Argument/Search/CreateArguments.php000064400000010542152427751160014727 0ustar00 '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; } } Command/Argument/Search/CursorArguments.php000064400000001603152427751160014777 0ustar00arguments, 'COUNT', $readSize); return $this; } /** * {@inheritDoc} */ public function toArray(): array { return $this->arguments; } } Command/Argument/Search/CommonArguments.php000064400000007723152427751160014763 0ustar00arguments[] = '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; } } Command/Argument/Search/ProfileArguments.php000064400000002641152427751160015125 0ustar00arguments[] = '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; } } Command/Argument/Search/SpellcheckArguments.php000064400000003042152427751160015576 0ustar00 '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; } } Command/Argument/Search/DropArguments.php000064400000001444152427751160014431 0ustar00arguments[] = 'DD'; return $this; } /** * @return array */ public function toArray(): array { return $this->arguments; } } Command/Argument/Search/SynUpdateArguments.php000064400000000541152427751160015436 0ustar00 '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; } } Command/Argument/Search/SearchArguments.php000064400000020456152427751160014736 0ustar00 '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; } } Command/Argument/Search/SugAddArguments.php000064400000001016152427751160014667 0ustar00arguments[] = 'INCR'; return $this; } } Command/Argument/Search/ExplainArguments.php000064400000000537152427751160015127 0ustar00arguments, '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; } } Command/Argument/TimeSeries/CommonArguments.php000064400000007722152427751160015626 0ustar00arguments, 'RETENTION', $retentionPeriod); return $this; } /** * Ignore samples with given time or value difference. * * @param int $maxTimeDiff Non-negative integer value in milliseconds * @param float $maxValDiff Non-negative float value * @return $this */ public function ignore(int $maxTimeDiff, float $maxValDiff): self { if ($maxTimeDiff < 0 || $maxValDiff < 0) { throw new UnexpectedValueException('Ignore does not accept negative values'); } array_push($this->arguments, 'IGNORE', $maxTimeDiff, $maxValDiff); 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; } } Command/Argument/TimeSeries/IncrByArguments.php000064400000001645152427751160015562 0ustar00arguments, 'TIMESTAMP', $timeStamp); return $this; } /** * Changes data storage from compressed (default) to uncompressed. * * @return $this */ public function uncompressed(): self { $this->arguments[] = 'UNCOMPRESSED'; return $this; } } Command/Argument/TimeSeries/MGetArguments.php000064400000000540152427751160015221 0ustar00arguments, '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; } } Command/Argument/TimeSeries/DecrByArguments.php000064400000000542152427751160015537 0ustar00arguments, 'ON_DUPLICATE', $policy); return $this; } } Command/Argument/TimeSeries/InfoArguments.php000064400000001464152427751160015266 0ustar00arguments[] = 'DEBUG'; return $this; } /** * {@inheritDoc} */ public function toArray(): array { return $this->arguments; } } Command/Argument/Geospatial/ByRadius.php000064400000001317152427751160014243 0ustar00radius = $radius; $this->setUnit($unit); } /** * {@inheritDoc} */ public function toArray(): array { return [self::KEYWORD, $this->radius, $this->unit]; } } Command/Argument/Geospatial/AbstractBy.php000064400000001634152427751160014561 0ustar00unit = $unit; } } Command/Argument/Geospatial/FromLonLat.php000064400000001463152427751160014540 0ustar00longitude = $longitude; $this->latitude = $latitude; } /** * {@inheritDoc} */ public function toArray(): array { return [self::KEYWORD, $this->longitude, $this->latitude]; } } Command/Argument/Geospatial/ByInterface.php000064400000000624152427751160014714 0ustar00width = $width; $this->height = $height; $this->setUnit($unit); } /** * {@inheritDoc} */ public function toArray(): array { return [self::KEYWORD, $this->width, $this->height, $this->unit]; } } Command/Argument/Geospatial/FromMember.php000064400000001245152427751160014554 0ustar00member = $member; } /** * {@inheritDoc} */ public function toArray(): array { return [self::KEYWORD, $this->member]; } } Command/Argument/Server/LimitOffsetCount.php000064400000001413152427751160015132 0ustar00offset = $offset; $this->count = $count; } /** * {@inheritDoc} */ public function toArray(): array { return [self::KEYWORD, $this->offset, $this->count]; } } Command/Argument/Server/To.php000064400000002074152427751160012262 0ustar00host = $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; } } Command/Argument/Server/LimitInterface.php000064400000000623152427751160014575 0ustar00prefix = $prefix; $prefixFirst = static::class . '::first'; $prefixFirstTwo = static::class . '::firstTwo'; $prefixAll = static::class . '::all'; $prefixInterleaved = static::class . '::interleaved'; $prefixSkipFirst = static::class . '::skipFirst'; $prefixSkipLast = static::class . '::skipLast'; $prefixSort = static::class . '::sort'; $prefixEvalKeys = static::class . '::evalKeys'; $prefixZsetStore = static::class . '::zsetStore'; $prefixMigrate = static::class . '::migrate'; $prefixGeoradius = static::class . '::georadius'; $this->commands = [ /* ---------------- Redis 1.2 ---------------- */ 'EXISTS' => $prefixAll, 'DEL' => $prefixAll, 'TYPE' => $prefixFirst, 'KEYS' => $prefixFirst, 'RENAME' => $prefixAll, 'RENAMENX' => $prefixAll, 'EXPIRE' => $prefixFirst, 'EXPIREAT' => $prefixFirst, 'TTL' => $prefixFirst, 'MOVE' => $prefixFirst, 'SORT' => $prefixSort, 'DUMP' => $prefixFirst, 'RESTORE' => $prefixFirst, 'SET' => $prefixFirst, 'SETNX' => $prefixFirst, 'MSET' => $prefixInterleaved, 'MSETNX' => $prefixInterleaved, 'GET' => $prefixFirst, 'MGET' => $prefixAll, 'GETSET' => $prefixFirst, 'INCR' => $prefixFirst, 'INCRBY' => $prefixFirst, 'DECR' => $prefixFirst, 'DECRBY' => $prefixFirst, 'RPUSH' => $prefixFirst, 'LPUSH' => $prefixFirst, 'LLEN' => $prefixFirst, 'LRANGE' => $prefixFirst, 'LTRIM' => $prefixFirst, 'LINDEX' => $prefixFirst, 'LSET' => $prefixFirst, 'LREM' => $prefixFirst, 'LPOP' => $prefixFirst, 'RPOP' => $prefixFirst, 'RPOPLPUSH' => $prefixAll, 'SADD' => $prefixFirst, 'SREM' => $prefixFirst, 'SPOP' => $prefixFirst, 'SMOVE' => $prefixSkipLast, 'SCARD' => $prefixFirst, 'SISMEMBER' => $prefixFirst, 'SINTER' => $prefixAll, 'SINTERSTORE' => $prefixAll, 'SUNION' => $prefixAll, 'SUNIONSTORE' => $prefixAll, 'SDIFF' => $prefixAll, 'SDIFFSTORE' => $prefixAll, 'SMEMBERS' => $prefixFirst, 'SMISMEMBER' => $prefixFirst, 'SRANDMEMBER' => $prefixFirst, 'ZADD' => $prefixFirst, 'ZINCRBY' => $prefixFirst, 'ZREM' => $prefixFirst, 'ZRANGE' => $prefixFirst, 'ZREVRANGE' => $prefixFirst, 'ZRANGEBYSCORE' => $prefixFirst, 'ZCARD' => $prefixFirst, 'ZSCORE' => $prefixFirst, 'ZREMRANGEBYSCORE' => $prefixFirst, /* ---------------- Redis 2.0 ---------------- */ 'SETEX' => $prefixFirst, 'APPEND' => $prefixFirst, 'SUBSTR' => $prefixFirst, 'BLPOP' => $prefixSkipLast, 'BRPOP' => $prefixSkipLast, 'ZUNIONSTORE' => $prefixZsetStore, 'ZINTERSTORE' => $prefixZsetStore, 'ZCOUNT' => $prefixFirst, 'ZRANK' => $prefixFirst, 'ZREVRANK' => $prefixFirst, 'ZREMRANGEBYRANK' => $prefixFirst, 'HSET' => $prefixFirst, 'HSETNX' => $prefixFirst, 'HMSET' => $prefixFirst, 'HINCRBY' => $prefixFirst, 'HGET' => $prefixFirst, 'HMGET' => $prefixFirst, 'HDEL' => $prefixFirst, 'HEXISTS' => $prefixFirst, 'HLEN' => $prefixFirst, 'HKEYS' => $prefixFirst, 'HVALS' => $prefixFirst, 'HGETALL' => $prefixFirst, 'SUBSCRIBE' => $prefixAll, 'UNSUBSCRIBE' => $prefixAll, 'PSUBSCRIBE' => $prefixAll, 'PUNSUBSCRIBE' => $prefixAll, 'PUBLISH' => $prefixFirst, /* ---------------- Redis 2.2 ---------------- */ 'PERSIST' => $prefixFirst, 'STRLEN' => $prefixFirst, 'SETRANGE' => $prefixFirst, 'GETRANGE' => $prefixFirst, 'SETBIT' => $prefixFirst, 'GETBIT' => $prefixFirst, 'RPUSHX' => $prefixFirst, 'LPUSHX' => $prefixFirst, 'LINSERT' => $prefixFirst, 'BRPOPLPUSH' => $prefixSkipLast, 'ZREVRANGEBYSCORE' => $prefixFirst, 'WATCH' => $prefixAll, /* ---------------- Redis 2.6 ---------------- */ 'PTTL' => $prefixFirst, 'PEXPIRE' => $prefixFirst, 'PEXPIREAT' => $prefixFirst, 'PSETEX' => $prefixFirst, 'INCRBYFLOAT' => $prefixFirst, 'BITOP' => $prefixSkipFirst, 'BITCOUNT' => $prefixFirst, 'HINCRBYFLOAT' => $prefixFirst, 'EVAL' => $prefixEvalKeys, 'EVALSHA' => $prefixEvalKeys, 'MIGRATE' => $prefixMigrate, /* ---------------- Redis 2.8 ---------------- */ 'SSCAN' => $prefixFirst, 'ZSCAN' => $prefixFirst, 'HSCAN' => $prefixFirst, 'PFADD' => $prefixFirst, 'PFCOUNT' => $prefixAll, 'PFMERGE' => $prefixAll, 'ZLEXCOUNT' => $prefixFirst, 'ZRANGEBYLEX' => $prefixFirst, 'ZREMRANGEBYLEX' => $prefixFirst, 'ZREVRANGEBYLEX' => $prefixFirst, 'BITPOS' => $prefixFirst, /* ---------------- Redis 3.2 ---------------- */ 'HSTRLEN' => $prefixFirst, 'BITFIELD' => $prefixFirst, 'GEOADD' => $prefixFirst, 'GEOHASH' => $prefixFirst, 'GEOPOS' => $prefixFirst, 'GEODIST' => $prefixFirst, 'GEORADIUS' => $prefixGeoradius, 'GEORADIUSBYMEMBER' => $prefixGeoradius, /* ---------------- Redis 5.0 ---------------- */ 'XADD' => $prefixFirst, 'XRANGE' => $prefixFirst, 'XREVRANGE' => $prefixFirst, 'XDEL' => $prefixFirst, 'XLEN' => $prefixFirst, 'XACK' => $prefixFirst, 'XTRIM' => $prefixFirst, 'ZPOPMIN' => $prefixFirst, 'ZPOPMAX' => $prefixFirst, /* ---------------- Redis 6.2 ---------------- */ 'GETDEL' => $prefixFirst, 'ZMSCORE' => $prefixFirst, 'LMOVE' => $prefixFirstTwo, 'BLMOVE' => $prefixFirstTwo, 'GEOSEARCH' => $prefixFirst, /* ---------------- Redis 7.0 ---------------- */ 'EXPIRETIME' => $prefixFirst, /* RedisJSON */ 'JSON.ARRAPPEND' => $prefixFirst, 'JSON.ARRINDEX' => $prefixFirst, 'JSON.ARRINSERT' => $prefixFirst, 'JSON.ARRLEN' => $prefixFirst, 'JSON.ARRPOP' => $prefixFirst, 'JSON.ARRTRIM' => $prefixFirst, 'JSON.CLEAR' => $prefixFirst, 'JSON.DEBUG MEMORY' => $prefixFirst, 'JSON.DEL' => $prefixFirst, 'JSON.FORGET' => $prefixFirst, 'JSON.GET' => $prefixFirst, 'JSON.MGET' => $prefixAll, 'JSON.NUMINCRBY' => $prefixFirst, 'JSON.OBJKEYS' => $prefixFirst, 'JSON.OBJLEN' => $prefixFirst, 'JSON.RESP' => $prefixFirst, 'JSON.SET' => $prefixFirst, 'JSON.STRAPPEND' => $prefixFirst, 'JSON.STRLEN' => $prefixFirst, 'JSON.TOGGLE' => $prefixFirst, 'JSON.TYPE' => $prefixFirst, /* RedisBloom */ 'BF.ADD' => $prefixFirst, 'BF.EXISTS' => $prefixFirst, 'BF.INFO' => $prefixFirst, 'BF.INSERT' => $prefixFirst, 'BF.LOADCHUNK' => $prefixFirst, 'BF.MADD' => $prefixFirst, 'BF.MEXISTS' => $prefixFirst, 'BF.RESERVE' => $prefixFirst, 'BF.SCANDUMP' => $prefixFirst, 'CF.ADD' => $prefixFirst, 'CF.ADDNX' => $prefixFirst, 'CF.COUNT' => $prefixFirst, 'CF.DEL' => $prefixFirst, 'CF.EXISTS' => $prefixFirst, 'CF.INFO' => $prefixFirst, 'CF.INSERT' => $prefixFirst, 'CF.INSERTNX' => $prefixFirst, 'CF.LOADCHUNK' => $prefixFirst, 'CF.MEXISTS' => $prefixFirst, 'CF.RESERVE' => $prefixFirst, 'CF.SCANDUMP' => $prefixFirst, 'CMS.INCRBY' => $prefixFirst, 'CMS.INFO' => $prefixFirst, 'CMS.INITBYDIM' => $prefixFirst, 'CMS.INITBYPROB' => $prefixFirst, 'CMS.QUERY' => $prefixFirst, 'TDIGEST.ADD' => $prefixFirst, 'TDIGEST.BYRANK' => $prefixFirst, 'TDIGEST.BYREVRANK' => $prefixFirst, 'TDIGEST.CDF' => $prefixFirst, 'TDIGEST.CREATE' => $prefixFirst, 'TDIGEST.INFO' => $prefixFirst, 'TDIGEST.MAX' => $prefixFirst, 'TDIGEST.MIN' => $prefixFirst, 'TDIGEST.QUANTILE' => $prefixFirst, 'TDIGEST.RANK' => $prefixFirst, 'TDIGEST.RESET' => $prefixFirst, 'TDIGEST.REVRANK' => $prefixFirst, 'TDIGEST.TRIMMED_MEAN' => $prefixFirst, 'TOPK.ADD' => $prefixFirst, 'TOPK.INCRBY' => $prefixFirst, 'TOPK.INFO' => $prefixFirst, 'TOPK.LIST' => $prefixFirst, 'TOPK.QUERY' => $prefixFirst, 'TOPK.RESERVE' => $prefixFirst, /* RediSearch */ 'FT.AGGREGATE' => $prefixFirst, 'FT.ALTER' => $prefixFirst, 'FT.CREATE' => $prefixFirst, 'FT.CURSOR DEL' => $prefixFirst, 'FT.CURSOR READ' => $prefixFirst, 'FT.DROPINDEX' => $prefixFirst, 'FT.EXPLAIN' => $prefixFirst, 'FT.INFO' => $prefixFirst, 'FT.PROFILE' => $prefixFirst, 'FT.SEARCH' => $prefixFirst, 'FT.SPELLCHECK' => $prefixFirst, 'FT.SYNDUMP' => $prefixFirst, 'FT.SYNUPDATE' => $prefixFirst, 'FT.TAGVALS' => $prefixFirst, /* Redis TimeSeries */ 'TS.ADD' => $prefixFirst, 'TS.ALTER' => $prefixFirst, 'TS.CREATE' => $prefixFirst, 'TS.DECRBY' => $prefixFirst, 'TS.DEL' => $prefixFirst, 'TS.GET' => $prefixFirst, 'TS.INCRBY' => $prefixFirst, 'TS.INFO' => $prefixFirst, 'TS.MGET' => $prefixFirst, 'TS.MRANGE' => $prefixFirst, 'TS.MREVRANGE' => $prefixFirst, 'TS.QUERYINDEX' => $prefixFirst, 'TS.RANGE' => $prefixFirst, 'TS.REVRANGE' => $prefixFirst, ]; } /** * Sets a prefix that is applied to all the keys. * * @param string $prefix Prefix for the keys. */ public function setPrefix($prefix) { $this->prefix = $prefix; } /** * Gets the current prefix. * * @return string */ public function getPrefix() { return $this->prefix; } /** * {@inheritdoc} */ public function process(CommandInterface $command) { if ($command instanceof PrefixableCommandInterface) { $command->prefixKeys($this->prefix); } elseif (isset($this->commands[$commandID = strtoupper($command->getId())])) { $this->commands[$commandID]($command, $this->prefix); } } /** * Sets an handler for the specified command ID. * * The callback signature must have 2 parameters of the following types: * * - Predis\Command\CommandInterface (command instance) * - String (prefix) * * When the callback argument is omitted or NULL, the previously * associated handler for the specified command ID is removed. * * @param string $commandID The ID of the command to be handled. * @param mixed $callback A valid callable object or NULL. * * @throws InvalidArgumentException */ public function setCommandHandler($commandID, $callback = null) { $commandID = strtoupper($commandID); if (!isset($callback)) { unset($this->commands[$commandID]); return; } if (!is_callable($callback)) { throw new InvalidArgumentException( 'Callback must be a valid callable object or NULL' ); } $this->commands[$commandID] = $callback; } /** * {@inheritdoc} */ public function __toString() { return $this->getPrefix(); } /** * Applies the specified prefix only the first argument. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function first(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $arguments[0] = "$prefix{$arguments[0]}"; $command->setRawArguments($arguments); } } /** * Applies the specified prefix only to the first two arguments. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function firstTwo(CommandInterface $command, $prefix) { $arguments = $command->getArguments(); $length = min(count($arguments), 2); for ($i = 0; $i < $length; $i++) { $arguments[$i] = "$prefix{$arguments[$i]}"; } $command->setRawArguments($arguments); } /** * Applies the specified prefix to all the arguments. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function all(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { foreach ($arguments as &$key) { $key = "$prefix$key"; } $command->setRawArguments($arguments); } } /** * Applies the specified prefix only to even arguments in the list. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function interleaved(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $length = count($arguments); for ($i = 0; $i < $length; $i += 2) { $arguments[$i] = "$prefix{$arguments[$i]}"; } $command->setRawArguments($arguments); } } /** * Applies the specified prefix to all the arguments but the first one. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function skipFirst(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $length = count($arguments); for ($i = 1; $i < $length; ++$i) { $arguments[$i] = "$prefix{$arguments[$i]}"; } $command->setRawArguments($arguments); } } /** * Applies the specified prefix to all the arguments but the last one. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function skipLast(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $length = count($arguments); for ($i = 0; $i < $length - 1; ++$i) { $arguments[$i] = "$prefix{$arguments[$i]}"; } $command->setRawArguments($arguments); } } /** * Applies the specified prefix to the keys of a SORT command. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function sort(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $arguments[0] = "$prefix{$arguments[0]}"; if (($count = count($arguments)) > 1) { for ($i = 1; $i < $count; ++$i) { switch (strtoupper($arguments[$i])) { case 'BY': case 'STORE': $arguments[$i] = "$prefix{$arguments[++$i]}"; break; case 'GET': $value = $arguments[++$i]; if ($value !== '#') { $arguments[$i] = "$prefix$value"; } break; case 'LIMIT': $i += 2; break; } } } $command->setRawArguments($arguments); } } /** * Applies the specified prefix to the keys of an EVAL-based command. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function evalKeys(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { for ($i = 2; $i < $arguments[1] + 2; ++$i) { $arguments[$i] = "$prefix{$arguments[$i]}"; } $command->setRawArguments($arguments); } } /** * Applies the specified prefix to the keys of Z[INTERSECTION|UNION]STORE. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function zsetStore(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $arguments[0] = "$prefix{$arguments[0]}"; $length = ((int) $arguments[1]) + 2; for ($i = 2; $i < $length; ++$i) { $arguments[$i] = "$prefix{$arguments[$i]}"; } $command->setRawArguments($arguments); } } /** * Applies the specified prefix to the key of a MIGRATE command. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function migrate(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $arguments[2] = "$prefix{$arguments[2]}"; $command->setRawArguments($arguments); } } /** * Applies the specified prefix to the key of a GEORADIUS command. * * @param CommandInterface $command Command instance. * @param string $prefix Prefix string. */ public static function georadius(CommandInterface $command, $prefix) { if ($arguments = $command->getArguments()) { $arguments[0] = "$prefix{$arguments[0]}"; $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; if (($count = count($arguments)) > $startIndex) { for ($i = $startIndex; $i < $count; ++$i) { switch (strtoupper($arguments[$i])) { case 'STORE': case 'STOREDIST': $arguments[$i] = "$prefix{$arguments[++$i]}"; break; } } } $command->setRawArguments($arguments); } } } Command/Processor/ProcessorChain.php000064400000006151152427751160013551 0ustar00add($processor); } } /** * {@inheritdoc} */ public function add(ProcessorInterface $processor) { $this->processors[] = $processor; } /** * {@inheritdoc} */ public function remove(ProcessorInterface $processor) { if (false !== $index = array_search($processor, $this->processors, true)) { unset($this[$index]); } } /** * {@inheritdoc} */ public function process(CommandInterface $command) { for ($i = 0; $i < $count = count($this->processors); ++$i) { $this->processors[$i]->process($command); } } /** * {@inheritdoc} */ public function getProcessors() { return $this->processors; } /** * Returns an iterator over the list of command processor in the chain. * * @return Traversable */ public function getIterator() { return new ArrayIterator($this->processors); } /** * Returns the number of command processors in the chain. * * @return int */ public function count() { return count($this->processors); } /** * @param int $index * @return bool */ #[ReturnTypeWillChange] public function offsetExists($index) { return isset($this->processors[$index]); } /** * @param int $index * @return ProcessorInterface */ #[ReturnTypeWillChange] public function offsetGet($index) { return $this->processors[$index]; } /** * @param int $index * @param ProcessorInterface $processor * @return void */ #[ReturnTypeWillChange] public function offsetSet($index, $processor) { if (!$processor instanceof ProcessorInterface) { throw new InvalidArgumentException( 'Processor chain accepts only instances of `Predis\Command\Processor\ProcessorInterface`' ); } $this->processors[$index] = $processor; } /** * @param int $index * @return void */ #[ReturnTypeWillChange] public function offsetUnset($index) { unset($this->processors[$index]); $this->processors = array_values($this->processors); } } Command/ScriptCommand.php000064400000004610152427751160011411 0ustar00getScript()); } /** * Specifies the number of arguments that should be considered as keys. * * The default behaviour for the base class is to return 0 to indicate that * all the elements of the arguments array should be considered as keys, but * subclasses can enforce a static number of keys. * * @return int */ protected function getKeysCount() { return 0; } /** * Returns the elements from the arguments that are identified as keys. * * @return array */ public function getKeys() { return array_slice($this->getArguments(), 2, $this->getKeysCount()); } /** * {@inheritdoc} */ public function setArguments(array $arguments) { if (($numkeys = $this->getKeysCount()) && $numkeys < 0) { $numkeys = count($arguments) + $numkeys; } $arguments = array_merge([$this->getScriptHash(), (int) $numkeys], $arguments); parent::setArguments($arguments); } /** * Returns arguments for EVAL command. * * @return array */ public function getEvalArguments() { $arguments = $this->getArguments(); $arguments[0] = $this->getScript(); return $arguments; } /** * Returns the equivalent EVAL command as a raw command instance. * * @return RawCommand */ public function getEvalCommand() { return new RawCommand('EVAL', $this->getEvalArguments()); } } Command/PrefixableCommandInterface.php000064400000001130152427751160014041 0ustar00getCommandClass($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; } } Command/Command.php000064400000005006152427751160010224 0ustar00arguments = $arguments; unset($this->slot); } /** * {@inheritdoc} */ public function setRawArguments(array $arguments) { $this->arguments = $arguments; unset($this->slot); } /** * {@inheritdoc} */ public function getArguments() { return $this->arguments; } /** * {@inheritdoc} */ public function getArgument($index) { if (isset($this->arguments[$index])) { return $this->arguments[$index]; } } /** * {@inheritdoc} */ public function setSlot($slot) { $this->slot = $slot; } /** * {@inheritdoc} */ public function getSlot() { return $this->slot ?? null; } /** * {@inheritdoc} */ public function parseResponse($data) { return $data; } /** * Normalizes the arguments array passed to a Redis command. * * @param array $arguments Arguments for a command. * * @return array */ public static function normalizeArguments(array $arguments) { if (count($arguments) === 1 && isset($arguments[0]) && is_array($arguments[0])) { return $arguments[0]; } return $arguments; } /** * Normalizes the arguments array passed to a variadic Redis command. * * @param array $arguments Arguments for a command. * * @return array */ public static function normalizeVariadic(array $arguments) { if (count($arguments) === 2 && is_array($arguments[1])) { return array_merge([$arguments[0]], $arguments[1]); } return $arguments; } /** * Remove all false values from arguments. * * @return void */ public function filterArguments(): void { $this->arguments = array_filter($this->arguments, static function ($argument) { return $argument !== false && $argument !== null; }); } } Command/CommandInterface.php000064400000003451152427751160012047 0ustar00commandID = strtoupper($commandID); $this->setArguments($arguments); } /** * Creates a new raw command using a variadic method. * * @param string $commandID Redis command ID * @param string ...$args Arguments list for the command * * @return CommandInterface */ public static function create($commandID, ...$args) { $arguments = func_get_args(); return new static(array_shift($arguments), $arguments); } /** * {@inheritdoc} */ public function getId() { return $this->commandID; } /** * {@inheritdoc} */ public function setArguments(array $arguments) { $this->arguments = $arguments; unset($this->slot); } /** * {@inheritdoc} */ public function setRawArguments(array $arguments) { $this->setArguments($arguments); } /** * {@inheritdoc} */ public function getArguments() { return $this->arguments; } /** * {@inheritdoc} */ public function getArgument($index) { if (isset($this->arguments[$index])) { return $this->arguments[$index]; } } /** * {@inheritdoc} */ public function setSlot($slot) { $this->slot = $slot; } /** * {@inheritdoc} */ public function getSlot() { return $this->slot ?? null; } /** * {@inheritdoc} */ public function parseResponse($data) { return $data; } } Command/RedisFactory.php000064400000006500152427751160011244 0ustar00commands = [ '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; } } Command/RawFactory.php000064400000002124152427751160010725 0ustar00getHashGeneratorByDescription($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(); } } Configuration/Option/Cluster.php000064400000005522152427751160012773 0ustar00getConnectionInitializerByString($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() ); } } Configuration/Option/Prefix.php000064400000002170152427751160012603 0ustar00createFactoryByArray($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; } } Configuration/Option/Connections.php000064400000011715152427751160013635 0ustar00createFactoryByArray($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; } } Configuration/Option/Replication.php000064400000007536152427751160013632 0ustar00getConnectionInitializerByString($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() ); } } Configuration/Option/Aggregate.php000064400000010020152427751160013225 0ustar00getConnectionInitializer($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; } } Configuration/Option/Exceptions.php000064400000001601152427751160013465 0ustar00 Option\Aggregate::class, 'cluster' => Option\Cluster::class, 'replication' => Option\Replication::class, 'connections' => Option\Connections::class, 'commands' => Option\Commands::class, 'exceptions' => Option\Exceptions::class, 'prefix' => Option\Prefix::class, 'crc16' => Option\CRC16::class, ]; /** @var array */ protected $options = []; /** @var array */ protected $input; /** * @param array|null $options Named array of client options */ public function __construct(?array $options = null) { $this->input = $options ?? []; } /** * {@inheritdoc} */ public function getDefault($option) { if (isset($this->handlers[$option])) { $handler = $this->handlers[$option]; $handler = new $handler(); return $handler->getDefault($this); } } /** * {@inheritdoc} */ public function defined($option) { return array_key_exists($option, $this->options) || array_key_exists($option, $this->input) ; } /** * {@inheritdoc} */ public function __isset($option) { return ( array_key_exists($option, $this->options) || array_key_exists($option, $this->input) ) && $this->__get($option) !== null; } /** * {@inheritdoc} */ public function __get($option) { if (isset($this->options[$option]) || array_key_exists($option, $this->options)) { return $this->options[$option]; } if (isset($this->input[$option]) || array_key_exists($option, $this->input)) { $value = $this->input[$option]; unset($this->input[$option]); if (isset($this->handlers[$option])) { $handler = $this->handlers[$option]; $handler = new $handler(); $value = $handler->filter($this, $value); } elseif (is_object($value) && method_exists($value, '__invoke')) { $value = $value($this); } return $this->options[$option] = $value; } if (isset($this->handlers[$option])) { return $this->options[$option] = $this->getDefault($option); } return; } } Response/Iterator/MultiBulk.php000064400000003734152427751160012575 0ustar00connection = $connection; $this->size = $size; $this->position = 0; $this->current = $size > 0 ? $this->getValue() : null; } /** * Handles the synchronization of the client with the Redis protocol when * the garbage collector kicks in (e.g. when the iterator goes out of the * scope of a foreach or it is unset). */ public function __destruct() { $this->drop(true); } /** * Drop queued elements that have not been read from the connection either * by consuming the rest of the multibulk response or quickly by closing the * underlying connection. * * @param bool $disconnect Consume the iterator or drop the connection. */ public function drop($disconnect = false) { if ($disconnect) { if ($this->valid()) { $this->position = $this->size; $this->connection->disconnect(); } } else { while ($this->valid()) { $this->next(); } } } /** * Reads the next item of the multibulk response from the connection. * * @return mixed */ protected function getValue() { return $this->connection->read(); } } Response/Iterator/MultiBulkTuple.php000064400000004413152427751160013602 0ustar00 $value pairs. */ class MultiBulkTuple extends MultiBulk implements OuterIterator { private $iterator; /** * @param MultiBulk $iterator Inner multibulk response iterator. */ public function __construct(MultiBulk $iterator) { $this->checkPreconditions($iterator); $this->size = count($iterator) / 2; $this->iterator = $iterator; $this->position = $iterator->getPosition(); $this->current = $this->size > 0 ? $this->getValue() : null; } /** * Checks for valid preconditions. * * @param MultiBulk $iterator Inner multibulk response iterator. * * @throws InvalidArgumentException * @throws UnexpectedValueException */ protected function checkPreconditions(MultiBulk $iterator) { if ($iterator->getPosition() !== 0) { throw new InvalidArgumentException( 'Cannot initialize a tuple iterator using an already initiated iterator.' ); } if (($size = count($iterator)) % 2 !== 0) { throw new UnexpectedValueException('Invalid response size for a tuple iterator.'); } } /** * @return MultiBulk */ #[ReturnTypeWillChange] public function getInnerIterator() { return $this->iterator; } /** * {@inheritdoc} */ public function __destruct() { $this->iterator->drop(true); } /** * {@inheritdoc} */ protected function getValue() { $k = $this->iterator->current(); $this->iterator->next(); $v = $this->iterator->current(); $this->iterator->next(); return [$k, $v]; } } Response/Iterator/MultiBulkIterator.php000064400000004605152427751160014305 0ustar00current; } /** * @return int|null */ #[ReturnTypeWillChange] public function key() { return $this->position; } /** * @return void */ #[ReturnTypeWillChange] public function next() { if (++$this->position < $this->size) { $this->current = $this->getValue(); } } /** * @return bool */ #[ReturnTypeWillChange] public function valid() { return $this->position < $this->size; } /** * Returns the number of items comprising the whole multibulk response. * * This method should be used instead of iterator_count() to get the size of * the current multibulk response since the former consumes the iteration to * count the number of elements, but our iterators do not support rewinding. * * @return int */ #[ReturnTypeWillChange] public function count() { return $this->size; } /** * Returns the current position of the iterator. * * @return int */ public function getPosition() { return $this->position; } /** * {@inheritdoc} */ abstract protected function getValue(); } Response/ServerException.php000064400000001612152427751160012212 0ustar00getMessage(), 2); return $errorType; } /** * Converts the exception to an instance of Predis\Response\Error. * * @return Error */ public function toErrorResponse() { return new Error($this->getMessage()); } } Response/ResponseInterface.php000064400000000571152427751160012507 0ustar00payload = $payload; } /** * Converts the response object to its string representation. * * @return string */ public function __toString() { return $this->payload; } /** * Returns the payload of status response. * * @return string */ public function getPayload() { return $this->payload; } /** * Returns an instance of a status response object. * * Common status responses such as OK or QUEUED are cached in order to lower * the global memory usage especially when using pipelines. * * @param string $payload Status response payload. * * @return self */ public static function get($payload) { switch ($payload) { case 'OK': case 'QUEUED': if (isset(self::$$payload)) { return self::$$payload; } return self::$$payload = new self($payload); default: return new self($payload); } } } Response/Error.php000064400000002123152427751160010154 0ustar00message = $message; } /** * {@inheritdoc} */ public function getMessage() { return $this->message; } /** * {@inheritdoc} */ public function getErrorType() { [$errorType] = explode(' ', $this->getMessage(), 2); return $errorType; } /** * Converts the object to its string representation. * * @return string */ public function __toString() { return $this->getMessage(); } } Response/ErrorInterface.php000064400000001351152427751160011777 0ustar00client = $client; if (isset($options['gc_maxlifetime'])) { $this->ttl = (int) $options['gc_maxlifetime']; } else { $this->ttl = ini_get('session.gc_maxlifetime'); } } /** * Registers this instance as the current session handler. */ public function register() { session_set_save_handler($this, true); } /** * @param string $save_path * @param string $session_id * @return bool */ #[ReturnTypeWillChange] public function open($save_path, $session_id) { // NOOP return true; } /** * @return bool */ #[ReturnTypeWillChange] public function close() { // NOOP return true; } /** * @param int $maxlifetime * @return bool */ #[ReturnTypeWillChange] public function gc($maxlifetime) { // NOOP return true; } /** * @param string $session_id * @return string */ #[ReturnTypeWillChange] public function read($session_id) { if ($data = $this->client->get($session_id)) { return $data; } return ''; } /** * @param string $session_id * @param string $session_data * @return bool */ #[ReturnTypeWillChange] public function write($session_id, $session_data) { $this->client->setex($session_id, $this->ttl, $session_data); return true; } /** * @param string $session_id * @return bool */ #[ReturnTypeWillChange] public function destroy($session_id) { $this->client->del($session_id); return true; } /** * Returns the underlying client instance. * * @return ClientInterface */ public function getClient() { return $this->client; } /** * Returns the session max lifetime value. * * @return int */ public function getMaxLifeTime() { return $this->ttl; } } Replication/RoleException.php000064400000000754152427751160012326 0ustar00disallowed = $this->getDisallowedOperations(); $this->readonly = $this->getReadOnlyOperations(); $this->readonlySHA1 = []; } /** * Returns if the specified command will perform a read-only operation * on Redis or not. * * @param CommandInterface $command Command instance. * * @return bool * @throws NotSupportedException */ public function isReadOperation(CommandInterface $command) { if (!$this->loadBalancing) { return false; } if (isset($this->disallowed[$id = $command->getId()])) { throw new NotSupportedException( "The command '$id' is not allowed in replication mode." ); } if (isset($this->readonly[$id])) { if (true === $readonly = $this->readonly[$id]) { return true; } return call_user_func($readonly, $command); } if (($eval = $id === 'EVAL') || $id === 'EVALSHA') { $argument = $command->getArgument(0); $sha1 = $eval ? sha1(strval($argument)) : $argument; if (isset($this->readonlySHA1[$sha1])) { if (true === $readonly = $this->readonlySHA1[$sha1]) { return true; } return call_user_func($readonly, $command); } } return false; } /** * Returns if the specified command is not allowed for execution in a master * / slave replication context. * * @param CommandInterface $command Command instance. * * @return bool */ public function isDisallowedOperation(CommandInterface $command) { return isset($this->disallowed[$command->getId()]); } /** * Checks if BITFIELD performs a read-only operation by looking for certain * SET and INCRYBY modifiers in the arguments array of the command. * * @param CommandInterface $command Command instance. * * @return bool */ protected function isBitfieldReadOnly(CommandInterface $command) { $arguments = $command->getArguments(); $argc = count($arguments); if ($argc >= 2) { for ($i = 1; $i < $argc; ++$i) { $argument = strtoupper($arguments[$i]); if ($argument === 'SET' || $argument === 'INCRBY') { return false; } } } return true; } /** * Checks if a GEORADIUS command is a readable operation by parsing the * arguments array of the specified command instance. * * @param CommandInterface $command Command instance. * * @return bool */ protected function isGeoradiusReadOnly(CommandInterface $command) { $arguments = $command->getArguments(); $argc = count($arguments); $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; if ($argc > $startIndex) { for ($i = $startIndex; $i < $argc; ++$i) { $argument = strtoupper($arguments[$i]); if ($argument === 'STORE' || $argument === 'STOREDIST') { return false; } } } return true; } /** * Marks a command as a read-only operation. * * When the behavior of a command can be decided only at runtime depending * on its arguments, a callable object can be provided to dynamically check * if the specified command performs a read or a write operation. * * @param string $commandID Command ID. * @param mixed $readonly A boolean value or a callable object. */ public function setCommandReadOnly($commandID, $readonly = true) { $commandID = strtoupper($commandID); if ($readonly) { $this->readonly[$commandID] = $readonly; } else { unset($this->readonly[$commandID]); } } /** * Marks a Lua script for EVAL and EVALSHA as a read-only operation. When * the behaviour of a script can be decided only at runtime depending on * its arguments, a callable object can be provided to dynamically check * if the passed instance of EVAL or EVALSHA performs write operations or * not. * * @param string $script Body of the Lua script. * @param mixed $readonly A boolean value or a callable object. */ public function setScriptReadOnly($script, $readonly = true) { $sha1 = sha1($script); if ($readonly) { $this->readonlySHA1[$sha1] = $readonly; } else { unset($this->readonlySHA1[$sha1]); } } /** * Returns the default list of disallowed commands. * * @return array */ protected function getDisallowedOperations() { return [ 'SHUTDOWN' => true, 'INFO' => true, 'DBSIZE' => true, 'LASTSAVE' => true, 'CONFIG' => true, 'MONITOR' => true, 'SLAVEOF' => true, 'SAVE' => true, 'BGSAVE' => true, 'BGREWRITEAOF' => true, 'SLOWLOG' => true, ]; } /** * Returns the default list of commands performing read-only operations. * * @return array */ protected function getReadOnlyOperations() { return [ 'EXISTS' => true, 'TYPE' => true, 'KEYS' => true, 'SCAN' => true, 'RANDOMKEY' => true, 'TTL' => true, 'GET' => true, 'MGET' => true, 'SUBSTR' => true, 'STRLEN' => true, 'GETRANGE' => true, 'GETBIT' => true, 'LLEN' => true, 'LRANGE' => true, 'LINDEX' => true, 'SCARD' => true, 'SISMEMBER' => true, 'SINTER' => true, 'SUNION' => true, 'SDIFF' => true, 'SMEMBERS' => true, 'SSCAN' => true, 'SRANDMEMBER' => true, 'ZRANGE' => true, 'ZREVRANGE' => true, 'ZRANGEBYSCORE' => true, 'ZREVRANGEBYSCORE' => true, 'ZCARD' => true, 'ZSCORE' => true, 'ZCOUNT' => true, 'ZRANK' => true, 'ZREVRANK' => true, 'ZSCAN' => true, 'ZLEXCOUNT' => true, 'ZRANGEBYLEX' => true, 'ZREVRANGEBYLEX' => true, 'HGET' => true, 'HMGET' => true, 'HEXISTS' => true, 'HLEN' => true, 'HKEYS' => true, 'HVALS' => true, 'HGETALL' => true, 'HSCAN' => true, 'HSTRLEN' => true, 'PING' => true, 'AUTH' => true, 'SELECT' => true, 'ECHO' => true, 'QUIT' => true, 'OBJECT' => true, 'BITCOUNT' => true, 'BITPOS' => true, 'TIME' => true, 'PFCOUNT' => true, 'BITFIELD' => [$this, 'isBitfieldReadOnly'], 'GEOHASH' => true, 'GEOPOS' => true, 'GEODIST' => true, 'GEORADIUS' => [$this, 'isGeoradiusReadOnly'], 'GEORADIUSBYMEMBER' => [$this, 'isGeoradiusReadOnly'], ]; } /** * Disables reads to slaves when using * a replication topology. * * @return self */ public function disableLoadBalancing(): self { $this->loadBalancing = false; return $this; } } Replication/MissingMasterException.php000064400000000720152427751160014203 0ustar00getParameters()}]" )); } if ($length === -1) { return; } $list = []; if ($length > 0) { $handlersCache = []; $reader = $connection->getProtocol()->getResponseReader(); for ($i = 0; $i < $length; ++$i) { $header = $connection->readLine(); $prefix = $header[0]; if (isset($handlersCache[$prefix])) { $handler = $handlersCache[$prefix]; } else { $handler = $reader->getHandler($prefix); $handlersCache[$prefix] = $handler; } $list[$i] = $handler->handle($connection, substr($header, 1)); } } return $list; } } Protocol/Text/Handler/IntegerResponse.php000064400000002262152427751160014507 0ustar00getParameters()}]" )); } return; } } Protocol/Text/Handler/StreamableMultiBulkResponse.php000064400000002612152427751160017021 0ustar00getParameters()}]" )); } return new MultiBulkIterator($connection, $length); } } Protocol/Text/Handler/BulkResponse.php000064400000002667152427751160014020 0ustar00getParameters()}]" )); } if ($length >= 0) { return substr($connection->readBuffer($length + 2), 0, -2); } if ($length == -1) { return; } CommunicationException::handle(new ProtocolException( $connection, "Value '$payload' is not a valid length for a bulk response [{$connection->getParameters()}]" )); return; } } Protocol/Text/Handler/ResponseHandlerInterface.php000064400000001530152427751160016305 0ustar00handlers = $this->getDefaultHandlers(); } /** * Returns the default handlers for the supported type of responses. * * @return array */ protected function getDefaultHandlers() { return [ '+' => new Handler\StatusResponse(), '-' => new Handler\ErrorResponse(), ':' => new Handler\IntegerResponse(), '$' => new Handler\BulkResponse(), '*' => new Handler\MultiBulkResponse(), ]; } /** * Sets the handler for the specified prefix identifying the response type. * * @param string $prefix Identifier of the type of response. * @param Handler\ResponseHandlerInterface $handler Response handler. */ public function setHandler($prefix, Handler\ResponseHandlerInterface $handler) { $this->handlers[$prefix] = $handler; } /** * Returns the response handler associated to a certain type of response. * * @param string $prefix Identifier of the type of response. * * @return Handler\ResponseHandlerInterface */ public function getHandler($prefix) { if (isset($this->handlers[$prefix])) { return $this->handlers[$prefix]; } return; } /** * {@inheritdoc} */ public function read(CompositeConnectionInterface $connection) { $header = $connection->readLine(); if ($header === '') { $this->onProtocolError($connection, 'Unexpected empty response header'); } $prefix = $header[0]; if (!isset($this->handlers[$prefix])) { $this->onProtocolError($connection, "Unknown response prefix: '$prefix'"); } return $this->handlers[$prefix]->handle($connection, substr($header, 1)); } /** * Handles protocol errors generated while reading responses from a * connection. * * @param CompositeConnectionInterface $connection Redis connection that generated the error. * @param string $message Error message. */ protected function onProtocolError(CompositeConnectionInterface $connection, $message) { CommunicationException::handle( new ProtocolException($connection, "$message [{$connection->getParameters()}]") ); } } Protocol/Text/RequestSerializer.php000064400000002060152427751160013474 0ustar00getId(); $arguments = $command->getArguments(); $cmdlen = strlen($commandID); $reqlen = count($arguments) + 1; $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n"; foreach ($arguments as $argument) { $arglen = strlen($argument); $buffer .= "\${$arglen}\r\n{$argument}\r\n"; } return $buffer; } } Protocol/Text/CompositeProtocolProcessor.php000064400000005361152427751160015405 0ustar00setRequestSerializer($serializer ?: new RequestSerializer()); $this->setResponseReader($reader ?: new ResponseReader()); } /** * {@inheritdoc} */ public function write(CompositeConnectionInterface $connection, CommandInterface $command) { $connection->writeBuffer($this->serializer->serialize($command)); } /** * {@inheritdoc} */ public function read(CompositeConnectionInterface $connection) { return $this->reader->read($connection); } /** * Sets the request serializer used by the protocol processor. * * @param RequestSerializerInterface $serializer Request serializer. */ public function setRequestSerializer(RequestSerializerInterface $serializer) { $this->serializer = $serializer; } /** * Returns the request serializer used by the protocol processor. * * @return RequestSerializerInterface */ public function getRequestSerializer() { return $this->serializer; } /** * Sets the response reader used by the protocol processor. * * @param ResponseReaderInterface $reader Response reader. */ public function setResponseReader(ResponseReaderInterface $reader) { $this->reader = $reader; } /** * Returns the Response reader used by the protocol processor. * * @return ResponseReaderInterface */ public function getResponseReader() { return $this->reader; } } Protocol/Text/ProtocolProcessor.php000064400000006400152427751160013515 0ustar00mbiterable = false; $this->serializer = new RequestSerializer(); } /** * {@inheritdoc} */ public function write(CompositeConnectionInterface $connection, CommandInterface $command) { $request = $this->serializer->serialize($command); $connection->writeBuffer($request); } /** * {@inheritdoc} */ public function read(CompositeConnectionInterface $connection) { $chunk = $connection->readLine(); $prefix = $chunk[0]; $payload = substr($chunk, 1); switch ($prefix) { case '+': return new StatusResponse($payload); case '$': $size = (int) $payload; if ($size === -1) { return; } return substr($connection->readBuffer($size + 2), 0, -2); case '*': $count = (int) $payload; if ($count === -1) { return; } if ($this->mbiterable) { return new MultiBulkIterator($connection, $count); } $multibulk = []; for ($i = 0; $i < $count; ++$i) { $multibulk[$i] = $this->read($connection); } return $multibulk; case ':': $integer = (int) $payload; return $integer == $payload ? $integer : $payload; case '-': return new ErrorResponse($payload); default: CommunicationException::handle(new ProtocolException( $connection, "Unknown response prefix: '$prefix' [{$connection->getParameters()}]" )); return; } } /** * Enables or disables returning multibulk responses as specialized PHP * iterators used to stream bulk elements of a multibulk response instead * returning a plain array. * * Streamable multibulk responses are not globally supported by the * abstractions built-in into Predis, such as transactions or pipelines. * Use them with care! * * @param bool $value Enable or disable streamable multibulk responses. */ public function useIterableMultibulk($value) { $this->mbiterable = (bool) $value; } } Protocol/RequestSerializerInterface.php000064400000001162152427751160014373 0ustar00getCommandFactory()->supports('multi', 'exec', 'discard')) { throw new ClientException( "'MULTI', 'EXEC' and 'DISCARD' are not supported by the current command factory." ); } parent::__construct($client); } /** * {@inheritdoc} */ protected function getConnection() { $connection = $this->getClient()->getConnection(); if (!$connection instanceof NodeConnectionInterface) { $class = __CLASS__; throw new ClientException("The class '$class' does not support aggregate connections."); } return $connection; } /** * {@inheritdoc} */ protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) { $commandFactory = $this->getClient()->getCommandFactory(); $connection->executeCommand($commandFactory->create('multi')); foreach ($commands as $command) { $connection->writeRequest($command); } foreach ($commands as $command) { $response = $connection->readResponse($command); if ($response instanceof ErrorResponseInterface) { $connection->executeCommand($commandFactory->create('discard')); throw new ServerException($response->getMessage()); } } $executed = $connection->executeCommand($commandFactory->create('exec')); if (!isset($executed)) { throw new ClientException( 'The underlying transaction has been aborted by the server.' ); } if (count($executed) !== count($commands)) { $expected = count($commands); $received = count($executed); throw new ClientException( "Invalid number of responses [expected $expected, received $received]." ); } $responses = []; $sizeOfPipe = count($commands); $exceptions = $this->throwServerExceptions(); for ($i = 0; $i < $sizeOfPipe; ++$i) { $command = $commands->dequeue(); $response = $executed[$i]; if (!$response instanceof ResponseInterface) { $responses[] = $command->parseResponse($response); } elseif ($response instanceof ErrorResponseInterface && $exceptions) { $this->exception($connection, $response); } else { $responses[] = $response; } unset($executed[$i]); } return $responses; } } Pipeline/Pipeline.php000064400000014754152427751160010614 0ustar00client = $client; $this->pipeline = new SplQueue(); } /** * Queues a command into the pipeline buffer. * * @param string $method Command ID. * @param array $arguments Arguments for the command. * * @return $this */ public function __call($method, $arguments) { $command = $this->client->createCommand($method, $arguments); $this->recordCommand($command); return $this; } /** * Queues a command instance into the pipeline buffer. * * @param CommandInterface $command Command to be queued in the buffer. */ protected function recordCommand(CommandInterface $command) { $this->pipeline->enqueue($command); } /** * Queues a command instance into the pipeline buffer. * * @param CommandInterface $command Command instance to be queued in the buffer. * * @return $this */ public function executeCommand(CommandInterface $command) { $this->recordCommand($command); return $this; } /** * Throws an exception on -ERR responses returned by Redis. * * @param ConnectionInterface $connection Redis connection that returned the error. * @param ErrorResponseInterface $response Instance of the error response. * * @throws ServerException */ protected function exception(ConnectionInterface $connection, ErrorResponseInterface $response) { $connection->disconnect(); $message = $response->getMessage(); throw new ServerException($message); } /** * Returns the underlying connection to be used by the pipeline. * * @return ConnectionInterface */ protected function getConnection() { $connection = $this->getClient()->getConnection(); if ($connection instanceof ReplicationInterface) { $connection->switchToMaster(); } return $connection; } /** * Implements the logic to flush the queued commands and read the responses * from the current connection. * * @param ConnectionInterface $connection Current connection instance. * @param SplQueue $commands Queued commands. * * @return array */ protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) { foreach ($commands as $command) { $connection->writeRequest($command); } $responses = []; $exceptions = $this->throwServerExceptions(); while (!$commands->isEmpty()) { $command = $commands->dequeue(); $response = $connection->readResponse($command); if (!$response instanceof ResponseInterface) { $responses[] = $command->parseResponse($response); } elseif ($response instanceof ErrorResponseInterface && $exceptions) { $this->exception($connection, $response); } else { $responses[] = $response; } } return $responses; } /** * Flushes the buffer holding all of the commands queued so far. * * @param bool $send Specifies if the commands in the buffer should be sent to Redis. * * @return $this */ public function flushPipeline($send = true) { if ($send && !$this->pipeline->isEmpty()) { $responses = $this->executePipeline($this->getConnection(), $this->pipeline); $this->responses = array_merge($this->responses, $responses); } else { $this->pipeline = new SplQueue(); } return $this; } /** * Marks the running status of the pipeline. * * @param bool $bool Sets the running status of the pipeline. * * @throws ClientException */ private function setRunning($bool) { if ($bool && $this->running) { throw new ClientException('The current pipeline context is already being executed.'); } $this->running = $bool; } /** * Handles the actual execution of the whole pipeline. * * @param mixed $callable Optional callback for execution. * * @return array * @throws Exception * @throws InvalidArgumentException */ public function execute($callable = null) { if ($callable && !is_callable($callable)) { throw new InvalidArgumentException('The argument must be a callable object.'); } $exception = null; $this->setRunning(true); try { if ($callable) { call_user_func($callable, $this); } $this->flushPipeline(); } catch (Exception $exception) { // NOOP } $this->setRunning(false); if ($exception) { throw $exception; } return $this->responses; } /** * Returns if the pipeline should throw exceptions on server errors. * * @return bool */ protected function throwServerExceptions() { return (bool) $this->client->getOptions()->exceptions; } /** * Returns the underlying client instance used by the pipeline object. * * @return ClientInterface */ public function getClient() { return $this->client; } } Pipeline/RelayPipeline.php000064400000004202152427751160011574 0ustar00getClient(); $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); } } } Pipeline/ConnectionErrorProof.php000064400000007244152427751160013162 0ustar00getClient()->getConnection(); } /** * {@inheritdoc} */ protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) { if ($connection instanceof NodeConnectionInterface) { return $this->executeSingleNode($connection, $commands); } elseif ($connection instanceof ClusterInterface) { return $this->executeCluster($connection, $commands); } else { $class = get_class($connection); throw new NotSupportedException("The connection class '$class' is not supported."); } } /** * {@inheritdoc} */ protected function executeSingleNode(NodeConnectionInterface $connection, SplQueue $commands) { $responses = []; $sizeOfPipe = count($commands); foreach ($commands as $command) { try { $connection->writeRequest($command); } catch (CommunicationException $exception) { return array_fill(0, $sizeOfPipe, $exception); } } for ($i = 0; $i < $sizeOfPipe; ++$i) { $command = $commands->dequeue(); try { $responses[$i] = $connection->readResponse($command); } catch (CommunicationException $exception) { $add = count($commands) - count($responses); $responses = array_merge($responses, array_fill(0, $add, $exception)); break; } } return $responses; } /** * {@inheritdoc} */ protected function executeCluster(ClusterInterface $connection, SplQueue $commands) { $responses = []; $sizeOfPipe = count($commands); $exceptions = []; foreach ($commands as $command) { $cmdConnection = $connection->getConnectionByCommand($command); if (isset($exceptions[spl_object_hash($cmdConnection)])) { continue; } try { $cmdConnection->writeRequest($command); } catch (CommunicationException $exception) { $exceptions[spl_object_hash($cmdConnection)] = $exception; } } for ($i = 0; $i < $sizeOfPipe; ++$i) { $command = $commands->dequeue(); $cmdConnection = $connection->getConnectionByCommand($command); $connectionHash = spl_object_hash($cmdConnection); if (isset($exceptions[$connectionHash])) { $responses[$i] = $exceptions[$connectionHash]; continue; } try { $responses[$i] = $cmdConnection->readResponse($command); } catch (CommunicationException $exception) { $responses[$i] = $exception; $exceptions[$connectionHash] = $exception; } } return $responses; } } Pipeline/FireAndForget.php000064400000001414152427751160011513 0ustar00isEmpty()) { $connection->writeRequest($commands->dequeue()); } $connection->disconnect(); return []; } } Pipeline/RelayAtomic.php000064400000003564152427751160011255 0ustar00getClient(); $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); } } } Monitor/Consumer.php000064400000010217152427751160010512 0ustar00assertClient($client); $this->client = $client; $this->start(); } /** * Automatically stops the consumer when the garbage collector kicks in. */ public function __destruct() { $this->stop(); } /** * Checks if the passed client instance satisfies the required conditions * needed to initialize a monitor consumer. * * @param ClientInterface $client Client instance used by the consumer. * * @throws NotSupportedException */ private function assertClient(ClientInterface $client) { if ($client->getConnection() instanceof ClusterInterface) { throw new NotSupportedException( 'Cannot initialize a monitor consumer over cluster connections.' ); } if (!$client->getCommandFactory()->supports('MONITOR')) { throw new NotSupportedException("'MONITOR' is not supported by the current command factory."); } } /** * Initializes the consumer and sends the MONITOR command to the server. */ protected function start() { $this->client->executeCommand( $this->client->createCommand('MONITOR') ); $this->valid = true; } /** * Stops the consumer. Internally this is done by disconnecting from server * since there is no way to terminate the stream initialized by MONITOR. */ public function stop() { $this->client->disconnect(); $this->valid = false; } /** * @return void */ #[ReturnTypeWillChange] public function rewind() { // NOOP } /** * Returns the last message payload retrieved from the server. * * @return object */ #[ReturnTypeWillChange] public function current() { return $this->getValue(); } /** * @return int|null */ #[ReturnTypeWillChange] public function key() { return $this->position; } /** * @return void */ #[ReturnTypeWillChange] public function next() { ++$this->position; } /** * Checks if the the consumer is still in a valid state to continue. * * @return bool */ #[ReturnTypeWillChange] public function valid() { return $this->valid; } /** * Waits for a new message from the server generated by MONITOR and returns * it when available. * * @return object */ private function getValue() { $database = 0; $client = null; $event = $this->client->getConnection()->read(); $callback = function ($matches) use (&$database, &$client) { if (2 === $count = count($matches)) { // Redis <= 2.4 $database = (int) $matches[1]; } if (4 === $count) { // Redis >= 2.6 $database = (int) $matches[2]; $client = $matches[3]; } return ' '; }; $event = preg_replace_callback('/ \(db (\d+)\) | \[(\d+) (.*?)\] /', $callback, $event, 1); @[$timestamp, $command, $arguments] = explode(' ', $event, 3); return (object) [ 'timestamp' => (float) $timestamp, 'database' => $database, 'client' => $client, 'command' => substr($command, 1, -1), 'arguments' => $arguments, ]; } } Client.php000064400000044535152427751160006520 0ustar00 */ class Client implements ClientInterface, IteratorAggregate { public const VERSION = '2.3.0'; /** @var OptionsInterface */ private $options; /** @var ConnectionInterface */ private $connection; /** @var Command\FactoryInterface */ private $commands; /** * @param mixed $parameters Connection parameters for one or more servers. * @param mixed $options Options to configure some behaviours of the client. */ public function __construct($parameters = null, $options = null) { $this->options = static::createOptions($options ?? new Options()); $this->connection = static::createConnection($this->options, $parameters ?? new Parameters()); $this->commands = $this->options->commands; } /** * Creates a new set of client options for the client. * * @param array|OptionsInterface $options Set of client options * * @return OptionsInterface * @throws InvalidArgumentException */ protected static function createOptions($options) { if (is_array($options)) { return new Options($options); } elseif ($options instanceof OptionsInterface) { return $options; } else { throw new InvalidArgumentException('Invalid type for client options'); } } /** * Creates single or aggregate connections from supplied arguments. * * This method accepts the following types to create a connection instance: * * - Array (dictionary: single connection, indexed: aggregate connections) * - String (URI for a single connection) * - Callable (connection initializer callback) * - Instance of Predis\Connection\ParametersInterface (used as-is) * - Instance of Predis\Connection\ConnectionInterface (returned as-is) * * When a callable is passed, it receives the original set of client options * and must return an instance of Predis\Connection\ConnectionInterface. * * Connections are created using the connection factory (in case of single * connections) or a specialized aggregate connection initializer (in case * of cluster and replication) retrieved from the supplied client options. * * @param OptionsInterface $options Client options container * @param mixed $parameters Connection parameters * * @return ConnectionInterface * @throws InvalidArgumentException */ protected static function createConnection(OptionsInterface $options, $parameters) { if ($parameters instanceof ConnectionInterface) { return $parameters; } if ($parameters instanceof ParametersInterface || is_string($parameters)) { return $options->connections->create($parameters); } if (is_array($parameters)) { if (!isset($parameters[0])) { return $options->connections->create($parameters); } elseif ($options->defined('cluster') && $initializer = $options->cluster) { return $initializer($parameters, true); } elseif ($options->defined('replication') && $initializer = $options->replication) { return $initializer($parameters, true); } elseif ($options->defined('aggregate') && $initializer = $options->aggregate) { return $initializer($parameters, false); } else { throw new InvalidArgumentException( 'Array of connection parameters requires `cluster`, `replication` or `aggregate` client option' ); } } if (is_callable($parameters)) { $connection = call_user_func($parameters, $options); if (!$connection instanceof ConnectionInterface) { throw new InvalidArgumentException('Callable parameters must return a valid connection'); } return $connection; } throw new InvalidArgumentException('Invalid type for connection parameters'); } /** * {@inheritdoc} */ public function getCommandFactory() { return $this->commands; } /** * {@inheritdoc} */ public function getOptions() { return $this->options; } /** * Creates a new client using a specific underlying connection. * * This method allows to create a new client instance by picking a specific * connection out of an aggregate one, with the same options of the original * client instance. * * The specified selector defines which logic to use to look for a suitable * connection by the specified value. Supported selectors are: * * - `id` * - `key` * - `slot` * - `command` * - `alias` * - `role` * * Internally the client relies on duck-typing and follows this convention: * * $selector string => getConnectionBy$selector($value) method * * This means that support for specific selectors may vary depending on the * actual logic implemented by connection classes and there is no interface * binding a connection class to implement any of these. * * @param string $selector Type of selector. * @param mixed $value Value to be used by the selector. * * @return ClientInterface */ public function getClientBy($selector, $value) { $selector = strtolower($selector); if (!in_array($selector, ['id', 'key', 'slot', 'role', 'alias', 'command'])) { throw new InvalidArgumentException("Invalid selector type: `$selector`"); } if (!method_exists($this->connection, $method = "getConnectionBy$selector")) { $class = get_class($this->connection); throw new InvalidArgumentException("Selecting connection by $selector is not supported by $class"); } if (!$connection = $this->connection->$method($value)) { throw new InvalidArgumentException("Cannot find a connection by $selector matching `$value`"); } return new static($connection, $this->getOptions()); } /** * Opens the underlying connection and connects to the server. */ public function connect() { $this->connection->connect(); } /** * Closes the underlying connection and disconnects from the server. */ public function disconnect() { $this->connection->disconnect(); } /** * Closes the underlying connection and disconnects from the server. * * This is the same as `Client::disconnect()` as it does not actually send * the `QUIT` command to Redis, but simply closes the connection. */ public function quit() { $this->disconnect(); } /** * Returns the current state of the underlying connection. * * @return bool */ public function isConnected() { return $this->connection->isConnected(); } /** * {@inheritdoc} */ public function getConnection() { return $this->connection; } /** * Applies the configured serializer and compression to given value. * * @param mixed $value * @return string */ public function pack($value) { return $this->connection instanceof RelayConnection ? $this->connection->pack($value) : $value; } /** * Deserializes and decompresses to given value. * * @param mixed $value * @return string */ public function unpack($value) { return $this->connection instanceof RelayConnection ? $this->connection->unpack($value) : $value; } /** * Executes a command without filtering its arguments, parsing the response, * applying any prefix to keys or throwing exceptions on Redis errors even * regardless of client options. * * It is possible to identify Redis error responses from normal responses * using the second optional argument which is populated by reference. * * @param array $arguments Command arguments as defined by the command signature. * @param bool $error Set to TRUE when Redis returned an error response. * * @return mixed */ public function executeRaw(array $arguments, &$error = null) { $error = false; $commandID = array_shift($arguments); $response = $this->connection->executeCommand( new RawCommand($commandID, $arguments) ); if ($response instanceof ResponseInterface) { if ($response instanceof ErrorResponseInterface) { $error = true; } return (string) $response; } return $response; } /** * {@inheritdoc} */ public function __call($commandID, $arguments) { return $this->executeCommand( $this->createCommand($commandID, $arguments) ); } /** * {@inheritdoc} */ public function createCommand($commandID, $arguments = []) { return $this->commands->create($commandID, $arguments); } /** * @param string $name * @return ContainerInterface */ public function __get(string $name) { return ContainerFactory::create($this, $name); } /** * @param string $name * @param mixed $value * @return mixed */ public function __set(string $name, $value) { throw new RuntimeException('Not allowed'); } /** * @param string $name * @return mixed */ public function __isset(string $name) { throw new RuntimeException('Not allowed'); } /** * {@inheritdoc} */ public function executeCommand(CommandInterface $command) { $response = $this->connection->executeCommand($command); if ($response instanceof ResponseInterface) { if ($response instanceof ErrorResponseInterface) { $response = $this->onErrorResponse($command, $response); } return $response; } return $command->parseResponse($response); } /** * Handles -ERR responses returned by Redis. * * @param CommandInterface $command Redis command that generated the error. * @param ErrorResponseInterface $response Instance of the error response. * * @return mixed * @throws ServerException */ protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $response) { if ($command instanceof ScriptCommand && $response->getErrorType() === 'NOSCRIPT') { $response = $this->executeCommand($command->getEvalCommand()); if (!$response instanceof ResponseInterface) { $response = $command->parseResponse($response); } return $response; } if ($this->options->exceptions) { throw new ServerException($response->getMessage()); } return $response; } /** * Executes the specified initializer method on `$this` by adjusting the * actual invocation depending on the arity (0, 1 or 2 arguments). This is * simply an utility method to create Redis contexts instances since they * follow a common initialization path. * * @param string $initializer Method name. * @param array $argv Arguments for the method. * * @return mixed */ private function sharedContextFactory($initializer, $argv = null) { switch (count($argv)) { case 0: return $this->$initializer(); case 1: return is_array($argv[0]) ? $this->$initializer($argv[0]) : $this->$initializer(null, $argv[0]); case 2: [$arg0, $arg1] = $argv; return $this->$initializer($arg0, $arg1); default: return $this->$initializer($this, $argv); } } /** * Creates a new pipeline context and returns it, or returns the results of * a pipeline executed inside the optionally provided callable object. * * @param mixed ...$arguments Array of options, a callable for execution, or both. * * @return Pipeline|array */ public function pipeline(...$arguments) { return $this->sharedContextFactory('createPipeline', func_get_args()); } /** * Actual pipeline context initializer method. * * @param array|null $options Options for the context. * @param mixed $callable Optional callable used to execute the context. * * @return Pipeline|array */ protected function createPipeline(?array $options = null, $callable = null) { if (isset($options['atomic']) && $options['atomic']) { $class = Atomic::class; } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) { $class = FireAndForget::class; } else { $class = Pipeline::class; } if ($this->connection instanceof RelayConnection) { if (isset($options['atomic']) && $options['atomic']) { $class = RelayAtomic::class; } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) { throw new NotSupportedException('The "relay" extension does not support fire-and-forget pipelines.'); } else { $class = RelayPipeline::class; } } /* * @var ClientContextInterface */ $pipeline = new $class($this); if (isset($callable)) { return $pipeline->execute($callable); } return $pipeline; } /** * Creates a new transaction context and returns it, or returns the results * of a transaction executed inside the optionally provided callable object. * * @param mixed ...$arguments Array of options, a callable for execution, or both. * * @return MultiExecTransaction|array */ public function transaction(...$arguments) { return $this->sharedContextFactory('createTransaction', func_get_args()); } /** * Actual transaction context initializer method. * * @param array|null $options Options for the context. * @param mixed $callable Optional callable used to execute the context. * * @return MultiExecTransaction|array */ protected function createTransaction(?array $options = null, $callable = null) { $transaction = new MultiExecTransaction($this, $options); if (isset($callable)) { return $transaction->execute($callable); } return $transaction; } /** * Creates a new publish/subscribe context and returns it, or starts its loop * inside the optionally provided callable object. * * @param mixed ...$arguments Array of options, a callable for execution, or both. * * @return PubSubConsumer|null */ public function pubSubLoop(...$arguments) { return $this->sharedContextFactory('createPubSub', func_get_args()); } /** * Actual publish/subscribe context initializer method. * * @param array|null $options Options for the context. * @param mixed $callable Optional callable used to execute the context. * * @return PubSubConsumer|null */ protected function createPubSub(?array $options = null, $callable = null) { if ($this->connection instanceof RelayConnection) { $pubsub = new RelayPubSubConsumer($this, $options); } else { $pubsub = new PubSubConsumer($this, $options); } if (!isset($callable)) { return $pubsub; } foreach ($pubsub as $message) { if (call_user_func($callable, $pubsub, $message) === false) { $pubsub->stop(); } } return null; } /** * Creates a new monitor consumer and returns it. * * @return MonitorConsumer */ public function monitor() { return new MonitorConsumer($this); } /** * @return Traversable */ #[ReturnTypeWillChange] public function getIterator() { $clients = []; $connection = $this->getConnection(); if (!$connection instanceof Traversable) { return new ArrayIterator([ (string) $connection => new static($connection, $this->getOptions()), ]); } foreach ($connection as $node) { $clients[(string) $node] = new static($node, $this->getOptions()); } return new ArrayIterator($clients); } } ClientContextInterface.php000064400000051544152427751160011704 0ustar00 [ ['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']; } } PredisException.php000064400000000624152427751160010376 0ustar00 * @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; } } } } NotSupportedException.php000064400000000711152427751160011613 0ustar00connection = $connection; } /** * Gets the connection that generated the exception. * * @return NodeConnectionInterface */ public function getConnection() { return $this->connection; } /** * Indicates if the receiver should reset the underlying connection. * * @return bool */ public function shouldResetConnection() { return true; } /** * Helper method to handle exceptions generated by a connection object. * * @param CommunicationException $exception Exception. * * @throws CommunicationException */ public static function handle(CommunicationException $exception) { if ($exception->shouldResetConnection()) { $connection = $exception->getConnection(); if ($connection->isConnected()) { $connection->disconnect(); } } throw $exception; } } ClientException.php000064400000000605152427751160010365 0ustar00 */ class InvalidRegexPatternRule implements Rule { public function getNodeType(): string { return StaticCall::class; } public function processNode(Node $node, Scope $scope): array { $patterns = $this->extractPatterns($node, $scope); $errors = []; foreach ($patterns as $pattern) { $errorMessage = $this->validatePattern($pattern); if ($errorMessage === null) { continue; } $errors[] = RuleErrorBuilder::message(sprintf('Regex pattern is invalid: %s', $errorMessage))->identifier('regexp.pattern')->build(); } return $errors; } /** * @return string[] */ private function extractPatterns(StaticCall $node, Scope $scope): array { if (!$node->class instanceof FullyQualified) { return []; } $isRegex = $node->class->toString() === Regex::class; $isPreg = $node->class->toString() === Preg::class; if (!$isRegex && !$isPreg) { return []; } if (!$node->name instanceof Node\Identifier || !Preg::isMatch('{^(match|isMatch|grep|replace|split)}', $node->name->name)) { return []; } $functionName = $node->name->name; if (!isset($node->getArgs()[0])) { return []; } $patternNode = $node->getArgs()[0]->value; $patternType = $scope->getType($patternNode); $patternStrings = []; foreach ($patternType->getConstantStrings() as $constantStringType) { if ($functionName === 'replaceCallbackArray') { continue; } $patternStrings[] = $constantStringType->getValue(); } foreach ($patternType->getConstantArrays() as $constantArrayType) { if ( in_array($functionName, [ 'replace', 'replaceCallback', ], true) ) { foreach ($constantArrayType->getValueTypes() as $arrayKeyType) { foreach ($arrayKeyType->getConstantStrings() as $constantString) { $patternStrings[] = $constantString->getValue(); } } } if ($functionName !== 'replaceCallbackArray') { continue; } foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) { foreach ($arrayKeyType->getConstantStrings() as $constantString) { $patternStrings[] = $constantString->getValue(); } } } return $patternStrings; } private function validatePattern(string $pattern): ?string { try { $msg = null; $prev = set_error_handler(function (int $severity, string $message, string $file) use (&$msg): bool { $msg = preg_replace("#^preg_match(_all)?\\(.*?\\): #", '', $message); return true; }); if ($pattern === '') { return 'Empty string is not a valid regular expression'; } Preg::match($pattern, ''); if ($msg !== null) { return $msg; } } catch (PcreException $e) { if ($e->getCode() === PREG_INTERNAL_ERROR && $msg !== null) { return $msg; } return preg_replace('{.*? failed executing ".*": }', '', $e->getMessage()); } finally { restore_error_handler(); } return null; } } PHPStan/PregMatchFlags.php000064400000004344152427755310011401 0ustar00getType($flagsArg->value); $constantScalars = $flagsType->getConstantScalarValues(); if ($constantScalars === []) { return null; } $internalFlagsTypes = []; foreach ($flagsType->getConstantScalarValues() as $constantScalarValue) { if (!is_int($constantScalarValue)) { return null; } $internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL); } return TypeCombinator::union(...$internalFlagsTypes); } static public function removeNullFromMatches(Type $matchesType): Type { return TypeTraverser::map($matchesType, static function (Type $type, callable $traverse): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } if ($type instanceof ConstantArrayType) { return new ConstantArrayType( $type->getKeyTypes(), array_map(static function (Type $valueType) use ($traverse): Type { return $traverse($valueType); }, $type->getValueTypes()), $type->getNextAutoIndexes(), [], $type->isList() ); } if ($type instanceof ArrayType) { return new ArrayType($type->getKeyType(), $traverse($type->getItemType())); } return TypeCombinator::removeNull($type); }); } } PHPStan/UnsafeStrictGroupsCallRule.php000064400000007027152427755310014011 0ustar00 */ final class UnsafeStrictGroupsCallRule implements Rule { /** * @var RegexArrayShapeMatcher */ private $regexShapeMatcher; public function __construct(RegexArrayShapeMatcher $regexShapeMatcher) { $this->regexShapeMatcher = $regexShapeMatcher; } public function getNodeType(): string { return StaticCall::class; } public function processNode(Node $node, Scope $scope): array { if (!$node->class instanceof FullyQualified) { return []; } $isRegex = $node->class->toString() === Regex::class; $isPreg = $node->class->toString() === Preg::class; if (!$isRegex && !$isPreg) { return []; } if (!$node->name instanceof Node\Identifier || !in_array($node->name->name, ['matchStrictGroups', 'isMatchStrictGroups', 'matchAllStrictGroups', 'isMatchAllStrictGroups'], true)) { return []; } $args = $node->getArgs(); if (!isset($args[0])) { return []; } $patternArg = $args[0] ?? null; if ($isPreg) { if (!isset($args[2])) { // no matches set, skip as the matches won't be used anyway return []; } $flagsArg = $args[3] ?? null; } else { $flagsArg = $args[2] ?? null; } if ($patternArg === null) { return []; } $flagsType = PregMatchFlags::getType($flagsArg, $scope); if ($flagsType === null) { return []; } $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); if ($matchedType === null) { return [ RuleErrorBuilder::message(sprintf('The %s call is potentially unsafe as $matches\' type could not be inferred.', $node->name->name)) ->identifier('composerPcre.maybeUnsafeStrictGroups') ->build(), ]; } if (count($matchedType->getConstantArrays()) === 1) { $matchedType = $matchedType->getConstantArrays()[0]; $nullableGroups = []; foreach ($matchedType->getValueTypes() as $index => $type) { if (TypeCombinator::containsNull($type)) { $nullableGroups[] = $matchedType->getKeyTypes()[$index]->getValue(); } } if (\count($nullableGroups) > 0) { return [ RuleErrorBuilder::message(sprintf( 'The %s call is unsafe as match group%s "%s" %s optional and may be null.', $node->name->name, \count($nullableGroups) > 1 ? 's' : '', implode('", "', $nullableGroups), \count($nullableGroups) > 1 ? 'are' : 'is' ))->identifier('composerPcre.unsafeStrictGroups')->build(), ]; } } return []; } } PHPStan/PregMatchParameterOutTypeExtension.php000064400000004215152427755310015511 0ustar00regexShapeMatcher = $regexShapeMatcher; } public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool { return $methodReflection->getDeclaringClass()->getName() === Preg::class && in_array($methodReflection->getName(), [ 'match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups', 'matchAll', 'isMatchAll', 'matchAllStrictGroups', 'isMatchAllStrictGroups' ], true) && $parameter->getName() === 'matches'; } public function getParameterOutTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type { $args = $methodCall->getArgs(); $patternArg = $args[0] ?? null; $matchesArg = $args[2] ?? null; $flagsArg = $args[3] ?? null; if ( $patternArg === null || $matchesArg === null ) { return null; } $flagsType = PregMatchFlags::getType($flagsArg, $scope); if ($flagsType === null) { return null; } if (stripos($methodReflection->getName(), 'matchAll') !== false) { return $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope); } return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope); } } PHPStan/PregReplaceCallbackClosureTypeExtension.php000064400000007035152427755310016454 0ustar00regexShapeMatcher = $regexShapeMatcher; } public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool { return in_array($methodReflection->getDeclaringClass()->getName(), [Preg::class, Regex::class], true) && in_array($methodReflection->getName(), ['replaceCallback', 'replaceCallbackStrictGroups'], true) && $parameter->getName() === 'replacement'; } public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type { $args = $methodCall->getArgs(); $patternArg = $args[0] ?? null; $flagsArg = $args[5] ?? null; if ( $patternArg === null ) { return null; } $flagsType = PregMatchFlags::getType($flagsArg, $scope); $matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); if ($matchesType === null) { return null; } if ($methodReflection->getName() === 'replaceCallbackStrictGroups' && count($matchesType->getConstantArrays()) === 1) { $matchesType = $matchesType->getConstantArrays()[0]; $matchesType = new ConstantArrayType( $matchesType->getKeyTypes(), array_map(static function (Type $valueType): Type { if (count($valueType->getConstantArrays()) === 1) { $valueTypeArray = $valueType->getConstantArrays()[0]; return new ConstantArrayType( $valueTypeArray->getKeyTypes(), array_map(static function (Type $valueType): Type { return TypeCombinator::removeNull($valueType); }, $valueTypeArray->getValueTypes()), $valueTypeArray->getNextAutoIndexes(), [], $valueTypeArray->isList() ); } return TypeCombinator::removeNull($valueType); }, $matchesType->getValueTypes()), $matchesType->getNextAutoIndexes(), [], $matchesType->isList() ); } return new ClosureType( [ new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), $matchesType, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()), ], new StringType() ); } } PHPStan/PregMatchTypeSpecifyingExtension.php000064400000007503152427755310015204 0ustar00regexShapeMatcher = $regexShapeMatcher; } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { $this->typeSpecifier = $typeSpecifier; } public function getClass(): string { return Preg::class; } public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context): bool { return in_array($methodReflection->getName(), [ 'match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups', 'matchAll', 'isMatchAll', 'matchAllStrictGroups', 'isMatchAllStrictGroups' ], true) && !$context->null(); } public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { $args = $node->getArgs(); $patternArg = $args[0] ?? null; $matchesArg = $args[2] ?? null; $flagsArg = $args[3] ?? null; if ( $patternArg === null || $matchesArg === null ) { return new SpecifiedTypes(); } $flagsType = PregMatchFlags::getType($flagsArg, $scope); if ($flagsType === null) { return new SpecifiedTypes(); } if (stripos($methodReflection->getName(), 'matchAll') !== false) { $matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope); } else { $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope); } if ($matchedType === null) { return new SpecifiedTypes(); } if ( in_array($methodReflection->getName(), ['matchStrictGroups', 'isMatchStrictGroups', 'matchAllStrictGroups', 'isMatchAllStrictGroups'], true) ) { $matchedType = PregMatchFlags::removeNullFromMatches($matchedType); } $overwrite = false; if ($context->false()) { $overwrite = true; $context = $context->negate(); } // @phpstan-ignore function.alreadyNarrowedType if (method_exists('PHPStan\Analyser\SpecifiedTypes', 'setRootExpr')) { $typeSpecifier = $this->typeSpecifier->create( $matchesArg->value, $matchedType, $context, $scope )->setRootExpr($node); return $overwrite ? $typeSpecifier->setAlwaysOverwriteTypes() : $typeSpecifier; } // @phpstan-ignore arguments.count return $this->typeSpecifier->create( $matchesArg->value, $matchedType, $context, // @phpstan-ignore argument.type $overwrite, $scope, $node ); } } ReplaceResult.php000064400000001346152427755310010046 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; final class ReplaceResult { /** * @readonly * @var string */ public $result; /** * @readonly * @var 0|positive-int */ public $count; /** * @readonly * @var bool */ public $matched; /** * @param 0|positive-int $count */ public function __construct(int $count, string $result) { $this->count = $count; $this->matched = (bool) $count; $this->result = $result; } } MatchWithOffsetsResult.php000064400000001736152427755310011720 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; final class MatchWithOffsetsResult { /** * An array of match group => pair of string matched + offset in bytes (or -1 if no match) * * @readonly * @var array * @phpstan-var array}> */ public $matches; /** * @readonly * @var bool */ public $matched; /** * @param 0|positive-int $count * @param array $matches * @phpstan-param array}> $matches */ public function __construct(int $count, array $matches) { $this->matches = $matches; $this->matched = (bool) $count; } } MatchStrictGroupsResult.php000064400000001364152427755310012120 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; final class MatchStrictGroupsResult { /** * An array of match group => string matched * * @readonly * @var array */ public $matches; /** * @readonly * @var bool */ public $matched; /** * @param 0|positive-int $count * @param array $matches */ public function __construct(int $count, array $matches) { $this->matches = $matches; $this->matched = (bool) $count; } } MatchAllResult.php000064400000001605152427755310010156 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; final class MatchAllResult { /** * An array of match group => list of matched strings * * @readonly * @var array> */ public $matches; /** * @readonly * @var 0|positive-int */ public $count; /** * @readonly * @var bool */ public $matched; /** * @param 0|positive-int $count * @param array> $matches */ public function __construct(int $count, array $matches) { $this->matches = $matches; $this->matched = (bool) $count; $this->count = $count; } } Preg.php000064400000042404152427755310006171 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; class Preg { /** @internal */ public const ARRAY_MSG = '$subject as an array is not supported. You can use \'foreach\' instead.'; /** @internal */ public const INVALID_TYPE_MSG = '$subject must be a string, %s given.'; /** * @param non-empty-string $pattern * @param array $matches Set by method * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported * @return 0|1 * * @param-out array $matches */ public static function match(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int { self::checkOffsetCapture($flags, 'matchWithOffsets'); $result = preg_match($pattern, $subject, $matches, $flags | PREG_UNMATCHED_AS_NULL, $offset); if ($result === false) { throw PcreException::fromFunction('preg_match', $pattern); } return $result; } /** * Variant of `match()` which outputs non-null matches (or throws) * * @param non-empty-string $pattern * @param array $matches Set by method * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported * @return 0|1 * @throws UnexpectedNullMatchException * * @param-out array $matches */ public static function matchStrictGroups(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int { $result = self::match($pattern, $subject, $matchesInternal, $flags, $offset); $matches = self::enforceNonNullMatches($pattern, $matchesInternal, 'match'); return $result; } /** * Runs preg_match with PREG_OFFSET_CAPTURE * * @param non-empty-string $pattern * @param array $matches Set by method * @param int-mask $flags PREG_UNMATCHED_AS_NULL and PREG_OFFSET_CAPTURE are always set, no other flags are supported * @return 0|1 * * @param-out array}> $matches */ public static function matchWithOffsets(string $pattern, string $subject, ?array &$matches, int $flags = 0, int $offset = 0): int { $result = preg_match($pattern, $subject, $matches, $flags | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE, $offset); if ($result === false) { throw PcreException::fromFunction('preg_match', $pattern); } return $result; } /** * @param non-empty-string $pattern * @param array $matches Set by method * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported * @return 0|positive-int * * @param-out array> $matches */ public static function matchAll(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int { self::checkOffsetCapture($flags, 'matchAllWithOffsets'); self::checkSetOrder($flags); $result = preg_match_all($pattern, $subject, $matches, $flags | PREG_UNMATCHED_AS_NULL, $offset); if (!is_int($result)) { // PHP < 8 may return null, 8+ returns int|false throw PcreException::fromFunction('preg_match_all', $pattern); } return $result; } /** * Variant of `match()` which outputs non-null matches (or throws) * * @param non-empty-string $pattern * @param array $matches Set by method * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported * @return 0|positive-int * @throws UnexpectedNullMatchException * * @param-out array> $matches */ public static function matchAllStrictGroups(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int { $result = self::matchAll($pattern, $subject, $matchesInternal, $flags, $offset); $matches = self::enforceNonNullMatchAll($pattern, $matchesInternal, 'matchAll'); return $result; } /** * Runs preg_match_all with PREG_OFFSET_CAPTURE * * @param non-empty-string $pattern * @param array $matches Set by method * @param int-mask $flags PREG_UNMATCHED_AS_NULL and PREG_MATCH_OFFSET are always set, no other flags are supported * @return 0|positive-int * * @param-out array}>> $matches */ public static function matchAllWithOffsets(string $pattern, string $subject, ?array &$matches, int $flags = 0, int $offset = 0): int { self::checkSetOrder($flags); $result = preg_match_all($pattern, $subject, $matches, $flags | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE, $offset); if (!is_int($result)) { // PHP < 8 may return null, 8+ returns int|false throw PcreException::fromFunction('preg_match_all', $pattern); } return $result; } /** * @param string|string[] $pattern * @param string|string[] $replacement * @param string $subject * @param int $count Set by method * * @param-out int<0, max> $count */ public static function replace($pattern, $replacement, $subject, int $limit = -1, ?int &$count = null): string { if (!is_scalar($subject)) { if (is_array($subject)) { throw new \InvalidArgumentException(static::ARRAY_MSG); } throw new \TypeError(sprintf(static::INVALID_TYPE_MSG, gettype($subject))); } $result = preg_replace($pattern, $replacement, $subject, $limit, $count); if ($result === null) { throw PcreException::fromFunction('preg_replace', $pattern); } return $result; } /** * @param string|string[] $pattern * @param ($flags is PREG_OFFSET_CAPTURE ? (callable(array}>): string) : callable(array): string) $replacement * @param string $subject * @param int $count Set by method * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set * * @param-out int<0, max> $count */ public static function replaceCallback($pattern, callable $replacement, $subject, int $limit = -1, ?int &$count = null, int $flags = 0): string { if (!is_scalar($subject)) { if (is_array($subject)) { throw new \InvalidArgumentException(static::ARRAY_MSG); } throw new \TypeError(sprintf(static::INVALID_TYPE_MSG, gettype($subject))); } $result = preg_replace_callback($pattern, $replacement, $subject, $limit, $count, $flags | PREG_UNMATCHED_AS_NULL); if ($result === null) { throw PcreException::fromFunction('preg_replace_callback', $pattern); } return $result; } /** * Variant of `replaceCallback()` which outputs non-null matches (or throws) * * @param string $pattern * @param ($flags is PREG_OFFSET_CAPTURE ? (callable(array}>): string) : callable(array): string) $replacement * @param string $subject * @param int $count Set by method * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set * * @param-out int<0, max> $count */ public static function replaceCallbackStrictGroups(string $pattern, callable $replacement, $subject, int $limit = -1, ?int &$count = null, int $flags = 0): string { return self::replaceCallback($pattern, function (array $matches) use ($pattern, $replacement) { return $replacement(self::enforceNonNullMatches($pattern, $matches, 'replaceCallback')); }, $subject, $limit, $count, $flags); } /** * @param ($flags is PREG_OFFSET_CAPTURE ? (array}>): string>) : array): string>) $pattern * @param string $subject * @param int $count Set by method * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set * * @param-out int<0, max> $count */ public static function replaceCallbackArray(array $pattern, $subject, int $limit = -1, ?int &$count = null, int $flags = 0): string { if (!is_scalar($subject)) { if (is_array($subject)) { throw new \InvalidArgumentException(static::ARRAY_MSG); } throw new \TypeError(sprintf(static::INVALID_TYPE_MSG, gettype($subject))); } $result = preg_replace_callback_array($pattern, $subject, $limit, $count, $flags | PREG_UNMATCHED_AS_NULL); if ($result === null) { $pattern = array_keys($pattern); throw PcreException::fromFunction('preg_replace_callback_array', $pattern); } return $result; } /** * @param int-mask $flags PREG_SPLIT_NO_EMPTY or PREG_SPLIT_DELIM_CAPTURE * @return list */ public static function split(string $pattern, string $subject, int $limit = -1, int $flags = 0): array { if (($flags & PREG_SPLIT_OFFSET_CAPTURE) !== 0) { throw new \InvalidArgumentException('PREG_SPLIT_OFFSET_CAPTURE is not supported as it changes the type of $matches, use splitWithOffsets() instead'); } $result = preg_split($pattern, $subject, $limit, $flags); if ($result === false) { throw PcreException::fromFunction('preg_split', $pattern); } return $result; } /** * @param int-mask $flags PREG_SPLIT_NO_EMPTY or PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_OFFSET_CAPTURE is always set * @return list * @phpstan-return list}> */ public static function splitWithOffsets(string $pattern, string $subject, int $limit = -1, int $flags = 0): array { $result = preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE); if ($result === false) { throw PcreException::fromFunction('preg_split', $pattern); } return $result; } /** * @template T of string|\Stringable * @param string $pattern * @param array $array * @param int-mask $flags PREG_GREP_INVERT * @return array */ public static function grep(string $pattern, array $array, int $flags = 0): array { $result = preg_grep($pattern, $array, $flags); if ($result === false) { throw PcreException::fromFunction('preg_grep', $pattern); } return $result; } /** * Variant of match() which returns a bool instead of int * * @param non-empty-string $pattern * @param array $matches Set by method * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported * * @param-out array $matches */ public static function isMatch(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool { return (bool) static::match($pattern, $subject, $matches, $flags, $offset); } /** * Variant of `isMatch()` which outputs non-null matches (or throws) * * @param non-empty-string $pattern * @param array $matches Set by method * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported * @throws UnexpectedNullMatchException * * @param-out array $matches */ public static function isMatchStrictGroups(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool { return (bool) self::matchStrictGroups($pattern, $subject, $matches, $flags, $offset); } /** * Variant of matchAll() which returns a bool instead of int * * @param non-empty-string $pattern * @param array $matches Set by method * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported * * @param-out array> $matches */ public static function isMatchAll(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool { return (bool) static::matchAll($pattern, $subject, $matches, $flags, $offset); } /** * Variant of `isMatchAll()` which outputs non-null matches (or throws) * * @param non-empty-string $pattern * @param array $matches Set by method * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported * * @param-out array> $matches */ public static function isMatchAllStrictGroups(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): bool { return (bool) self::matchAllStrictGroups($pattern, $subject, $matches, $flags, $offset); } /** * Variant of matchWithOffsets() which returns a bool instead of int * * Runs preg_match with PREG_OFFSET_CAPTURE * * @param non-empty-string $pattern * @param array $matches Set by method * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported * * @param-out array}> $matches */ public static function isMatchWithOffsets(string $pattern, string $subject, ?array &$matches, int $flags = 0, int $offset = 0): bool { return (bool) static::matchWithOffsets($pattern, $subject, $matches, $flags, $offset); } /** * Variant of matchAllWithOffsets() which returns a bool instead of int * * Runs preg_match_all with PREG_OFFSET_CAPTURE * * @param non-empty-string $pattern * @param array $matches Set by method * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported * * @param-out array}>> $matches */ public static function isMatchAllWithOffsets(string $pattern, string $subject, ?array &$matches, int $flags = 0, int $offset = 0): bool { return (bool) static::matchAllWithOffsets($pattern, $subject, $matches, $flags, $offset); } private static function checkOffsetCapture(int $flags, string $useFunctionName): void { if (($flags & PREG_OFFSET_CAPTURE) !== 0) { throw new \InvalidArgumentException('PREG_OFFSET_CAPTURE is not supported as it changes the type of $matches, use ' . $useFunctionName . '() instead'); } } private static function checkSetOrder(int $flags): void { if (($flags & PREG_SET_ORDER) !== 0) { throw new \InvalidArgumentException('PREG_SET_ORDER is not supported as it changes the type of $matches'); } } /** * @param array $matches * @return array * @throws UnexpectedNullMatchException */ private static function enforceNonNullMatches(string $pattern, array $matches, string $variantMethod) { foreach ($matches as $group => $match) { if (is_string($match) || (is_array($match) && is_string($match[0]))) { continue; } throw new UnexpectedNullMatchException('Pattern "'.$pattern.'" had an unexpected unmatched group "'.$group.'", make sure the pattern always matches or use '.$variantMethod.'() instead.'); } /** @var array */ return $matches; } /** * @param array> $matches * @return array> * @throws UnexpectedNullMatchException */ private static function enforceNonNullMatchAll(string $pattern, array $matches, string $variantMethod) { foreach ($matches as $group => $groupMatches) { foreach ($groupMatches as $match) { if (null === $match) { throw new UnexpectedNullMatchException('Pattern "'.$pattern.'" had an unexpected unmatched group "'.$group.'", make sure the pattern always matches or use '.$variantMethod.'() instead.'); } } } /** @var array> */ return $matches; } } UnexpectedNullMatchException.php000064400000000777152427755310013076 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; class UnexpectedNullMatchException extends PcreException { public static function fromFunction($function, $pattern) { throw new \LogicException('fromFunction should not be called on '.self::class.', use '.PcreException::class); } } Regex.php000064400000016301152427755310006343 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; class Regex { /** * @param non-empty-string $pattern */ public static function isMatch(string $pattern, string $subject, int $offset = 0): bool { return (bool) Preg::match($pattern, $subject, $matches, 0, $offset); } /** * @param non-empty-string $pattern * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported */ public static function match(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchResult { self::checkOffsetCapture($flags, 'matchWithOffsets'); $count = Preg::match($pattern, $subject, $matches, $flags, $offset); return new MatchResult($count, $matches); } /** * Variant of `match()` which returns non-null matches (or throws) * * @param non-empty-string $pattern * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported * @throws UnexpectedNullMatchException */ public static function matchStrictGroups(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchStrictGroupsResult { // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups $count = Preg::matchStrictGroups($pattern, $subject, $matches, $flags, $offset); return new MatchStrictGroupsResult($count, $matches); } /** * Runs preg_match with PREG_OFFSET_CAPTURE * * @param non-empty-string $pattern * @param int-mask $flags PREG_UNMATCHED_AS_NULL and PREG_MATCH_OFFSET are always set, no other flags are supported */ public static function matchWithOffsets(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchWithOffsetsResult { $count = Preg::matchWithOffsets($pattern, $subject, $matches, $flags, $offset); return new MatchWithOffsetsResult($count, $matches); } /** * @param non-empty-string $pattern * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported */ public static function matchAll(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchAllResult { self::checkOffsetCapture($flags, 'matchAllWithOffsets'); self::checkSetOrder($flags); $count = Preg::matchAll($pattern, $subject, $matches, $flags, $offset); return new MatchAllResult($count, $matches); } /** * Variant of `matchAll()` which returns non-null matches (or throws) * * @param non-empty-string $pattern * @param int-mask $flags PREG_UNMATCHED_AS_NULL is always set, no other flags are supported * @throws UnexpectedNullMatchException */ public static function matchAllStrictGroups(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchAllStrictGroupsResult { self::checkOffsetCapture($flags, 'matchAllWithOffsets'); self::checkSetOrder($flags); // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups $count = Preg::matchAllStrictGroups($pattern, $subject, $matches, $flags, $offset); return new MatchAllStrictGroupsResult($count, $matches); } /** * Runs preg_match_all with PREG_OFFSET_CAPTURE * * @param non-empty-string $pattern * @param int-mask $flags PREG_UNMATCHED_AS_NULL and PREG_MATCH_OFFSET are always set, no other flags are supported */ public static function matchAllWithOffsets(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchAllWithOffsetsResult { self::checkSetOrder($flags); $count = Preg::matchAllWithOffsets($pattern, $subject, $matches, $flags, $offset); return new MatchAllWithOffsetsResult($count, $matches); } /** * @param string|string[] $pattern * @param string|string[] $replacement * @param string $subject */ public static function replace($pattern, $replacement, $subject, int $limit = -1): ReplaceResult { $result = Preg::replace($pattern, $replacement, $subject, $limit, $count); return new ReplaceResult($count, $result); } /** * @param string|string[] $pattern * @param ($flags is PREG_OFFSET_CAPTURE ? (callable(array}>): string) : callable(array): string) $replacement * @param string $subject * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set */ public static function replaceCallback($pattern, callable $replacement, $subject, int $limit = -1, int $flags = 0): ReplaceResult { $result = Preg::replaceCallback($pattern, $replacement, $subject, $limit, $count, $flags); return new ReplaceResult($count, $result); } /** * Variant of `replaceCallback()` which outputs non-null matches (or throws) * * @param string $pattern * @param ($flags is PREG_OFFSET_CAPTURE ? (callable(array}>): string) : callable(array): string) $replacement * @param string $subject * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set */ public static function replaceCallbackStrictGroups($pattern, callable $replacement, $subject, int $limit = -1, int $flags = 0): ReplaceResult { $result = Preg::replaceCallbackStrictGroups($pattern, $replacement, $subject, $limit, $count, $flags); return new ReplaceResult($count, $result); } /** * @param ($flags is PREG_OFFSET_CAPTURE ? (array}>): string>) : array): string>) $pattern * @param string $subject * @param int-mask $flags PREG_OFFSET_CAPTURE is supported, PREG_UNMATCHED_AS_NULL is always set */ public static function replaceCallbackArray(array $pattern, $subject, int $limit = -1, int $flags = 0): ReplaceResult { $result = Preg::replaceCallbackArray($pattern, $subject, $limit, $count, $flags); return new ReplaceResult($count, $result); } private static function checkOffsetCapture(int $flags, string $useFunctionName): void { if (($flags & PREG_OFFSET_CAPTURE) !== 0) { throw new \InvalidArgumentException('PREG_OFFSET_CAPTURE is not supported as it changes the return type, use '.$useFunctionName.'() instead'); } } private static function checkSetOrder(int $flags): void { if (($flags & PREG_SET_ORDER) !== 0) { throw new \InvalidArgumentException('PREG_SET_ORDER is not supported as it changes the return type'); } } } MatchAllStrictGroupsResult.php000064400000001573152427755310012553 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; final class MatchAllStrictGroupsResult { /** * An array of match group => list of matched strings * * @readonly * @var array> */ public $matches; /** * @readonly * @var 0|positive-int */ public $count; /** * @readonly * @var bool */ public $matched; /** * @param 0|positive-int $count * @param array> $matches */ public function __construct(int $count, array $matches) { $this->matches = $matches; $this->matched = (bool) $count; $this->count = $count; } } PcreException.php000064400000002536152427755310010046 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; class PcreException extends \RuntimeException { /** * @param string $function * @param string|string[] $pattern * @return self */ public static function fromFunction($function, $pattern) { $code = preg_last_error(); if (is_array($pattern)) { $pattern = implode(', ', $pattern); } return new PcreException($function.'(): failed executing "'.$pattern.'": '.self::pcreLastErrorMessage($code), $code); } /** * @param int $code * @return string */ private static function pcreLastErrorMessage($code) { if (function_exists('preg_last_error_msg')) { return preg_last_error_msg(); } $constants = get_defined_constants(true); if (!isset($constants['pcre']) || !is_array($constants['pcre'])) { return 'UNDEFINED_ERROR'; } foreach ($constants['pcre'] as $const => $val) { if ($val === $code && substr($const, -6) === '_ERROR') { return $const; } } return 'UNDEFINED_ERROR'; } } MatchAllWithOffsetsResult.php000064400000002231152427755310012340 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; final class MatchAllWithOffsetsResult { /** * An array of match group => list of matches, every match being a pair of string matched + offset in bytes (or -1 if no match) * * @readonly * @var array> * @phpstan-var array}>> */ public $matches; /** * @readonly * @var 0|positive-int */ public $count; /** * @readonly * @var bool */ public $matched; /** * @param 0|positive-int $count * @param array> $matches * @phpstan-param array}>> $matches */ public function __construct(int $count, array $matches) { $this->matches = $matches; $this->matched = (bool) $count; $this->count = $count; } } MatchResult.php000064400000001362152427755310007525 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; final class MatchResult { /** * An array of match group => string matched * * @readonly * @var array */ public $matches; /** * @readonly * @var bool */ public $matched; /** * @param 0|positive-int $count * @param array $matches */ public function __construct(int $count, array $matches) { $this->matches = $matches; $this->matched = (bool) $count; } } Errors/GatewayError.php000064400000000113152427755670011163 0ustar00code = $code; $this->message = $message; $this->httpStatusCode = $httpStatusCode; } public function getHttpStatusCode() { return $this->httpStatusCode; } }Errors/ServerError.php000064400000000112152427755670011027 0ustar00field = $field; } public function getField() { return $this->field; } }Plan.php000064400000000525152427755670006175 0ustar00getEntityUrl(). 'list'; return $this->request('GET', $relativeUrl, $options); } } Addon.php000064400000001054152427755670006326 0ustar00getEntityUrl(); return $this->request('DELETE', $entityUrl . $this->id); } public function fetchAll($attributes = array()) { $entityUrl = $this->getEntityUrl(); return $this->request('GET', $entityUrl , $attributes); } } Utility.php000064400000005206152427755670006747 0ustar00hashEquals($expectedSignature, $actualSignature); } if ($verified === false) { throw new Errors\SignatureVerificationError( 'Invalid signature passed'); } } private function hashEquals($expectedSignature, $actualSignature) { if (strlen($expectedSignature) === strlen($actualSignature)) { $res = $expectedSignature ^ $actualSignature; $return = 0; for ($i = strlen($res) - 1; $i >= 0; $i--) { $return |= ord($res[$i]); } return ($return === 0); } return false; } } ArrayableInterface.php000064400000000261152427755670011023 0ustar00payment_id) === true) { $relativeUrl = 'payments/' . $this->payment_id. '/transfers'; return $this->request('GET', $relativeUrl, $options); } return parent::all($options); } /** * Create a direct transfer from merchant's account to * any of the linked accounts, without linking it to a * payment */ public function create($attributes = array()) { return parent::create($attributes); } public function edit($attributes = null) { $entityUrl = $this->getEntityUrl() . $this->id; return $this->request('PATCH', $entityUrl, $attributes); } /** * Create a reversal for a transfer */ public function reverse($attributes = array()) { $relativeUrl = $this->getEntityUrl() . $this->id . '/reversals'; return $this->request('POST', $relativeUrl, $attributes); } /** * Fetches all reversals */ public function reversals($attributes = array()) { $relativeUrl = $this->getEntityUrl() . $this->id . '/reversals'; return $this->request('GET', $relativeUrl, $attributes); } } QrCode.php000064400000002735152427755670006465 0ustar00getEntityUrl() ; return $this->request('POST', $relativeUrl, $attributes); } /** * Fetch QR code details based QR id * @param $id * @return Entity|QrCode */ public function fetch($id) { $relativeUrl = "payments/". $this->getEntityUrl(). $id ; return $this->request('GET', $relativeUrl); } /** * Close the QR code based on id * @return Entity|QrCode */ public function close() { $relativeUrl = "payments/{$this->getEntityUrl()}{$this->id}/close" ; return $this->request('POST', $relativeUrl); } /** * Fetch all QR code details * @param array $options * @return Entity|QrCode */ public function all($options = array()) { $relativeUrl = "payments/". $this->getEntityUrl(); return $this->request('GET', $relativeUrl, $options); } /** * Fetch payments made to a QR Code based on QR id * @param array $options * @return Entity|QrCode */ public function fetchAllPayments($options = array()) { $relativeUrl = "payments/{$this->getEntityUrl()}{$this->id}/payments" ; return $this->request('GET', $relativeUrl, $options); } } Item.php000064400000001216152427755670006177 0ustar00getEntityUrl() . $this->id; return $this->request('PATCH', $url, $attributes); } public function all($options = array()) { return parent::all($options); } public function delete() { $url = $this->getEntityUrl() . $this->id; return $this->request('DELETE', $url); } } Entity.php000064400000013660152427755670006563 0ustar00getEntityUrl(); return $this->request('POST', $entityUrl, $attributes); } protected function fetch($id) { $entityUrl = $this->getEntityUrl(); $this->validateIdPresence($id); $relativeUrl = $entityUrl . $id; return $this->request('GET', $relativeUrl); } protected function validateIdPresence($id) { if ($id !== null) { return; } $path = explode('\\', get_class($this)); $class = strtolower(array_pop($path)); $message = 'The ' . $class . ' id provided is null'; $code = Errors\ErrorCode::BAD_REQUEST_ERROR; throw new Errors\BadRequestError($message, $code, 500); } protected function all($options = array()) { $entityUrl = $this->getEntityUrl(); return $this->request('GET', $entityUrl, $options); } protected function getEntityUrl() { $fullClassName = get_class($this); $pos = strrpos($fullClassName, '\\'); $className = substr($fullClassName, $pos + 1); $className = $this->snakeCase($className); return $className.'s/'; } protected function snakeCase($input) { $delimiter = '_'; $output = preg_replace('/\s+/u', '', ucwords($input)); $output = preg_replace('/(.)(?=[A-Z])/u', '$1'.$delimiter, $output); $output = strtolower($output); return $output; } /** * Makes a HTTP request using Request class and assuming the API returns * formatted entity or collection result, wraps the returned JSON as entity * and returns. * * @param string $method * @param string $relativeUrl * @param array $data * @param array $additionHeader * @param string $apiVersion * * @return Entity */ protected function request($method, $relativeUrl, $data = null, $apiVersion = "v1") { $request = new Request(); $response = $request->request($method, $relativeUrl, $data, $apiVersion); if ((isset($response['entity'])) and ($response['entity'] == $this->getEntity())) { $this->fill($response); return $this; } else { return static::buildEntity($response); } } /** * Given the JSON response of an API call, wraps it to corresponding entity * class or a collection and returns the same. * * @param array $data * * @return Entity */ protected static function buildEntity($data) { $entities = static::getDefinedEntitiesArray(); if (isset($data['entity'])) { if (in_array($data['entity'], $entities)) { $class = static::getEntityClass($data['entity']); $entity = new $class; } else { $entity = new static; } } else { $entity = new static; } $entity->fill($data); return $entity; } protected static function getDefinedEntitiesArray() { return array( 'collection', 'payment', 'refund', 'order', 'customer', 'token', 'settlement'); } protected static function getEntityClass($name) { return __NAMESPACE__.'\\'.ucfirst($name); } protected function getEntity() { $class = get_class($this); $pos = strrpos($class, '\\'); $entity = strtolower(substr($class, $pos)); return $entity; } public function fill($data) { $attributes = array(); if(is_array($data)) { foreach ($data as $key => $value) { if (is_array($value)) { if (static::isAssocArray($value) === false) { $collection = array(); foreach ($value as $v) { if (is_array($v)) { $entity = static::buildEntity($v); array_push($collection, $entity); } else { array_push($collection, $v); } } $value = $collection; } else { $value = static::buildEntity($value); } } $attributes[$key] = $value; } } $this->attributes = $attributes; } public static function isAssocArray($arr) { return array_keys($arr) !== range(0, count($arr) - 1); } public function toArray() { return $this->convertToArray($this->attributes); } protected function convertToArray($attributes) { $array = $attributes; foreach ($attributes as $key => $value) { if (is_object($value)) { $array[$key] = $value->toArray(); } else if (is_array($value) and self::isAssocArray($value) == false) { $array[$key] = $this->convertToArray($value); } } return $array; } public function setFile($attributes) { if(isset($attributes['file'])){ $attributes['file'] = new \CURLFILE( $attributes['file'], mime_content_type($attributes['file']) ); } return $attributes; } } Payment.php000064400000012237152427755670006723 0ustar00getEntityUrl() . $this->id; return $this->request(Requests::PATCH, $url, $attributes); } /** * @param $id Payment id */ public function refund($attributes = array()) { $refund = new Refund; $attributes = array_merge($attributes, array('payment_id' => $this->id)); return $refund->create($attributes); } /** * @param $id Payment id */ public function capture($attributes = array()) { $relativeUrl = $this->getEntityUrl() . $this->id . '/capture'; return $this->request('POST', $relativeUrl, $attributes); } public function transfer($attributes = array()) { $relativeUrl = $this->getEntityUrl() . $this->id . '/transfers'; return $this->request('POST', $relativeUrl, $attributes); } public function refunds() { $refund = new Refund; $options = array('payment_id' => $this->id); return $refund->all($options); } public function transfers() { $transfer = new Transfer(); $transfer->payment_id = $this->id; return $transfer->all(); } public function bankTransfer() { $relativeUrl = $this->getEntityUrl() . $this->id . '/bank_transfer'; return $this->request('GET', $relativeUrl); } public function fetchMultipleRefund($options = array()) { $relativeUrl = $this->getEntityUrl() . $this->id . '/refunds'; return $this->request('GET', $relativeUrl, $options); } public function fetchRefund($refundId) { $relativeUrl = $this->getEntityUrl() . $this->id . '/refunds/'.$refundId; return $this->request('GET', $relativeUrl); } public function createRecurring($attributes = array()) { $relativeUrl = $this->getEntityUrl() . 'create/recurring'; return $this->request('POST', $relativeUrl, $attributes); } /** * fetch Card Details * * @param id $id * * @return card */ public function fetchCardDetails() { $relativeUrl = $this->getEntityUrl() . $this->id . '/card'; return $this->request('GET', $relativeUrl); } /** * fetchPaymentDowntime * */ public function fetchPaymentDowntime() { $relativeUrl = $this->getEntityUrl() . 'downtimes'; return $this->request('GET', $relativeUrl); } /** * fetch Payment Downtime Id * * @param id $id * * @return card */ public function fetchPaymentDowntimeById($id) { $relativeUrl = $this->getEntityUrl() . 'downtimes' . $id; return $this->request('GET', $relativeUrl); } /** * create Payment Json * * @param array $attributes */ public function createPaymentJson($attributes = array()) { $relativeUrl = $this->getEntityUrl() . 'create/json'; return $this->request('POST', $relativeUrl, $attributes); } /** * Submit otp * * @param id $id * * @param array $attributes */ public function otpSubmit($attributes = array()) { $relativeUrl = $this->getEntityUrl(). $this->id . '/otp/submit'; return $this->request('POST', $relativeUrl, $attributes); } /** * Generate otp * * @param id $id * * @param array $attributes */ public function otpGenerate($id) { $relativeUrl = $this->getEntityUrl(). $id . '/otp_generate'; return $this->request('POST', $relativeUrl); } /** * Resend otp * * @param id $id * * @param array $attributes */ public function otpResend() { $relativeUrl = $this->getEntityUrl(). $this->id . '/otp/resend'; return $this->request('POST', $relativeUrl); } public function createUpi($attributes = array()) { $relativeUrl = $this->getEntityUrl() . 'create/upi'; return $this->request('POST', $relativeUrl, $attributes); } public function validateVpa($attributes = array()) { $relativeUrl = $this->getEntityUrl() . 'validate/vpa'; return $this->request('POST', $relativeUrl, $attributes); } public function fetchPaymentMethods() { $relativeUrl = 'methods'; return $this->request('GET', $relativeUrl); } public function expandedDetails($options = array()) { $relativeUrl = $this->getEntityUrl(). $this->id; return $this->request('GET', $relativeUrl, $options); } } Account.php000064400000003174152427755670006702 0ustar00getEntityUrl(); return $this->request('POST', $entityUrl, $attributes, 'v2'); } public function fetch($id) { $entityUrl = $this->getEntityUrl(); return $this->request('GET', $entityUrl . $id, null, 'v2'); } public function delete() { $entityUrl = $this->getEntityUrl(); return $this->request('DELETE', $entityUrl . $this->id, null, 'v2'); } public function edit($attributes = array()) { $url = $this->getEntityUrl() . $this->id; return $this->request('PATCH', $url, $attributes, 'v2'); } public function stakeholders() { $stakeholder = new Stakeholder(); $stakeholder['account_id'] = $this->id; return $stakeholder; } public function products() { $product = new Product(); $product['account_id'] = $this->id; return $product; } public function webhooks() { $webhook = new Webhook(); $webhook['account_id'] = $this->id; return $webhook; } public function uploadAccountDoc($attributes = array()) { $attributes = $this->setFile($attributes); $entityUrl = $this->getEntityUrl() .$this->id .'/documents'; return $this->request('POST', $entityUrl, $attributes, 'v2'); } public function fetchAccountDoc() { $entityUrl = $this->getEntityUrl() .$this->id .'/documents'; return $this->request('GET', $entityUrl, null, 'v2'); } } Order.php000064400000002630152427755670006355 0ustar00getEntityUrl() . $this->id; return $this->request('PATCH', $url, $attributes); } public function payments() { $relativeUrl = $this->getEntityUrl().$this->id.'/payments'; return $this->request('GET', $relativeUrl); } public function transfers($options = array()) { $relativeUrl = $this->getEntityUrl().$this->id; return $this->request('GET', $relativeUrl, $options); } public function viewRtoReview() { $relativeUrl = $this->getEntityUrl(). $this->id .'/rto_review'; return $this->request('POST', $relativeUrl); } public function editFulfillment($attributes = array()) { $relativeUrl = $this->getEntityUrl(). $this->id .'/fulfillment'; return $this->request('POST', $relativeUrl, $attributes); } } Api.php000064400000003230152427755670006010 0ustar00 $title, 'version' => $version ); array_push(self::$appsDetails, $app); } public function getAppsDetails() { return self::$appsDetails; } public function setBaseUrl($baseUrl) { self::$baseUrl = $baseUrl; } /** * @param string $name * @return mixed */ public function __get($name) { $className = __NAMESPACE__.'\\'.ucwords($name); $entity = new $className(); return $entity; } public static function getBaseUrl() { return self::$baseUrl; } public static function getKey() { return self::$key; } public static function getSecret() { return self::$secret; } public static function getFullUrl($relativeUrl, $apiVersion = "v1") { return self::getBaseUrl() . "/". $apiVersion . "/". $relativeUrl; } } Customer.php000064400000002740152427755670007105 0ustar00getEntityUrl().$this->id; return $this->request('PUT', $entityUrl, $attributes); } public function tokens() { $token = new Token(); $token['customer_id'] = $this->id; return $token; } public function addBankAccount($attributes = array()) { $entityUrl = $this->getEntityUrl().$this->id. '/bank_account'; return $this->request('POST', $entityUrl, $attributes); } public function deleteBankAccount($bank_id) { $entityUrl = $this->getEntityUrl() . $this->id. '/bank_account/'. $bank_id; return $this->request('DELETE', $entityUrl); } public function requestEligibilityCheck($attributes = array()) { $entityUrl = $this->getEntityUrl(). '/eligibility'; return $this->request('POST', $entityUrl, $attributes); } public function fetchEligibility($id) { $entityUrl = $this->getEntityUrl(). '/eligibility/'. $id; return $this->request('GET', $entityUrl); } } Subscription.php000064400000004306152427755670007770 0ustar00getEntityUrl() . $this->id . '/cancel'; return $this->request('POST', $relativeUrl, $attributes); } public function createAddon($attributes = array()) { $relativeUrl = $this->getEntityUrl() . $this->id . '/addons'; return $this->request('POST', $relativeUrl, $attributes); } /** * Create a Registration Link * @param array $attributes * @return array */ public function createSubscriptionRegistration($attributes = array()) { $relativeUrl = 'subscription_registration/auth_links'; return $this->request('POST', $relativeUrl, $attributes); } public function update($attributes = array()) { $relativeUrl = $this->getEntityUrl() . $this->id; return $this->request('PATCH', $relativeUrl, $attributes); } public function pendingUpdate() { $relativeUrl = $this->getEntityUrl() . $this->id . '/retrieve_scheduled_changes'; return $this->request('GET', $relativeUrl, null); } public function cancelScheduledChanges() { $relativeUrl = $this->getEntityUrl() . $this->id . '/cancel_scheduled_changes'; return $this->request('POST', $relativeUrl, null); } public function pause($attributes = array()) { $relativeUrl = $this->getEntityUrl() . $this->id.'/pause'; return $this->request('POST', $relativeUrl, $attributes); } public function resume($attributes = array()) { $relativeUrl = $this->getEntityUrl() . $this->id.'/resume'; return $this->request('POST', $relativeUrl, $attributes); } public function deleteOffer($offerId) { $relativeUrl = $this->getEntityUrl() . $this->id.'/'.$offerId; return $this->request('DELETE', $relativeUrl); } } Collection.php000064400000000464152427755670007400 0ustar00attributes['count'])) { return $this->attributes['count']; } return $count; } } VirtualAccount.php000064400000002523152427755670010246 0ustar00getEntityUrl() . $this->id . '/close'; return $this->request('POST', $relativeUrl); } public function payments($options = array()) { $relativeUrl = $this->getEntityUrl() . $this->id . '/payments'; return $this->request('GET', $relativeUrl, $options); } public function addReceiver($attributes = array()) { $relativeUrl = $this->getEntityUrl() . $this->id . '/receivers'; return $this->request('POST', $relativeUrl, $attributes); } public function addAllowedPayer($attributes = array()) { $relativeUrl = $this->getEntityUrl() . $this->id . '/allowed_payers'; return $this->request('POST', $relativeUrl, $attributes); } public function deleteAllowedPayer($allowedPlayerId) { $relativeUrl = $this->getEntityUrl() . $this->id . '/allowed_payers/'.$allowedPlayerId; return $this->request('DELETE', $relativeUrl); } }Resource.php000064400000002206152427755670007070 0ustar00attributes); } public function offsetExists($offset): bool { return (isset($this->attributes[$offset])); } public function offsetSet($offset, $value): void { $this->attributes[$offset] = $value; } #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->attributes[$offset]; } public function offsetUnset($offset): void { unset($this->attributes[$offset]); } public function __get($key) { return $this->attributes[$key]; } public function __set($key, $value) { return $this->attributes[$key] = $value; } public function __isset($key) { return (isset($this->attributes[$key])); } public function __unset($key) { unset($this->attributes[$key]); } }PaymentPage.php000064400000001106152427755670007511 0ustar00getEntityUrl() . $id . '/activate'; return $this->request('PATCH', $relativeUrl); } public function deactivate($id) { $relativeUrl = $this->getEntityUrl() . $id . '/deactivate'; return $this->request('PATCH', $relativeUrl); } }Request.php000064400000007504152427755670006737 0ustar00assertMethod($method); if (!($uri instanceof UriInterface)) { $uri = new Uri($uri); } $this->method = strtoupper($method); $this->uri = $uri; $this->setHeaders($headers); $this->protocol = $version; if (!isset($this->headerNames['host'])) { $this->updateHostFromUri(); } if ($body !== '' && $body !== null) { $this->stream = Utils::streamFor($body); } } public function getRequestTarget(): string { if ($this->requestTarget !== null) { return $this->requestTarget; } $target = $this->uri->getPath(); if ($target === '') { $target = '/'; } if ($this->uri->getQuery() != '') { $target .= '?'.$this->uri->getQuery(); } return $target; } public function withRequestTarget($requestTarget): RequestInterface { if (preg_match('#\s#', $requestTarget)) { throw new InvalidArgumentException( 'Invalid request target provided; cannot contain whitespace' ); } $new = clone $this; $new->requestTarget = $requestTarget; return $new; } public function getMethod(): string { return $this->method; } public function withMethod($method): RequestInterface { $this->assertMethod($method); $new = clone $this; $new->method = strtoupper($method); return $new; } public function getUri(): UriInterface { return $this->uri; } public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface { if ($uri === $this->uri) { return $this; } $new = clone $this; $new->uri = $uri; if (!$preserveHost || !isset($this->headerNames['host'])) { $new->updateHostFromUri(); } return $new; } private function updateHostFromUri(): void { $host = $this->uri->getHost(); if ($host == '') { return; } if (($port = $this->uri->getPort()) !== null) { $host .= ':'.$port; } if (isset($this->headerNames['host'])) { $header = $this->headerNames['host']; } else { $header = 'Host'; $this->headerNames['host'] = 'Host'; } // Ensure Host is the first header. // See: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 $this->headers = [$header => [$host]] + $this->headers; } /** * @param mixed $method */ private function assertMethod($method): void { if (!is_string($method) || $method === '') { throw new InvalidArgumentException('Method must be a non-empty string.'); } } } Refund.php000064400000001360152427755670006524 0ustar00getEntityUrl() . $this->id; return $this->request('PATCH', $url, $attributes); } public function refund($options = array()) { $relativeUrl = $this->getEntityUrl() . $this->id . '/refund'; return $this->request('POST', $relativeUrl, $options); } }FundAccount.php000064400000001012152427755670007504 0ustar00getEntityUrl() ."ondemand" ; return $this->request('POST', $relativeUrl, $attributes); } /** * Fetch single settlement entity * @param string $id * @return Settlement */ public function fetch($id) { return parent::fetch($id); } /** * Get all settlements according to options * @param array $options * @return Collection */ public function all($options = array()) { return parent::all($options); } /** * Get combined report of settlements * @param array $options * @return array */ public function reports($options = array()) { $relativeUrl = $this->getEntityUrl() . 'report/combined'; return $this->request('GET', $relativeUrl, $options); } /** * Get Settlement Recon * @param array $options * @return array */ public function settlementRecon($options = array()) { $relativeUrl = $this->getEntityUrl() . 'recon/combined'; return $this->request('GET', $relativeUrl, $options); } /** * fetch Ondemand Settlement by Id * @param string $id * @param array $options * @return array */ public function fetchOndemandSettlementById($id, $options = array()) { $relativeUrl = $this->getEntityUrl(). "ondemand/" . $id; return $this->request('GET', $relativeUrl, $options); } /** * fetch all Ondemand Settlement * @return array */ public function fetchAllOndemandSettlement($options = array()) { $relativeUrl = $this->getEntityUrl(). "ondemand/"; return $this->request('GET', $relativeUrl, $options); } } Token.php000064400000002745152427755670006371 0ustar00getEntityUrl(); return $this->request('POST', $url, $attributes); } /** * @param $id Token id */ public function fetch($id) { $relativeUrl = 'customers/'.$this->customer_id.'/'.$this->getEntityUrl().$id; return $this->request('GET', $relativeUrl); } public function fetchCardPropertiesByToken($attributes = array()) { $relativeUrl = $this->getEntityUrl(). '/fetch'; return $this->request('POST', $relativeUrl, $attributes); } public function all($options = array()) { $relativeUrl = 'customers/'.$this->customer_id.'/'.$this->getEntityUrl(); return $this->request('GET', $relativeUrl, $options); } public function delete($id) { $relativeUrl = 'customers/'.$this->customer_id.'/'.$this->getEntityUrl().$id; return $this->request('DELETE', $relativeUrl); } public function deleteToken($attributes = array()) { $relativeUrl = $this->getEntityUrl(). '/delete'; return $this->request('POST', $relativeUrl, $attributes); } public function processPaymentOnAlternatePAorPG($attributes = array()) { $relativeUrl = $this->getEntityUrl().'service_provider_tokens/token_transactional_data'; return $this->request('POST', $relativeUrl, $attributes); } } Dispute.php000064400000001275152427755670006723 0ustar00getEntityUrl(). $this->id. '/accept'; return $this->request('POST', $entityUrl); } public function contest($attributes = array()) { $entityUrl = $this->getEntityUrl(). $this->id. '/contest'; return $this->request('PATCH', $entityUrl, $attributes); } }Card.php000064400000000602152427755670006150 0ustar00getEntityUrl() . '/fingerprints'; return $this->request('POST', $entityUrl, $attributes); } } Stakeholder.php000064400000002631152427755670007550 0ustar00account_id .'/'.$this->getEntityUrl(); return $this->request('POST', $url, $attributes, 'v2'); } public function fetch($id) { $entityUrl = 'accounts/'.$this->account_id .'/'.$this->getEntityUrl().'/'.$id; return $this->request('GET', $entityUrl, null, 'v2'); } public function all($options = array()) { $relativeUrl = 'accounts/'.$this->account_id.'/'.$this->getEntityUrl(); return $this->request('GET', $relativeUrl, $options, 'v2'); } public function edit($id, $attributes = array()) { $entityUrl = 'accounts/'.$this->account_id .'/'.$this->getEntityUrl().'/'.$id; return $this->request('PATCH', $entityUrl, $attributes, 'v2'); } public function uploadStakeholderDoc($id, $attributes = array()) { $attributes = $this->setFile($attributes); $entityUrl = 'accounts/'.$this->account_id .'/'.$this->getEntityUrl().'/'.$id.'/documents'; return $this->request('POST', $entityUrl, $attributes, 'v2'); } public function fetchStakeholderDoc($id) { $entityUrl = 'accounts/'.$this->account_id .'/'.$this->getEntityUrl().'/'.$id.'/documents'; return $this->request('GET', $entityUrl, null, 'v2'); } } PaymentLink.php000064400000003367152427755670007545 0ustar00getEntityUrl() . $this->id . '/cancel'; return $this->request(Requests::POST, $url); } public function edit($attributes = array()) { $relativeUrl = $this->getEntityUrl() . $this->id; $attributes = json_encode($attributes); Request::addHeader('Content-Type', 'application/json'); return $this->request('PATCH', $relativeUrl, $attributes); } /** * Send/re-send notification with short url by given medium * * @param $medium - sms|email * * @return array */ public function notifyBy($medium) { $url = $this->getEntityUrl() . $this->id . '/notify_by/' . $medium; $r = new Request(); return $r->request(Requests::POST, $url); } } Webhook.php000064400000003371152427755670006703 0ustar00account_id)) { $url = 'accounts/'. $this->account_id . '/' .$this->getEntityUrl(); return $this->request('POST', $url, $attributes, 'v2'); } return parent::create($attributes); } public function fetch($id) { if(isset($this->account_id)) { $url = 'accounts/'. $this->account_id . '/' .$this->getEntityUrl() . $id; return $this->request('GET', $url, null, 'v2'); } return parent::fetch($id); } public function all($options = array()) { if(isset($this->account_id)) { $url = 'accounts/'. $this->account_id . '/' .$this->getEntityUrl(); return $this->request('GET', $url, $options, 'v2'); } return parent::all($options); } /** * Patches given webhook with new attributes * * @param array $attributes * @param string $id * @return Webhook */ public function edit($attributes, $id) { $url = $this->getEntityUrl() . $id; if(isset($this->account_id)) { $url = 'accounts/'.$this->account_id .'/'. $url; return $this->request('PATCH', $url, $attributes, 'v2'); } return $this->request(Requests::PUT, $url, $attributes); } public function delete($id) { $url = 'accounts/'. $this->account_id . '/' .$this->getEntityUrl(). $id; return $this->request('DELETE', $url, null, 'v2'); } } Document.php000064400000000461152427755670007060 0ustar00setFile($attributes); return parent::create($attributes); } public function fetch($id) { return parent::fetch($id); } } Product.php000064400000001617152427755670006726 0ustar00account_id .'/'.$this->getEntityUrl(); return $this->request('POST', $url, $attributes, 'v2'); } public function fetch($id) { $entityUrl = 'accounts/'.$this->account_id .'/'.$this->getEntityUrl().'/'.$id; return $this->request('GET', $entityUrl, null, 'v2'); } public function edit($id, $attributes = array()) { $entityUrl = 'accounts/'.$this->account_id .'/'.$this->getEntityUrl().'/'.$id; return $this->request('PATCH', $entityUrl, $attributes, 'v2'); } public function fetchTnc($product_name) { $entityUrl = $this->getEntityUrl().'/'.$product_name.'/tnc'; return $this->request('GET', $entityUrl,null , 'v2'); } } Invoice.php000064400000004503152427755670006677 0ustar00getEntityUrl() . $this->id . '/cancel'; return $this->request(Requests::POST, $url); } /** * Send/re-send notification for invoice by given medium * * @param $medium - sms|email * * @return array */ public function notifyBy($medium) { $url = $this->getEntityUrl() . $this->id . '/notify_by/' . $medium; $r = new Request(); return $r->request(Requests::POST, $url); } /** * Patches given invoice with new attributes * * @param array $attributes * * @return Invoice */ public function edit($attributes = array()) { $url = $this->getEntityUrl() . $this->id; return $this->request(Requests::PATCH, $url, $attributes); } /** * Issues drafted invoice * * @return Invoice */ public function issue() { $url = $this->getEntityUrl() . $this->id . '/issue'; return $this->request(Requests::POST, $url); } /** * Deletes drafted invoice * * @return Invoice */ public function delete() { $url = $this->getEntityUrl() . $this->id; $r = new Request(); return $r->request(Requests::DELETE, $url); } } Exception/MalformedUriException.php000064400000000365152430110220013452 0ustar00 'application/vnd.1000minds.decision-model+xml', '3dml' => 'text/vnd.in3d.3dml', '3ds' => 'image/x-3ds', '3g2' => 'video/3gpp2', '3gp' => 'video/3gp', '3gpp' => 'video/3gpp', '3mf' => 'model/3mf', '7z' => 'application/x-7z-compressed', '7zip' => 'application/x-7z-compressed', '123' => 'application/vnd.lotus-1-2-3', 'aab' => 'application/x-authorware-bin', 'aac' => 'audio/aac', 'aam' => 'application/x-authorware-map', 'aas' => 'application/x-authorware-seg', 'abw' => 'application/x-abiword', 'ac' => 'application/vnd.nokia.n-gage.ac+xml', 'ac3' => 'audio/ac3', 'acc' => 'application/vnd.americandynamics.acc', 'ace' => 'application/x-ace-compressed', 'acu' => 'application/vnd.acucobol', 'acutc' => 'application/vnd.acucorp', 'adp' => 'audio/adpcm', 'adts' => 'audio/aac', 'aep' => 'application/vnd.audiograph', 'afm' => 'application/x-font-type1', 'afp' => 'application/vnd.ibm.modcap', 'age' => 'application/vnd.age', 'ahead' => 'application/vnd.ahead.space', 'ai' => 'application/pdf', 'aif' => 'audio/x-aiff', 'aifc' => 'audio/x-aiff', 'aiff' => 'audio/x-aiff', 'air' => 'application/vnd.adobe.air-application-installer-package+zip', 'ait' => 'application/vnd.dvb.ait', 'ami' => 'application/vnd.amiga.ami', 'aml' => 'application/automationml-aml+xml', 'amlx' => 'application/automationml-amlx+zip', 'amr' => 'audio/amr', 'apk' => 'application/vnd.android.package-archive', 'apng' => 'image/apng', 'appcache' => 'text/cache-manifest', 'appinstaller' => 'application/appinstaller', 'application' => 'application/x-ms-application', 'appx' => 'application/appx', 'appxbundle' => 'application/appxbundle', 'apr' => 'application/vnd.lotus-approach', 'arc' => 'application/x-freearc', 'arj' => 'application/x-arj', 'asc' => 'application/pgp-signature', 'asf' => 'video/x-ms-asf', 'asm' => 'text/x-asm', 'aso' => 'application/vnd.accpac.simply.aso', 'asx' => 'video/x-ms-asf', 'atc' => 'application/vnd.acucorp', 'atom' => 'application/atom+xml', 'atomcat' => 'application/atomcat+xml', 'atomdeleted' => 'application/atomdeleted+xml', 'atomsvc' => 'application/atomsvc+xml', 'atx' => 'application/vnd.antix.game-component', 'au' => 'audio/x-au', 'avci' => 'image/avci', 'avcs' => 'image/avcs', 'avi' => 'video/x-msvideo', 'avif' => 'image/avif', 'aw' => 'application/applixware', 'azf' => 'application/vnd.airzip.filesecure.azf', 'azs' => 'application/vnd.airzip.filesecure.azs', 'azv' => 'image/vnd.airzip.accelerator.azv', 'azw' => 'application/vnd.amazon.ebook', 'b16' => 'image/vnd.pco.b16', 'bat' => 'application/x-msdownload', 'bcpio' => 'application/x-bcpio', 'bdf' => 'application/x-font-bdf', 'bdm' => 'application/vnd.syncml.dm+wbxml', 'bdoc' => 'application/x-bdoc', 'bed' => 'application/vnd.realvnc.bed', 'bh2' => 'application/vnd.fujitsu.oasysprs', 'bin' => 'application/octet-stream', 'blb' => 'application/x-blorb', 'blorb' => 'application/x-blorb', 'bmi' => 'application/vnd.bmi', 'bmml' => 'application/vnd.balsamiq.bmml+xml', 'bmp' => 'image/bmp', 'book' => 'application/vnd.framemaker', 'box' => 'application/vnd.previewsystems.box', 'boz' => 'application/x-bzip2', 'bpk' => 'application/octet-stream', 'bpmn' => 'application/octet-stream', 'bsp' => 'model/vnd.valve.source.compiled-map', 'btf' => 'image/prs.btif', 'btif' => 'image/prs.btif', 'buffer' => 'application/octet-stream', 'bz' => 'application/x-bzip', 'bz2' => 'application/x-bzip2', 'c' => 'text/x-c', 'c4d' => 'application/vnd.clonk.c4group', 'c4f' => 'application/vnd.clonk.c4group', 'c4g' => 'application/vnd.clonk.c4group', 'c4p' => 'application/vnd.clonk.c4group', 'c4u' => 'application/vnd.clonk.c4group', 'c11amc' => 'application/vnd.cluetrust.cartomobile-config', 'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg', 'cab' => 'application/vnd.ms-cab-compressed', 'caf' => 'audio/x-caf', 'cap' => 'application/vnd.tcpdump.pcap', 'car' => 'application/vnd.curl.car', 'cat' => 'application/vnd.ms-pki.seccat', 'cb7' => 'application/x-cbr', 'cba' => 'application/x-cbr', 'cbr' => 'application/x-cbr', 'cbt' => 'application/x-cbr', 'cbz' => 'application/x-cbr', 'cc' => 'text/x-c', 'cco' => 'application/x-cocoa', 'cct' => 'application/x-director', 'ccxml' => 'application/ccxml+xml', 'cdbcmsg' => 'application/vnd.contact.cmsg', 'cdf' => 'application/x-netcdf', 'cdfx' => 'application/cdfx+xml', 'cdkey' => 'application/vnd.mediastation.cdkey', 'cdmia' => 'application/cdmi-capability', 'cdmic' => 'application/cdmi-container', 'cdmid' => 'application/cdmi-domain', 'cdmio' => 'application/cdmi-object', 'cdmiq' => 'application/cdmi-queue', 'cdr' => 'application/cdr', 'cdx' => 'chemical/x-cdx', 'cdxml' => 'application/vnd.chemdraw+xml', 'cdy' => 'application/vnd.cinderella', 'cer' => 'application/pkix-cert', 'cfs' => 'application/x-cfs-compressed', 'cgm' => 'image/cgm', 'chat' => 'application/x-chat', 'chm' => 'application/vnd.ms-htmlhelp', 'chrt' => 'application/vnd.kde.kchart', 'cif' => 'chemical/x-cif', 'cii' => 'application/vnd.anser-web-certificate-issue-initiation', 'cil' => 'application/vnd.ms-artgalry', 'cjs' => 'application/node', 'cla' => 'application/vnd.claymore', 'class' => 'application/octet-stream', 'cld' => 'model/vnd.cld', 'clkk' => 'application/vnd.crick.clicker.keyboard', 'clkp' => 'application/vnd.crick.clicker.palette', 'clkt' => 'application/vnd.crick.clicker.template', 'clkw' => 'application/vnd.crick.clicker.wordbank', 'clkx' => 'application/vnd.crick.clicker', 'clp' => 'application/x-msclip', 'cmc' => 'application/vnd.cosmocaller', 'cmdf' => 'chemical/x-cmdf', 'cml' => 'chemical/x-cml', 'cmp' => 'application/vnd.yellowriver-custom-menu', 'cmx' => 'image/x-cmx', 'cod' => 'application/vnd.rim.cod', 'coffee' => 'text/coffeescript', 'com' => 'application/x-msdownload', 'conf' => 'text/plain', 'cpio' => 'application/x-cpio', 'cpl' => 'application/cpl+xml', 'cpp' => 'text/x-c', 'cpt' => 'application/mac-compactpro', 'crd' => 'application/x-mscardfile', 'crl' => 'application/pkix-crl', 'crt' => 'application/x-x509-ca-cert', 'crx' => 'application/x-chrome-extension', 'cryptonote' => 'application/vnd.rig.cryptonote', 'csh' => 'application/x-csh', 'csl' => 'application/vnd.citationstyles.style+xml', 'csml' => 'chemical/x-csml', 'csp' => 'application/vnd.commonspace', 'csr' => 'application/octet-stream', 'css' => 'text/css', 'cst' => 'application/x-director', 'csv' => 'text/csv', 'cu' => 'application/cu-seeme', 'curl' => 'text/vnd.curl', 'cwl' => 'application/cwl', 'cww' => 'application/prs.cww', 'cxt' => 'application/x-director', 'cxx' => 'text/x-c', 'dae' => 'model/vnd.collada+xml', 'daf' => 'application/vnd.mobius.daf', 'dart' => 'application/vnd.dart', 'dataless' => 'application/vnd.fdsn.seed', 'davmount' => 'application/davmount+xml', 'dbf' => 'application/vnd.dbf', 'dbk' => 'application/docbook+xml', 'dcr' => 'application/x-director', 'dcurl' => 'text/vnd.curl.dcurl', 'dd2' => 'application/vnd.oma.dd2+xml', 'ddd' => 'application/vnd.fujixerox.ddd', 'ddf' => 'application/vnd.syncml.dmddf+xml', 'dds' => 'image/vnd.ms-dds', 'deb' => 'application/x-debian-package', 'def' => 'text/plain', 'deploy' => 'application/octet-stream', 'der' => 'application/x-x509-ca-cert', 'dfac' => 'application/vnd.dreamfactory', 'dgc' => 'application/x-dgc-compressed', 'dib' => 'image/bmp', 'dic' => 'text/x-c', 'dir' => 'application/x-director', 'dis' => 'application/vnd.mobius.dis', 'disposition-notification' => 'message/disposition-notification', 'dist' => 'application/octet-stream', 'distz' => 'application/octet-stream', 'djv' => 'image/vnd.djvu', 'djvu' => 'image/vnd.djvu', 'dll' => 'application/octet-stream', 'dmg' => 'application/x-apple-diskimage', 'dmn' => 'application/octet-stream', 'dmp' => 'application/vnd.tcpdump.pcap', 'dms' => 'application/octet-stream', 'dna' => 'application/vnd.dna', 'doc' => 'application/msword', 'docm' => 'application/vnd.ms-word.template.macroEnabled.12', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'dot' => 'application/msword', 'dotm' => 'application/vnd.ms-word.template.macroEnabled.12', 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'dp' => 'application/vnd.osgi.dp', 'dpg' => 'application/vnd.dpgraph', 'dpx' => 'image/dpx', 'dra' => 'audio/vnd.dra', 'drle' => 'image/dicom-rle', 'dsc' => 'text/prs.lines.tag', 'dssc' => 'application/dssc+der', 'dtb' => 'application/x-dtbook+xml', 'dtd' => 'application/xml-dtd', 'dts' => 'audio/vnd.dts', 'dtshd' => 'audio/vnd.dts.hd', 'dump' => 'application/octet-stream', 'dvb' => 'video/vnd.dvb.file', 'dvi' => 'application/x-dvi', 'dwd' => 'application/atsc-dwd+xml', 'dwf' => 'model/vnd.dwf', 'dwg' => 'image/vnd.dwg', 'dxf' => 'image/vnd.dxf', 'dxp' => 'application/vnd.spotfire.dxp', 'dxr' => 'application/x-director', 'ear' => 'application/java-archive', 'ecelp4800' => 'audio/vnd.nuera.ecelp4800', 'ecelp7470' => 'audio/vnd.nuera.ecelp7470', 'ecelp9600' => 'audio/vnd.nuera.ecelp9600', 'ecma' => 'application/ecmascript', 'edm' => 'application/vnd.novadigm.edm', 'edx' => 'application/vnd.novadigm.edx', 'efif' => 'application/vnd.picsel', 'ei6' => 'application/vnd.pg.osasli', 'elc' => 'application/octet-stream', 'emf' => 'image/emf', 'eml' => 'message/rfc822', 'emma' => 'application/emma+xml', 'emotionml' => 'application/emotionml+xml', 'emz' => 'application/x-msmetafile', 'eol' => 'audio/vnd.digital-winds', 'eot' => 'application/vnd.ms-fontobject', 'eps' => 'application/postscript', 'epub' => 'application/epub+zip', 'es3' => 'application/vnd.eszigno3+xml', 'esa' => 'application/vnd.osgi.subsystem', 'esf' => 'application/vnd.epson.esf', 'et3' => 'application/vnd.eszigno3+xml', 'etx' => 'text/x-setext', 'eva' => 'application/x-eva', 'evy' => 'application/x-envoy', 'exe' => 'application/octet-stream', 'exi' => 'application/exi', 'exp' => 'application/express', 'exr' => 'image/aces', 'ext' => 'application/vnd.novadigm.ext', 'ez' => 'application/andrew-inset', 'ez2' => 'application/vnd.ezpix-album', 'ez3' => 'application/vnd.ezpix-package', 'f' => 'text/x-fortran', 'f4v' => 'video/mp4', 'f77' => 'text/x-fortran', 'f90' => 'text/x-fortran', 'fbs' => 'image/vnd.fastbidsheet', 'fcdt' => 'application/vnd.adobe.formscentral.fcdt', 'fcs' => 'application/vnd.isac.fcs', 'fdf' => 'application/vnd.fdf', 'fdt' => 'application/fdt+xml', 'fe_launch' => 'application/vnd.denovo.fcselayout-link', 'fg5' => 'application/vnd.fujitsu.oasysgp', 'fgd' => 'application/x-director', 'fh' => 'image/x-freehand', 'fh4' => 'image/x-freehand', 'fh5' => 'image/x-freehand', 'fh7' => 'image/x-freehand', 'fhc' => 'image/x-freehand', 'fig' => 'application/x-xfig', 'fits' => 'image/fits', 'flac' => 'audio/x-flac', 'fli' => 'video/x-fli', 'flo' => 'application/vnd.micrografx.flo', 'flv' => 'video/x-flv', 'flw' => 'application/vnd.kde.kivio', 'flx' => 'text/vnd.fmi.flexstor', 'fly' => 'text/vnd.fly', 'fm' => 'application/vnd.framemaker', 'fnc' => 'application/vnd.frogans.fnc', 'fo' => 'application/vnd.software602.filler.form+xml', 'for' => 'text/x-fortran', 'fpx' => 'image/vnd.fpx', 'frame' => 'application/vnd.framemaker', 'fsc' => 'application/vnd.fsc.weblaunch', 'fst' => 'image/vnd.fst', 'ftc' => 'application/vnd.fluxtime.clip', 'fti' => 'application/vnd.anser-web-funds-transfer-initiation', 'fvt' => 'video/vnd.fvt', 'fxp' => 'application/vnd.adobe.fxp', 'fxpl' => 'application/vnd.adobe.fxp', 'fzs' => 'application/vnd.fuzzysheet', 'g2w' => 'application/vnd.geoplan', 'g3' => 'image/g3fax', 'g3w' => 'application/vnd.geospace', 'gac' => 'application/vnd.groove-account', 'gam' => 'application/x-tads', 'gbr' => 'application/rpki-ghostbusters', 'gca' => 'application/x-gca-compressed', 'gdl' => 'model/vnd.gdl', 'gdoc' => 'application/vnd.google-apps.document', 'ged' => 'text/vnd.familysearch.gedcom', 'geo' => 'application/vnd.dynageo', 'geojson' => 'application/geo+json', 'gex' => 'application/vnd.geometry-explorer', 'ggb' => 'application/vnd.geogebra.file', 'ggt' => 'application/vnd.geogebra.tool', 'ghf' => 'application/vnd.groove-help', 'gif' => 'image/gif', 'gim' => 'application/vnd.groove-identity-message', 'glb' => 'model/gltf-binary', 'gltf' => 'model/gltf+json', 'gml' => 'application/gml+xml', 'gmx' => 'application/vnd.gmx', 'gnumeric' => 'application/x-gnumeric', 'gpg' => 'application/gpg-keys', 'gph' => 'application/vnd.flographit', 'gpx' => 'application/gpx+xml', 'gqf' => 'application/vnd.grafeq', 'gqs' => 'application/vnd.grafeq', 'gram' => 'application/srgs', 'gramps' => 'application/x-gramps-xml', 'gre' => 'application/vnd.geometry-explorer', 'grv' => 'application/vnd.groove-injector', 'grxml' => 'application/srgs+xml', 'gsf' => 'application/x-font-ghostscript', 'gsheet' => 'application/vnd.google-apps.spreadsheet', 'gslides' => 'application/vnd.google-apps.presentation', 'gtar' => 'application/x-gtar', 'gtm' => 'application/vnd.groove-tool-message', 'gtw' => 'model/vnd.gtw', 'gv' => 'text/vnd.graphviz', 'gxf' => 'application/gxf', 'gxt' => 'application/vnd.geonext', 'gz' => 'application/gzip', 'gzip' => 'application/gzip', 'h' => 'text/x-c', 'h261' => 'video/h261', 'h263' => 'video/h263', 'h264' => 'video/h264', 'hal' => 'application/vnd.hal+xml', 'hbci' => 'application/vnd.hbci', 'hbs' => 'text/x-handlebars-template', 'hdd' => 'application/x-virtualbox-hdd', 'hdf' => 'application/x-hdf', 'heic' => 'image/heic', 'heics' => 'image/heic-sequence', 'heif' => 'image/heif', 'heifs' => 'image/heif-sequence', 'hej2' => 'image/hej2k', 'held' => 'application/atsc-held+xml', 'hh' => 'text/x-c', 'hjson' => 'application/hjson', 'hlp' => 'application/winhlp', 'hpgl' => 'application/vnd.hp-hpgl', 'hpid' => 'application/vnd.hp-hpid', 'hps' => 'application/vnd.hp-hps', 'hqx' => 'application/mac-binhex40', 'hsj2' => 'image/hsj2', 'htc' => 'text/x-component', 'htke' => 'application/vnd.kenameaapp', 'htm' => 'text/html', 'html' => 'text/html', 'hvd' => 'application/vnd.yamaha.hv-dic', 'hvp' => 'application/vnd.yamaha.hv-voice', 'hvs' => 'application/vnd.yamaha.hv-script', 'i2g' => 'application/vnd.intergeo', 'icc' => 'application/vnd.iccprofile', 'ice' => 'x-conference/x-cooltalk', 'icm' => 'application/vnd.iccprofile', 'ico' => 'image/x-icon', 'ics' => 'text/calendar', 'ief' => 'image/ief', 'ifb' => 'text/calendar', 'ifm' => 'application/vnd.shana.informed.formdata', 'iges' => 'model/iges', 'igl' => 'application/vnd.igloader', 'igm' => 'application/vnd.insors.igm', 'igs' => 'model/iges', 'igx' => 'application/vnd.micrografx.igx', 'iif' => 'application/vnd.shana.informed.interchange', 'img' => 'application/octet-stream', 'imp' => 'application/vnd.accpac.simply.imp', 'ims' => 'application/vnd.ms-ims', 'in' => 'text/plain', 'ini' => 'text/plain', 'ink' => 'application/inkml+xml', 'inkml' => 'application/inkml+xml', 'install' => 'application/x-install-instructions', 'iota' => 'application/vnd.astraea-software.iota', 'ipfix' => 'application/ipfix', 'ipk' => 'application/vnd.shana.informed.package', 'irm' => 'application/vnd.ibm.rights-management', 'irp' => 'application/vnd.irepository.package+xml', 'iso' => 'application/x-iso9660-image', 'itp' => 'application/vnd.shana.informed.formtemplate', 'its' => 'application/its+xml', 'ivp' => 'application/vnd.immervision-ivp', 'ivu' => 'application/vnd.immervision-ivu', 'jad' => 'text/vnd.sun.j2me.app-descriptor', 'jade' => 'text/jade', 'jam' => 'application/vnd.jam', 'jar' => 'application/java-archive', 'jardiff' => 'application/x-java-archive-diff', 'java' => 'text/x-java-source', 'jhc' => 'image/jphc', 'jisp' => 'application/vnd.jisp', 'jls' => 'image/jls', 'jlt' => 'application/vnd.hp-jlyt', 'jng' => 'image/x-jng', 'jnlp' => 'application/x-java-jnlp-file', 'joda' => 'application/vnd.joost.joda-archive', 'jp2' => 'image/jp2', 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'jpf' => 'image/jpx', 'jpg' => 'image/jpeg', 'jpg2' => 'image/jp2', 'jpgm' => 'video/jpm', 'jpgv' => 'video/jpeg', 'jph' => 'image/jph', 'jpm' => 'video/jpm', 'jpx' => 'image/jpx', 'js' => 'application/javascript', 'json' => 'application/json', 'json5' => 'application/json5', 'jsonld' => 'application/ld+json', 'jsonml' => 'application/jsonml+json', 'jsx' => 'text/jsx', 'jt' => 'model/jt', 'jxr' => 'image/jxr', 'jxra' => 'image/jxra', 'jxrs' => 'image/jxrs', 'jxs' => 'image/jxs', 'jxsc' => 'image/jxsc', 'jxsi' => 'image/jxsi', 'jxss' => 'image/jxss', 'kar' => 'audio/midi', 'karbon' => 'application/vnd.kde.karbon', 'kdb' => 'application/octet-stream', 'kdbx' => 'application/x-keepass2', 'key' => 'application/x-iwork-keynote-sffkey', 'kfo' => 'application/vnd.kde.kformula', 'kia' => 'application/vnd.kidspiration', 'kml' => 'application/vnd.google-earth.kml+xml', 'kmz' => 'application/vnd.google-earth.kmz', 'kne' => 'application/vnd.kinar', 'knp' => 'application/vnd.kinar', 'kon' => 'application/vnd.kde.kontour', 'kpr' => 'application/vnd.kde.kpresenter', 'kpt' => 'application/vnd.kde.kpresenter', 'kpxx' => 'application/vnd.ds-keypoint', 'ksp' => 'application/vnd.kde.kspread', 'ktr' => 'application/vnd.kahootz', 'ktx' => 'image/ktx', 'ktx2' => 'image/ktx2', 'ktz' => 'application/vnd.kahootz', 'kwd' => 'application/vnd.kde.kword', 'kwt' => 'application/vnd.kde.kword', 'lasxml' => 'application/vnd.las.las+xml', 'latex' => 'application/x-latex', 'lbd' => 'application/vnd.llamagraphics.life-balance.desktop', 'lbe' => 'application/vnd.llamagraphics.life-balance.exchange+xml', 'les' => 'application/vnd.hhe.lesson-player', 'less' => 'text/less', 'lgr' => 'application/lgr+xml', 'lha' => 'application/octet-stream', 'link66' => 'application/vnd.route66.link66+xml', 'list' => 'text/plain', 'list3820' => 'application/vnd.ibm.modcap', 'listafp' => 'application/vnd.ibm.modcap', 'litcoffee' => 'text/coffeescript', 'lnk' => 'application/x-ms-shortcut', 'log' => 'text/plain', 'lostxml' => 'application/lost+xml', 'lrf' => 'application/octet-stream', 'lrm' => 'application/vnd.ms-lrm', 'ltf' => 'application/vnd.frogans.ltf', 'lua' => 'text/x-lua', 'luac' => 'application/x-lua-bytecode', 'lvp' => 'audio/vnd.lucent.voice', 'lwp' => 'application/vnd.lotus-wordpro', 'lzh' => 'application/octet-stream', 'm1v' => 'video/mpeg', 'm2a' => 'audio/mpeg', 'm2v' => 'video/mpeg', 'm3a' => 'audio/mpeg', 'm3u' => 'text/plain', 'm3u8' => 'application/vnd.apple.mpegurl', 'm4a' => 'audio/x-m4a', 'm4p' => 'application/mp4', 'm4s' => 'video/iso.segment', 'm4u' => 'application/vnd.mpegurl', 'm4v' => 'video/x-m4v', 'm13' => 'application/x-msmediaview', 'm14' => 'application/x-msmediaview', 'm21' => 'application/mp21', 'ma' => 'application/mathematica', 'mads' => 'application/mads+xml', 'maei' => 'application/mmt-aei+xml', 'mag' => 'application/vnd.ecowin.chart', 'maker' => 'application/vnd.framemaker', 'man' => 'text/troff', 'manifest' => 'text/cache-manifest', 'map' => 'application/json', 'mar' => 'application/octet-stream', 'markdown' => 'text/markdown', 'mathml' => 'application/mathml+xml', 'mb' => 'application/mathematica', 'mbk' => 'application/vnd.mobius.mbk', 'mbox' => 'application/mbox', 'mc1' => 'application/vnd.medcalcdata', 'mcd' => 'application/vnd.mcd', 'mcurl' => 'text/vnd.curl.mcurl', 'md' => 'text/markdown', 'mdb' => 'application/x-msaccess', 'mdi' => 'image/vnd.ms-modi', 'mdx' => 'text/mdx', 'me' => 'text/troff', 'mesh' => 'model/mesh', 'meta4' => 'application/metalink4+xml', 'metalink' => 'application/metalink+xml', 'mets' => 'application/mets+xml', 'mfm' => 'application/vnd.mfmp', 'mft' => 'application/rpki-manifest', 'mgp' => 'application/vnd.osgeo.mapguide.package', 'mgz' => 'application/vnd.proteus.magazine', 'mid' => 'audio/midi', 'midi' => 'audio/midi', 'mie' => 'application/x-mie', 'mif' => 'application/vnd.mif', 'mime' => 'message/rfc822', 'mj2' => 'video/mj2', 'mjp2' => 'video/mj2', 'mjs' => 'text/javascript', 'mk3d' => 'video/x-matroska', 'mka' => 'audio/x-matroska', 'mkd' => 'text/x-markdown', 'mks' => 'video/x-matroska', 'mkv' => 'video/x-matroska', 'mlp' => 'application/vnd.dolby.mlp', 'mmd' => 'application/vnd.chipnuts.karaoke-mmd', 'mmf' => 'application/vnd.smaf', 'mml' => 'text/mathml', 'mmr' => 'image/vnd.fujixerox.edmics-mmr', 'mng' => 'video/x-mng', 'mny' => 'application/x-msmoney', 'mobi' => 'application/x-mobipocket-ebook', 'mods' => 'application/mods+xml', 'mov' => 'video/quicktime', 'movie' => 'video/x-sgi-movie', 'mp2' => 'audio/mpeg', 'mp2a' => 'audio/mpeg', 'mp3' => 'audio/mpeg', 'mp4' => 'video/mp4', 'mp4a' => 'audio/mp4', 'mp4s' => 'application/mp4', 'mp4v' => 'video/mp4', 'mp21' => 'application/mp21', 'mpc' => 'application/vnd.mophun.certificate', 'mpd' => 'application/dash+xml', 'mpe' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mpf' => 'application/media-policy-dataset+xml', 'mpg' => 'video/mpeg', 'mpg4' => 'video/mp4', 'mpga' => 'audio/mpeg', 'mpkg' => 'application/vnd.apple.installer+xml', 'mpm' => 'application/vnd.blueice.multipass', 'mpn' => 'application/vnd.mophun.application', 'mpp' => 'application/vnd.ms-project', 'mpt' => 'application/vnd.ms-project', 'mpy' => 'application/vnd.ibm.minipay', 'mqy' => 'application/vnd.mobius.mqy', 'mrc' => 'application/marc', 'mrcx' => 'application/marcxml+xml', 'ms' => 'text/troff', 'mscml' => 'application/mediaservercontrol+xml', 'mseed' => 'application/vnd.fdsn.mseed', 'mseq' => 'application/vnd.mseq', 'msf' => 'application/vnd.epson.msf', 'msg' => 'application/vnd.ms-outlook', 'msh' => 'model/mesh', 'msi' => 'application/x-msdownload', 'msix' => 'application/msix', 'msixbundle' => 'application/msixbundle', 'msl' => 'application/vnd.mobius.msl', 'msm' => 'application/octet-stream', 'msp' => 'application/octet-stream', 'msty' => 'application/vnd.muvee.style', 'mtl' => 'model/mtl', 'mts' => 'model/vnd.mts', 'mus' => 'application/vnd.musician', 'musd' => 'application/mmt-usd+xml', 'musicxml' => 'application/vnd.recordare.musicxml+xml', 'mvb' => 'application/x-msmediaview', 'mvt' => 'application/vnd.mapbox-vector-tile', 'mwf' => 'application/vnd.mfer', 'mxf' => 'application/mxf', 'mxl' => 'application/vnd.recordare.musicxml', 'mxmf' => 'audio/mobile-xmf', 'mxml' => 'application/xv+xml', 'mxs' => 'application/vnd.triscape.mxs', 'mxu' => 'video/vnd.mpegurl', 'n-gage' => 'application/vnd.nokia.n-gage.symbian.install', 'n3' => 'text/n3', 'nb' => 'application/mathematica', 'nbp' => 'application/vnd.wolfram.player', 'nc' => 'application/x-netcdf', 'ncx' => 'application/x-dtbncx+xml', 'nfo' => 'text/x-nfo', 'ngdat' => 'application/vnd.nokia.n-gage.data', 'nitf' => 'application/vnd.nitf', 'nlu' => 'application/vnd.neurolanguage.nlu', 'nml' => 'application/vnd.enliven', 'nnd' => 'application/vnd.noblenet-directory', 'nns' => 'application/vnd.noblenet-sealer', 'nnw' => 'application/vnd.noblenet-web', 'npx' => 'image/vnd.net-fpx', 'nq' => 'application/n-quads', 'nsc' => 'application/x-conference', 'nsf' => 'application/vnd.lotus-notes', 'nt' => 'application/n-triples', 'ntf' => 'application/vnd.nitf', 'numbers' => 'application/x-iwork-numbers-sffnumbers', 'nzb' => 'application/x-nzb', 'oa2' => 'application/vnd.fujitsu.oasys2', 'oa3' => 'application/vnd.fujitsu.oasys3', 'oas' => 'application/vnd.fujitsu.oasys', 'obd' => 'application/x-msbinder', 'obgx' => 'application/vnd.openblox.game+xml', 'obj' => 'model/obj', 'oda' => 'application/oda', 'odb' => 'application/vnd.oasis.opendocument.database', 'odc' => 'application/vnd.oasis.opendocument.chart', 'odf' => 'application/vnd.oasis.opendocument.formula', 'odft' => 'application/vnd.oasis.opendocument.formula-template', 'odg' => 'application/vnd.oasis.opendocument.graphics', 'odi' => 'application/vnd.oasis.opendocument.image', 'odm' => 'application/vnd.oasis.opendocument.text-master', 'odp' => 'application/vnd.oasis.opendocument.presentation', 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', 'odt' => 'application/vnd.oasis.opendocument.text', 'oga' => 'audio/ogg', 'ogex' => 'model/vnd.opengex', 'ogg' => 'audio/ogg', 'ogv' => 'video/ogg', 'ogx' => 'application/ogg', 'omdoc' => 'application/omdoc+xml', 'onepkg' => 'application/onenote', 'onetmp' => 'application/onenote', 'onetoc' => 'application/onenote', 'onetoc2' => 'application/onenote', 'opf' => 'application/oebps-package+xml', 'opml' => 'text/x-opml', 'oprc' => 'application/vnd.palm', 'opus' => 'audio/ogg', 'org' => 'text/x-org', 'osf' => 'application/vnd.yamaha.openscoreformat', 'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml', 'osm' => 'application/vnd.openstreetmap.data+xml', 'otc' => 'application/vnd.oasis.opendocument.chart-template', 'otf' => 'font/otf', 'otg' => 'application/vnd.oasis.opendocument.graphics-template', 'oth' => 'application/vnd.oasis.opendocument.text-web', 'oti' => 'application/vnd.oasis.opendocument.image-template', 'otp' => 'application/vnd.oasis.opendocument.presentation-template', 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', 'ott' => 'application/vnd.oasis.opendocument.text-template', 'ova' => 'application/x-virtualbox-ova', 'ovf' => 'application/x-virtualbox-ovf', 'owl' => 'application/rdf+xml', 'oxps' => 'application/oxps', 'oxt' => 'application/vnd.openofficeorg.extension', 'p' => 'text/x-pascal', 'p7a' => 'application/x-pkcs7-signature', 'p7b' => 'application/x-pkcs7-certificates', 'p7c' => 'application/pkcs7-mime', 'p7m' => 'application/pkcs7-mime', 'p7r' => 'application/x-pkcs7-certreqresp', 'p7s' => 'application/pkcs7-signature', 'p8' => 'application/pkcs8', 'p10' => 'application/x-pkcs10', 'p12' => 'application/x-pkcs12', 'pac' => 'application/x-ns-proxy-autoconfig', 'pages' => 'application/x-iwork-pages-sffpages', 'pas' => 'text/x-pascal', 'paw' => 'application/vnd.pawaafile', 'pbd' => 'application/vnd.powerbuilder6', 'pbm' => 'image/x-portable-bitmap', 'pcap' => 'application/vnd.tcpdump.pcap', 'pcf' => 'application/x-font-pcf', 'pcl' => 'application/vnd.hp-pcl', 'pclxl' => 'application/vnd.hp-pclxl', 'pct' => 'image/x-pict', 'pcurl' => 'application/vnd.curl.pcurl', 'pcx' => 'image/x-pcx', 'pdb' => 'application/x-pilot', 'pde' => 'text/x-processing', 'pdf' => 'application/pdf', 'pem' => 'application/x-x509-user-cert', 'pfa' => 'application/x-font-type1', 'pfb' => 'application/x-font-type1', 'pfm' => 'application/x-font-type1', 'pfr' => 'application/font-tdpfr', 'pfx' => 'application/x-pkcs12', 'pgm' => 'image/x-portable-graymap', 'pgn' => 'application/x-chess-pgn', 'pgp' => 'application/pgp', 'phar' => 'application/octet-stream', 'php' => 'application/x-httpd-php', 'php3' => 'application/x-httpd-php', 'php4' => 'application/x-httpd-php', 'phps' => 'application/x-httpd-php-source', 'phtml' => 'application/x-httpd-php', 'pic' => 'image/x-pict', 'pkg' => 'application/octet-stream', 'pki' => 'application/pkixcmp', 'pkipath' => 'application/pkix-pkipath', 'pkpass' => 'application/vnd.apple.pkpass', 'pl' => 'application/x-perl', 'plb' => 'application/vnd.3gpp.pic-bw-large', 'plc' => 'application/vnd.mobius.plc', 'plf' => 'application/vnd.pocketlearn', 'pls' => 'application/pls+xml', 'pm' => 'application/x-perl', 'pml' => 'application/vnd.ctc-posml', 'png' => 'image/png', 'pnm' => 'image/x-portable-anymap', 'portpkg' => 'application/vnd.macports.portpkg', 'pot' => 'application/vnd.ms-powerpoint', 'potm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', 'ppa' => 'application/vnd.ms-powerpoint', 'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12', 'ppd' => 'application/vnd.cups-ppd', 'ppm' => 'image/x-portable-pixmap', 'pps' => 'application/vnd.ms-powerpoint', 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 'ppt' => 'application/powerpoint', 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'pqa' => 'application/vnd.palm', 'prc' => 'model/prc', 'pre' => 'application/vnd.lotus-freelance', 'prf' => 'application/pics-rules', 'provx' => 'application/provenance+xml', 'ps' => 'application/postscript', 'psb' => 'application/vnd.3gpp.pic-bw-small', 'psd' => 'application/x-photoshop', 'psf' => 'application/x-font-linux-psf', 'pskcxml' => 'application/pskc+xml', 'pti' => 'image/prs.pti', 'ptid' => 'application/vnd.pvi.ptid1', 'pub' => 'application/x-mspublisher', 'pvb' => 'application/vnd.3gpp.pic-bw-var', 'pwn' => 'application/vnd.3m.post-it-notes', 'pya' => 'audio/vnd.ms-playready.media.pya', 'pyo' => 'model/vnd.pytha.pyox', 'pyox' => 'model/vnd.pytha.pyox', 'pyv' => 'video/vnd.ms-playready.media.pyv', 'qam' => 'application/vnd.epson.quickanime', 'qbo' => 'application/vnd.intu.qbo', 'qfx' => 'application/vnd.intu.qfx', 'qps' => 'application/vnd.publishare-delta-tree', 'qt' => 'video/quicktime', 'qwd' => 'application/vnd.quark.quarkxpress', 'qwt' => 'application/vnd.quark.quarkxpress', 'qxb' => 'application/vnd.quark.quarkxpress', 'qxd' => 'application/vnd.quark.quarkxpress', 'qxl' => 'application/vnd.quark.quarkxpress', 'qxt' => 'application/vnd.quark.quarkxpress', 'ra' => 'audio/x-realaudio', 'ram' => 'audio/x-pn-realaudio', 'raml' => 'application/raml+yaml', 'rapd' => 'application/route-apd+xml', 'rar' => 'application/x-rar', 'ras' => 'image/x-cmu-raster', 'rcprofile' => 'application/vnd.ipunplugged.rcprofile', 'rdf' => 'application/rdf+xml', 'rdz' => 'application/vnd.data-vision.rdz', 'relo' => 'application/p2p-overlay+xml', 'rep' => 'application/vnd.businessobjects', 'res' => 'application/x-dtbresource+xml', 'rgb' => 'image/x-rgb', 'rif' => 'application/reginfo+xml', 'rip' => 'audio/vnd.rip', 'ris' => 'application/x-research-info-systems', 'rl' => 'application/resource-lists+xml', 'rlc' => 'image/vnd.fujixerox.edmics-rlc', 'rld' => 'application/resource-lists-diff+xml', 'rm' => 'audio/x-pn-realaudio', 'rmi' => 'audio/midi', 'rmp' => 'audio/x-pn-realaudio-plugin', 'rms' => 'application/vnd.jcp.javame.midlet-rms', 'rmvb' => 'application/vnd.rn-realmedia-vbr', 'rnc' => 'application/relax-ng-compact-syntax', 'rng' => 'application/xml', 'roa' => 'application/rpki-roa', 'roff' => 'text/troff', 'rp9' => 'application/vnd.cloanto.rp9', 'rpm' => 'audio/x-pn-realaudio-plugin', 'rpss' => 'application/vnd.nokia.radio-presets', 'rpst' => 'application/vnd.nokia.radio-preset', 'rq' => 'application/sparql-query', 'rs' => 'application/rls-services+xml', 'rsa' => 'application/x-pkcs7', 'rsat' => 'application/atsc-rsat+xml', 'rsd' => 'application/rsd+xml', 'rsheet' => 'application/urc-ressheet+xml', 'rss' => 'application/rss+xml', 'rtf' => 'text/rtf', 'rtx' => 'text/richtext', 'run' => 'application/x-makeself', 'rusd' => 'application/route-usd+xml', 'rv' => 'video/vnd.rn-realvideo', 's' => 'text/x-asm', 's3m' => 'audio/s3m', 'saf' => 'application/vnd.yamaha.smaf-audio', 'sass' => 'text/x-sass', 'sbml' => 'application/sbml+xml', 'sc' => 'application/vnd.ibm.secure-container', 'scd' => 'application/x-msschedule', 'scm' => 'application/vnd.lotus-screencam', 'scq' => 'application/scvp-cv-request', 'scs' => 'application/scvp-cv-response', 'scss' => 'text/x-scss', 'scurl' => 'text/vnd.curl.scurl', 'sda' => 'application/vnd.stardivision.draw', 'sdc' => 'application/vnd.stardivision.calc', 'sdd' => 'application/vnd.stardivision.impress', 'sdkd' => 'application/vnd.solent.sdkm+xml', 'sdkm' => 'application/vnd.solent.sdkm+xml', 'sdp' => 'application/sdp', 'sdw' => 'application/vnd.stardivision.writer', 'sea' => 'application/octet-stream', 'see' => 'application/vnd.seemail', 'seed' => 'application/vnd.fdsn.seed', 'sema' => 'application/vnd.sema', 'semd' => 'application/vnd.semd', 'semf' => 'application/vnd.semf', 'senmlx' => 'application/senml+xml', 'sensmlx' => 'application/sensml+xml', 'ser' => 'application/java-serialized-object', 'setpay' => 'application/set-payment-initiation', 'setreg' => 'application/set-registration-initiation', 'sfd-hdstx' => 'application/vnd.hydrostatix.sof-data', 'sfs' => 'application/vnd.spotfire.sfs', 'sfv' => 'text/x-sfv', 'sgi' => 'image/sgi', 'sgl' => 'application/vnd.stardivision.writer-global', 'sgm' => 'text/sgml', 'sgml' => 'text/sgml', 'sh' => 'application/x-sh', 'shar' => 'application/x-shar', 'shex' => 'text/shex', 'shf' => 'application/shf+xml', 'shtml' => 'text/html', 'sid' => 'image/x-mrsid-image', 'sieve' => 'application/sieve', 'sig' => 'application/pgp-signature', 'sil' => 'audio/silk', 'silo' => 'model/mesh', 'sis' => 'application/vnd.symbian.install', 'sisx' => 'application/vnd.symbian.install', 'sit' => 'application/x-stuffit', 'sitx' => 'application/x-stuffitx', 'siv' => 'application/sieve', 'skd' => 'application/vnd.koan', 'skm' => 'application/vnd.koan', 'skp' => 'application/vnd.koan', 'skt' => 'application/vnd.koan', 'sldm' => 'application/vnd.ms-powerpoint.slide.macroenabled.12', 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', 'slim' => 'text/slim', 'slm' => 'text/slim', 'sls' => 'application/route-s-tsid+xml', 'slt' => 'application/vnd.epson.salt', 'sm' => 'application/vnd.stepmania.stepchart', 'smf' => 'application/vnd.stardivision.math', 'smi' => 'application/smil', 'smil' => 'application/smil', 'smv' => 'video/x-smv', 'smzip' => 'application/vnd.stepmania.package', 'snd' => 'audio/basic', 'snf' => 'application/x-font-snf', 'so' => 'application/octet-stream', 'spc' => 'application/x-pkcs7-certificates', 'spdx' => 'text/spdx', 'spf' => 'application/vnd.yamaha.smaf-phrase', 'spl' => 'application/x-futuresplash', 'spot' => 'text/vnd.in3d.spot', 'spp' => 'application/scvp-vp-response', 'spq' => 'application/scvp-vp-request', 'spx' => 'audio/ogg', 'sql' => 'application/x-sql', 'src' => 'application/x-wais-source', 'srt' => 'application/x-subrip', 'sru' => 'application/sru+xml', 'srx' => 'application/sparql-results+xml', 'ssdl' => 'application/ssdl+xml', 'sse' => 'application/vnd.kodak-descriptor', 'ssf' => 'application/vnd.epson.ssf', 'ssml' => 'application/ssml+xml', 'sst' => 'application/octet-stream', 'st' => 'application/vnd.sailingtracker.track', 'stc' => 'application/vnd.sun.xml.calc.template', 'std' => 'application/vnd.sun.xml.draw.template', 'step' => 'application/STEP', 'stf' => 'application/vnd.wt.stf', 'sti' => 'application/vnd.sun.xml.impress.template', 'stk' => 'application/hyperstudio', 'stl' => 'model/stl', 'stp' => 'application/STEP', 'stpx' => 'model/step+xml', 'stpxz' => 'model/step-xml+zip', 'stpz' => 'model/step+zip', 'str' => 'application/vnd.pg.format', 'stw' => 'application/vnd.sun.xml.writer.template', 'styl' => 'text/stylus', 'stylus' => 'text/stylus', 'sub' => 'text/vnd.dvb.subtitle', 'sus' => 'application/vnd.sus-calendar', 'susp' => 'application/vnd.sus-calendar', 'sv4cpio' => 'application/x-sv4cpio', 'sv4crc' => 'application/x-sv4crc', 'svc' => 'application/vnd.dvb.service', 'svd' => 'application/vnd.svd', 'svg' => 'image/svg+xml', 'svgz' => 'image/svg+xml', 'swa' => 'application/x-director', 'swf' => 'application/x-shockwave-flash', 'swi' => 'application/vnd.aristanetworks.swi', 'swidtag' => 'application/swid+xml', 'sxc' => 'application/vnd.sun.xml.calc', 'sxd' => 'application/vnd.sun.xml.draw', 'sxg' => 'application/vnd.sun.xml.writer.global', 'sxi' => 'application/vnd.sun.xml.impress', 'sxm' => 'application/vnd.sun.xml.math', 'sxw' => 'application/vnd.sun.xml.writer', 't' => 'text/troff', 't3' => 'application/x-t3vm-image', 't38' => 'image/t38', 'taglet' => 'application/vnd.mynfc', 'tao' => 'application/vnd.tao.intent-module-archive', 'tap' => 'image/vnd.tencent.tap', 'tar' => 'application/x-tar', 'tcap' => 'application/vnd.3gpp2.tcap', 'tcl' => 'application/x-tcl', 'td' => 'application/urc-targetdesc+xml', 'teacher' => 'application/vnd.smart.teacher', 'tei' => 'application/tei+xml', 'teicorpus' => 'application/tei+xml', 'tex' => 'application/x-tex', 'texi' => 'application/x-texinfo', 'texinfo' => 'application/x-texinfo', 'text' => 'text/plain', 'tfi' => 'application/thraud+xml', 'tfm' => 'application/x-tex-tfm', 'tfx' => 'image/tiff-fx', 'tga' => 'image/x-tga', 'tgz' => 'application/x-tar', 'thmx' => 'application/vnd.ms-officetheme', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'tk' => 'application/x-tcl', 'tmo' => 'application/vnd.tmobile-livetv', 'toml' => 'application/toml', 'torrent' => 'application/x-bittorrent', 'tpl' => 'application/vnd.groove-tool-template', 'tpt' => 'application/vnd.trid.tpt', 'tr' => 'text/troff', 'tra' => 'application/vnd.trueapp', 'trig' => 'application/trig', 'trm' => 'application/x-msterminal', 'ts' => 'video/mp2t', 'tsd' => 'application/timestamped-data', 'tsv' => 'text/tab-separated-values', 'ttc' => 'font/collection', 'ttf' => 'font/ttf', 'ttl' => 'text/turtle', 'ttml' => 'application/ttml+xml', 'twd' => 'application/vnd.simtech-mindmapper', 'twds' => 'application/vnd.simtech-mindmapper', 'txd' => 'application/vnd.genomatix.tuxedo', 'txf' => 'application/vnd.mobius.txf', 'txt' => 'text/plain', 'u3d' => 'model/u3d', 'u8dsn' => 'message/global-delivery-status', 'u8hdr' => 'message/global-headers', 'u8mdn' => 'message/global-disposition-notification', 'u8msg' => 'message/global', 'u32' => 'application/x-authorware-bin', 'ubj' => 'application/ubjson', 'udeb' => 'application/x-debian-package', 'ufd' => 'application/vnd.ufdl', 'ufdl' => 'application/vnd.ufdl', 'ulx' => 'application/x-glulx', 'umj' => 'application/vnd.umajin', 'unityweb' => 'application/vnd.unity', 'uo' => 'application/vnd.uoml+xml', 'uoml' => 'application/vnd.uoml+xml', 'uri' => 'text/uri-list', 'uris' => 'text/uri-list', 'urls' => 'text/uri-list', 'usda' => 'model/vnd.usda', 'usdz' => 'model/vnd.usdz+zip', 'ustar' => 'application/x-ustar', 'utz' => 'application/vnd.uiq.theme', 'uu' => 'text/x-uuencode', 'uva' => 'audio/vnd.dece.audio', 'uvd' => 'application/vnd.dece.data', 'uvf' => 'application/vnd.dece.data', 'uvg' => 'image/vnd.dece.graphic', 'uvh' => 'video/vnd.dece.hd', 'uvi' => 'image/vnd.dece.graphic', 'uvm' => 'video/vnd.dece.mobile', 'uvp' => 'video/vnd.dece.pd', 'uvs' => 'video/vnd.dece.sd', 'uvt' => 'application/vnd.dece.ttml+xml', 'uvu' => 'video/vnd.uvvu.mp4', 'uvv' => 'video/vnd.dece.video', 'uvva' => 'audio/vnd.dece.audio', 'uvvd' => 'application/vnd.dece.data', 'uvvf' => 'application/vnd.dece.data', 'uvvg' => 'image/vnd.dece.graphic', 'uvvh' => 'video/vnd.dece.hd', 'uvvi' => 'image/vnd.dece.graphic', 'uvvm' => 'video/vnd.dece.mobile', 'uvvp' => 'video/vnd.dece.pd', 'uvvs' => 'video/vnd.dece.sd', 'uvvt' => 'application/vnd.dece.ttml+xml', 'uvvu' => 'video/vnd.uvvu.mp4', 'uvvv' => 'video/vnd.dece.video', 'uvvx' => 'application/vnd.dece.unspecified', 'uvvz' => 'application/vnd.dece.zip', 'uvx' => 'application/vnd.dece.unspecified', 'uvz' => 'application/vnd.dece.zip', 'vbox' => 'application/x-virtualbox-vbox', 'vbox-extpack' => 'application/x-virtualbox-vbox-extpack', 'vcard' => 'text/vcard', 'vcd' => 'application/x-cdlink', 'vcf' => 'text/x-vcard', 'vcg' => 'application/vnd.groove-vcard', 'vcs' => 'text/x-vcalendar', 'vcx' => 'application/vnd.vcx', 'vdi' => 'application/x-virtualbox-vdi', 'vds' => 'model/vnd.sap.vds', 'vhd' => 'application/x-virtualbox-vhd', 'vis' => 'application/vnd.visionary', 'viv' => 'video/vnd.vivo', 'vlc' => 'application/videolan', 'vmdk' => 'application/x-virtualbox-vmdk', 'vob' => 'video/x-ms-vob', 'vor' => 'application/vnd.stardivision.writer', 'vox' => 'application/x-authorware-bin', 'vrml' => 'model/vrml', 'vsd' => 'application/vnd.visio', 'vsf' => 'application/vnd.vsf', 'vss' => 'application/vnd.visio', 'vst' => 'application/vnd.visio', 'vsw' => 'application/vnd.visio', 'vtf' => 'image/vnd.valve.source.texture', 'vtt' => 'text/vtt', 'vtu' => 'model/vnd.vtu', 'vxml' => 'application/voicexml+xml', 'w3d' => 'application/x-director', 'wad' => 'application/x-doom', 'wadl' => 'application/vnd.sun.wadl+xml', 'war' => 'application/java-archive', 'wasm' => 'application/wasm', 'wav' => 'audio/x-wav', 'wax' => 'audio/x-ms-wax', 'wbmp' => 'image/vnd.wap.wbmp', 'wbs' => 'application/vnd.criticaltools.wbs+xml', 'wbxml' => 'application/wbxml', 'wcm' => 'application/vnd.ms-works', 'wdb' => 'application/vnd.ms-works', 'wdp' => 'image/vnd.ms-photo', 'weba' => 'audio/webm', 'webapp' => 'application/x-web-app-manifest+json', 'webm' => 'video/webm', 'webmanifest' => 'application/manifest+json', 'webp' => 'image/webp', 'wg' => 'application/vnd.pmi.widget', 'wgsl' => 'text/wgsl', 'wgt' => 'application/widget', 'wif' => 'application/watcherinfo+xml', 'wks' => 'application/vnd.ms-works', 'wm' => 'video/x-ms-wm', 'wma' => 'audio/x-ms-wma', 'wmd' => 'application/x-ms-wmd', 'wmf' => 'image/wmf', 'wml' => 'text/vnd.wap.wml', 'wmlc' => 'application/wmlc', 'wmls' => 'text/vnd.wap.wmlscript', 'wmlsc' => 'application/vnd.wap.wmlscriptc', 'wmv' => 'video/x-ms-wmv', 'wmx' => 'video/x-ms-wmx', 'wmz' => 'application/x-msmetafile', 'woff' => 'font/woff', 'woff2' => 'font/woff2', 'word' => 'application/msword', 'wpd' => 'application/vnd.wordperfect', 'wpl' => 'application/vnd.ms-wpl', 'wps' => 'application/vnd.ms-works', 'wqd' => 'application/vnd.wqd', 'wri' => 'application/x-mswrite', 'wrl' => 'model/vrml', 'wsc' => 'message/vnd.wfa.wsc', 'wsdl' => 'application/wsdl+xml', 'wspolicy' => 'application/wspolicy+xml', 'wtb' => 'application/vnd.webturbo', 'wvx' => 'video/x-ms-wvx', 'x3d' => 'model/x3d+xml', 'x3db' => 'model/x3d+fastinfoset', 'x3dbz' => 'model/x3d+binary', 'x3dv' => 'model/x3d-vrml', 'x3dvz' => 'model/x3d+vrml', 'x3dz' => 'model/x3d+xml', 'x32' => 'application/x-authorware-bin', 'x_b' => 'model/vnd.parasolid.transmit.binary', 'x_t' => 'model/vnd.parasolid.transmit.text', 'xaml' => 'application/xaml+xml', 'xap' => 'application/x-silverlight-app', 'xar' => 'application/vnd.xara', 'xav' => 'application/xcap-att+xml', 'xbap' => 'application/x-ms-xbap', 'xbd' => 'application/vnd.fujixerox.docuworks.binder', 'xbm' => 'image/x-xbitmap', 'xca' => 'application/xcap-caps+xml', 'xcs' => 'application/calendar+xml', 'xdf' => 'application/xcap-diff+xml', 'xdm' => 'application/vnd.syncml.dm+xml', 'xdp' => 'application/vnd.adobe.xdp+xml', 'xdssc' => 'application/dssc+xml', 'xdw' => 'application/vnd.fujixerox.docuworks', 'xel' => 'application/xcap-el+xml', 'xenc' => 'application/xenc+xml', 'xer' => 'application/patch-ops-error+xml', 'xfdf' => 'application/xfdf', 'xfdl' => 'application/vnd.xfdl', 'xht' => 'application/xhtml+xml', 'xhtm' => 'application/vnd.pwg-xhtml-print+xml', 'xhtml' => 'application/xhtml+xml', 'xhvml' => 'application/xv+xml', 'xif' => 'image/vnd.xiff', 'xl' => 'application/excel', 'xla' => 'application/vnd.ms-excel', 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', 'xlc' => 'application/vnd.ms-excel', 'xlf' => 'application/xliff+xml', 'xlm' => 'application/vnd.ms-excel', 'xls' => 'application/vnd.ms-excel', 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', 'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlt' => 'application/vnd.ms-excel', 'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12', 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'xlw' => 'application/vnd.ms-excel', 'xm' => 'audio/xm', 'xml' => 'application/xml', 'xns' => 'application/xcap-ns+xml', 'xo' => 'application/vnd.olpc-sugar', 'xop' => 'application/xop+xml', 'xpi' => 'application/x-xpinstall', 'xpl' => 'application/xproc+xml', 'xpm' => 'image/x-xpixmap', 'xpr' => 'application/vnd.is-xpr', 'xps' => 'application/vnd.ms-xpsdocument', 'xpw' => 'application/vnd.intercon.formnet', 'xpx' => 'application/vnd.intercon.formnet', 'xsd' => 'application/xml', 'xsf' => 'application/prs.xsf+xml', 'xsl' => 'application/xml', 'xslt' => 'application/xslt+xml', 'xsm' => 'application/vnd.syncml+xml', 'xspf' => 'application/xspf+xml', 'xul' => 'application/vnd.mozilla.xul+xml', 'xvm' => 'application/xv+xml', 'xvml' => 'application/xv+xml', 'xwd' => 'image/x-xwindowdump', 'xyz' => 'chemical/x-xyz', 'xz' => 'application/x-xz', 'yaml' => 'text/yaml', 'yang' => 'application/yang', 'yin' => 'application/yin+xml', 'yml' => 'text/yaml', 'ymp' => 'text/x-suse-ymp', 'z' => 'application/x-compress', 'z1' => 'application/x-zmachine', 'z2' => 'application/x-zmachine', 'z3' => 'application/x-zmachine', 'z4' => 'application/x-zmachine', 'z5' => 'application/x-zmachine', 'z6' => 'application/x-zmachine', 'z7' => 'application/x-zmachine', 'z8' => 'application/x-zmachine', 'zaz' => 'application/vnd.zzazz.deck+xml', 'zip' => 'application/zip', 'zir' => 'application/vnd.zul', 'zirz' => 'application/vnd.zul', 'zmm' => 'application/vnd.handheld-entertainment+xml', 'zsh' => 'text/x-scriptzsh', ]; /** * Determines the mimetype of a file by looking at its extension. * * @see https://raw.githubusercontent.com/jshttp/mime-db/master/db.json */ public static function fromFilename(string $filename): ?string { return self::fromExtension(pathinfo($filename, PATHINFO_EXTENSION)); } /** * Maps a file extensions to a mimetype. * * @see https://raw.githubusercontent.com/jshttp/mime-db/master/db.json */ public static function fromExtension(string $extension): ?string { return self::MIME_TYPES[strtolower($extension)] ?? null; } } UriResolver.php000064400000020603152430110220007525 0ustar00getScheme() != '') { return $rel->withPath(self::removeDotSegments($rel->getPath())); } if ($rel->getAuthority() != '') { $targetAuthority = $rel->getAuthority(); $targetPath = self::removeDotSegments($rel->getPath()); $targetQuery = $rel->getQuery(); } else { $targetAuthority = $base->getAuthority(); if ($rel->getPath() === '') { $targetPath = $base->getPath(); $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery(); } else { if ($rel->getPath()[0] === '/') { $targetPath = $rel->getPath(); } else { if ($targetAuthority != '' && $base->getPath() === '') { $targetPath = '/'.$rel->getPath(); } else { $lastSlashPos = strrpos($base->getPath(), '/'); if ($lastSlashPos === false) { $targetPath = $rel->getPath(); } else { $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1).$rel->getPath(); } } } $targetPath = self::removeDotSegments($targetPath); $targetQuery = $rel->getQuery(); } } return new Uri(Uri::composeComponents( $base->getScheme(), $targetAuthority, $targetPath, $targetQuery, $rel->getFragment() )); } /** * Returns the target URI as a relative reference from the base URI. * * This method is the counterpart to resolve(): * * (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) * * One use-case is to use the current request URI as base URI and then generate relative links in your documents * to reduce the document size or offer self-contained downloadable document archives. * * $base = new Uri('http://example.com/a/b/'); * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'. * echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'. * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'. * echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'. * * This method also accepts a target that is already relative and will try to relativize it further. Only a * relative-path reference will be returned as-is. * * echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well */ public static function relativize(UriInterface $base, UriInterface $target): UriInterface { if ($target->getScheme() !== '' && ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '') ) { return $target; } if (Uri::isRelativePathReference($target)) { // As the target is already highly relative we return it as-is. It would be possible to resolve // the target with `$target = self::resolve($base, $target);` and then try make it more relative // by removing a duplicate query. But let's not do that automatically. return $target; } if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) { return $target->withScheme(''); } // We must remove the path before removing the authority because if the path starts with two slashes, the URI // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also // invalid. $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost(''); if ($base->getPath() !== $target->getPath()) { return $emptyPathUri->withPath(self::getRelativePath($base, $target)); } if ($base->getQuery() === $target->getQuery()) { // Only the target fragment is left. And it must be returned even if base and target fragment are the same. return $emptyPathUri->withQuery(''); } // If the base URI has a query but the target has none, we cannot return an empty path reference as it would // inherit the base query component when resolving. if ($target->getQuery() === '') { $segments = explode('/', $target->getPath()); /** @var string $lastSegment */ $lastSegment = end($segments); return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); } return $emptyPathUri; } private static function getRelativePath(UriInterface $base, UriInterface $target): string { $sourceSegments = explode('/', $base->getPath()); $targetSegments = explode('/', $target->getPath()); array_pop($sourceSegments); $targetLastSegment = array_pop($targetSegments); foreach ($sourceSegments as $i => $segment) { if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) { unset($sourceSegments[$i], $targetSegments[$i]); } else { break; } } $targetSegments[] = $targetLastSegment; $relativePath = str_repeat('../', count($sourceSegments)).implode('/', $targetSegments); // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./". // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used // as the first segment of a relative-path reference, as it would be mistaken for a scheme name. if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) { $relativePath = "./$relativePath"; } elseif ('/' === $relativePath[0]) { if ($base->getAuthority() != '' && $base->getPath() === '') { // In this case an extra slash is added by resolve() automatically. So we must not add one here. $relativePath = ".$relativePath"; } else { $relativePath = "./$relativePath"; } } return $relativePath; } private function __construct() { // cannot be instantiated } } UploadedFile.php000064400000011403152430110220007577 0ustar00setError($errorStatus); $this->size = $size; $this->clientFilename = $clientFilename; $this->clientMediaType = $clientMediaType; if ($this->isOk()) { $this->setStreamOrFile($streamOrFile); } } /** * Depending on the value set file or stream variable * * @param StreamInterface|string|resource $streamOrFile * * @throws InvalidArgumentException */ private function setStreamOrFile($streamOrFile): void { if (is_string($streamOrFile)) { $this->file = $streamOrFile; } elseif (is_resource($streamOrFile)) { $this->stream = new Stream($streamOrFile); } elseif ($streamOrFile instanceof StreamInterface) { $this->stream = $streamOrFile; } else { throw new InvalidArgumentException( 'Invalid stream or file provided for UploadedFile' ); } } /** * @throws InvalidArgumentException */ private function setError(int $error): void { if (false === in_array($error, UploadedFile::ERRORS, true)) { throw new InvalidArgumentException( 'Invalid error status for UploadedFile' ); } $this->error = $error; } private static function isStringNotEmpty($param): bool { return is_string($param) && false === empty($param); } /** * Return true if there is no upload error */ private function isOk(): bool { return $this->error === UPLOAD_ERR_OK; } public function isMoved(): bool { return $this->moved; } /** * @throws RuntimeException if is moved or not ok */ private function validateActive(): void { if (false === $this->isOk()) { throw new RuntimeException('Cannot retrieve stream due to upload error'); } if ($this->isMoved()) { throw new RuntimeException('Cannot retrieve stream after it has already been moved'); } } public function getStream(): StreamInterface { $this->validateActive(); if ($this->stream instanceof StreamInterface) { return $this->stream; } /** @var string $file */ $file = $this->file; return new LazyOpenStream($file, 'r+'); } public function moveTo($targetPath): void { $this->validateActive(); if (false === self::isStringNotEmpty($targetPath)) { throw new InvalidArgumentException( 'Invalid path provided for move operation; must be a non-empty string' ); } if ($this->file) { $this->moved = PHP_SAPI === 'cli' ? rename($this->file, $targetPath) : move_uploaded_file($this->file, $targetPath); } else { Utils::copyToStream( $this->getStream(), new LazyOpenStream($targetPath, 'w') ); $this->moved = true; } if (false === $this->moved) { throw new RuntimeException( sprintf('Uploaded file could not be moved to %s', $targetPath) ); } } public function getSize(): ?int { return $this->size; } public function getError(): int { return $this->error; } public function getClientFilename(): ?string { return $this->clientFilename; } public function getClientMediaType(): ?string { return $this->clientMediaType; } } CachingStream.php000064400000010761152430110220007760 0ustar00remoteStream = $stream; $this->stream = $target ?: new Stream(Utils::tryFopen('php://temp', 'r+')); } public function getSize(): ?int { $remoteSize = $this->remoteStream->getSize(); if (null === $remoteSize) { return null; } return max($this->stream->getSize(), $remoteSize); } public function rewind(): void { $this->seek(0); } public function seek($offset, $whence = SEEK_SET): void { if ($whence === SEEK_SET) { $byte = $offset; } elseif ($whence === SEEK_CUR) { $byte = $offset + $this->tell(); } elseif ($whence === SEEK_END) { $size = $this->remoteStream->getSize(); if ($size === null) { $size = $this->cacheEntireStream(); } $byte = $size + $offset; } else { throw new \InvalidArgumentException('Invalid whence'); } $diff = $byte - $this->stream->getSize(); if ($diff > 0) { // Read the remoteStream until we have read in at least the amount // of bytes requested, or we reach the end of the file. while ($diff > 0 && !$this->remoteStream->eof()) { $this->read($diff); $diff = $byte - $this->stream->getSize(); } } else { // We can just do a normal seek since we've already seen this byte. $this->stream->seek($byte); } } public function read($length): string { // Perform a regular read on any previously read data from the buffer $data = $this->stream->read($length); $remaining = $length - strlen($data); // More data was requested so read from the remote stream if ($remaining) { // If data was written to the buffer in a position that would have // been filled from the remote stream, then we must skip bytes on // the remote stream to emulate overwriting bytes from that // position. This mimics the behavior of other PHP stream wrappers. $remoteData = $this->remoteStream->read( $remaining + $this->skipReadBytes ); if ($this->skipReadBytes) { $len = strlen($remoteData); $remoteData = substr($remoteData, $this->skipReadBytes); $this->skipReadBytes = max(0, $this->skipReadBytes - $len); } $data .= $remoteData; $this->stream->write($remoteData); } return $data; } public function write($string): int { // When appending to the end of the currently read stream, you'll want // to skip bytes from being read from the remote stream to emulate // other stream wrappers. Basically replacing bytes of data of a fixed // length. $overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell(); if ($overflow > 0) { $this->skipReadBytes += $overflow; } return $this->stream->write($string); } public function eof(): bool { return $this->stream->eof() && $this->remoteStream->eof(); } /** * Close both the remote stream and buffer stream */ public function close(): void { $this->remoteStream->close(); $this->stream->close(); } private function cacheEntireStream(): int { $target = new FnStream(['write' => 'strlen']); Utils::copyToStream($this, $target); return $this->tell(); } } StreamWrapper.php000064400000011041152430110220010034 0ustar00isReadable()) { $mode = $stream->isWritable() ? 'r+' : 'r'; } elseif ($stream->isWritable()) { $mode = 'w'; } else { throw new \InvalidArgumentException('The stream must be readable, ' .'writable, or both.'); } return fopen('guzzle://stream', $mode, false, self::createStreamContext($stream)); } /** * Creates a stream context that can be used to open a stream as a php stream resource. * * @return resource */ public static function createStreamContext(StreamInterface $stream) { return stream_context_create([ 'guzzle' => ['stream' => $stream], ]); } /** * Registers the stream wrapper if needed */ public static function register(): void { if (!in_array('guzzle', stream_get_wrappers())) { stream_wrapper_register('guzzle', __CLASS__); } } public function stream_open(string $path, string $mode, int $options, ?string &$opened_path = null): bool { $options = stream_context_get_options($this->context); if (!isset($options['guzzle']['stream'])) { return false; } $this->mode = $mode; $this->stream = $options['guzzle']['stream']; return true; } public function stream_read(int $count): string { return $this->stream->read($count); } public function stream_write(string $data): int { return $this->stream->write($data); } public function stream_tell(): int { return $this->stream->tell(); } public function stream_eof(): bool { return $this->stream->eof(); } public function stream_seek(int $offset, int $whence): bool { $this->stream->seek($offset, $whence); return true; } /** * @return resource|false */ public function stream_cast(int $cast_as) { $stream = clone $this->stream; $resource = $stream->detach(); return $resource ?? false; } /** * @return array{ * dev: int, * ino: int, * mode: int, * nlink: int, * uid: int, * gid: int, * rdev: int, * size: int, * atime: int, * mtime: int, * ctime: int, * blksize: int, * blocks: int * }|false */ public function stream_stat() { if ($this->stream->getSize() === null) { return false; } static $modeMap = [ 'r' => 33060, 'rb' => 33060, 'r+' => 33206, 'w' => 33188, 'wb' => 33188, ]; return [ 'dev' => 0, 'ino' => 0, 'mode' => $modeMap[$this->mode], 'nlink' => 0, 'uid' => 0, 'gid' => 0, 'rdev' => 0, 'size' => $this->stream->getSize() ?: 0, 'atime' => 0, 'mtime' => 0, 'ctime' => 0, 'blksize' => 0, 'blocks' => 0, ]; } /** * @return array{ * dev: int, * ino: int, * mode: int, * nlink: int, * uid: int, * gid: int, * rdev: int, * size: int, * atime: int, * mtime: int, * ctime: int, * blksize: int, * blocks: int * } */ public function url_stat(string $path, int $flags): array { return [ 'dev' => 0, 'ino' => 0, 'mode' => 0, 'nlink' => 0, 'uid' => 0, 'gid' => 0, 'rdev' => 0, 'size' => 0, 'atime' => 0, 'mtime' => 0, 'ctime' => 0, 'blksize' => 0, 'blocks' => 0, ]; } } InflateStream.php000064400000002607152430110220010006 0ustar00 15 + 32]); $this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource)); } } LimitStream.php000064400000010311152430110220007471 0ustar00stream = $stream; $this->setLimit($limit); $this->setOffset($offset); } public function eof(): bool { // Always return true if the underlying stream is EOF if ($this->stream->eof()) { return true; } // No limit and the underlying stream is not at EOF if ($this->limit === -1) { return false; } return $this->stream->tell() >= $this->offset + $this->limit; } /** * Returns the size of the limited subset of data */ public function getSize(): ?int { if (null === ($length = $this->stream->getSize())) { return null; } elseif ($this->limit === -1) { return $length - $this->offset; } else { return min($this->limit, $length - $this->offset); } } /** * Allow for a bounded seek on the read limited stream */ public function seek($offset, $whence = SEEK_SET): void { if ($whence !== SEEK_SET || $offset < 0) { throw new \RuntimeException(sprintf( 'Cannot seek to offset %s with whence %s', $offset, $whence )); } $offset += $this->offset; if ($this->limit !== -1) { if ($offset > $this->offset + $this->limit) { $offset = $this->offset + $this->limit; } } $this->stream->seek($offset); } /** * Give a relative tell() */ public function tell(): int { return $this->stream->tell() - $this->offset; } /** * Set the offset to start limiting from * * @param int $offset Offset to seek to and begin byte limiting from * * @throws \RuntimeException if the stream cannot be seeked. */ public function setOffset(int $offset): void { $current = $this->stream->tell(); if ($current !== $offset) { // If the stream cannot seek to the offset position, then read to it if ($this->stream->isSeekable()) { $this->stream->seek($offset); } elseif ($current > $offset) { throw new \RuntimeException("Could not seek to stream offset $offset"); } else { $this->stream->read($offset - $current); } } $this->offset = $offset; } /** * Set the limit of bytes that the decorator allows to be read from the * stream. * * @param int $limit Number of bytes to allow to be read from the stream. * Use -1 for no limit. */ public function setLimit(int $limit): void { $this->limit = $limit; } public function read($length): string { if ($this->limit === -1) { return $this->stream->read($length); } // Check if the current position is less than the total allowed // bytes + original offset $remaining = ($this->offset + $this->limit) - $this->stream->tell(); if ($remaining > 0) { // Only return the amount of requested data, ensuring that the byte // limit is not exceeded return $this->stream->read(min($remaining, $length)); } return ''; } } HttpFactory.php000064400000006003152430110220007511 0ustar00getSize(); } return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType); } public function createStream(string $content = ''): StreamInterface { return Utils::streamFor($content); } public function createStreamFromFile(string $file, string $mode = 'r'): StreamInterface { try { $resource = Utils::tryFopen($file, $mode); } catch (\RuntimeException $e) { if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) { throw new \InvalidArgumentException(sprintf('Invalid file opening mode "%s"', $mode), 0, $e); } throw $e; } return Utils::streamFor($resource); } public function createStreamFromResource($resource): StreamInterface { return Utils::streamFor($resource); } public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface { if (empty($method)) { if (!empty($serverParams['REQUEST_METHOD'])) { $method = $serverParams['REQUEST_METHOD']; } else { throw new \InvalidArgumentException('Cannot determine HTTP method'); } } return new ServerRequest($method, $uri, [], null, '1.1', $serverParams); } public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface { return new Response($code, [], null, '1.1', $reasonPhrase); } public function createRequest(string $method, $uri): RequestInterface { return new Request($method, $uri); } public function createUri(string $uri = ''): UriInterface { return new Uri($uri); } } StreamDecoratorTrait.php000064400000006366152430110220011360 0ustar00stream = $stream; } /** * Magic method used to create a new stream if streams are not added in * the constructor of a decorator (e.g., LazyOpenStream). * * @return StreamInterface */ public function __get(string $name) { if ($name === 'stream') { $this->stream = $this->createStream(); return $this->stream; } throw new \UnexpectedValueException("$name not found on class"); } public function __toString(): string { try { if ($this->isSeekable()) { $this->seek(0); } return $this->getContents(); } catch (\Throwable $e) { if (\PHP_VERSION_ID >= 70400) { throw $e; } trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); return ''; } } public function getContents(): string { return Utils::copyToString($this); } /** * Allow decorators to implement custom methods * * @return mixed */ public function __call(string $method, array $args) { /** @var callable $callable */ $callable = [$this->stream, $method]; $result = ($callable)(...$args); // Always return the wrapped object if the result is a return $this return $result === $this->stream ? $this : $result; } public function close(): void { $this->stream->close(); } /** * @return mixed */ public function getMetadata($key = null) { return $this->stream->getMetadata($key); } public function detach() { return $this->stream->detach(); } public function getSize(): ?int { return $this->stream->getSize(); } public function eof(): bool { return $this->stream->eof(); } public function tell(): int { return $this->stream->tell(); } public function isReadable(): bool { return $this->stream->isReadable(); } public function isWritable(): bool { return $this->stream->isWritable(); } public function isSeekable(): bool { return $this->stream->isSeekable(); } public function rewind(): void { $this->seek(0); } public function seek($offset, $whence = SEEK_SET): void { $this->stream->seek($offset, $whence); } public function read($length): string { return $this->stream->read($length); } public function write($string): int { return $this->stream->write($string); } /** * Implement in subclasses to dynamically create streams when requested. * * @throws \BadMethodCallException */ protected function createStream(): StreamInterface { throw new \BadMethodCallException('Not implemented'); } } Uri.php000064400000052704152430110220006012 0ustar00 80, 'https' => 443, 'ftp' => 21, 'gopher' => 70, 'nntp' => 119, 'news' => 119, 'telnet' => 23, 'tn3270' => 23, 'imap' => 143, 'pop' => 110, 'ldap' => 389, ]; /** * Unreserved characters for use in a regex. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3 */ private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; /** * Sub-delims for use in a regex. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 */ private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;='; private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26']; /** @var string Uri scheme. */ private $scheme = ''; /** @var string Uri user info. */ private $userInfo = ''; /** @var string Uri host. */ private $host = ''; /** @var int|null Uri port. */ private $port; /** @var string Uri path. */ private $path = ''; /** @var string Uri query string. */ private $query = ''; /** @var string Uri fragment. */ private $fragment = ''; /** @var string|null String representation */ private $composedComponents; public function __construct(string $uri = '') { if ($uri !== '') { $parts = self::parse($uri); if ($parts === false) { throw new MalformedUriException("Unable to parse URI: $uri"); } $this->applyParts($parts); } } /** * UTF-8 aware \parse_url() replacement. * * The internal function produces broken output for non ASCII domain names * (IDN) when used with locales other than "C". * * On the other hand, cURL understands IDN correctly only when UTF-8 locale * is configured ("C.UTF-8", "en_US.UTF-8", etc.). * * @see https://bugs.php.net/bug.php?id=52923 * @see https://www.php.net/manual/en/function.parse-url.php#114817 * @see https://curl.haxx.se/libcurl/c/CURLOPT_URL.html#ENCODING * * @return array|false */ private static function parse(string $url) { // If IPv6 $prefix = ''; if (preg_match('%^(.*://\[[0-9:a-f]+\])(.*?)$%', $url, $matches)) { /** @var array{0:string, 1:string, 2:string} $matches */ $prefix = $matches[1]; $url = $matches[2]; } /** @var string */ $encodedUrl = preg_replace_callback( '%[^:/@?&=#]+%usD', static function ($matches) { return urlencode($matches[0]); }, $url ); $result = parse_url($prefix.$encodedUrl); if ($result === false) { return false; } return array_map('urldecode', $result); } public function __toString(): string { if ($this->composedComponents === null) { $this->composedComponents = self::composeComponents( $this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment ); } return $this->composedComponents; } /** * Composes a URI reference string from its various components. * * Usually this method does not need to be called manually but instead is used indirectly via * `Psr\Http\Message\UriInterface::__toString`. * * PSR-7 UriInterface treats an empty component the same as a missing component as * getQuery(), getFragment() etc. always return a string. This explains the slight * difference to RFC 3986 Section 5.3. * * Another adjustment is that the authority separator is added even when the authority is missing/empty * for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with * `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to * that format). * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.3 */ public static function composeComponents(?string $scheme, ?string $authority, string $path, ?string $query, ?string $fragment): string { $uri = ''; // weak type checks to also accept null until we can add scalar type hints if ($scheme != '') { $uri .= $scheme.':'; } if ($authority != '' || $scheme === 'file') { $uri .= '//'.$authority; } if ($authority != '' && $path != '' && $path[0] != '/') { $path = '/'.$path; } $uri .= $path; if ($query != '') { $uri .= '?'.$query; } if ($fragment != '') { $uri .= '#'.$fragment; } return $uri; } /** * Whether the URI has the default port of the current scheme. * * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used * independently of the implementation. */ public static function isDefaultPort(UriInterface $uri): bool { return $uri->getPort() === null || (isset(self::DEFAULT_PORTS[$uri->getScheme()]) && $uri->getPort() === self::DEFAULT_PORTS[$uri->getScheme()]); } /** * Whether the URI is absolute, i.e. it has a scheme. * * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative * to another URI, the base URI. Relative references can be divided into several forms: * - network-path references, e.g. '//example.com/path' * - absolute-path references, e.g. '/path' * - relative-path references, e.g. 'subpath' * * @see Uri::isNetworkPathReference * @see Uri::isAbsolutePathReference * @see Uri::isRelativePathReference * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4 */ public static function isAbsolute(UriInterface $uri): bool { return $uri->getScheme() !== ''; } /** * Whether the URI is a network-path reference. * * A relative reference that begins with two slash characters is termed an network-path reference. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 */ public static function isNetworkPathReference(UriInterface $uri): bool { return $uri->getScheme() === '' && $uri->getAuthority() !== ''; } /** * Whether the URI is a absolute-path reference. * * A relative reference that begins with a single slash character is termed an absolute-path reference. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 */ public static function isAbsolutePathReference(UriInterface $uri): bool { return $uri->getScheme() === '' && $uri->getAuthority() === '' && isset($uri->getPath()[0]) && $uri->getPath()[0] === '/'; } /** * Whether the URI is a relative-path reference. * * A relative reference that does not begin with a slash character is termed a relative-path reference. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 */ public static function isRelativePathReference(UriInterface $uri): bool { return $uri->getScheme() === '' && $uri->getAuthority() === '' && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/'); } /** * Whether the URI is a same-document reference. * * A same-document reference refers to a URI that is, aside from its fragment * component, identical to the base URI. When no base URI is given, only an empty * URI reference (apart from its fragment) is considered a same-document reference. * * @param UriInterface $uri The URI to check * @param UriInterface|null $base An optional base URI to compare against * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.4 */ public static function isSameDocumentReference(UriInterface $uri, ?UriInterface $base = null): bool { if ($base !== null) { $uri = UriResolver::resolve($base, $uri); return ($uri->getScheme() === $base->getScheme()) && ($uri->getAuthority() === $base->getAuthority()) && ($uri->getPath() === $base->getPath()) && ($uri->getQuery() === $base->getQuery()); } return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === ''; } /** * Creates a new URI with a specific query string value removed. * * Any existing query string values that exactly match the provided key are * removed. * * @param UriInterface $uri URI to use as a base. * @param string $key Query string key to remove. */ public static function withoutQueryValue(UriInterface $uri, string $key): UriInterface { $result = self::getFilteredQueryString($uri, [$key]); return $uri->withQuery(implode('&', $result)); } /** * Creates a new URI with a specific query string value. * * Any existing query string values that exactly match the provided key are * removed and replaced with the given key value pair. * * A value of null will set the query string key without a value, e.g. "key" * instead of "key=value". * * @param UriInterface $uri URI to use as a base. * @param string $key Key to set. * @param string|null $value Value to set */ public static function withQueryValue(UriInterface $uri, string $key, ?string $value): UriInterface { $result = self::getFilteredQueryString($uri, [$key]); $result[] = self::generateQueryString($key, $value); return $uri->withQuery(implode('&', $result)); } /** * Creates a new URI with multiple specific query string values. * * It has the same behavior as withQueryValue() but for an associative array of key => value. * * @param UriInterface $uri URI to use as a base. * @param (string|null)[] $keyValueArray Associative array of key and values */ public static function withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface { $result = self::getFilteredQueryString($uri, array_keys($keyValueArray)); foreach ($keyValueArray as $key => $value) { $result[] = self::generateQueryString((string) $key, $value !== null ? (string) $value : null); } return $uri->withQuery(implode('&', $result)); } /** * Creates a URI from a hash of `parse_url` components. * * @see https://www.php.net/manual/en/function.parse-url.php * * @throws MalformedUriException If the components do not form a valid URI. */ public static function fromParts(array $parts): UriInterface { $uri = new self(); $uri->applyParts($parts); $uri->validateState(); return $uri; } public function getScheme(): string { return $this->scheme; } public function getAuthority(): string { $authority = $this->host; if ($this->userInfo !== '') { $authority = $this->userInfo.'@'.$authority; } if ($this->port !== null) { $authority .= ':'.$this->port; } return $authority; } public function getUserInfo(): string { return $this->userInfo; } public function getHost(): string { return $this->host; } public function getPort(): ?int { return $this->port; } public function getPath(): string { return $this->path; } public function getQuery(): string { return $this->query; } public function getFragment(): string { return $this->fragment; } public function withScheme($scheme): UriInterface { $scheme = $this->filterScheme($scheme); if ($this->scheme === $scheme) { return $this; } $new = clone $this; $new->scheme = $scheme; $new->composedComponents = null; $new->removeDefaultPort(); $new->validateState(); return $new; } public function withUserInfo($user, $password = null): UriInterface { $info = $this->filterUserInfoComponent($user); if ($password !== null) { $info .= ':'.$this->filterUserInfoComponent($password); } if ($this->userInfo === $info) { return $this; } $new = clone $this; $new->userInfo = $info; $new->composedComponents = null; $new->validateState(); return $new; } public function withHost($host): UriInterface { $host = $this->filterHost($host); if ($this->host === $host) { return $this; } $new = clone $this; $new->host = $host; $new->composedComponents = null; $new->validateState(); return $new; } public function withPort($port): UriInterface { $port = $this->filterPort($port); if ($this->port === $port) { return $this; } $new = clone $this; $new->port = $port; $new->composedComponents = null; $new->removeDefaultPort(); $new->validateState(); return $new; } public function withPath($path): UriInterface { $path = $this->filterPath($path); if ($this->path === $path) { return $this; } $new = clone $this; $new->path = $path; $new->composedComponents = null; $new->validateState(); return $new; } public function withQuery($query): UriInterface { $query = $this->filterQueryAndFragment($query); if ($this->query === $query) { return $this; } $new = clone $this; $new->query = $query; $new->composedComponents = null; return $new; } public function withFragment($fragment): UriInterface { $fragment = $this->filterQueryAndFragment($fragment); if ($this->fragment === $fragment) { return $this; } $new = clone $this; $new->fragment = $fragment; $new->composedComponents = null; return $new; } public function jsonSerialize(): string { return $this->__toString(); } /** * Apply parse_url parts to a URI. * * @param array $parts Array of parse_url parts to apply. */ private function applyParts(array $parts): void { $this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : ''; $this->userInfo = isset($parts['user']) ? $this->filterUserInfoComponent($parts['user']) : ''; $this->host = isset($parts['host']) ? $this->filterHost($parts['host']) : ''; $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : ''; if (isset($parts['pass'])) { $this->userInfo .= ':'.$this->filterUserInfoComponent($parts['pass']); } $this->removeDefaultPort(); } /** * @param mixed $scheme * * @throws \InvalidArgumentException If the scheme is invalid. */ private function filterScheme($scheme): string { if (!is_string($scheme)) { throw new \InvalidArgumentException('Scheme must be a string'); } return \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); } /** * @param mixed $component * * @throws \InvalidArgumentException If the user info is invalid. */ private function filterUserInfoComponent($component): string { if (!is_string($component)) { throw new \InvalidArgumentException('User info must be a string'); } return preg_replace_callback( '/(?:[^%'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $component ); } /** * @param mixed $host * * @throws \InvalidArgumentException If the host is invalid. */ private function filterHost($host): string { if (!is_string($host)) { throw new \InvalidArgumentException('Host must be a string'); } return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); } /** * @param mixed $port * * @throws \InvalidArgumentException If the port is invalid. */ private function filterPort($port): ?int { if ($port === null) { return null; } $port = (int) $port; if (0 > $port || 0xFFFF < $port) { throw new \InvalidArgumentException( sprintf('Invalid port: %d. Must be between 0 and 65535', $port) ); } return $port; } /** * @param (string|int)[] $keys * * @return string[] */ private static function getFilteredQueryString(UriInterface $uri, array $keys): array { $current = $uri->getQuery(); if ($current === '') { return []; } $decodedKeys = array_map(function ($k): string { return rawurldecode((string) $k); }, $keys); return array_filter(explode('&', $current), function ($part) use ($decodedKeys) { return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true); }); } private static function generateQueryString(string $key, ?string $value): string { // Query string separators ("=", "&") within the key or value need to be encoded // (while preventing double-encoding) before setting the query string. All other // chars that need percent-encoding will be encoded by withQuery(). $queryString = strtr($key, self::QUERY_SEPARATORS_REPLACEMENT); if ($value !== null) { $queryString .= '='.strtr($value, self::QUERY_SEPARATORS_REPLACEMENT); } return $queryString; } private function removeDefaultPort(): void { if ($this->port !== null && self::isDefaultPort($this)) { $this->port = null; } } /** * Filters the path of a URI * * @param mixed $path * * @throws \InvalidArgumentException If the path is invalid. */ private function filterPath($path): string { if (!is_string($path)) { throw new \InvalidArgumentException('Path must be a string'); } return preg_replace_callback( '/(?:[^'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $path ); } /** * Filters the query string or fragment of a URI. * * @param mixed $str * * @throws \InvalidArgumentException If the query or fragment is invalid. */ private function filterQueryAndFragment($str): string { if (!is_string($str)) { throw new \InvalidArgumentException('Query and fragment must be a string'); } return preg_replace_callback( '/(?:[^'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $str ); } private function rawurlencodeMatchZero(array $match): string { return rawurlencode($match[0]); } private function validateState(): void { if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) { $this->host = self::HTTP_DEFAULT_HOST; } if ($this->getAuthority() === '') { if (0 === strpos($this->path, '//')) { throw new MalformedUriException('The path of a URI without an authority must not start with two slashes "//"'); } if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) { throw new MalformedUriException('A relative URI must not have a path beginning with a segment containing a colon'); } } } } Utils.php000064400000037107152430110220006353 0ustar00 $v) { if (!in_array(strtolower((string) $k), $keys)) { $result[$k] = $v; } } return $result; } /** * Copy the contents of a stream into another stream until the given number * of bytes have been read. * * @param StreamInterface $source Stream to read from * @param StreamInterface $dest Stream to write to * @param int $maxLen Maximum number of bytes to read. Pass -1 * to read the entire stream. * * @throws \RuntimeException on error. */ public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): void { $bufferSize = 8192; if ($maxLen === -1) { while (!$source->eof()) { if (!$dest->write($source->read($bufferSize))) { break; } } } else { $remaining = $maxLen; while ($remaining > 0 && !$source->eof()) { $buf = $source->read(min($bufferSize, $remaining)); $len = strlen($buf); if (!$len) { break; } $remaining -= $len; $dest->write($buf); } } } /** * Copy the contents of a stream into a string until the given number of * bytes have been read. * * @param StreamInterface $stream Stream to read * @param int $maxLen Maximum number of bytes to read. Pass -1 * to read the entire stream. * * @throws \RuntimeException on error. */ public static function copyToString(StreamInterface $stream, int $maxLen = -1): string { $buffer = ''; if ($maxLen === -1) { while (!$stream->eof()) { $buf = $stream->read(1048576); if ($buf === '') { break; } $buffer .= $buf; } return $buffer; } $len = 0; while (!$stream->eof() && $len < $maxLen) { $buf = $stream->read($maxLen - $len); if ($buf === '') { break; } $buffer .= $buf; $len = strlen($buffer); } return $buffer; } /** * Calculate a hash of a stream. * * This method reads the entire stream to calculate a rolling hash, based * on PHP's `hash_init` functions. * * @param StreamInterface $stream Stream to calculate the hash for * @param string $algo Hash algorithm (e.g. md5, crc32, etc) * @param bool $rawOutput Whether or not to use raw output * * @throws \RuntimeException on error. */ public static function hash(StreamInterface $stream, string $algo, bool $rawOutput = false): string { $pos = $stream->tell(); if ($pos > 0) { $stream->rewind(); } $ctx = hash_init($algo); while (!$stream->eof()) { hash_update($ctx, $stream->read(1048576)); } $out = hash_final($ctx, $rawOutput); $stream->seek($pos); return $out; } /** * Clone and modify a request with the given changes. * * This method is useful for reducing the number of clones needed to mutate * a message. * * The changes can be one of: * - method: (string) Changes the HTTP method. * - set_headers: (array) Sets the given headers. * - remove_headers: (array) Remove the given headers. * - body: (mixed) Sets the given body. * - uri: (UriInterface) Set the URI. * - query: (string) Set the query string value of the URI. * - version: (string) Set the protocol version. * * @param RequestInterface $request Request to clone and modify. * @param array $changes Changes to apply. */ public static function modifyRequest(RequestInterface $request, array $changes): RequestInterface { if (!$changes) { return $request; } $headers = $request->getHeaders(); if (!isset($changes['uri'])) { $uri = $request->getUri(); } else { // Remove the host header if one is on the URI if ($host = $changes['uri']->getHost()) { $changes['set_headers']['Host'] = $host; if ($port = $changes['uri']->getPort()) { $standardPorts = ['http' => 80, 'https' => 443]; $scheme = $changes['uri']->getScheme(); if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) { $changes['set_headers']['Host'] .= ':'.$port; } } } $uri = $changes['uri']; } if (!empty($changes['remove_headers'])) { $headers = self::caselessRemove($changes['remove_headers'], $headers); } if (!empty($changes['set_headers'])) { $headers = self::caselessRemove(array_keys($changes['set_headers']), $headers); $headers = $changes['set_headers'] + $headers; } if (isset($changes['query'])) { $uri = $uri->withQuery($changes['query']); } if ($request instanceof ServerRequestInterface) { $new = (new ServerRequest( $changes['method'] ?? $request->getMethod(), $uri, $headers, $changes['body'] ?? $request->getBody(), $changes['version'] ?? $request->getProtocolVersion(), $request->getServerParams() )) ->withParsedBody($request->getParsedBody()) ->withQueryParams($request->getQueryParams()) ->withCookieParams($request->getCookieParams()) ->withUploadedFiles($request->getUploadedFiles()); foreach ($request->getAttributes() as $key => $value) { $new = $new->withAttribute($key, $value); } return $new; } return new Request( $changes['method'] ?? $request->getMethod(), $uri, $headers, $changes['body'] ?? $request->getBody(), $changes['version'] ?? $request->getProtocolVersion() ); } /** * Read a line from the stream up to the maximum allowed buffer length. * * @param StreamInterface $stream Stream to read from * @param int|null $maxLength Maximum buffer length */ public static function readLine(StreamInterface $stream, ?int $maxLength = null): string { $buffer = ''; $size = 0; while (!$stream->eof()) { if ('' === ($byte = $stream->read(1))) { return $buffer; } $buffer .= $byte; // Break when a new line is found or the max length - 1 is reached if ($byte === "\n" || ++$size === $maxLength - 1) { break; } } return $buffer; } /** * Redact the password in the user info part of a URI. */ public static function redactUserInfo(UriInterface $uri): UriInterface { $userInfo = $uri->getUserInfo(); if (false !== ($pos = \strpos($userInfo, ':'))) { return $uri->withUserInfo(\substr($userInfo, 0, $pos), '***'); } return $uri; } /** * Create a new stream based on the input type. * * Options is an associative array that can contain the following keys: * - metadata: Array of custom metadata. * - size: Size of the stream. * * This method accepts the following `$resource` types: * - `Psr\Http\Message\StreamInterface`: Returns the value as-is. * - `string`: Creates a stream object that uses the given string as the contents. * - `resource`: Creates a stream object that wraps the given PHP stream resource. * - `Iterator`: If the provided value implements `Iterator`, then a read-only * stream object will be created that wraps the given iterable. Each time the * stream is read from, data from the iterator will fill a buffer and will be * continuously called until the buffer is equal to the requested read size. * Subsequent read calls will first read from the buffer and then call `next` * on the underlying iterator until it is exhausted. * - `object` with `__toString()`: If the object has the `__toString()` method, * the object will be cast to a string and then a stream will be returned that * uses the string value. * - `NULL`: When `null` is passed, an empty stream object is returned. * - `callable` When a callable is passed, a read-only stream object will be * created that invokes the given callable. The callable is invoked with the * number of suggested bytes to read. The callable can return any number of * bytes, but MUST return `false` when there is no more data to return. The * stream object that wraps the callable will invoke the callable until the * number of requested bytes are available. Any additional bytes will be * buffered and used in subsequent reads. * * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data * @param array{size?: int, metadata?: array} $options Additional options * * @throws \InvalidArgumentException if the $resource arg is not valid. */ public static function streamFor($resource = '', array $options = []): StreamInterface { if (is_scalar($resource)) { $stream = self::tryFopen('php://temp', 'r+'); if ($resource !== '') { fwrite($stream, (string) $resource); fseek($stream, 0); } return new Stream($stream, $options); } switch (gettype($resource)) { case 'resource': /* * The 'php://input' is a special stream with quirks and inconsistencies. * We avoid using that stream by reading it into php://temp */ /** @var resource $resource */ if ((\stream_get_meta_data($resource)['uri'] ?? '') === 'php://input') { $stream = self::tryFopen('php://temp', 'w+'); stream_copy_to_stream($resource, $stream); fseek($stream, 0); $resource = $stream; } return new Stream($resource, $options); case 'object': /** @var object $resource */ if ($resource instanceof StreamInterface) { return $resource; } elseif ($resource instanceof \Iterator) { return new PumpStream(function () use ($resource) { if (!$resource->valid()) { return false; } $result = $resource->current(); $resource->next(); return $result; }, $options); } elseif (method_exists($resource, '__toString')) { return self::streamFor((string) $resource, $options); } break; case 'NULL': return new Stream(self::tryFopen('php://temp', 'r+'), $options); } if (is_callable($resource)) { return new PumpStream($resource, $options); } throw new \InvalidArgumentException('Invalid resource type: '.gettype($resource)); } /** * Safely opens a PHP stream resource using a filename. * * When fopen fails, PHP normally raises a warning. This function adds an * error handler that checks for errors and throws an exception instead. * * @param string $filename File to open * @param string $mode Mode used to open the file * * @return resource * * @throws \RuntimeException if the file cannot be opened */ public static function tryFopen(string $filename, string $mode) { $ex = null; set_error_handler(static function (int $errno, string $errstr) use ($filename, $mode, &$ex): bool { $ex = new \RuntimeException(sprintf( 'Unable to open "%s" using mode "%s": %s', $filename, $mode, $errstr )); return true; }); try { /** @var resource $handle */ $handle = fopen($filename, $mode); } catch (\Throwable $e) { $ex = new \RuntimeException(sprintf( 'Unable to open "%s" using mode "%s": %s', $filename, $mode, $e->getMessage() ), 0, $e); } restore_error_handler(); if ($ex) { /** @var $ex \RuntimeException */ throw $ex; } return $handle; } /** * Safely gets the contents of a given stream. * * When stream_get_contents fails, PHP normally raises a warning. This * function adds an error handler that checks for errors and throws an * exception instead. * * @param resource $stream * * @throws \RuntimeException if the stream cannot be read */ public static function tryGetContents($stream): string { $ex = null; set_error_handler(static function (int $errno, string $errstr) use (&$ex): bool { $ex = new \RuntimeException(sprintf( 'Unable to read stream contents: %s', $errstr )); return true; }); try { /** @var string|false $contents */ $contents = stream_get_contents($stream); if ($contents === false) { $ex = new \RuntimeException('Unable to read stream contents'); } } catch (\Throwable $e) { $ex = new \RuntimeException(sprintf( 'Unable to read stream contents: %s', $e->getMessage() ), 0, $e); } restore_error_handler(); if ($ex) { /** @var $ex \RuntimeException */ throw $ex; } return $contents; } /** * Returns a UriInterface for the given value. * * This function accepts a string or UriInterface and returns a * UriInterface for the given value. If the value is already a * UriInterface, it is returned as-is. * * @param string|UriInterface $uri * * @throws \InvalidArgumentException */ public static function uriFor($uri): UriInterface { if ($uri instanceof UriInterface) { return $uri; } if (is_string($uri)) { return new Uri($uri); } throw new \InvalidArgumentException('URI must be a string or UriInterface'); } } MessageTrait.php000064400000017100152430110220007632 0ustar00 array of values */ private $headers = []; /** @var string[] Map of lowercase header name => original name at registration */ private $headerNames = []; /** @var string */ private $protocol = '1.1'; /** @var StreamInterface|null */ private $stream; public function getProtocolVersion(): string { return $this->protocol; } public function withProtocolVersion($version): MessageInterface { if ($this->protocol === $version) { return $this; } $new = clone $this; $new->protocol = $version; return $new; } public function getHeaders(): array { return $this->headers; } public function hasHeader($header): bool { return isset($this->headerNames[strtolower($header)]); } public function getHeader($header): array { $header = strtolower($header); if (!isset($this->headerNames[$header])) { return []; } $header = $this->headerNames[$header]; return $this->headers[$header]; } public function getHeaderLine($header): string { return implode(', ', $this->getHeader($header)); } public function withHeader($header, $value): MessageInterface { $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); $normalized = strtolower($header); $new = clone $this; if (isset($new->headerNames[$normalized])) { unset($new->headers[$new->headerNames[$normalized]]); } $new->headerNames[$normalized] = $header; $new->headers[$header] = $value; return $new; } public function withAddedHeader($header, $value): MessageInterface { $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); $normalized = strtolower($header); $new = clone $this; if (isset($new->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; $new->headers[$header] = array_merge($this->headers[$header], $value); } else { $new->headerNames[$normalized] = $header; $new->headers[$header] = $value; } return $new; } public function withoutHeader($header): MessageInterface { $normalized = strtolower($header); if (!isset($this->headerNames[$normalized])) { return $this; } $header = $this->headerNames[$normalized]; $new = clone $this; unset($new->headers[$header], $new->headerNames[$normalized]); return $new; } public function getBody(): StreamInterface { if (!$this->stream) { $this->stream = Utils::streamFor(''); } return $this->stream; } public function withBody(StreamInterface $body): MessageInterface { if ($body === $this->stream) { return $this; } $new = clone $this; $new->stream = $body; return $new; } /** * @param (string|string[])[] $headers */ private function setHeaders(array $headers): void { $this->headerNames = $this->headers = []; foreach ($headers as $header => $value) { // Numeric array keys are converted to int by PHP. $header = (string) $header; $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); $normalized = strtolower($header); if (isset($this->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; $this->headers[$header] = array_merge($this->headers[$header], $value); } else { $this->headerNames[$normalized] = $header; $this->headers[$header] = $value; } } } /** * @param mixed $value * * @return string[] */ private function normalizeHeaderValue($value): array { if (!is_array($value)) { return $this->trimAndValidateHeaderValues([$value]); } if (count($value) === 0) { throw new \InvalidArgumentException('Header value can not be an empty array.'); } return $this->trimAndValidateHeaderValues($value); } /** * Trims whitespace from the header values. * * Spaces and tabs ought to be excluded by parsers when extracting the field value from a header field. * * header-field = field-name ":" OWS field-value OWS * OWS = *( SP / HTAB ) * * @param mixed[] $values Header values * * @return string[] Trimmed header values * * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4 */ private function trimAndValidateHeaderValues(array $values): array { return array_map(function ($value) { if (!is_scalar($value) && null !== $value) { throw new \InvalidArgumentException(sprintf( 'Header value must be scalar or null but %s provided.', is_object($value) ? get_class($value) : gettype($value) )); } $trimmed = trim((string) $value, " \t"); $this->assertValue($trimmed); return $trimmed; }, array_values($values)); } /** * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 * * @param mixed $header */ private function assertHeader($header): void { if (!is_string($header)) { throw new \InvalidArgumentException(sprintf( 'Header name must be a string but %s provided.', is_object($header) ? get_class($header) : gettype($header) )); } if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $header)) { throw new \InvalidArgumentException( sprintf('"%s" is not valid header name.', $header) ); } } /** * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 * * field-value = *( field-content / obs-fold ) * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] * field-vchar = VCHAR / obs-text * VCHAR = %x21-7E * obs-text = %x80-FF * obs-fold = CRLF 1*( SP / HTAB ) */ private function assertValue(string $value): void { // The regular expression intentionally does not support the obs-fold production, because as // per RFC 7230#3.2.4: // // A sender MUST NOT generate a message that includes // line folding (i.e., that has any field-value that contains a match to // the obs-fold rule) unless the message is intended for packaging // within the message/http media type. // // Clients must not send a request with line folding and a server sending folded headers is // likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting // folding is not likely to break any legitimate use case. if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) { throw new \InvalidArgumentException( sprintf('"%s" is not valid header value.', $value) ); } } } Stream.php000064400000016332152430110220006503 0ustar00size = $options['size']; } $this->customMetadata = $options['metadata'] ?? []; $this->stream = $stream; $meta = stream_get_meta_data($this->stream); $this->seekable = $meta['seekable']; $this->readable = (bool) preg_match(self::READABLE_MODES, $meta['mode']); $this->writable = (bool) preg_match(self::WRITABLE_MODES, $meta['mode']); $this->uri = $this->getMetadata('uri'); } /** * Closes the stream when the destructed */ public function __destruct() { $this->close(); } public function __toString(): string { try { if ($this->isSeekable()) { $this->seek(0); } return $this->getContents(); } catch (\Throwable $e) { if (\PHP_VERSION_ID >= 70400) { throw $e; } trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); return ''; } } public function getContents(): string { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->readable) { throw new \RuntimeException('Cannot read from non-readable stream'); } return Utils::tryGetContents($this->stream); } public function close(): void { if (isset($this->stream)) { if (is_resource($this->stream)) { fclose($this->stream); } $this->detach(); } } public function detach() { if (!isset($this->stream)) { return null; } $result = $this->stream; unset($this->stream); $this->size = $this->uri = null; $this->readable = $this->writable = $this->seekable = false; return $result; } public function getSize(): ?int { if ($this->size !== null) { return $this->size; } if (!isset($this->stream)) { return null; } // Clear the stat cache if the stream has a URI if ($this->uri) { clearstatcache(true, $this->uri); } $stats = fstat($this->stream); if (is_array($stats) && isset($stats['size'])) { $this->size = $stats['size']; return $this->size; } return null; } public function isReadable(): bool { return $this->readable; } public function isWritable(): bool { return $this->writable; } public function isSeekable(): bool { return $this->seekable; } public function eof(): bool { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } return feof($this->stream); } public function tell(): int { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } $result = ftell($this->stream); if ($result === false) { throw new \RuntimeException('Unable to determine stream position'); } return $result; } public function rewind(): void { $this->seek(0); } public function seek($offset, $whence = SEEK_SET): void { $whence = (int) $whence; if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->seekable) { throw new \RuntimeException('Stream is not seekable'); } if (fseek($this->stream, $offset, $whence) === -1) { throw new \RuntimeException('Unable to seek to stream position ' .$offset.' with whence '.var_export($whence, true)); } } public function read($length): string { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->readable) { throw new \RuntimeException('Cannot read from non-readable stream'); } if ($length < 0) { throw new \RuntimeException('Length parameter cannot be negative'); } if (0 === $length) { return ''; } try { $string = fread($this->stream, $length); } catch (\Exception $e) { throw new \RuntimeException('Unable to read from stream', 0, $e); } if (false === $string) { throw new \RuntimeException('Unable to read from stream'); } return $string; } public function write($string): int { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->writable) { throw new \RuntimeException('Cannot write to a non-writable stream'); } // We can't know the size after writing anything $this->size = null; $result = fwrite($this->stream, $string); if ($result === false) { throw new \RuntimeException('Unable to write to stream'); } return $result; } /** * @return mixed */ public function getMetadata($key = null) { if (!isset($this->stream)) { return $key ? null : []; } elseif (!$key) { return $this->customMetadata + stream_get_meta_data($this->stream); } elseif (isset($this->customMetadata[$key])) { return $this->customMetadata[$key]; } $meta = stream_get_meta_data($this->stream); return $meta[$key] ?? null; } } Message.php000064400000020217152430110220006631 0ustar00getMethod().' ' .$message->getRequestTarget()) .' HTTP/'.$message->getProtocolVersion(); if (!$message->hasHeader('host')) { $msg .= "\r\nHost: ".$message->getUri()->getHost(); } } elseif ($message instanceof ResponseInterface) { $msg = 'HTTP/'.$message->getProtocolVersion().' ' .$message->getStatusCode().' ' .$message->getReasonPhrase(); } else { throw new \InvalidArgumentException('Unknown message type'); } foreach ($message->getHeaders() as $name => $values) { if (is_string($name) && strtolower($name) === 'set-cookie') { foreach ($values as $value) { $msg .= "\r\n{$name}: ".$value; } } else { $msg .= "\r\n{$name}: ".implode(', ', $values); } } return "{$msg}\r\n\r\n".$message->getBody(); } /** * Get a short summary of the message body. * * Will return `null` if the response is not printable. * * @param MessageInterface $message The message to get the body summary * @param int $truncateAt The maximum allowed size of the summary */ public static function bodySummary(MessageInterface $message, int $truncateAt = 120): ?string { $body = $message->getBody(); if (!$body->isSeekable() || !$body->isReadable()) { return null; } $size = $body->getSize(); if ($size === 0) { return null; } $body->rewind(); $summary = $body->read($truncateAt); $body->rewind(); if ($size > $truncateAt) { $summary .= ' (truncated...)'; } // Matches any printable character, including unicode characters: // letters, marks, numbers, punctuation, spacing, and separators. if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary) !== 0) { return null; } return $summary; } /** * Attempts to rewind a message body and throws an exception on failure. * * The body of the message will only be rewound if a call to `tell()` * returns a value other than `0`. * * @param MessageInterface $message Message to rewind * * @throws \RuntimeException */ public static function rewindBody(MessageInterface $message): void { $body = $message->getBody(); if ($body->tell()) { $body->rewind(); } } /** * Parses an HTTP message into an associative array. * * The array contains the "start-line" key containing the start line of * the message, "headers" key containing an associative array of header * array values, and a "body" key containing the body of the message. * * @param string $message HTTP request or response to parse. */ public static function parseMessage(string $message): array { if (!$message) { throw new \InvalidArgumentException('Invalid message'); } $message = ltrim($message, "\r\n"); $messageParts = preg_split("/\r?\n\r?\n/", $message, 2); if ($messageParts === false || count($messageParts) !== 2) { throw new \InvalidArgumentException('Invalid message: Missing header delimiter'); } [$rawHeaders, $body] = $messageParts; $rawHeaders .= "\r\n"; // Put back the delimiter we split previously $headerParts = preg_split("/\r?\n/", $rawHeaders, 2); if ($headerParts === false || count($headerParts) !== 2) { throw new \InvalidArgumentException('Invalid message: Missing status line'); } [$startLine, $rawHeaders] = $headerParts; if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') { // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0 $rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders); } /** @var array[] $headerLines */ $count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER); // If these aren't the same, then one line didn't match and there's an invalid header. if ($count !== substr_count($rawHeaders, "\n")) { // Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4 if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) { throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding'); } throw new \InvalidArgumentException('Invalid header syntax'); } $headers = []; foreach ($headerLines as $headerLine) { $headers[$headerLine[1]][] = $headerLine[2]; } return [ 'start-line' => $startLine, 'headers' => $headers, 'body' => $body, ]; } /** * Constructs a URI for an HTTP request message. * * @param string $path Path from the start-line * @param array $headers Array of headers (each value an array). */ public static function parseRequestUri(string $path, array $headers): string { $hostKey = array_filter(array_keys($headers), function ($k) { // Numeric array keys are converted to int by PHP. $k = (string) $k; return strtolower($k) === 'host'; }); // If no host is found, then a full URI cannot be constructed. if (!$hostKey) { return $path; } $host = $headers[reset($hostKey)][0]; $scheme = substr($host, -4) === ':443' ? 'https' : 'http'; return $scheme.'://'.$host.'/'.ltrim($path, '/'); } /** * Parses a request message string into a request object. * * @param string $message Request message string. */ public static function parseRequest(string $message): RequestInterface { $data = self::parseMessage($message); $matches = []; if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) { throw new \InvalidArgumentException('Invalid request string'); } $parts = explode(' ', $data['start-line'], 3); $version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1'; $request = new Request( $parts[0], $matches[1] === '/' ? self::parseRequestUri($parts[1], $data['headers']) : $parts[1], $data['headers'], $data['body'], $version ); return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]); } /** * Parses a response message string into a response object. * * @param string $message Response message string. */ public static function parseResponse(string $message): ResponseInterface { $data = self::parseMessage($message); // According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 // the space between status-code and reason-phrase is required. But // browsers accept responses without space and reason as well. if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) { throw new \InvalidArgumentException('Invalid response string: '.$data['start-line']); } $parts = explode(' ', $data['start-line'], 3); return new Response( (int) $parts[1], $data['headers'], $data['body'], explode('/', $parts[0])[1], $parts[2] ?? null ); } } DroppingStream.php000064400000002254152430110220010204 0ustar00stream = $stream; $this->maxLength = $maxLength; } public function write($string): int { $diff = $this->maxLength - $this->stream->getSize(); // Begin returning 0 when the underlying stream is too large. if ($diff <= 0) { return 0; } // Write the stream or a subset of the stream if needed. if (strlen($string) < $diff) { return $this->stream->write($string); } return $this->stream->write(substr($string, 0, $diff)); } } ServerRequest.php000064400000022515152430110220010067 0ustar00serverParams = $serverParams; parent::__construct($method, $uri, $headers, $body, $version); } /** * Return an UploadedFile instance array. * * @param array $files An array which respect $_FILES structure * * @throws InvalidArgumentException for unrecognized values */ public static function normalizeFiles(array $files): array { $normalized = []; foreach ($files as $key => $value) { if ($value instanceof UploadedFileInterface) { $normalized[$key] = $value; } elseif (is_array($value) && isset($value['tmp_name'])) { $normalized[$key] = self::createUploadedFileFromSpec($value); } elseif (is_array($value)) { $normalized[$key] = self::normalizeFiles($value); continue; } else { throw new InvalidArgumentException('Invalid value in files specification'); } } return $normalized; } /** * Create and return an UploadedFile instance from a $_FILES specification. * * If the specification represents an array of values, this method will * delegate to normalizeNestedFileSpec() and return that return value. * * @param array $value $_FILES struct * * @return UploadedFileInterface|UploadedFileInterface[] */ private static function createUploadedFileFromSpec(array $value) { if (is_array($value['tmp_name'])) { return self::normalizeNestedFileSpec($value); } return new UploadedFile( $value['tmp_name'], (int) $value['size'], (int) $value['error'], $value['name'], $value['type'] ); } /** * Normalize an array of file specifications. * * Loops through all nested files and returns a normalized array of * UploadedFileInterface instances. * * @return UploadedFileInterface[] */ private static function normalizeNestedFileSpec(array $files = []): array { $normalizedFiles = []; foreach (array_keys($files['tmp_name']) as $key) { $spec = [ 'tmp_name' => $files['tmp_name'][$key], 'size' => $files['size'][$key] ?? null, 'error' => $files['error'][$key] ?? null, 'name' => $files['name'][$key] ?? null, 'type' => $files['type'][$key] ?? null, ]; $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec); } return $normalizedFiles; } /** * Return a ServerRequest populated with superglobals: * $_GET * $_POST * $_COOKIE * $_FILES * $_SERVER */ public static function fromGlobals(): ServerRequestInterface { $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $headers = getallheaders(); $uri = self::getUriFromGlobals(); $body = new CachingStream(new LazyOpenStream('php://input', 'r+')); $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1'; $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER); return $serverRequest ->withCookieParams($_COOKIE) ->withQueryParams($_GET) ->withParsedBody($_POST) ->withUploadedFiles(self::normalizeFiles($_FILES)); } private static function extractHostAndPortFromAuthority(string $authority): array { $uri = 'http://'.$authority; $parts = parse_url($uri); if (false === $parts) { return [null, null]; } $host = $parts['host'] ?? null; $port = $parts['port'] ?? null; return [$host, $port]; } /** * Get a Uri populated with values from $_SERVER. */ public static function getUriFromGlobals(): UriInterface { $uri = new Uri(''); $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http'); $hasPort = false; if (isset($_SERVER['HTTP_HOST'])) { [$host, $port] = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']); if ($host !== null) { $uri = $uri->withHost($host); } if ($port !== null) { $hasPort = true; $uri = $uri->withPort($port); } } elseif (isset($_SERVER['SERVER_NAME'])) { $uri = $uri->withHost($_SERVER['SERVER_NAME']); } elseif (isset($_SERVER['SERVER_ADDR'])) { $uri = $uri->withHost($_SERVER['SERVER_ADDR']); } if (!$hasPort && isset($_SERVER['SERVER_PORT'])) { $uri = $uri->withPort($_SERVER['SERVER_PORT']); } $hasQuery = false; if (isset($_SERVER['REQUEST_URI'])) { $requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2); $uri = $uri->withPath($requestUriParts[0]); if (isset($requestUriParts[1])) { $hasQuery = true; $uri = $uri->withQuery($requestUriParts[1]); } } if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) { $uri = $uri->withQuery($_SERVER['QUERY_STRING']); } return $uri; } public function getServerParams(): array { return $this->serverParams; } public function getUploadedFiles(): array { return $this->uploadedFiles; } public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface { $new = clone $this; $new->uploadedFiles = $uploadedFiles; return $new; } public function getCookieParams(): array { return $this->cookieParams; } public function withCookieParams(array $cookies): ServerRequestInterface { $new = clone $this; $new->cookieParams = $cookies; return $new; } public function getQueryParams(): array { return $this->queryParams; } public function withQueryParams(array $query): ServerRequestInterface { $new = clone $this; $new->queryParams = $query; return $new; } /** * @return array|object|null */ public function getParsedBody() { return $this->parsedBody; } public function withParsedBody($data): ServerRequestInterface { $new = clone $this; $new->parsedBody = $data; return $new; } public function getAttributes(): array { return $this->attributes; } /** * @return mixed */ public function getAttribute($attribute, $default = null) { if (false === array_key_exists($attribute, $this->attributes)) { return $default; } return $this->attributes[$attribute]; } public function withAttribute($attribute, $value): ServerRequestInterface { $new = clone $this; $new->attributes[$attribute] = $value; return $new; } public function withoutAttribute($attribute): ServerRequestInterface { if (false === array_key_exists($attribute, $this->attributes)) { return $this; } $new = clone $this; unset($new->attributes[$attribute]); return $new; } } AppendStream.php000064400000013473152430110220007636 0ustar00addStream($stream); } } public function __toString(): string { try { $this->rewind(); return $this->getContents(); } catch (\Throwable $e) { if (\PHP_VERSION_ID >= 70400) { throw $e; } trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); return ''; } } /** * Add a stream to the AppendStream * * @param StreamInterface $stream Stream to append. Must be readable. * * @throws \InvalidArgumentException if the stream is not readable */ public function addStream(StreamInterface $stream): void { if (!$stream->isReadable()) { throw new \InvalidArgumentException('Each stream must be readable'); } // The stream is only seekable if all streams are seekable if (!$stream->isSeekable()) { $this->seekable = false; } $this->streams[] = $stream; } public function getContents(): string { return Utils::copyToString($this); } /** * Closes each attached stream. */ public function close(): void { $this->pos = $this->current = 0; $this->seekable = true; foreach ($this->streams as $stream) { $stream->close(); } $this->streams = []; } /** * Detaches each attached stream. * * Returns null as it's not clear which underlying stream resource to return. */ public function detach() { $this->pos = $this->current = 0; $this->seekable = true; foreach ($this->streams as $stream) { $stream->detach(); } $this->streams = []; return null; } public function tell(): int { return $this->pos; } /** * Tries to calculate the size by adding the size of each stream. * * If any of the streams do not return a valid number, then the size of the * append stream cannot be determined and null is returned. */ public function getSize(): ?int { $size = 0; foreach ($this->streams as $stream) { $s = $stream->getSize(); if ($s === null) { return null; } $size += $s; } return $size; } public function eof(): bool { return !$this->streams || ($this->current >= count($this->streams) - 1 && $this->streams[$this->current]->eof()); } public function rewind(): void { $this->seek(0); } /** * Attempts to seek to the given position. Only supports SEEK_SET. */ public function seek($offset, $whence = SEEK_SET): void { if (!$this->seekable) { throw new \RuntimeException('This AppendStream is not seekable'); } elseif ($whence !== SEEK_SET) { throw new \RuntimeException('The AppendStream can only seek with SEEK_SET'); } $this->pos = $this->current = 0; // Rewind each stream foreach ($this->streams as $i => $stream) { try { $stream->rewind(); } catch (\Exception $e) { throw new \RuntimeException('Unable to seek stream ' .$i.' of the AppendStream', 0, $e); } } // Seek to the actual position by reading from each stream while ($this->pos < $offset && !$this->eof()) { $result = $this->read(min(8096, $offset - $this->pos)); if ($result === '') { break; } } } /** * Reads from all of the appended streams until the length is met or EOF. */ public function read($length): string { $buffer = ''; $total = count($this->streams) - 1; $remaining = $length; $progressToNext = false; while ($remaining > 0) { // Progress to the next stream if needed. if ($progressToNext || $this->streams[$this->current]->eof()) { $progressToNext = false; if ($this->current === $total) { break; } ++$this->current; } $result = $this->streams[$this->current]->read($remaining); if ($result === '') { $progressToNext = true; continue; } $buffer .= $result; $remaining = $length - strlen($buffer); } $this->pos += strlen($buffer); return $buffer; } public function isReadable(): bool { return true; } public function isWritable(): bool { return false; } public function isSeekable(): bool { return $this->seekable; } public function write($string): int { throw new \RuntimeException('Cannot write to an AppendStream'); } /** * @return mixed */ public function getMetadata($key = null) { return $key ? null : []; } } Query.php000064400000007712152430110220006357 0ustar00 '1', 'foo[b]' => '2'])`. * * @param string $str Query string to parse * @param int|bool $urlEncoding How the query string is encoded */ public static function parse(string $str, $urlEncoding = true): array { $result = []; if ($str === '') { return $result; } if ($urlEncoding === true) { $decoder = function ($value) { return rawurldecode(str_replace('+', ' ', (string) $value)); }; } elseif ($urlEncoding === PHP_QUERY_RFC3986) { $decoder = 'rawurldecode'; } elseif ($urlEncoding === PHP_QUERY_RFC1738) { $decoder = 'urldecode'; } else { $decoder = function ($str) { return $str; }; } foreach (explode('&', $str) as $kvp) { $parts = explode('=', $kvp, 2); $key = $decoder($parts[0]); $value = isset($parts[1]) ? $decoder($parts[1]) : null; if (!array_key_exists($key, $result)) { $result[$key] = $value; } else { if (!is_array($result[$key])) { $result[$key] = [$result[$key]]; } $result[$key][] = $value; } } return $result; } /** * Build a query string from an array of key value pairs. * * This function can use the return value of `parse()` to build a query * string. This function does not modify the provided keys when an array is * encountered (like `http_build_query()` would). * * @param array $params Query string parameters. * @param int|false $encoding Set to false to not encode, * PHP_QUERY_RFC3986 to encode using * RFC3986, or PHP_QUERY_RFC1738 to * encode using RFC1738. * @param bool $treatBoolsAsInts Set to true to encode as 0/1, and * false as false/true. */ public static function build(array $params, $encoding = PHP_QUERY_RFC3986, bool $treatBoolsAsInts = true): string { if (!$params) { return ''; } if ($encoding === false) { $encoder = function (string $str): string { return $str; }; } elseif ($encoding === PHP_QUERY_RFC3986) { $encoder = 'rawurlencode'; } elseif ($encoding === PHP_QUERY_RFC1738) { $encoder = 'urlencode'; } else { throw new \InvalidArgumentException('Invalid type'); } $castBool = $treatBoolsAsInts ? static function ($v) { return (int) $v; } : static function ($v) { return $v ? 'true' : 'false'; }; $qs = ''; foreach ($params as $k => $v) { $k = $encoder((string) $k); if (!is_array($v)) { $qs .= $k; $v = is_bool($v) ? $castBool($v) : $v; if ($v !== null) { $qs .= '='.$encoder((string) $v); } $qs .= '&'; } else { foreach ($v as $vv) { $qs .= $k; $vv = is_bool($vv) ? $castBool($vv) : $vv; if ($vv !== null) { $qs .= '='.$encoder((string) $vv); } $qs .= '&'; } } } return $qs ? (string) substr($qs, 0, -1) : ''; } } Response.php000064400000011453152430110220007045 0ustar00 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 510 => 'Not Extended', 511 => 'Network Authentication Required', ]; /** @var string */ private $reasonPhrase; /** @var int */ private $statusCode; /** * @param int $status Status code * @param (string|string[])[] $headers Response headers * @param string|resource|StreamInterface|null $body Response body * @param string $version Protocol version * @param string|null $reason Reason phrase (when empty a default will be used based on the status code) */ public function __construct( int $status = 200, array $headers = [], $body = null, string $version = '1.1', ?string $reason = null ) { $this->assertStatusCodeRange($status); $this->statusCode = $status; if ($body !== '' && $body !== null) { $this->stream = Utils::streamFor($body); } $this->setHeaders($headers); if ($reason == '' && isset(self::PHRASES[$this->statusCode])) { $this->reasonPhrase = self::PHRASES[$this->statusCode]; } else { $this->reasonPhrase = (string) $reason; } $this->protocol = $version; } public function getStatusCode(): int { return $this->statusCode; } public function getReasonPhrase(): string { return $this->reasonPhrase; } public function withStatus($code, $reasonPhrase = ''): ResponseInterface { $this->assertStatusCodeIsInteger($code); $code = (int) $code; $this->assertStatusCodeRange($code); $new = clone $this; $new->statusCode = $code; if ($reasonPhrase == '' && isset(self::PHRASES[$new->statusCode])) { $reasonPhrase = self::PHRASES[$new->statusCode]; } $new->reasonPhrase = (string) $reasonPhrase; return $new; } /** * @param mixed $statusCode */ private function assertStatusCodeIsInteger($statusCode): void { if (filter_var($statusCode, FILTER_VALIDATE_INT) === false) { throw new \InvalidArgumentException('Status code must be an integer value.'); } } private function assertStatusCodeRange(int $statusCode): void { if ($statusCode < 100 || $statusCode >= 600) { throw new \InvalidArgumentException('Status code must be an integer value between 1xx and 5xx.'); } } } LazyOpenStream.php000064400000002100152430110220010151 0ustar00filename = $filename; $this->mode = $mode; // unsetting the property forces the first access to go through // __get(). unset($this->stream); } /** * Creates the underlying stream lazily when required. */ protected function createStream(): StreamInterface { return Utils::streamFor(Utils::tryFopen($this->filename, $this->mode)); } } Rfc7230.php000064400000001225152430110220006271 0ustar00@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m"; public const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)"; } BufferStream.php000064400000006220152430110220007630 0ustar00hwm = $hwm; } public function __toString(): string { return $this->getContents(); } public function getContents(): string { $buffer = $this->buffer; $this->buffer = ''; return $buffer; } public function close(): void { $this->buffer = ''; } public function detach() { $this->close(); return null; } public function getSize(): ?int { return strlen($this->buffer); } public function isReadable(): bool { return true; } public function isWritable(): bool { return true; } public function isSeekable(): bool { return false; } public function rewind(): void { $this->seek(0); } public function seek($offset, $whence = SEEK_SET): void { throw new \RuntimeException('Cannot seek a BufferStream'); } public function eof(): bool { return strlen($this->buffer) === 0; } public function tell(): int { throw new \RuntimeException('Cannot determine the position of a BufferStream'); } /** * Reads data from the buffer. */ public function read($length): string { $currentLength = strlen($this->buffer); if ($length >= $currentLength) { // No need to slice the buffer because we don't have enough data. $result = $this->buffer; $this->buffer = ''; } else { // Slice up the result to provide a subset of the buffer. $result = substr($this->buffer, 0, $length); $this->buffer = substr($this->buffer, $length); } return $result; } /** * Writes data to the buffer. */ public function write($string): int { $this->buffer .= $string; if (strlen($this->buffer) >= $this->hwm) { return 0; } return strlen($string); } /** * @return mixed */ public function getMetadata($key = null) { if ($key === 'hwm') { return $this->hwm; } return $key ? null : []; } } UriComparator.php000064400000002176152430110220010040 0ustar00getHost(), $modified->getHost()) !== 0) { return true; } if ($original->getScheme() !== $modified->getScheme()) { return true; } if (self::computePort($original) !== self::computePort($modified)) { return true; } return false; } private static function computePort(UriInterface $uri): int { $port = $uri->getPort(); if (null !== $port) { return $port; } return 'https' === $uri->getScheme() ? 443 : 80; } private function __construct() { // cannot be instantiated } } MultipartStream.php000064400000012071152430110220010401 0ustar00boundary = $boundary ?: bin2hex(random_bytes(20)); $this->stream = $this->createStream($elements); } public function getBoundary(): string { return $this->boundary; } public function isWritable(): bool { return false; } /** * Get the headers needed before transferring the content of a POST file * * @param string[] $headers */ private function getHeaders(array $headers): string { $str = ''; foreach ($headers as $key => $value) { $str .= "{$key}: {$value}\r\n"; } return "--{$this->boundary}\r\n".trim($str)."\r\n\r\n"; } /** * Create the aggregate stream that will be used to upload the POST data */ protected function createStream(array $elements = []): StreamInterface { $stream = new AppendStream(); foreach ($elements as $element) { if (!is_array($element)) { throw new \UnexpectedValueException('An array is expected'); } $this->addElement($stream, $element); } // Add the trailing boundary with CRLF $stream->addStream(Utils::streamFor("--{$this->boundary}--\r\n")); return $stream; } private function addElement(AppendStream $stream, array $element): void { foreach (['contents', 'name'] as $key) { if (!array_key_exists($key, $element)) { throw new \InvalidArgumentException("A '{$key}' key is required"); } } $element['contents'] = Utils::streamFor($element['contents']); if (empty($element['filename'])) { $uri = $element['contents']->getMetadata('uri'); if ($uri && \is_string($uri) && \substr($uri, 0, 6) !== 'php://' && \substr($uri, 0, 7) !== 'data://') { $element['filename'] = $uri; } } [$body, $headers] = $this->createElement( $element['name'], $element['contents'], $element['filename'] ?? null, $element['headers'] ?? [] ); $stream->addStream(Utils::streamFor($this->getHeaders($headers))); $stream->addStream($body); $stream->addStream(Utils::streamFor("\r\n")); } /** * @param string[] $headers * * @return array{0: StreamInterface, 1: string[]} */ private function createElement(string $name, StreamInterface $stream, ?string $filename, array $headers): array { // Set a default content-disposition header if one was no provided $disposition = self::getHeader($headers, 'content-disposition'); if (!$disposition) { $headers['Content-Disposition'] = ($filename === '0' || $filename) ? sprintf( 'form-data; name="%s"; filename="%s"', $name, basename($filename) ) : "form-data; name=\"{$name}\""; } // Set a default content-length header if one was no provided $length = self::getHeader($headers, 'content-length'); if (!$length) { if ($length = $stream->getSize()) { $headers['Content-Length'] = (string) $length; } } // Set a default Content-Type if one was not supplied $type = self::getHeader($headers, 'content-type'); if (!$type && ($filename === '0' || $filename)) { $headers['Content-Type'] = MimeType::fromFilename($filename) ?? 'application/octet-stream'; } return [$stream, $headers]; } /** * @param string[] $headers */ private static function getHeader(array $headers, string $key): ?string { $lowercaseHeader = strtolower($key); foreach ($headers as $k => $v) { if (strtolower((string) $k) === $lowercaseHeader) { return $v; } } return null; } } Header.php000064400000007545152430110220006446 0ustar00]+>|[^=]+/', $kvp, $matches)) { $m = $matches[0]; if (isset($m[1])) { $part[trim($m[0], $trimmed)] = trim($m[1], $trimmed); } else { $part[] = trim($m[0], $trimmed); } } } if ($part) { $params[] = $part; } } } return $params; } /** * Converts an array of header values that may contain comma separated * headers into an array of headers with no comma separated values. * * @param string|array $header Header to normalize. * * @deprecated Use self::splitList() instead. */ public static function normalize($header): array { $result = []; foreach ((array) $header as $value) { foreach (self::splitList($value) as $parsed) { $result[] = $parsed; } } return $result; } /** * Splits a HTTP header defined to contain a comma-separated list into * each individual value. Empty values will be removed. * * Example headers include 'accept', 'cache-control' and 'if-none-match'. * * This method must not be used to parse headers that are not defined as * a list, such as 'user-agent' or 'set-cookie'. * * @param string|string[] $values Header value as returned by MessageInterface::getHeader() * * @return string[] */ public static function splitList($values): array { if (!\is_array($values)) { $values = [$values]; } $result = []; foreach ($values as $value) { if (!\is_string($value)) { throw new \TypeError('$header must either be a string or an array containing strings.'); } $v = ''; $isQuoted = false; $isEscaped = false; for ($i = 0, $max = \strlen($value); $i < $max; ++$i) { if ($isEscaped) { $v .= $value[$i]; $isEscaped = false; continue; } if (!$isQuoted && $value[$i] === ',') { $v = \trim($v); if ($v !== '') { $result[] = $v; } $v = ''; continue; } if ($isQuoted && $value[$i] === '\\') { $isEscaped = true; $v .= $value[$i]; continue; } if ($value[$i] === '"') { $isQuoted = !$isQuoted; $v .= $value[$i]; continue; } $v .= $value[$i]; } $v = \trim($v); if ($v !== '') { $result[] = $v; } } return $result; } } PumpStream.php000064400000010766152430110220007352 0ustar00source = $source; $this->size = $options['size'] ?? null; $this->metadata = $options['metadata'] ?? []; $this->buffer = new BufferStream(); } public function __toString(): string { try { return Utils::copyToString($this); } catch (\Throwable $e) { if (\PHP_VERSION_ID >= 70400) { throw $e; } trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); return ''; } } public function close(): void { $this->detach(); } public function detach() { $this->tellPos = 0; $this->source = null; return null; } public function getSize(): ?int { return $this->size; } public function tell(): int { return $this->tellPos; } public function eof(): bool { return $this->source === null; } public function isSeekable(): bool { return false; } public function rewind(): void { $this->seek(0); } public function seek($offset, $whence = SEEK_SET): void { throw new \RuntimeException('Cannot seek a PumpStream'); } public function isWritable(): bool { return false; } public function write($string): int { throw new \RuntimeException('Cannot write to a PumpStream'); } public function isReadable(): bool { return true; } public function read($length): string { $data = $this->buffer->read($length); $readLen = strlen($data); $this->tellPos += $readLen; $remaining = $length - $readLen; if ($remaining) { $this->pump($remaining); $data .= $this->buffer->read($remaining); $this->tellPos += strlen($data) - $readLen; } return $data; } public function getContents(): string { $result = ''; while (!$this->eof()) { $result .= $this->read(1000000); } return $result; } /** * @return mixed */ public function getMetadata($key = null) { if (!$key) { return $this->metadata; } return $this->metadata[$key] ?? null; } private function pump(int $length): void { if ($this->source !== null) { do { $data = ($this->source)($length); if ($data === false || $data === null) { $this->source = null; return; } $this->buffer->write($data); $length -= strlen($data); } while ($length > 0); } } } NoSeekStream.php000064400000001014152430110220007577 0ustar00 */ private $methods; /** * @param array $methods Hash of method name to a callable. */ public function __construct(array $methods) { $this->methods = $methods; // Create the functions on the class foreach ($methods as $name => $fn) { $this->{'_fn_'.$name} = $fn; } } /** * Lazily determine which methods are not implemented. * * @throws \BadMethodCallException */ public function __get(string $name): void { throw new \BadMethodCallException(str_replace('_fn_', '', $name) .'() is not implemented in the FnStream'); } /** * The close method is called on the underlying stream only if possible. */ public function __destruct() { if (isset($this->_fn_close)) { ($this->_fn_close)(); } } /** * An unserialize would allow the __destruct to run when the unserialized value goes out of scope. * * @throws \LogicException */ public function __wakeup(): void { throw new \LogicException('FnStream should never be unserialized'); } /** * Adds custom functionality to an underlying stream by intercepting * specific method calls. * * @param StreamInterface $stream Stream to decorate * @param array $methods Hash of method name to a closure * * @return FnStream */ public static function decorate(StreamInterface $stream, array $methods) { // If any of the required methods were not provided, then simply // proxy to the decorated stream. foreach (array_diff(self::SLOTS, array_keys($methods)) as $diff) { /** @var callable $callable */ $callable = [$stream, $diff]; $methods[$diff] = $callable; } return new self($methods); } public function __toString(): string { try { /** @var string */ return ($this->_fn___toString)(); } catch (\Throwable $e) { if (\PHP_VERSION_ID >= 70400) { throw $e; } trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); return ''; } } public function close(): void { ($this->_fn_close)(); } public function detach() { return ($this->_fn_detach)(); } public function getSize(): ?int { return ($this->_fn_getSize)(); } public function tell(): int { return ($this->_fn_tell)(); } public function eof(): bool { return ($this->_fn_eof)(); } public function isSeekable(): bool { return ($this->_fn_isSeekable)(); } public function rewind(): void { ($this->_fn_rewind)(); } public function seek($offset, $whence = SEEK_SET): void { ($this->_fn_seek)($offset, $whence); } public function isWritable(): bool { return ($this->_fn_isWritable)(); } public function write($string): int { return ($this->_fn_write)($string); } public function isReadable(): bool { return ($this->_fn_isReadable)(); } public function read($length): string { return ($this->_fn_read)($length); } public function getContents(): string { return ($this->_fn_getContents)(); } /** * @return mixed */ public function getMetadata($key = null) { return ($this->_fn_getMetadata)($key); } } UriNormalizer.php000064400000020416152430110220010050 0ustar00getPath() === '' && ($uri->getScheme() === 'http' || $uri->getScheme() === 'https') ) { $uri = $uri->withPath('/'); } if ($flags & self::REMOVE_DEFAULT_HOST && $uri->getScheme() === 'file' && $uri->getHost() === 'localhost') { $uri = $uri->withHost(''); } if ($flags & self::REMOVE_DEFAULT_PORT && $uri->getPort() !== null && Uri::isDefaultPort($uri)) { $uri = $uri->withPort(null); } if ($flags & self::REMOVE_DOT_SEGMENTS && !Uri::isRelativePathReference($uri)) { $uri = $uri->withPath(UriResolver::removeDotSegments($uri->getPath())); } if ($flags & self::REMOVE_DUPLICATE_SLASHES) { $uri = $uri->withPath(preg_replace('#//++#', '/', $uri->getPath())); } if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') { $queryKeyValues = explode('&', $uri->getQuery()); sort($queryKeyValues); $uri = $uri->withQuery(implode('&', $queryKeyValues)); } return $uri; } /** * Whether two URIs can be considered equivalent. * * Both URIs are normalized automatically before comparison with the given $normalizations bitmask. The method also * accepts relative URI references and returns true when they are equivalent. This of course assumes they will be * resolved against the same base URI. If this is not the case, determination of equivalence or difference of * relative references does not mean anything. * * @param UriInterface $uri1 An URI to compare * @param UriInterface $uri2 An URI to compare * @param int $normalizations A bitmask of normalizations to apply, see constants * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-6.1 */ public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, int $normalizations = self::PRESERVING_NORMALIZATIONS): bool { return (string) self::normalize($uri1, $normalizations) === (string) self::normalize($uri2, $normalizations); } private static function capitalizePercentEncoding(UriInterface $uri): UriInterface { $regex = '/(?:%[A-Fa-f0-9]{2})++/'; $callback = function (array $match): string { return strtoupper($match[0]); }; return $uri->withPath( preg_replace_callback($regex, $callback, $uri->getPath()) )->withQuery( preg_replace_callback($regex, $callback, $uri->getQuery()) ); } private static function decodeUnreservedCharacters(UriInterface $uri): UriInterface { $regex = '/%(?:2D|2E|5F|7E|3[0-9]|[46][1-9A-F]|[57][0-9A])/i'; $callback = function (array $match): string { return rawurldecode($match[0]); }; return $uri->withPath( preg_replace_callback($regex, $callback, $uri->getPath()) )->withQuery( preg_replace_callback($regex, $callback, $uri->getQuery()) ); } private function __construct() { // cannot be instantiated } } AWS/CRT/HTTP/Message.php000064400000005252152430150760010511 0ustar00method = $method; $this->path = $path; $this->query = $query; $this->headers = new Headers($headers); $this->acquire(self::$crt->http_message_new_from_blob(self::marshall($this))); } public function __destruct() { self::$crt->http_message_release($this->release()); parent::__destruct(); } public function toBlob() { return self::$crt->http_message_to_blob($this->native); } protected static function marshall($msg) { $buf = ""; $buf .= Encoding::encodeString($msg->method); $buf .= Encoding::encodeString($msg->pathAndQuery()); $buf .= Headers::marshall($msg->headers); return $buf; } protected static function _unmarshall($buf, $class=Message::class) { $method = Encoding::readString($buf); $path_and_query = Encoding::readString($buf); $parts = explode("?", $path_and_query, 2); $path = isset($parts[0]) ? $parts[0] : ""; $query = isset($parts[1]) ? $parts[1] : ""; $headers = Headers::unmarshall($buf); // Turn query params back into a dictionary if (strlen($query)) { $query = rawurldecode($query); $query = explode("&", $query); $query = array_reduce($query, function($params, $pair) { list($param, $value) = explode("=", $pair, 2); $params[$param] = $value; return $params; }, []); } else { $query = []; } return new $class($method, $path, $query, $headers->toArray()); } public function pathAndQuery() { $path = $this->path; $queries = []; foreach ($this->query as $param => $value) { $queries []= urlencode($param) . "=" . urlencode($value); } $query = implode("&", $queries); if (strlen($query)) { $path = implode("?", [$path, $query]); } return $path; } public function method() { return $this->method; } public function path() { return $this->path; } public function query() { return $this->query; } public function headers() { return $this->headers; } } AWS/CRT/HTTP/Request.php000064400000001657152430150760010562 0ustar00body_stream = $body_stream; } public static function marshall($request) { return parent::marshall($request); } public static function unmarshall($buf) { return parent::_unmarshall($buf, Request::class); } public function body_stream() { return $this->body_stream; } } AWS/CRT/HTTP/Response.php000064400000001267152430150760010725 0ustar00status_code = $status_code; } public static function marshall($response) { return parent::marshall($response); } public static function unmarshall($buf) { return parent::_unmarshall($buf, Response::class); } public function status_code() { return $this->status_code; } } AWS/CRT/HTTP/Headers.php000064400000002313152430150760010473 0ustar00headers = $headers; } public static function marshall($headers) { $buf = ""; foreach ($headers->headers as $header => $value) { $buf .= Encoding::encodeString($header); $buf .= Encoding::encodeString($value); } return $buf; } public static function unmarshall($buf) { $strings = Encoding::readStrings($buf); $headers = []; for ($idx = 0; $idx < count($strings);) { $headers[$strings[$idx++]] = $strings[$idx++]; } return new Headers($headers); } public function count() { return count($this->headers); } public function get($header) { return isset($this->headers[$header]) ? $this->headers[$header] : null; } public function set($header, $value) { $this->headers[$header] = $value; } public function toArray() { return $this->headers; } } AWS/CRT/IO/InputStream.php000064400000002701152430150760011124 0ustar00stream = $stream; $options = self::$crt->input_stream_options_new(); // The stream implementation in native just converts the PHP stream into // a native php_stream* and executes operations entirely in native self::$crt->input_stream_options_set_user_data($options, $stream); $this->acquire(self::$crt->input_stream_new($options)); self::$crt->input_stream_options_release($options); } public function __destruct() { $this->release(); parent::__destruct(); } public function eof() { return self::$crt->input_stream_eof($this->native); } public function length() { return self::$crt->input_stream_get_length($this->native); } public function read($length = 0) { if ($length == 0) { $length = $this->length(); } return self::$crt->input_stream_read($this->native, $length); } public function seek($offset, $basis) { return self::$crt->input_stream_seek($this->native, $offset, $basis); } } AWS/CRT/IO/EventLoopGroup.php000064400000002347152430150760011607 0ustar00 0, ]; } function __construct(array $options = []) { parent::__construct(); $options = new Options($options, self::defaults()); $elg_options = self::$crt->event_loop_group_options_new(); self::$crt->event_loop_group_options_set_max_threads($elg_options, $options->getInt('max_threads')); $this->acquire(self::$crt->event_loop_group_new($elg_options)); self::$crt->event_loop_group_options_release($elg_options); } function __destruct() { self::$crt->event_loop_group_release($this->release()); parent::__destruct(); } } AWS/CRT/Internal/Extension.php000064400000001350152430150760012071 0ustar00sign_request_aws($signable->native, $signing_config->native, function($result, $error_code) use ($on_complete) { $signing_result = SigningResult::fromNative($result); $on_complete($signing_result, $error_code); }, null); } static function testVerifySigV4ASigning($signable, $signing_config, $expected_canonical_request, $signature, $ecc_key_pub_x, $ecc_key_pub_y) { return self::$crt->test_verify_sigv4a_signing($signable, $signing_config, $expected_canonical_request, $signature, $ecc_key_pub_x, $ecc_key_pub_y); } } AWS/CRT/Auth/SigningResult.php000064400000001610152430150760012036 0ustar00acquire($native); } function __destruct() { // No destruction necessary, SigningResults are transient, just release $this->release(); parent::__destruct(); } public static function fromNative($ptr) { return new SigningResult($ptr); } public function applyToHttpRequest(&$http_request) { self::$crt->signing_result_apply_to_http_request($this->native, $http_request->native); // Update http_request from native $http_request = Request::unmarshall($http_request->toBlob()); } }AWS/CRT/Auth/AwsCredentials.php000064400000005034152430150760012155 0ustar00 '', 'secret_access_key' => '', 'session_token' => '', 'expiration_timepoint_seconds' => 0, ]; } private $access_key_id; private $secret_access_key; private $session_token; private $expiration_timepoint_seconds = 0; public function __get($name) { return $this->$name; } function __construct(array $options = []) { parent::__construct(); $options = new Options($options, self::defaults()); $this->access_key_id = $options->access_key_id->asString(); $this->secret_access_key = $options->secret_access_key->asString(); $this->session_token = $options->session_token ? $options->session_token->asString() : null; $this->expiration_timepoint_seconds = $options->expiration_timepoint_seconds->asInt(); if (strlen($this->access_key_id) == 0) { throw new \InvalidArgumentException("access_key_id must be provided"); } if (strlen($this->secret_access_key) == 0) { throw new \InvalidArgumentException("secret_access_key must be provided"); } $creds_options = self::$crt->aws_credentials_options_new(); self::$crt->aws_credentials_options_set_access_key_id($creds_options, $this->access_key_id); self::$crt->aws_credentials_options_set_secret_access_key($creds_options, $this->secret_access_key); self::$crt->aws_credentials_options_set_session_token($creds_options, $this->session_token); self::$crt->aws_credentials_options_set_expiration_timepoint_seconds($creds_options, $this->expiration_timepoint_seconds); $this->acquire(self::$crt->aws_credentials_new($creds_options)); self::$crt->aws_credentials_options_release($creds_options); } function __destruct() { self::$crt->aws_credentials_release($this->release()); parent::__destruct(); } } AWS/CRT/Auth/SigningAlgorithm.php000064400000000345152430150760012512 0ustar00credentials_provider_release($this->release()); parent::__destruct(); } } AWS/CRT/Auth/StaticCredentialsProvider.php000064400000002504152430150760014364 0ustar00$name; } function __construct(array $options = []) { parent::__construct(); $this->credentials = new AwsCredentials($options); $provider_options = self::$crt->credentials_provider_static_options_new(); self::$crt->credentials_provider_static_options_set_access_key_id($provider_options, $this->credentials->access_key_id); self::$crt->credentials_provider_static_options_set_secret_access_key($provider_options, $this->credentials->secret_access_key); self::$crt->credentials_provider_static_options_set_session_token($provider_options, $this->credentials->session_token); $this->acquire(self::$crt->credentials_provider_static_new($provider_options)); self::$crt->credentials_provider_static_options_release($provider_options); } } AWS/CRT/Auth/SignedBodyHeaderType.php000064400000000353152430150760013246 0ustar00signable_new_from_http_request($http_message->native); }); } public static function fromChunk($chunk_stream, $previous_signature="") { if (!($chunk_stream instanceof InputStream)) { $chunk_stream = new InputStream($chunk_stream); } return new Signable(function() use($chunk_stream, $previous_signature) { return self::$crt->signable_new_from_chunk($chunk_stream->native, $previous_signature); }); } public static function fromCanonicalRequest($canonical_request) { return new Signable(function() use($canonical_request) { return self::$crt->signable_new_from_canonical_request($canonical_request); }); } protected function __construct($ctor) { parent::__construct(); $this->acquire($ctor()); } function __destruct() { self::$crt->signable_release($this->release()); parent::__destruct(); } }AWS/CRT/Auth/SigningConfigAWS.php000064400000005771152430150760012354 0ustar00 SigningAlgorithm::SIGv4, 'signature_type' => SignatureType::HTTP_REQUEST_HEADERS, 'credentials_provider' => null, 'region' => null, 'service' => null, 'use_double_uri_encode' => false, 'should_normalize_uri_path' => false, 'omit_session_token' => false, 'signed_body_value' => null, 'signed_body_header_type' => SignedBodyHeaderType::NONE, 'expiration_in_seconds' => 0, 'date' => time(), 'should_sign_header' => null, ]; } private $options; public function __construct(array $options = []) { parent::__construct(); $this->options = $options = new Options($options, self::defaults()); $sc = $this->acquire(self::$crt->signing_config_aws_new()); self::$crt->signing_config_aws_set_algorithm($sc, $options->algorithm->asInt()); self::$crt->signing_config_aws_set_signature_type($sc, $options->signature_type->asInt()); if ($credentials_provider = $options->credentials_provider->asObject()) { self::$crt->signing_config_aws_set_credentials_provider( $sc, $credentials_provider->native); } self::$crt->signing_config_aws_set_region( $sc, $options->region->asString()); self::$crt->signing_config_aws_set_service( $sc, $options->service->asString()); self::$crt->signing_config_aws_set_use_double_uri_encode( $sc, $options->use_double_uri_encode->asBool()); self::$crt->signing_config_aws_set_should_normalize_uri_path( $sc, $options->should_normalize_uri_path->asBool()); self::$crt->signing_config_aws_set_omit_session_token( $sc, $options->omit_session_token->asBool()); self::$crt->signing_config_aws_set_signed_body_value( $sc, $options->signed_body_value->asString()); self::$crt->signing_config_aws_set_signed_body_header_type( $sc, $options->signed_body_header_type->asInt()); self::$crt->signing_config_aws_set_expiration_in_seconds( $sc, $options->expiration_in_seconds->asInt()); self::$crt->signing_config_aws_set_date($sc, $options->date->asInt()); if ($should_sign_header = $options->should_sign_header->asCallable()) { self::$crt->signing_config_aws_set_should_sign_header_fn($sc, $should_sign_header); } } function __destruct() { self::$crt->signing_config_aws_release($this->release()); parent::__destruct(); } public function __get($name) { return $this->options->get($name); } }AWS/CRT/Log.php000064400000002022152430150760007057 0ustar00= self::NONE && $level <= self::TRACE); CRT::log_set_level($level); } public static function log($level, $message) { CRT::log_message($level, $message); } } AWS/CRT/NativeResource.php000064400000002000152430150760011270 0ustar00native = $handle; } protected function release() { $native = $this->native; $this->native = null; return $native; } function __destruct() { // Should have been destroyed and released by derived resource assert($this->native == null); unset(self::$resources[spl_object_hash($this)]); } } AWS/CRT/CRT.php000064400000031676152430150760007007 0ustar00aws_crt_last_error(); } /** * @param integer $error Error code from the CRT, usually delivered via callback or {@see last_error} * @return string Human-readable description of the provided error code */ public static function error_str($error) { return self::$impl->aws_crt_error_str((int) $error); } /** * @param integer $error Error code from the CRT, usually delivered via callback or {@see last_error} * @return string Name/enum identifier for the provided error code */ public static function error_name($error) { return self::$impl->aws_crt_error_name((int) $error); } public static function log_to_stdout() { return self::$impl->aws_crt_log_to_stdout(); } public static function log_to_stderr() { return self::$impl->aws_crt_log_to_stderr(); } public static function log_to_file($filename) { return self::$impl->aws_crt_log_to_file($filename); } public static function log_to_stream($stream) { return self::$impl->aws_crt_log_to_stream($stream); } public static function log_set_level($level) { return self::$impl->aws_crt_log_set_level($level); } public static function log_stop() { return self::$impl->aws_crt_log_stop(); } public static function log_message($level, $message) { return self::$impl->aws_crt_log_message($level, $message); } /** * @return object Pointer to native event_loop_group_options */ function event_loop_group_options_new() { return self::$impl->aws_crt_event_loop_group_options_new(); } /** * @param object $elg_options Pointer to native event_loop_group_options */ function event_loop_group_options_release($elg_options) { self::$impl->aws_crt_event_loop_group_options_release($elg_options); } /** * @param object $elg_options Pointer to native event_loop_group_options * @param integer $max_threads Maximum number of threads to allow the event loop group to use, default: 0/1 per CPU core */ function event_loop_group_options_set_max_threads($elg_options, $max_threads) { self::$impl->aws_crt_event_loop_group_options_set_max_threads($elg_options, (int)$max_threads); } /** * @param object Pointer to event_loop_group_options, {@see event_loop_group_options_new} * @return object Pointer to the new event loop group */ function event_loop_group_new($options) { return self::$impl->aws_crt_event_loop_group_new($options); } /** * @param object $elg Pointer to the event loop group to release */ function event_loop_group_release($elg) { self::$impl->aws_crt_event_loop_group_release($elg); } /** * return object Pointer to native AWS credentials options */ function aws_credentials_options_new() { return self::$impl->aws_crt_credentials_options_new(); } function aws_credentials_options_release($options) { self::$impl->aws_crt_credentials_options_release($options); } function aws_credentials_options_set_access_key_id($options, $access_key_id) { self::$impl->aws_crt_credentials_options_set_access_key_id($options, $access_key_id); } function aws_credentials_options_set_secret_access_key($options, $secret_access_key) { self::$impl->aws_crt_credentials_options_set_secret_access_key($options, $secret_access_key); } function aws_credentials_options_set_session_token($options, $session_token) { self::$impl->aws_crt_credentials_options_set_session_token($options, $session_token); } function aws_credentials_options_set_expiration_timepoint_seconds($options, $expiration_timepoint_seconds) { self::$impl->aws_crt_credentials_options_set_expiration_timepoint_seconds($options, $expiration_timepoint_seconds); } function aws_credentials_new($options) { return self::$impl->aws_crt_credentials_new($options); } function aws_credentials_release($credentials) { self::$impl->aws_crt_credentials_release($credentials); } function credentials_provider_release($provider) { self::$impl->aws_crt_credentials_provider_release($provider); } function credentials_provider_static_options_new() { return self::$impl->aws_crt_credentials_provider_static_options_new(); } function credentials_provider_static_options_release($options) { self::$impl->aws_crt_credentials_provider_static_options_release($options); } function credentials_provider_static_options_set_access_key_id($options, $access_key_id) { self::$impl->aws_crt_credentials_provider_static_options_set_access_key_id($options, $access_key_id); } function credentials_provider_static_options_set_secret_access_key($options, $secret_access_key) { self::$impl->aws_crt_credentials_provider_static_options_set_secret_access_key($options, $secret_access_key); } function credentials_provider_static_options_set_session_token($options, $session_token) { self::$impl->aws_crt_credentials_provider_static_options_set_session_token($options, $session_token); } function credentials_provider_static_new($options) { return self::$impl->aws_crt_credentials_provider_static_new($options); } function input_stream_options_new() { return self::$impl->aws_crt_input_stream_options_new(); } function input_stream_options_release($options) { self::$impl->aws_crt_input_stream_options_release($options); } function input_stream_options_set_user_data($options, $user_data) { self::$impl->aws_crt_input_stream_options_set_user_data($options, $user_data); } function input_stream_new($options) { return self::$impl->aws_crt_input_stream_new($options); } function input_stream_release($stream) { self::$impl->aws_crt_input_stream_release($stream); } function input_stream_seek($stream, $offset, $basis) { return self::$impl->aws_crt_input_stream_seek($stream, $offset, $basis); } function input_stream_read($stream, $length) { return self::$impl->aws_crt_input_stream_read($stream, $length); } function input_stream_eof($stream) { return self::$impl->aws_crt_input_stream_eof($stream); } function input_stream_get_length($stream) { return self::$impl->aws_crt_input_stream_get_length($stream); } function http_message_new_from_blob($blob) { return self::$impl->aws_crt_http_message_new_from_blob($blob); } function http_message_to_blob($message) { return self::$impl->aws_crt_http_message_to_blob($message); } function http_message_release($message) { self::$impl->aws_crt_http_message_release($message); } function signing_config_aws_new() { return self::$impl->aws_crt_signing_config_aws_new(); } function signing_config_aws_release($signing_config) { return self::$impl->aws_crt_signing_config_aws_release($signing_config); } function signing_config_aws_set_algorithm($signing_config, $algorithm) { self::$impl->aws_crt_signing_config_aws_set_algorithm($signing_config, (int)$algorithm); } function signing_config_aws_set_signature_type($signing_config, $signature_type) { self::$impl->aws_crt_signing_config_aws_set_signature_type($signing_config, (int)$signature_type); } function signing_config_aws_set_credentials_provider($signing_config, $credentials_provider) { self::$impl->aws_crt_signing_config_aws_set_credentials_provider($signing_config, $credentials_provider); } function signing_config_aws_set_region($signing_config, $region) { self::$impl->aws_crt_signing_config_aws_set_region($signing_config, $region); } function signing_config_aws_set_service($signing_config, $service) { self::$impl->aws_crt_signing_config_aws_set_service($signing_config, $service); } function signing_config_aws_set_use_double_uri_encode($signing_config, $use_double_uri_encode) { self::$impl->aws_crt_signing_config_aws_set_use_double_uri_encode($signing_config, $use_double_uri_encode); } function signing_config_aws_set_should_normalize_uri_path($signing_config, $should_normalize_uri_path) { self::$impl->aws_crt_signing_config_aws_set_should_normalize_uri_path($signing_config, $should_normalize_uri_path); } function signing_config_aws_set_omit_session_token($signing_config, $omit_session_token) { self::$impl->aws_crt_signing_config_aws_set_omit_session_token($signing_config, $omit_session_token); } function signing_config_aws_set_signed_body_value($signing_config, $signed_body_value) { self::$impl->aws_crt_signing_config_aws_set_signed_body_value($signing_config, $signed_body_value); } function signing_config_aws_set_signed_body_header_type($signing_config, $signed_body_header_type) { self::$impl->aws_crt_signing_config_aws_set_signed_body_header_type($signing_config, $signed_body_header_type); } function signing_config_aws_set_expiration_in_seconds($signing_config, $expiration_in_seconds) { self::$impl->aws_crt_signing_config_aws_set_expiration_in_seconds($signing_config, $expiration_in_seconds); } function signing_config_aws_set_date($signing_config, $timestamp) { self::$impl->aws_crt_signing_config_aws_set_date($signing_config, $timestamp); } function signing_config_aws_set_should_sign_header_fn($signing_config, $should_sign_header_fn) { self::$impl->aws_crt_signing_config_aws_set_should_sign_header_fn($signing_config, $should_sign_header_fn); } function signable_new_from_http_request($http_message) { return self::$impl->aws_crt_signable_new_from_http_request($http_message); } function signable_new_from_chunk($chunk_stream, $previous_signature) { return self::$impl->aws_crt_signable_new_from_chunk($chunk_stream, $previous_signature); } function signable_new_from_canonical_request($canonical_request) { return self::$impl->aws_crt_signable_new_from_canonical_request($canonical_request); } function signable_release($signable) { self::$impl->aws_crt_signable_release($signable); } function signing_result_release($signing_result) { self::$impl->aws_crt_signing_result_release($signing_result); } function signing_result_apply_to_http_request($signing_result, $http_message) { return self::$impl->aws_crt_signing_result_apply_to_http_request( $signing_result, $http_message); } function sign_request_aws($signable, $signing_config, $on_complete, $user_data) { return self::$impl->aws_crt_sign_request_aws($signable, $signing_config, $on_complete, $user_data); } function test_verify_sigv4a_signing($signable, $signing_config, $expected_canonical_request, $signature, $ecc_key_pub_x, $ecc_key_pub_y) { return self::$impl->aws_crt_test_verify_sigv4a_signing($signable, $signing_config, $expected_canonical_request, $signature, $ecc_key_pub_x, $ecc_key_pub_y); } public static function crc32($input, $previous = 0) { return self::$impl->aws_crt_crc32($input, $previous); } public static function crc32c($input, $previous = 0) { return self::$impl->aws_crt_crc32c($input, $previous); } } AWS/CRT/Options.php000064400000003273152430150760010002 0ustar00value = $value; } public function asObject() { return $this->value; } public function asMixed() { return $this->value; } public function asInt() { return empty($this->value) ? 0 : (int)$this->value; } public function asBool() { return boolval($this->value); } public function asString() { return !empty($this->value) ? strval($this->value) : ""; } public function asArray() { return is_array($this->value) ? $this->value : (!empty($this->value) ? [$this->value] : []); } public function asCallable() { return is_callable($this->value) ? $this->value : null; } } final class Options { private $options; public function __construct($opts = [], $defaults = []) { $this->options = array_replace($defaults, empty($opts) ? [] : $opts); } public function __get($name) { return $this->get($name); } public function asArray() { return $this->options; } public function toArray() { return array_merge_recursive([], $this->options); } public function get($name) { return new OptionValue($this->options[$name]); } public function getInt($name) { return $this->get($name)->asInt(); } public function getString($name) { return $this->get($name)->asString(); } public function getBool($name) { return $this->get($name)->asBool(); } }