first commit

This commit is contained in:
Claudecio Martins
2026-06-16 10:04:10 -03:00
commit a951944997
4463 changed files with 419677 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
# =================== MySQL DEFAULT ===================
DEFAULT_DB_DRIVER=mysql
DEFAULT_DB_HOST=127.0.0.1
DEFAULT_DB_PORT=3306
DEFAULT_DB_NAME=meu_banco
DEFAULT_DB_USERNAME=root
DEFAULT_DB_PASSWORD=senha123
DEFAULT_DB_CHARSET=utf8mb4
# =================== PostgreSQL DEFAULT ===================
PGSQL_DB_DRIVER=pgsql
PGSQL_DB_HOST=127.0.0.1
PGSQL_DB_PORT=5432
PGSQL_DB_NAME=meu_banco_pg
PGSQL_DB_USERNAME=postgres
PGSQL_DB_PASSWORD=senha123
PGSQL_DB_SCHEMA=public
PGSQL_DB_TIMEZONE=UTC
PGSQL_DB_SSLMODE=disable
# =================== Redis DEFAULT ===================
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DATABASE=0
# =================== Redis CACHE (opcional) ===================
REDIS_CACHE_HOST=127.0.0.1
REDIS_CACHE_PORT=6379
REDIS_CACHE_PASSWORD=
REDIS_CACHE_DATABASE=1
+2
View File
@@ -0,0 +1,2 @@
# Para não subir a pasta .git
.git/
+23
View File
@@ -0,0 +1,23 @@
{
"name": "claudecio/krothiumapi",
"description": "Framework PHP para desenvolvimento rápido de api para aplicações web.",
"type": "library",
"license": "MIT",
"autoload": {
"psr-4": {
"KrothiumAPI\\": "src/"
}
},
"require": {
"php": ">=8.2",
"predis/predis": "v3.4.0",
"vlucas/phpdotenv": "v5.6.2"
},
"authors": [
{
"name": "Claudecio Martins",
"email": "contato@claudecio.is-a.dev",
"role": "Developer"
}
]
}
+610
View File
@@ -0,0 +1,610 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4f1705049fc0c5e519bca82b08ba05dc",
"packages": [
{
"name": "graham-campbell/result-type",
"version": "v1.1.4",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.5"
},
"require-dev": {
"phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
},
"type": "library",
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"time": "2025-12-27T19:43:20+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.5",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be",
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.5"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"time": "2025-12-27T19:41:33+00:00"
},
{
"name": "predis/predis",
"version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/predis/predis.git",
"reference": "1183f5732e6b10efd33f64984a96726eaecb59aa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/predis/predis/zipball/1183f5732e6b10efd33f64984a96726eaecb59aa",
"reference": "1183f5732e6b10efd33f64984a96726eaecb59aa",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"psr/http-message": "^1.0|^2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.3",
"phpstan/phpstan": "^1.9",
"phpunit/phpcov": "^6.0 || ^8.0",
"phpunit/phpunit": "^8.0 || ~9.4.4"
},
"suggest": {
"ext-relay": "Faster connection with in-memory caching (>=0.6.2)"
},
"type": "library",
"autoload": {
"psr-4": {
"Predis\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Till Krüss",
"homepage": "https://till.im",
"role": "Maintainer"
}
],
"description": "A flexible and feature-complete Redis/Valkey client for PHP.",
"homepage": "http://github.com/predis/predis",
"keywords": [
"nosql",
"predis",
"redis"
],
"support": {
"issues": "https://github.com/predis/predis/issues",
"source": "https://github.com/predis/predis/tree/v3.4.0"
},
"funding": [
{
"url": "https://github.com/sponsors/tillkruss",
"type": "github"
}
],
"time": "2026-02-11T17:30:28+00:00"
},
{
"name": "psr/http-message",
"version": "2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/2.0"
},
"time": "2023-04-04T09:54:51+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "141046a8f9477948ff284fa65be2095baafb94f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
"reference": "141046a8f9477948ff284fa65be2095baafb94f2",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-04-10T17:25:58+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
"reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-04-10T16:19:22+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.6.2",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.1.3",
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3",
"symfony/polyfill-ctype": "^1.24",
"symfony/polyfill-mbstring": "^1.24",
"symfony/polyfill-php80": "^1.24"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-filter": "*",
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "5.6-dev"
}
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
"type": "tidelift"
}
],
"time": "2025-04-30T23:37:27+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=8.2"
},
"platform-dev": [],
"plugin-api-version": "2.6.0"
}
+45
View File
@@ -0,0 +1,45 @@
<?php
// Importa autoload do Composer
require_once realpath(path: __DIR__ . '/../vendor/autoload.php');
use Dotenv\Dotenv;
use KrothiumAPI\KrothiumAPI;
// Carrega variáveis de ambiente
$dotenv = Dotenv::createImmutable(paths: realpath(path: __DIR__ . '/../'));
$dotenv->load();
// ======================================
// Inicializa KrothiumAPI com configs
// ======================================
KrothiumAPI::init(config: [
'errors' => [
'error_log' => realpath(path: __DIR__ . '/../storage/Logs/php-error.log'),
],
'constants' => [
'APP_SYS_MODE' => 'DEV', // DEV | PROD
'ROOT_SYSTEM_PATH' => realpath(path: __DIR__ . "/.."),
'INI_SYSTEM_PATH' => realpath(path: __DIR__ . "/../src"),
'MODULE_PATH' => realpath(path: __DIR__ . "/../src/Module"),
'STORAGE_FOLDER_PATH' => realpath(path: __DIR__ . "/../storage"),
'COMPONENT_PATH' => realpath(path: __DIR__ . "/../src/Common/Component"),
'ROUTER_ALLOWED_ORIGINS' => [
'*'
]
],
'system' => [
'enable_session' => true,
'default_timezone' => 'America/Fortaleza',
],
'logger' => [
'driver' => 'FILE',
'logDir' => realpath(path: __DIR__ . '/../storage/Logs')
]
]);
// Importa rotas da API v0
// ============================
// Dispara o roteador
// ============================
KrothiumAPI::routerDispatch();
+120
View File
@@ -0,0 +1,120 @@
<?php
namespace KrothiumAPI\Database;
use PDO;
use Dotenv\Dotenv;
use RuntimeException;
use KrothiumAPI\Database\Drivers\MySQLDriver;
use KrothiumAPI\Database\Drivers\PostgreSQLDriver;
class DBManager {
private static array $connections = [];
private static bool $envLoaded = false;
private static function loadEnv(): void {
// Garante que ROOT_SYSTEM_PATH esteja definido
if (!defined('ROOT_SYSTEM_PATH')) {
define('ROOT_SYSTEM_PATH', dirname(dirname(__DIR__)));
}
if (!self::$envLoaded) {
$envFile = ROOT_SYSTEM_PATH . '/.env';
if ((ROOT_SYSTEM_PATH !== null) && file_exists(filename: $envFile)) {
$dotenv = Dotenv::createImmutable(paths: ROOT_SYSTEM_PATH);
$dotenv->load();
}
self::$envLoaded = true;
}
}
/**
* Retorna conexão por nome + schema
*/
public static function getConnection(string $connectionName = 'DEFAULT', ?string $schema = null): mixed {
self::loadEnv();
$connectionName = strtoupper(string: $connectionName);
$key = $connectionName . ($schema ? "_$schema" : '');
if (!isset(self::$connections[$key])) {
$driver = $_ENV["{$connectionName}_DB_DRIVER"];
switch (strtolower(string: $driver)) {
case 'mysql':
$conn = new MySQLDriver(envName: $connectionName);
break;
case 'pgsql':
case 'postgresql':
$conn = new PostgreSQLDriver(envName: $connectionName, schema: $schema);
break;
default:
throw new RuntimeException(message: "Unknown driver: {$driver}");
}
self::$connections[$key] = $conn;
}
return self::$connections[$key];
}
public static function disconnect(string $connectionName = 'DEFAULT', ?string $schema = null): void {
$connectionName = strtoupper(string: $connectionName);
$key = $connectionName . ($schema ? "_$schema" : '');
self::$connections[$key] = null;
}
/** Métodos auxiliares (execute, fetchAll, fetchOne, etc.) */
public static function execute(string $sql, array $params = [], string $connectionName = 'DEFAULT', ?string $schema = null): bool {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if (method_exists(object_or_class: $conn, method: 'execute')) {
return $conn->execute($sql, $params);
}
throw new RuntimeException(message: "The database connection '{$connectionName}' does not support execute()");
}
public static function fetchAll(string $sql, array $params = [], string $connectionName = 'DEFAULT', ?string $schema = null): array {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if (method_exists(object_or_class: $conn, method: 'fetchAll')) {
return $conn->fetchAll($sql, $params);
}
throw new RuntimeException(message: "The database connection '{$connectionName}' does not support fetchAll()");
}
public static function fetchOne(string $sql, array $params = [], string $connectionName = 'DEFAULT', ?string $schema = null): ?array {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if (method_exists(object_or_class: $conn, method: 'fetchOne')) {
return $conn->fetchOne($sql, $params);
}
throw new RuntimeException(message: "The database connection '{$connectionName}' does not support fetchOne()");
}
// Transações
public static function beginTransaction(string $connectionName = 'DEFAULT', ?string $schema = null): void {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if (method_exists(object_or_class: $conn, method: 'beginTransaction')) $conn->beginTransaction();
}
public static function commit(string $connectionName = 'DEFAULT', ?string $schema = null): void {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if ($conn instanceof PDO && $conn->inTransaction()) {
$conn->commit();
} elseif (method_exists(object_or_class: $conn, method: 'getPDO')) {
$pdo = $conn->getPDO();
if ($pdo instanceof PDO && $pdo->inTransaction()) {
$pdo->commit();
}
}
}
public static function rollback(string $connectionName = 'DEFAULT', ?string $schema = null): void {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if ($conn instanceof PDO && $conn->inTransaction()) {
$conn->rollback();
} elseif (method_exists(object_or_class: $conn, method: 'getPDO')) {
$pdo = $conn->getPDO();
if ($pdo instanceof PDO && $pdo->inTransaction()) {
$pdo->rollback();
}
}
}
public static function lastInsertId(string $connectionName = 'DEFAULT', ?string $schema = null): string {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if (method_exists(object_or_class: $conn, method: 'getPDO')) return $conn->getPDO()->lastInsertId();
throw new RuntimeException(message: "The database connection '{$connectionName}' does not support lastInsertId()");
}
}
@@ -0,0 +1,32 @@
<?php
namespace KrothiumAPI\Database\Drivers;
use PDO;
use PDOException;
use RuntimeException;
class MySQLDriver extends PDOAbstract {
protected function connect(string $envName): void {
$host = $_ENV["{$envName}_DB_HOST"];
$port = $_ENV["{$envName}_DB_PORT"] ?? 3306;
$dbname = $_ENV["{$envName}_DB_NAME"];
$user = $_ENV["{$envName}_DB_USERNAME"];
$password = $_ENV["{$envName}_DB_PASSWORD"];
$charset = $_ENV["{$envName}_DB_CHARSET"] ?? 'utf8mb4';
try {
$this->connection = new PDO(
dsn: "mysql:host={$host};port={$port};dbname={$dbname};charset={$charset}",
username: $user,
password: $password,
options: [
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
} catch (PDOException $e) {
throw new RuntimeException(message: "Error connecting to MySQL: {$e->getMessage()}");
}
}
}
@@ -0,0 +1,54 @@
<?php
namespace KrothiumAPI\Database\Drivers;
use PDO;
abstract class PDOAbstract {
protected PDO $connection;
public function __construct(string $envName) {
$this->connect(envName: $envName);
}
abstract protected function connect(string $envName): void;
public function execute(string $sql, array $params = []): bool {
$stmt = $this->connection->prepare(query: $sql);
return $stmt->execute(params: $params);
}
public function fetchAll(string $sql, array $params = []): array {
$stmt = $this->connection->prepare(query: $sql);
$stmt->execute(params: $params);
return $stmt->fetchAll(mode: PDO::FETCH_ASSOC);
}
public function fetchOne(string $sql, array $params = []): ?array {
$stmt = $this->connection->prepare(query: $sql);
$stmt->execute(params: $params);
$result = $stmt->fetch(mode: PDO::FETCH_ASSOC);
return $result ?: null;
}
public function beginTransaction(): void {
if (!$this->connection->inTransaction()) {
$this->connection->beginTransaction();
}
}
public function commit(): void {
if ($this->connection->inTransaction()) {
$this->connection->commit();
}
}
public function rollback(): void {
if ($this->connection->inTransaction()) {
$this->connection->rollBack();
}
}
public function getPDO(): PDO {
return $this->connection;
}
}
@@ -0,0 +1,50 @@
<?php
namespace KrothiumAPI\Database\Drivers;
use PDO;
use PDOException;
use RuntimeException;
class PostgreSQLDriver extends PDOAbstract {
public function __construct(string $envName, ?string $schema = null) {
$this->connect(envName: $envName, schema: $schema);
}
protected function connect(string $envName, ?string $schema = null): void {
$host = $_ENV["{$envName}_DB_HOST"];
$port = $_ENV["{$envName}_DB_PORT"] ?? 5432;
$dbname = $_ENV["{$envName}_DB_NAME"];
$user = $_ENV["{$envName}_DB_USERNAME"];
$pass = $_ENV["{$envName}_DB_PASSWORD"];
$schema ??= $_ENV["{$envName}_DB_SCHEMA"] ?? 'public';
$systemTimeZone = $_ENV["{$envName}_DB_TIMEZONE"] ?? 'UTC';
$sslMode = $_ENV["{$envName}_DB_SSLMODE"] ?? 'disable'; // disable, require, verify-ca, verify-full
try {
$this->connection = new PDO(
dsn: "pgsql:host={$host};port={$port};dbname={$dbname};sslmode={$sslMode}",
username: $user,
password: $pass,
options: [
PDO::ATTR_PERSISTENT => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
// Define o fuso horário da sessão
$this->connection->exec(statement: "SET TIME ZONE '{$systemTimeZone}'");
// Define o schema padrão
$this->connection->exec(statement: "SET search_path TO {$schema}, public");
} catch (PDOException $e) {
throw new RuntimeException(
message: "Error connecting to PostgreSQL: {$e->getMessage()}"
);
}
}
// Método auxiliar para alterar schema dinamicamente
public function setSchema(string $schema): void {
$this->connection->exec(statement: "SET search_path TO {$schema}, public");
}
}
@@ -0,0 +1,85 @@
<?php
namespace KrothiumAPI\Database\Drivers;
use Predis\Client as PredisClient;
use RuntimeException;
class RedisDriver {
protected PredisClient $connection;
protected string $envName;
public function __construct(string $envName = 'DEFAULT') {
$this->envName = strtoupper(string: $envName);
$this->connect();
}
protected function connect(): void {
$host = $_ENV["{$this->envName}_REDIS_HOST"] ?? $_ENV['REDIS_HOST'] ?? '127.0.0.1';
$port = (int) ($_ENV["{$this->envName}_REDIS_PORT"] ?? $_ENV['REDIS_PORT'] ?? 6379);
$password = $_ENV["{$this->envName}_REDIS_PASSWORD"] ?? $_ENV['REDIS_PASSWORD'] ?? null;
$database = isset($_ENV["{$this->envName}_REDIS_DATABASE"]) ? (int) $_ENV["{$this->envName}_REDIS_DATABASE"] : (isset($_ENV['REDIS_DATABASE']) ? (int) $_ENV['REDIS_DATABASE'] : 0);
// Usa Predis
try {
$parameters = [
'scheme' => 'tcp',
'host' => $host,
'port' => $port,
'database' => $database,
];
if ($password) $parameters['password'] = $password;
$this->connection = new PredisClient(parameters: $parameters);
// testa a conexão (Predis conecta sob demanda, connect() força handshake)
$this->connection->connect();
} catch (\Throwable $e) {
throw new RuntimeException(message: "Error connecting to Redis: {$e->getMessage()}");
}
}
/** Retorna o cliente interno (Predis ou Redis) */
public function getClient(): PredisClient {
return $this->connection;
}
/** Define um valor (string). Se $ttl for informado, adiciona expire em segundos. */
public function set(string $key, mixed $value, ?int $ttl = null): bool {
// Predis: set or setex
if ($ttl !== null && $ttl > 0) {
$resp = $this->connection->setex($key, $ttl, $value);
return (bool) $resp;
}
$resp = $this->connection->set($key, $value);
return (bool) $resp;
}
/** Recupera um valor por chave */
public function get(string $key): mixed {
return $this->connection->get($key);
}
/** Remove chaves, retorna número de chaves removidas */
public function del(string ...$keys): int {
if (empty($keys)) return 0;
return (int) $this->connection->del(...$keys);
}
/** Verifica se uma chave existe */
public function exists(string $key): bool {
return (bool) $this->connection->exists($key);
}
/** Define TTL (segundos) para uma chave */
public function expire(string $key, int $seconds): bool {
return (bool) $this->connection->expire($key, $seconds);
}
/** Proxy para comandos não-explicitados */
public function __call(string $name, array $arguments) {
try {
return $this->connection->{$name}(...$arguments);
} catch (\Throwable $e) {
throw new RuntimeException("Redis command '{$name}' failed: {$e->getMessage()}");
}
}
}
@@ -0,0 +1,109 @@
<?php
namespace KrothiumAPI\Database;
use KrothiumAPI\Database\Drivers\RedisDriver;
class RedisManager {
/**
* Armazena múltiplas conexões Redis nomeadas.
* Exemplo: [
* 'DEFAULT' => RedisDriver,
* 'CACHE' => RedisDriver,
* 'QUEUE' => RedisDriver
* ]
*/
private static array $connections = [];
/**
* Retorna a conexão Redis para o ambiente informado.
* Se não existir, cria uma nova.
*/
private static function connect(string $envName = 'DEFAULT'): RedisDriver {
if (!isset(self::$connections[$envName])) {
self::$connections[$envName] = new RedisDriver(envName: $envName);
}
return self::$connections[$envName];
}
/**
* Armazena um valor no Redis associado a uma chave, com uma TTL opcional.
*
* Este método garante que a conexão com o Redis seja estabelecida e então delega a operação SET para o driver.
*
* @param string $key A chave sob a qual o valor será armazenado.
* @param mixed $value O valor a ser armazenado (pode ser string, int, array, etc.).
* @param int|null $ttl O tempo de vida (Time-To-Live) em segundos para a chave (opcional).
* @return bool Retorna `true` em caso de sucesso.
*/
public static function set(string $key, mixed $value, ?int $ttl = null, string $envName = 'DEFAULT'): bool {
$conn = self::connect(envName: $envName);
return $conn->set(key: $key, value: $value, ttl: $ttl);
}
/**
* Recupera o valor armazenado no Redis associado a uma chave.
*
* Este método delega a operação GET para o driver de conexão, recuperando o valor de uma chave específica.
*
* @param string $key A chave cujo valor deve ser recuperado.
* @return mixed O valor da chave, ou `null` se a chave não existir.
*/
public static function get(string $key, string $envName = 'DEFAULT'): mixed {
$conn = self::connect(envName: $envName);
return $conn->get(key: $key);
}
/**
* Remove uma ou mais chaves do Redis.
*
* Este método recebe um array de chaves e remove cada uma delas individualmente do serviço Redis.
*
* @param array $keys Um array de strings contendo as chaves a serem removidas.
* @return int O número total de chaves removidas.
*/
public static function del(array $keys, string $envName = 'DEFAULT'): int {
$conn = self::connect(envName: $envName);
$deleted = 0;
foreach ($keys as $key) {
$deleted += $conn->del(keys: $key);
}
return $deleted;
}
/**
* Verifica a existência de uma chave no Redis.
*
* @param string $key A chave a ser verificada.
* @return bool Retorna `true` se a chave existir no Redis, `false` caso contrário.
*/
public static function exists(string $key, string $envName = 'DEFAULT'): bool {
$conn = self::connect(envName: $envName);
return $conn->exists(key: $key);
}
/**
* Define um tempo de vida (TTL) para uma chave existente no Redis.
*
* @param string $key A chave para a qual o TTL será definido.
* @param int $seconds O tempo de vida em segundos.
* @return bool Retorna `true` se o TTL foi definido com sucesso, `false` caso contrário (ex: a chave não existe).
*/
public static function expire(string $key, int $seconds, string $envName = 'DEFAULT'): bool {
$conn = self::connect(envName: $envName);
return $conn->expire(key: $key, seconds: $seconds);
}
/**
* Permite chamar qualquer método nativo do driver Redis (método mágico __call).
*
* Este método atua como um proxy para invocar comandos diretamente no driver subjacente do Redis, permitindo flexibilidade para comandos que não estão mapeados explicitamente nos outros métodos da classe.
*
* @param string $method O nome do método/comando Redis a ser chamado.
* @param array $args Um array de argumentos a serem passados para o método.
* @return mixed O resultado da execução do comando Redis.
*/
public static function call(string $method, array $args = [], string $envName = 'DEFAULT'): mixed {
$conn = self::connect(envName: $envName);
return $conn->__call(name: $method, arguments: $args);
}
}
@@ -0,0 +1,32 @@
<?php
namespace KrothiumAPI\Helpers;
class ConstHelper {
/**
* Recupera o valor de uma constante global de forma segura, permitindo um valor padrão de fallback.
* * Este método atua como um wrapper de proteção para a função nativa `constant()` do PHP. Ele previne
* erros de execução (Warning/Error) que ocorreriam ao tentar acessar uma constante não definida
* diretamente. É ideal para acessar configurações dinâmicas, variáveis de ambiente ou flags de
* sistema onde a existência da constante não é garantida em todos os ambientes (desenvolvimento,
* homologação e produção).
* *
* * ---
* ## Lógica de Funcionamento
* 1. **Verificação de Existência:** Utiliza `defined()` para checar se o identificador fornecido existe na tabela de símbolos globais do script.
* 2. **Resolução de Valor:** Caso a constante exista, o método retorna seu valor original (que pode ser qualquer tipo escalar ou array, dependendo da versão do PHP).
* 3. **Tratamento de Fallback:** Se a constante não estiver definida, o método retorna o valor estipulado no parâmetro `$default`, garantindo que o fluxo da aplicação não seja interrompido.
* * ---
* ## Exemplos de Uso
* - **Configuração de API:** `Config::get('API_KEY', 'default_key_123');`
* - **Flags de Debug:** `Config::get('DEBUG_MODE', false);`
* * @param string $constant_name O nome da constante global a ser verificada e recuperada.
* @param mixed $default O valor a ser retornado caso a constante não esteja definida. O padrão é `null`.
* @return mixed Retorna o valor da constante se definida; caso contrário, retorna o valor de `$default`.
*/
public static function get(string $constant_name, $default = null): mixed {
if (defined(constant_name: $constant_name)) {
return constant(name: $constant_name);
}
return $default;
}
}
@@ -0,0 +1,189 @@
<?php
namespace KrothiumAPI\Helpers;
class RequestHelper {
/**
* Extrai, decodifica e filtra dados de requisições HTTP de forma polimórfica.
*
* Este método é o motor de captura de dados da aplicação. Ele abstrai a complexidade de lidar com
* diferentes verbos HTTP (GET, POST, PUT, PATCH, DELETE) e formatos de conteúdo (Form-Data, JSON,
* URL-Encoded). O método detecta automaticamente se a requisição contém um payload JSON, gerencia
* a leitura do fluxo `php://input` para métodos não-nativos do PHP e aplica filtros de
* higienização customizados ou padrões.
*
*
*
* ---
* ## Fluxo de Inteligência
* 1. **Mapeamento de Superglobais:** Utiliza `filter_input_array` para métodos padrão (GET/POST/COOKIE), garantindo acesso seguro às variáveis globais do PHP.
* 2. **Processamento JSON:** Se o cabeçalho `Content-Type` indicar JSON, o método lê o corpo bruto da requisição, decodifica-o e valida a sintaxe. Caso o JSON seja inválido, interrompe a execução com um erro 400.
* 3. **Suporte a Métodos Modernos (REST):** Para PUT, PATCH e DELETE, extrai os dados via `php://input`. Se não for JSON, processa como string de consulta (`parse_str`), permitindo que esses métodos funcionem como um formulário convencional.
* 4. **Limpeza de Protocolo:** Remove automaticamente a chave `_method`, comumente usada para emular métodos HTTP em formulários HTML, evitando que dados de controle "vazem" para a lógica de negócio.
* 5. **Filtragem:** Delega ao método interno `applyFilters` a responsabilidade de validar os dados finais contra o mapa de filtros fornecido.
*
* ---
* ## Parâmetros e Retorno
* - **Tipos de Retorno:** Pode retornar o `array` processado (padrão) ou a `string` bruta (útil para logs ou validações externas).
* - **Segurança:** Bloqueia a execução em caso de payloads corrompidos via `errorJson`.
*
* @param string $form_type O método de entrada (GET, POST, PUT, PATCH, DELETE, COOKIE, SERVER).
* @param array|null $filters Mapa de filtros de higienização (ex: `FILTER_SANITIZE_STRING`).
* @param string $return_type Define o formato de saída: 'array' ou 'string'.
* @return mixed O conjunto de dados filtrados ou a string bruta da requisição.
*/
public static function getRequestData(string $form_type = 'GET', ?array $filters = null, string $return_type = 'array'): mixed {
$method = strtoupper(string: trim(string: $form_type));
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
$isJson = self::isJsonContentType(contentType: $contentType);
$inputMap = [
'GET' => INPUT_GET,
'POST' => INPUT_POST,
'COOKIE' => INPUT_COOKIE,
'SERVER' => INPUT_SERVER,
];
// GET/POST/COOKIE/SERVER
if(isset($inputMap[$method])) {
// POST JSON: lê o body
if($method === 'POST' && $isJson) {
$raw = file_get_contents(filename: 'php://input') ?: '';
if($return_type === 'string') {
return $raw;
}
$data = json_decode(json: $raw, associative: true);
if(json_last_error() !== JSON_ERROR_NONE) {
self::errorJson(statusCode: 400, message: 'Invalid JSON: ' . json_last_error_msg());
}
unset($data['_method']);
return self::applyFilters(data: $data ?? [], filters: $filters);
}
$form = filter_input_array(type: $inputMap[$method], options: $filters ?? FILTER_DEFAULT) ?? [];
return is_array(value: $form) ? $form : [];
}
// PUT/PATCH/DELETE
if(in_array(needle: $method, haystack: ['PUT', 'PATCH', 'DELETE'], strict: true)) {
$raw = file_get_contents(filename: 'php://input') ?: '';
if($raw === '') {
return [];
}
if($isJson) {
if($return_type === 'string') {
return $raw;
}
$data = json_decode(json: $raw, associative: true);
if(json_last_error() !== JSON_ERROR_NONE) {
self::errorJson(statusCode: 400, message: 'Invalid JSON: ' . json_last_error_msg());
}
} else {
parse_str($raw, $data);
}
unset($data['_method']);
return self::applyFilters(data: $data ?? [], filters: $filters);
}
// Default: GET
$form = filter_input_array(type: INPUT_GET, options: $filters ?? FILTER_DEFAULT) ?? [];
return is_array(value: $form) ? $form : [];
}
/**
* Verifica se o cabeçalho de tipo de conteúdo (Content-Type) da requisição indica um formato JSON.
*
* Este método auxiliar é fundamental para a estratégia de parsing polimórfico do sistema. Em vez de
* realizar uma comparação estrita, ele utiliza uma busca de substring insensível a maiúsculas
* e minúsculas para identificar a presença da palavra "json". Isso garante compatibilidade com
* diversas variações de cabeçalhos comuns em APIs modernas, incluindo definições de charset e
* Media Types específicos.
*
*
*
* ---
* ## Casos de Compatibilidade
* O método retorna `true` para padrões como:
* - `application/json` (Padrão RFC 4627)
* - `application/json; charset=utf-8` (Com especificação de codificação)
* - `application/vnd.api+json` (Padrão JSON:API)
* - `text/json` (Legado ou variações de servidores específicos)
*
* ---
* ## Lógica de Implementação
* 1. **Busca Flexível:** Utiliza `stripos()` para localizar a agulha 'json' em qualquer posição da string.
* 2. **Performance:** Por ser um método estático e focado em uma única responsabilidade, oferece baixo custo computacional para o ciclo de vida da requisição.
* 3. **Normalização:** Previne erros de detecção causados por variações de caixa (UpperCase vs LowerCase) enviadas por diferentes clientes HTTP (Browsers, Postman, Mobile).
*
* @param string $contentType O valor bruto extraído do cabeçalho `$_SERVER['CONTENT_TYPE']`.
* @return bool Retorna `true` se o formato JSON for detectado, caso contrário, `false`.
*/
private static function isJsonContentType(string $contentType): bool {
// pega application/json, application/json; charset=utf-8, application/vnd.api+json, etc.
return stripos(haystack: $contentType, needle: 'json') !== false;
}
/**
* Interrompe a execução do script e envia uma resposta de erro padronizada em formato JSON.
* * Este método é o mecanismo de terminação segura da aplicação para falhas de requisição. Ele garante
* que o cliente (API, Mobile ou Frontend) receba um status code HTTP semanticamente correto e um
* corpo de resposta estruturado. Ao utilizar o tipo de retorno `never`, o método sinaliza ao
* analisador estático e ao desenvolvedor que o fluxo de código é encerrado imediatamente após
* sua invocação, prevenindo a execução de processos subsequentes indesejados.
* *
* * ---
* ## Mecanismo de Resposta
* 1. **Definição de Status:** Utiliza `http_response_code` para definir o estado da resposta (ex: 400 para Bad Request, 401 para Unauthorized).
* 2. **Negociação de Conteúdo:** Força o cabeçalho `Content-Type` para `application/json` com codificação UTF-8, assegurando que caracteres especiais sejam renderizados corretamente no cliente.
* 3. **Padronização de Payload:** Encapsula a mensagem de erro em um objeto JSON com as chaves `status` (fixo como "error") e `message` (dinâmica), facilitando o tratamento de erros no lado do cliente.
* 4. **Encerramento:** Finaliza o processo via `exit`, impedindo que qualquer HTML ou saída residual corrompa o JSON enviado.
* * ---
* ## Exemplos de Aplicação
* - Falha na decodificação de payloads JSON malformados.
* - Erros de validação crítica em rotas de API.
* - Bloqueio de acesso por falta de privilégios.
* * @param int $statusCode Código de status HTTP (ex: 400, 403, 404, 500).
* @param string $message Mensagem descritiva detalhando o motivo do erro.
* @return never Este método encerra a execução do script e nunca retorna ao chamador.
*/
private static function errorJson(int $statusCode, string $message): never {
http_response_code(response_code: $statusCode);
header(header: "Content-Type: application/json; charset=utf-8");
echo json_encode(value: [
"status" => "error",
"message" => $message,
]);
exit;
}
/**
* Aplica filtros de higienização e validação em um conjunto de dados brutos de forma segura.
*
* Este método atua como uma camada interna de proteção, permitindo que arrays associativos (como
* payloads JSON ou dados de formulários) sejam processados utilizando as mesmas regras nativas do
* PHP aplicadas em `filter_input_array`. Ele é essencial para garantir que as entradas de dados
* estejam em conformidade com os tipos esperados (inteiros, strings sanitizadas, flags booleanas, etc.)
* antes de serem distribuídas para as camadas de serviço ou objetos de transferência (DTOs).
*
*
*
* ---
* ## Mecanismo de Filtragem
* 1. **Verificação de Nulidade:** Se nenhum mapa de filtros for fornecido, o método retorna os dados brutos integralmente, permitindo flexibilidade em rotas que não exigem tipagem estrita inicial.
* 2. **Processamento `filter_var_array`:** Utiliza a função nativa do PHP para mapear as chaves do array contra as definições de filtros. A opção `add_empty: false` é crucial para garantir que o resultado contenha apenas os campos originalmente presentes no input, evitando a criação de chaves indesejadas com valores nulos.
* 3. **Normalização de Saída:** Como as funções de filtro do PHP podem retornar `false` ou `null` em cenários de erro ou inputs corrompidos, este método assegura a consistência da tipagem sempre retornando um `array` (mesmo que vazio).
*
* ---
* ## Segurança e Integridade
* - **Padronização:** Garante que a mesma lógica de segurança usada para superglobais seja aplicada a dados decodificados manualmente (como JSON do `php://input`).
* - **Redução de Side-Effects:** O tratamento do retorno previne erros de iteração (foreach) em camadas superiores.
*
* @param array $data O array associativo de dados brutos a serem processados.
* @param array|null $filters O mapa de definições de filtros (ex: `['id' => FILTER_VALIDATE_INT]`).
* @return array O conjunto de dados resultantes após a validação e higienização.
*/
private static function applyFilters(array $data, ?array $filters): array {
if($filters === null) {
return $data;
}
$filtered = filter_var_array(array: $data, options: $filters, add_empty: false);
return is_array(value: $filtered) ? $filtered : [];
}
}
+396
View File
@@ -0,0 +1,396 @@
<?php
namespace KrothiumAPI\Http;
use Exception;
use KrothiumAPI\Helpers\ConstHelper;
class Router {
private static $routes = [];
private static $params = [];
private static $APP_SYS_MODE = null;
private static string $basePath = '';
private static $currentGroupPrefix = '';
private static $currentGroupMiddlewares = [];
private static $ROUTER_ALLOWED_ORIGINS = ['*'];
private static array $requiredConstants = ['APP_SYS_MODE'];
private static array $allowedHttpRequests = ['GET','POST','PUT','PATCH','DELETE','OPTIONS'];
/**
* Inicializa o roteador e define as configurações essenciais, como caminhos base, modos de operação e permissões de CORS.
*
* Este método estático é o ponto de partida para configurar o roteador (`Router`) e garantir que todas as variáveis de ambiente necessárias estejam prontas para processar requisições.
*
* #### Fluxo de Operação:
* 1. **Verificação de Constantes:** Chama `self::checkRequiredConstants()` para garantir que todas as constantes obrigatórias do sistema estejam definidas. (Se falhar, a execução é encerrada).
* 2. **Definição do Caminho Base (`$basePath`):** Verifica se a constante `ROUTER_BASE_PATH` está definida. Se estiver, define o caminho base da aplicação, garantindo que ele comece com `/`.
* 3. **Registro na Sessão:** Armazena o caminho base (`$basePath`) na sessão (`$_SESSION['ROUTER_BASE_PATH']`).
* 4. **Definição de Modos:** Define as propriedades estáticas `self::$ROUTER_MODE` (modo do roteador, e.g., 'JSON', 'WEB') e `self::$APP_SYS_MODE` (modo do sistema, e.g., 'DEV', 'PROD') com seus valores em
* caixa alta (uppercase).
* 5. **Configuração de CORS:** Se o roteador estiver no modo 'JSON' e a constante `ROUTER_ALLOWED_ORIGINS` estiver definida, define os domínios permitidos para requisições *Cross-Origin* (CORS).
*
* @return void
*/
public static function init() {
// Verifica as constantes obrigatórias
self::checkRequiredConstants();
// Verifica se tem diretório base definido
self::$basePath = ConstHelper::get(constant_name: 'ROUTER_BASE_PATH') ? '/' . trim(string: ConstHelper::get(constant_name: 'ROUTER_BASE_PATH'), characters: '/') : '';
$_SESSION['ROUTER_BASE_PATH'] = self::$basePath;
// Define os modos do roteador e do sistema
self::$APP_SYS_MODE = strtoupper(string: ConstHelper::get(constant_name: 'APP_SYS_MODE'));
// Define os domínios permitidos para CORS
if (ConstHelper::get(constant_name: 'ROUTER_ALLOWED_ORIGINS') !== null) {
self::$ROUTER_ALLOWED_ORIGINS = ConstHelper::get(constant_name: 'ROUTER_ALLOWED_ORIGINS');
}
}
/**
* Envia uma resposta JSON padronizada de erro e encerra a execução do script.
*
* Este método estático é um utilitário para endpoints de API, usado para comunicar falhas de forma consistente. Ele define o código de status HTTP do erro e envia uma mensagem detalhada no corpo da resposta JSON.
*
* #### Fluxo de Operação:
* 1. **Define o Código HTTP:** O código de status HTTP (`$code`) é definido usando `http_response_code()` (ex: 400 Bad Request, 401 Unauthorized, 500 Internal Server Error).
* 2. **Define o Cabeçalho:** O cabeçalho `Content-Type` é configurado para `application/json; charset=utf-8`.
* 3. **Envia o JSON:** Uma resposta JSON é construída com um status fixo de 'error' e a mensagem de erro fornecida (`$msg`).
* 4. **Encerra a Execução:** A execução do script é finalizada com `exit`, impedindo que códigos adicionais sejam processados após o envio da resposta.
*
* @param int $code O código de status HTTP a ser enviado na resposta de erro.
* @param string $msg A mensagem de erro detalhada a ser incluída no corpo da resposta JSON.
* @return void Este método não retorna um valor, pois ele finaliza a execução do script.
*/
private static function error(int $code, string $msg): void {
http_response_code(response_code: $code);
header(header: 'Content-Type: application/json; charset=utf-8');
echo json_encode(value: [
"status" => 'error',
"message" => $msg
]
);
exit;
}
/**
* Verifica se todas as constantes de configuração essenciais estão definidas no ambiente de execução.
*
* Este método estático é um **verificador de saúde** (health check) usado para garantir que o ambiente da
* aplicação esteja corretamente configurado antes de prosseguir com a execução. Ele itera sobre uma lista
* pré-definida de constantes (`self::$requiredConstants`) que são consideradas cruciais para a operação do sistema.
*
* #### Fluxo de Operação:
* 1. **Iteração:** Percorre o array estático que lista os nomes das constantes obrigatórias.
* 2. **Verificação:** Para cada nome de constante, ele usa `defined()` para verificar se a constante existe no escopo global do PHP.
* 3. **Ação em Caso de Falha:** Se uma constante obrigatória **não estiver definida**, o método assume uma falha crítica de configuração.
* Ele chama o método `self::error()`, que envia uma resposta JSON com o código HTTP **500 Internal Server Error** e uma mensagem
* detalhando qual constante está faltando, encerrando a execução do script.
*
* @return void Este método não retorna um valor em caso de sucesso; ele apenas garante que as constantes existam. Em caso de falha, ele envia uma resposta HTTP de erro e encerra o script.
*/
private static function checkRequiredConstants(): void {
foreach (self::$requiredConstants as $constant) {
if (!defined(constant_name: $constant)) {
self::error(
code: 500,
msg: "Constant '{$constant}' not defined."
);
}
}
}
// =====================================
// Métodos HTTP para definição de rotas
// =====================================
public static function get(string $uri, array $handler, array $middlewares = []): void {
self::addRoute(method: 'GET', uri: $uri, handler: $handler, middlewares: $middlewares);
}
public static function post(string $uri, array $handler, array $middlewares = []): void {
self::addRoute(method: 'POST', uri: $uri, handler: $handler, middlewares: $middlewares);
}
public static function put(string $uri, array $handler, array $middlewares = []): void {
self::addRoute(method: 'PUT', uri: $uri, handler: $handler, middlewares: $middlewares);
}
public static function patch(string $uri, array $handler, array $middlewares = []): void {
self::addRoute(method: 'PATCH', uri: $uri, handler: $handler, middlewares: $middlewares);
}
public static function delete(string $uri, array $handler, array $middlewares = []): void {
self::addRoute(method: 'DELETE', uri: $uri, handler: $handler, middlewares: $middlewares);
}
/**
* Adiciona uma nova definição de rota à lista de rotas do roteador.
*
* Este método privado é o núcleo do registro de rotas. Ele constrói o caminho final da rota (URI) combinando o prefixo de grupo atual, se houver, com o URI fornecido, e armazena os detalhes da rota (controlador, ação e middlewares) em um array estático (`self::$routes`).
*
* @param string $method O método HTTP (e.g., 'GET', 'POST', 'PATCH').
* @param string $uri A URI específica da rota (relativa ao prefixo do grupo, se houver).
* @param array $handler Um array contendo a classe do controlador e o método de ação (ex: ['Controller', 'method']).
* @param array $middlewares Um array opcional de middlewares específicos desta rota.
* @return void
*/
private static function addRoute(string $method, string $uri, array $handler, array $middlewares = []) {
$path = '/' . trim(string: self::$currentGroupPrefix . '/' . trim(string: $uri, characters: '/'), characters: '/');
[$controller, $action] = $handler;
self::$routes[$method][] = [
'method' => $method,
'path' => $path,
'controller' => $controller,
'action' => $action,
'middlewares' => array_merge(self::$currentGroupMiddlewares, $middlewares)
];
}
/**
* Agrupa um conjunto de rotas sob um prefixo de URI e aplica middlewares em comum.
*
* Este método estático é uma ferramenta poderosa para organizar rotas, permitindo que todas as rotas definidas dentro da função de callback (`$callback`) herdem um prefixo de URI comum e uma lista de middlewares.
*
* #### Fluxo de Operação:
* 1. **Backup de Contexto:** Os prefixos e middlewares atuais (`self::$currentGroupPrefix` e `self::$currentGroupMiddlewares`) são salvos temporariamente. Isso é essencial para suportar o aninhamento (grupos dentro de grupos).
* 2. **Definição do Novo Contexto:** O prefixo do novo grupo (`$prefix`) é concatenado ao prefixo existente (`$previousPrefix`), e os novos middlewares são mesclados com os existentes.
* 3. **Execução das Rotas:** A função de callback (`$callback`) é executada. Todas as chamadas de rotas (`GET`, `POST`, etc.) feitas aqui dentro usarão o novo prefixo e herdarão os novos middlewares.
* 4. **Restauração do Contexto:** Após a execução do callback, os prefixos e middlewares originais são restaurados. Isso garante que rotas definidas após o grupo não sejam afetadas pelo prefixo ou middlewares internos do grupo.
*
* @param string $prefix O prefixo da URI a ser aplicado a todas as rotas dentro do grupo (ex: '/api/v1').
* @param callable $callback A função que contém a definição das rotas a serem agrupadas.
* @param array $middlewares Um array opcional de middlewares que serão aplicados a todas as rotas dentro deste grupo e em seus subgrupos.
* @return void
*/
public static function group(string $prefix, callable $callback, array $middlewares = []): void {
$previousPrefix = self::$currentGroupPrefix ?? '';
$previousMiddlewares = self::$currentGroupMiddlewares ?? [];
self::$currentGroupPrefix = $previousPrefix . $prefix;
self::$currentGroupMiddlewares = array_merge($previousMiddlewares, $middlewares);
$callback();
self::$currentGroupPrefix = $previousPrefix;
self::$currentGroupMiddlewares = $previousMiddlewares;
}
/**
* Verifica se o caminho da requisição (URI) corresponde ao padrão de uma rota registrada.
*
* Este método estático privado é essencial para o roteador. Ele compara o caminho da URI solicitada pelo
* cliente com o padrão de rota (`$routePath`) e extrai quaisquer parâmetros dinâmicos presentes na URI.
*
* @param string $routePath O padrão de URI da rota registrada (pode conter placeholders como '/users/{id}').
* @param string $requestPath A URI real da requisição (ex: '/users/123').
* @return bool Retorna `true` se o `$requestPath` corresponder ao `$routePath`; caso contrário, retorna `false`.
*/
private static function matchPath($routePath, $requestPath): bool {
self::$params = [];
$routeParts = explode(separator: '/', string: trim(string: $routePath, characters: '/'));
$reqParts = explode(separator: '/', string: trim(string: $requestPath, characters: '/'));
if(count($routeParts) !== count($reqParts)) return false;
foreach ($routeParts as $i => $part) {
if (preg_match(pattern: '/^{\w+}$/', subject: $part)) {
self::$params[] = $reqParts[$i];
} elseif ($part !== $reqParts[$i]) {
return false;
}
}
return true;
}
/**
* Prepara e retorna o array final de parâmetros a ser passado para o método de ação do controlador.
*
* Este método estático privado retorna os parâmetros de rota dinâmicos extraídos do URI (`self::$params`).
*
* @return array Um array indexado numericamente contendo a lista final de argumentos de rota para o método do controlador.
*/
private static function prepareMethodParameters(): array {
return array_values(array: self::$params);
}
/**
* Determina se uma rota registrada corresponde ao método HTTP e à URI da requisição atual.
*
* Este método estático privado é o principal mecanismo de correspondência de rotas do roteador.
* Ele verifica se o método HTTP da rota é o mesmo da requisição e, em seguida, usa o método auxiliar
* `matchPath` para verificar se o padrão da URI da rota corresponde ao caminho solicitado, considerando
* quaisquer parâmetros dinâmicos.
*
* @param string $method O método HTTP da requisição atual (e.g., 'GET', 'POST').
* @param string $uri A URI solicitada pelo cliente (caminho da requisição).
* @param array $route Um array de definição de rota contendo as chaves 'method' e 'path'.
* @return bool Retorna `true` se o método HTTP e o caminho da URI corresponderem; caso contrário, retorna `false`.
*/
private static function matchRoute(string $method, string $uri, array $route): bool {
return $route['method'] === $method && self::matchPath(routePath: $route['path'], requestPath: $uri);
}
/**
* Configura os cabeçalhos Cross-Origin Resource Sharing (CORS) para requisições de API.
*
* Este método privado verifica se a requisição é permitida de acordo com a política de CORS definida na aplicação.
* Ele permite ou nega o acesso de origens externas com base nas configurações e no modo de operação do sistema.
*
* @param string $method O método HTTP da requisição atual (e.g., 'OPTIONS', 'GET', 'POST').
* @return void Este método encerra a execução em caso de requisições OPTIONS ou de origem não permitida.
*/
private static function corsSetup(string $method): void {
$origin = $_SERVER['HTTP_ORIGIN'] ?? '*';
$allowAll = in_array(needle: '*', haystack: self::$ROUTER_ALLOWED_ORIGINS);
$isAllowed = in_array(needle: $origin, haystack: self::$ROUTER_ALLOWED_ORIGINS);
if ($allowAll || $isAllowed || self::$APP_SYS_MODE === 'DEV') {
header(header: "Access-Control-Allow-Origin: $origin");
} else {
self::error(code: 403, msg: "Origin '{$origin}' not allowed by CORS.");
}
$allowedRequests = implode(separator: ', ', array: self::$allowedHttpRequests);
header(header: "Access-Control-Allow-Methods: {$allowedRequests}");
header(header: 'Access-Control-Allow-Headers: Content-Type, Authorization');
if ($method === 'OPTIONS') {
http_response_code(response_code: 204);
exit;
}
}
/**
* Executa sequencialmente a pilha de Middlewares vinculada a uma rota.
*
* Este método atua como uma barreira de segurança e processamento pré-execução (Pipeline). Ele permite
* interceptar a requisição antes que ela chegue ao controlador, sendo ideal para verificações de
* autenticação, autorização de perfis (ACL), logs de acesso ou manutenção de sistema. O método
* suporta injeção dinâmica de argumentos para os middlewares e oferece dois níveis de bloqueio:
* um booleano simples e um detalhado com respostas JSON customizadas.
*
* ---
* ## Mecanismo de Execução
* 1. **Validação de Formato:** Verifica se a definição do middleware segue o padrão esperado: `[Classe, 'metodo', ...argumentos]`.
* 2. **Instanciação Dinâmica:** Localiza a classe e o método, instanciando-os em tempo de execução.
* 3. **Gestão de Bloqueios:**
* - **Bloqueio Booleano:** Se o middleware retornar explicitamente `false`, a requisição é negada com um erro 403 padrão.
* - **Bloqueio Estruturado:** Se retornar um `array`, o método analisa chaves como `block`, `status` e `response_code` para montar uma resposta JSON rica e encerrar o script.
* 4. **Continuidade:** Se todos os middlewares retornarem `true` (ou um array indicando sucesso), o fluxo retorna `true`, permitindo que o `dispatch()` prossiga para o controlador.
*
* @param array $middlewares Lista de arrays contendo a definição dos middlewares da rota.
* @return bool Retorna `true` se a requisição passou por todos os filtros sem ser bloqueada.
*/
public static function runMiddlewares(array $middlewares): bool {
foreach ($middlewares as $middleware) {
try {
if (!is_array(value: $middleware) || count(value: $middleware) < 2) {
self::error(code: 500, msg: "Invalid middleware format. Expected: [Class::class, 'method', ...args]");
}
$class = $middleware[0];
$method = $middleware[1];
$args = array_slice(array: $middleware, offset: 2);
if (!class_exists(class: $class)) {
self::error(code: 500, msg: "Middleware class '{$class}' not found.");
}
if (!method_exists(object_or_class: $class, method: $method)) {
self::error(code: 500, msg: "Method '{$method}' does not exist in class '{$class}'.");
}
$instance = new $class();
$result = call_user_func_array(callback: [$instance, $method], args: $args);
// bloqueio simples
if ($result === false) {
self::error(code: 403, msg: "{$class}::{$method} blocked the request.");
}
// bloqueio detalhado
if (is_array(value: $result)) {
$block = $result['block'] ?? null;
$status = $result['status'] ?? null;
$shouldBlock = ($block === true) || ($status !== null && $status !== 'success');
if ($shouldBlock) {
$code = (int) ($result['response_code'] ?? 403);
$msg = (string) ($result['message'] ?? 'Blocked by middleware');
$json_response = [
"message" => $msg ?? "{$class}::{$method} blocked the request."
];
if(isset($result['output']) && (!empty($result['output']) || $result['output'] !== null || $result['output'] !== '')) {
$json_response['output'] = $result['output'];
}
http_response_code(response_code: $code);
header(header: 'Content-Type: application/json; charset=utf-8');
echo json_encode(value: $json_response);
exit;
}
}
} catch (Exception $e) {
self::error(code: 500, msg: $e->getMessage());
}
}
return true;
}
/**
* Orquestra o ciclo de vida da requisição, realizando o roteamento e a execução do controlador.
* * Este método é o ponto de entrada principal (Front Controller) que transforma uma requisição
* HTTP bruta em uma ação de software. Ele gerencia desde a validação de constantes de ambiente
* até a resolução de parâmetros dinâmicos da URL, passando por suporte a emulação de métodos REST
* (via `_method`), configuração de CORS, execução de Middlewares e, por fim, a invocação do
* par Controller/Action correspondente.
* * @return void Este método encerra a execução do script (`exit`) ao encontrar e executar uma rota válida.
*/
public static function dispatch(): void {
self::checkRequiredConstants();
$method = $_SERVER['REQUEST_METHOD'];
$uri = trim(string: parse_url(url: $_SERVER['REQUEST_URI'], component: PHP_URL_PATH), characters: '/');
if ($method === 'POST' && isset($_POST['_method'])) {
$method = strtoupper(string: $_POST['_method']);
}
if (!in_array(needle: $method, haystack: self::$allowedHttpRequests)) {
self::error(code: 405, msg: "HTTP method '{$method}' not allowed.");
}
self::corsSetup(method: $method);
// remove basePath
if (!empty(self::$basePath) && str_starts_with(haystack: $uri, needle: trim(string: self::$basePath, characters: '/'))) {
$uri = substr(string: $uri, offset: strlen(string: trim(string: self::$basePath, characters: '/')));
}
$uri = trim(string: $uri, characters: '/');
foreach (self::$routes[$method] ?? [] as $route) {
if (!self::matchRoute(method: $method, uri: $uri, route: $route)) continue;
// roda middlewares (se barrar, o runMiddlewares já respondeu JSON)
if (!empty($route['middlewares']) && !self::runMiddlewares(middlewares: $route['middlewares'])) {
return;
}
$controller = new $route['controller']();
$action = $route['action'];
if (!method_exists(object_or_class: $controller, method: $action)) {
self::error(code: 500, msg: "Method '{$action}' not found.");
}
$params = self::prepareMethodParameters();
// se teu controller já dá echo/json, tu nem precisa setar 200/header aqui
call_user_func_array(callback: [$controller, $action], args: $params);
exit;
}
self::error(
code: 404,
msg: 'Page not found.'
);
}
}
+178
View File
@@ -0,0 +1,178 @@
<?php
namespace KrothiumAPI;
use KrothiumAPI\Http\Router;
use KrothiumAPI\Helpers\ConstHelper;
use KrothiumAPI\Services\LoggerService;
class KrothiumAPI {
private static array $config;
/**
* Inicializa e configura os componentes essenciais da aplicação.
*
* Este método estático é o ponto de partida para a inicialização do sistema. Ele carrega as configurações, define o tratamento de erros, gerencia a sessão, configura o fuso horário e inicializa o sistema de logs e o roteador.
*
* #### Fluxo de Operação:
* 1. **Carregamento de Configurações:** Armazena o array de configurações (`$config`) na propriedade estática da classe.
* 2. **Setup Essencial:** Chama métodos privados para configurar:
* * `setupErrors()`: Configuração de exibição de erros.
* * `setupSession()`: Inicialização da sessão PHP.
* * `setupTimezone()`: Definição do fuso horário da aplicação.
* * `setupConstants()`: Definição de constantes globais (se houver).
* * `setupLogger()`: Inicialização do sistema de logs.
* * `setupErrorHandlers()`: Definição de manipuladores de exceção e erros customizados.
* 3. **Inicialização do Roteador:** Chama `Router::init()` para inicializar o sistema de roteamento, preparando-o para receber e despachar requisições.
*
* @param array $config Um array de configurações iniciais a serem aplicadas na aplicação. Padrão: `[]`.
* @return void
*/
public static function init(array $config = []) {
self::$config = $config;
self::setupConstants();
// Inicia o router
Router::init();
self::setupErrors();
self::setupSession();
self::setupTimezone();
self::setupLogger();
self::setupErrorHandlers();
}
/**
* Configura erros
*/
private static function setupErrors(): void {
$errors = self::$config['errors'];
if(ConstHelper::get(constant_name: 'APP_SYS_MODE') === 'DEV') {
ini_set(option: 'display_errors', value: 1);
ini_set(option: 'display_startup_errors', value: 1);
ini_set(option: 'log_errors', value: E_ALL);
error_reporting(error_level: 1);
if(isset($errors['error_log'])) {
ini_set(option: 'error_log', value: $errors['error_log']);
}
}
}
/**
* Configura constantes
*/
private static function setupConstants() {
$constants = self::$config['constants'];
if(!empty($constants)) {
foreach ($constants as $name => $value) {
if (!defined(constant_name: $name)) {
define(constant_name: $name, value: $value);
}
}
}
}
/**
* Configura timezone
*/
private static function setupTimezone(): void {
$timezone = self::$config['system']['default_timezone'];
if(!empty($timezone)) {
date_default_timezone_set(timezoneId: $timezone);
}
}
/**
* Configura sessão
*/
private static function setupSession(): void {
$startSession = self::$config['system']['enable_session'];
if (($startSession === true) && session_status() === PHP_SESSION_NONE) {
session_start();
}
}
/**
* Configura logger
*/
private static function setupLogger(): void {
$logConfig = self::$config['logger'];
if(!empty($logConfig)) {
LoggerService::init(
driver: $logConfig['driver'],
logDir: $logConfig['logDir']
);
}
}
/**
* Configura handlers de erro para modo JSON
*/
private static function setupErrorHandlers(): void {
// Captura warnings / notices
set_error_handler(callback: function ($errno, $errstr, $errfile, $errline) {
self::jsonErrorResponse(
message: "Erro PHP: {$errstr}",
code: $errno,
extra: [
'file' => $errfile,
'line' => $errline
]
);
});
// Captura exceptions não tratadas
set_exception_handler(callback: function ($exception) {
self::jsonErrorResponse(
message: $exception->getMessage(),
code: $exception->getCode(),
extra: [
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTrace()
]
);
});
// Captura fatal errors (shutdown)
register_shutdown_function(callback: function () {
$error = error_get_last();
if ($error !== null) {
self::jsonErrorResponse(
message: $error['message'],
code: $error['type'],
extra: [
'file' => $error['file'],
'line' => $error['line']
]
);
}
});
}
/**
* Retorna resposta JSON de erro e encerra execução
*/
private static function jsonErrorResponse(string $message, int $code = 500, array $extra = []): void {
// Evita headers duplicados
if (!headers_sent()) {
http_response_code(response_code: 500);
header(header: 'Content-Type: application/json; charset=utf-8');
}
echo json_encode(
value: [
'status' => 'error',
'message' => $message,
'code' => $code,
'extra' => $extra,
],
flags: JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
);
exit;
}
/**
* Dispara roteador
*/
public static function routerDispatch() {
if (php_sapi_name() !== 'cli') {
Router::dispatch();
}
}
}
@@ -0,0 +1,64 @@
<?php
namespace KrothiumAPI\Services;
use DateTime;
use Exception;
use KrothiumAPI\Helpers\ConstHelper;
class LoggerService {
private static string $logDir;
public const DRIVER_FILE = 'FILE';
private static bool $initialized = false;
private static string $driver = self::DRIVER_FILE;
/**
* Inicializa o Logger
*/
public static function init(string $driver = self::DRIVER_FILE,?string $logDir = null): void {
self::$driver = strtoupper(string: $driver);
if (!defined(constant_name: 'STORAGE_FOLDER_PATH')) {
throw new Exception(message: "Constant 'STORAGE_FOLDER_PATH' is not defined.");
}
self::$logDir = $logDir ?? ConstHelper::get(constant_name: 'STORAGE_FOLDER_PATH') . '/logs';
if (self::$driver === self::DRIVER_FILE && !is_dir(filename: self::$logDir)) {
if (!mkdir(directory: self::$logDir, permissions: 0775, recursive: true) && !is_dir(filename: self::$logDir)) {
throw new Exception(message: "Failed to create log directory: " . self::$logDir);
}
}
self::$initialized = true;
}
/**
* Log genérico
*/
public static function log(string $message, string $level = 'INFO', array $context = []): void {
if (!self::$initialized) {
throw new Exception(message: "LoggerService is not initialized. Call LoggerService::init() first.");
}
switch (self::$driver) {
case self::DRIVER_FILE:
self::logToFile(message: $message, level: $level);
break;
}
}
// Métodos auxiliares por nível
public static function info(string $message, array $context = []): void { self::log(message: $message, level: 'INFO', context: $context); }
public static function warning(string $message, array $context = []): void { self::log(message: $message, level: 'WARNING', context: $context); }
public static function error(string $message, array $context = []): void { self::log(message: $message, level: 'ERROR', context: $context); }
public static function debug(string $message, array $context = []): void { self::log(message: $message, level: 'DEBUG', context: $context); }
/**
* Log para arquivo
*/
private static function logToFile(string $message, string $level): void {
$date = (new DateTime())->format(format: 'Y-m-d');
$now = (new DateTime())->format(format: 'Y-m-d H:i:s');
$filename = self::$logDir . "/app-{$date}.log";
$logMessage = "[$now][$level] $message" . PHP_EOL;
file_put_contents(filename: $filename, data: $logMessage, flags: FILE_APPEND);
}
}
+202
View File
@@ -0,0 +1,202 @@
<?php
namespace KrothiumAPI\Utils;
class HttpUtil {
/**
* Captura e filtra dados de entrada HTTP (GET, POST, COOKIE, SERVER) de forma segura.
*
* Este método centraliza a obtenção de dados de requisição, aplicando opcionalmente filtros.
* Ele lida com submissões **POST** padrão (urlencoded) e submissões **JSON** (tipo "application/json"),
* retornando os dados como um array associativo ou como a string JSON bruta, se especificado.
* Em caso de JSON inválido, a função encerra a execução e retorna um erro HTTP 500.
*
* @param string $form_type O tipo de entrada a ser filtrada (constantes PHP: INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER). O padrão é INPUT_GET.
* @param array|null $filters Uma matriz de filtros a serem aplicados aos dados de entrada, compatível com a função `filter_input_array()`.
* @return array|string Retorna um array associativo dos dados de entrada filtrados (ou array vazio se não houver dados), ou a string JSON bruta se $asJson for true e o POST for JSON.
* @return mixed Encerra a execução e envia uma resposta HTTP 500 se ocorrer um erro de decodificação JSON.
*/
public static function getRequestBody(string $form_type = 'GET', ?array $filters = null, string $return_type = 'array'): mixed {
$method = strtoupper(string: trim(string: $form_type));
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
$isJson = self::isJsonContentType(contentType: $contentType);
$inputMap = [
'GET' => INPUT_GET,
'POST' => INPUT_POST,
'COOKIE' => INPUT_COOKIE,
'SERVER' => INPUT_SERVER,
];
// GET/POST/COOKIE/SERVER
if (isset($inputMap[$method])) {
// POST JSON: lê o body
if ($method === 'POST' && $isJson) {
$raw = file_get_contents(filename: 'php://input') ?: '';
if ($return_type === 'string') {
return $raw;
}
$data = json_decode(json: $raw, associative: true);
if (json_last_error() !== JSON_ERROR_NONE) {
self::errorJson(statusCode: 400, message: 'Invalid JSON: ' . json_last_error_msg());
}
unset($data['_method']);
return self::applyFilters(data: $data ?? [], filters: $filters);
}
$form = filter_input_array(type: $inputMap[$method], options: $filters ?? FILTER_DEFAULT) ?? [];
return is_array(value: $form) ? $form : [];
}
// PUT/PATCH/DELETE
if (in_array(needle: $method, haystack: ['PUT', 'PATCH', 'DELETE'], strict: true)) {
$raw = file_get_contents(filename: 'php://input') ?: '';
if ($raw === '') {
return [];
}
if ($isJson) {
if ($return_type === 'string') {
return $raw;
}
$data = json_decode(json: $raw, associative: true);
if (json_last_error() !== JSON_ERROR_NONE) {
self::errorJson(statusCode: 400, message: 'Invalid JSON: ' . json_last_error_msg());
}
} else {
parse_str($raw, $data);
}
unset($data['_method']);
return self::applyFilters(data: $data ?? [], filters: $filters);
}
// Default: GET
$form = filter_input_array(type: INPUT_GET, options: $filters ?? FILTER_DEFAULT) ?? [];
return is_array(value: $form) ? $form : [];
}
/**
* Despacha uma resposta JSON customizada e encerra a execução do script.
* Ele permite a mesclagem (merge) de um array de saída personalizado
* diretamente na raiz do objeto JSON, além de suportar mensagens de feedback opcionais.
*
* @param int $response_code Código de status HTTP (ex: 200, 201, 403).
* @param string|null $message Mensagem de texto opcional para o cliente.
* @param array|null $output Array associativo de dados extras a serem mesclados na resposta.
* @return void Este método interrompe o fluxo do programa imediatamente.
*/
public static function jsonResponse(int $response_code, ?string $message = null, ?array $output = null): void {
// Constrói o array de resposta com os campos "message" e "data" se eles forem fornecidos
$response = [];
if ($message) {
$response['message'] = $message;
}
if ($output) {
$response = array_merge($response, $output);
}
// Define o código de resposta HTTP e o cabeçalho de conteúdo, e envia a resposta JSON
http_response_code(response_code: $response_code);
header(header: 'Content-Type: application/json; charset=utf-8');
if (!empty($response)) {
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
}
exit;
}
/**
* Realiza o redirecionamento do navegador para uma nova URL e encerra a execução do script.
*
* @param string $url O endereço de destino (URL absoluta ou caminho relativo) para onde o usuário será redirecionado.
* @return void Não retorna valor, pois encerra a execução do processo PHP.
*/
public static function redirect(string $url): void {
// Remove barra final do subpath para evitar //
$subpath = rtrim(string: self::getSubpath(), characters: '/');
// Garante que a URL comece com /
$url = str_starts_with(haystack: $url, needle: '/')
? $url
: "/{$url}";
// Persiste sessão antes do redirect
session_write_close();
// Monta URL final
$location = "{$subpath}{$url}" . self::getQueryString();
header(header: "Location: {$location}");
exit;
}
/**
* Extrai a string de query da URI da requisição.
*
* @return string|null A string de query completa, ou `null` se a URI não contiver uma string de query.
*/
public static function getQueryString(): ?string {
$parts = explode(separator: '?', string: $_SERVER['REQUEST_URI'], limit: 2); // limite 2 garante que só divide em duas partes
$request = $parts[1] ?? null; // se não existir, define null
return ($request !== null && $request !== '') ? "?{$request}" : '';
}
public static function getSubpath(): string {
return $_SESSION['ROUTER_BASE_PATH'] ?? '';
}
/**
* Verifica se o cabeçalho de tipo de conteúdo (Content-Type) da requisição indica um formato JSON.
*
* @param string $contentType O valor bruto extraído do cabeçalho `$_SERVER['CONTENT_TYPE']`.
* @return bool Retorna `true` se o formato JSON for detectado, caso contrário, `false`.
*/
private static function isJsonContentType(string $contentType): bool {
// pega application/json, application/json; charset=utf-8, application/vnd.api+json, etc.
return stripos(haystack: $contentType, needle: 'json') !== false;
}
/**
* Interrompe a execução do script e envia uma resposta de erro padronizada em formato JSON.
*
* @param int $statusCode Código de status HTTP (ex: 400, 403, 404, 500).
* @param string $message Mensagem descritiva detalhando o motivo do erro.
* @return never Este método encerra a execução do script e nunca retorna ao chamador.
*/
private static function errorJson(int $statusCode, string $message): never {
http_response_code(response_code: $statusCode);
header(header: "Content-Type: application/json; charset=utf-8");
echo json_encode(value: [
"status" => "error",
"message" => $message,
]);
exit;
}
/**
* Aplica filtros de higienização e validação em um conjunto de dados brutos de forma segura.
*
* @param array $data O array associativo de dados brutos a serem processados.
* @param array|null $filters O mapa de definições de filtros (ex: `['id' => FILTER_VALIDATE_INT]`).
* @return array O conjunto de dados resultantes após a validação e higienização.
*/
private static function applyFilters(array $data, ?array $filters): array {
if ($filters === null) {
return $data;
}
$filtered = filter_var_array(array: $data, options: $filters, add_empty: false);
return is_array(value: $filtered) ? $filtered : [];
}
/**
* Extrai o token de autenticação do tipo 'Bearer' do cabeçalho da requisição HTTP.
*
* @return string|null O token de autenticação do tipo 'Bearer' como uma string, ou `null`
* se o cabeçalho não for encontrado ou não estiver no formato esperado.
*/
public static function getBearerToken(): ?string {
// Tenta pegar o header de todas as fontes possíveis
$headers = $_SERVER['Authorization'] ?? $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? (function_exists(function: 'apache_request_headers') ? apache_request_headers() : []);
// Se veio do apache_request_headers, normaliza chaves
if (is_array(value: $headers)) {
$headers = array_change_key_case(array: $headers, case: CASE_LOWER);
$headers = $headers['authorization'] ?? '';
}
$headers = trim(string: $headers);
if ($headers && preg_match(pattern: '/Bearer\s(\S+)/', subject: $headers, matches: $matches)) {
return $matches[1];
}
return null;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit5598d6c42b75c33b245408cd5e519a5c::getLoader();
@@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}
@@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,15 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);
@@ -0,0 +1,12 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
);
@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);
@@ -0,0 +1,18 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'),
'Predis\\' => array($vendorDir . '/predis/predis/src'),
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
'KrothiumAPI\\' => array($baseDir . '/src'),
'GrahamCampbell\\ResultType\\' => array($vendorDir . '/graham-campbell/result-type/src'),
'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'),
);
@@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit5598d6c42b75c33b245408cd5e519a5c
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit5598d6c42b75c33b245408cd5e519a5c', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit5598d6c42b75c33b245408cd5e519a5c', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit5598d6c42b75c33b245408cd5e519a5c::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit5598d6c42b75c33b245408cd5e519a5c::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}
@@ -0,0 +1,99 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit5598d6c42b75c33b245408cd5e519a5c
{
public static $files = array (
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
);
public static $prefixLengthsPsr4 = array (
'S' =>
array (
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Polyfill\\Ctype\\' => 23,
),
'P' =>
array (
'Psr\\Http\\Message\\' => 17,
'Predis\\' => 7,
'PhpOption\\' => 10,
),
'K' =>
array (
'KrothiumAPI\\' => 12,
),
'G' =>
array (
'GrahamCampbell\\ResultType\\' => 26,
),
'D' =>
array (
'Dotenv\\' => 7,
),
);
public static $prefixDirsPsr4 = array (
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
'Symfony\\Polyfill\\Mbstring\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),
'Symfony\\Polyfill\\Ctype\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
),
'Psr\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-message/src',
),
'Predis\\' =>
array (
0 => __DIR__ . '/..' . '/predis/predis/src',
),
'PhpOption\\' =>
array (
0 => __DIR__ . '/..' . '/phpoption/phpoption/src/PhpOption',
),
'KrothiumAPI\\' =>
array (
0 => __DIR__ . '/../..' . '/src',
),
'GrahamCampbell\\ResultType\\' =>
array (
0 => __DIR__ . '/..' . '/graham-campbell/result-type/src',
),
'Dotenv\\' =>
array (
0 => __DIR__ . '/..' . '/vlucas/phpdotenv/src',
),
);
public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit5598d6c42b75c33b245408cd5e519a5c::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit5598d6c42b75c33b245408cd5e519a5c::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit5598d6c42b75c33b245408cd5e519a5c::$classMap;
}, null, ClassLoader::class);
}
}
@@ -0,0 +1,619 @@
{
"packages": [
{
"name": "graham-campbell/result-type",
"version": "v1.1.4",
"version_normalized": "1.1.4.0",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.5"
},
"require-dev": {
"phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
},
"time": "2025-12-27T19:43:20+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"install-path": "../graham-campbell/result-type"
},
{
"name": "phpoption/phpoption",
"version": "1.9.5",
"version_normalized": "1.9.5.0",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be",
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
},
"time": "2025-12-27T19:41:33+00:00",
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.5"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"install-path": "../phpoption/phpoption"
},
{
"name": "predis/predis",
"version": "v3.4.0",
"version_normalized": "3.4.0.0",
"source": {
"type": "git",
"url": "https://github.com/predis/predis.git",
"reference": "1183f5732e6b10efd33f64984a96726eaecb59aa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/predis/predis/zipball/1183f5732e6b10efd33f64984a96726eaecb59aa",
"reference": "1183f5732e6b10efd33f64984a96726eaecb59aa",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"psr/http-message": "^1.0|^2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.3",
"phpstan/phpstan": "^1.9",
"phpunit/phpcov": "^6.0 || ^8.0",
"phpunit/phpunit": "^8.0 || ~9.4.4"
},
"suggest": {
"ext-relay": "Faster connection with in-memory caching (>=0.6.2)"
},
"time": "2026-02-11T17:30:28+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Predis\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Till Krüss",
"homepage": "https://till.im",
"role": "Maintainer"
}
],
"description": "A flexible and feature-complete Redis/Valkey client for PHP.",
"homepage": "http://github.com/predis/predis",
"keywords": [
"nosql",
"predis",
"redis"
],
"support": {
"issues": "https://github.com/predis/predis/issues",
"source": "https://github.com/predis/predis/tree/v3.4.0"
},
"funding": [
{
"url": "https://github.com/sponsors/tillkruss",
"type": "github"
}
],
"install-path": "../predis/predis"
},
{
"name": "psr/http-message",
"version": "2.0",
"version_normalized": "2.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"time": "2023-04-04T09:54:51+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/2.0"
},
"install-path": "../psr/http-message"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.37.0",
"version_normalized": "1.37.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "141046a8f9477948ff284fa65be2095baafb94f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
"reference": "141046a8f9477948ff284fa65be2095baafb94f2",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"time": "2026-04-10T16:19:22+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-ctype"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.37.0",
"version_normalized": "1.37.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"time": "2026-04-10T17:25:58+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-mbstring"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.37.0",
"version_normalized": "1.37.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
"reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"time": "2026-04-10T16:19:22+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-php80"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.6.2",
"version_normalized": "5.6.2.0",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.1.3",
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3",
"symfony/polyfill-ctype": "^1.24",
"symfony/polyfill-mbstring": "^1.24",
"symfony/polyfill-php80": "^1.24"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-filter": "*",
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"time": "2025-04-30T23:37:27+00:00",
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "5.6-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
"type": "tidelift"
}
],
"install-path": "../vlucas/phpdotenv"
}
],
"dev": true,
"dev-package-names": []
}
@@ -0,0 +1,95 @@
<?php return array(
'root' => array(
'name' => 'claudecio/krothiumapi',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'b738b2f594cf8825ed0637eb6d4e315380afa0d6',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'claudecio/krothiumapi' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'b738b2f594cf8825ed0637eb6d4e315380afa0d6',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'graham-campbell/result-type' => array(
'pretty_version' => 'v1.1.4',
'version' => '1.1.4.0',
'reference' => 'e01f4a821471308ba86aa202fed6698b6b695e3b',
'type' => 'library',
'install_path' => __DIR__ . '/../graham-campbell/result-type',
'aliases' => array(),
'dev_requirement' => false,
),
'phpoption/phpoption' => array(
'pretty_version' => '1.9.5',
'version' => '1.9.5.0',
'reference' => '75365b91986c2405cf5e1e012c5595cd487a98be',
'type' => 'library',
'install_path' => __DIR__ . '/../phpoption/phpoption',
'aliases' => array(),
'dev_requirement' => false,
),
'predis/predis' => array(
'pretty_version' => 'v3.4.0',
'version' => '3.4.0.0',
'reference' => '1183f5732e6b10efd33f64984a96726eaecb59aa',
'type' => 'library',
'install_path' => __DIR__ . '/../predis/predis',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-message' => array(
'pretty_version' => '2.0',
'version' => '2.0.0.0',
'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-message',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-ctype' => array(
'pretty_version' => 'v1.37.0',
'version' => '1.37.0.0',
'reference' => '141046a8f9477948ff284fa65be2095baafb94f2',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-mbstring' => array(
'pretty_version' => 'v1.37.0',
'version' => '1.37.0.0',
'reference' => '6a21eb99c6973357967f6ce3708cd55a6bec6315',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.37.0',
'version' => '1.37.0.0',
'reference' => 'dfb55726c3a76ea3b6459fcfda1ec2d80a682411',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(),
'dev_requirement' => false,
),
'vlucas/phpdotenv' => array(
'pretty_version' => 'v5.6.2',
'version' => '5.6.2.0',
'reference' => '24ac4c74f91ee2c193fa1aaa5c249cb0822809af',
'type' => 'library',
'install_path' => __DIR__ . '/../vlucas/phpdotenv',
'aliases' => array(),
'dev_requirement' => false,
),
),
);
@@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020-2024 Graham Campbell <hello@gjcampbell.co.uk>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,33 @@
{
"name": "graham-campbell/result-type",
"description": "An Implementation Of The Result Type",
"keywords": ["result", "result-type", "Result", "Result Type", "Result-Type", "Graham Campbell", "GrahamCampbell"],
"license": "MIT",
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.5"
},
"require-dev": {
"phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
},
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"GrahamCampbell\\Tests\\ResultType\\": "tests/"
}
},
"config": {
"preferred-install": "dist"
}
}
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
/*
* This file is part of Result Type.
*
* (c) Graham Campbell <hello@gjcampbell.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace GrahamCampbell\ResultType;
use PhpOption\None;
use PhpOption\Some;
/**
* @template T
* @template E
*
* @extends \GrahamCampbell\ResultType\Result<T,E>
*/
final class Error extends Result
{
/**
* @var E
*/
private $value;
/**
* Internal constructor for an error value.
*
* @param E $value
*
* @return void
*/
private function __construct($value)
{
$this->value = $value;
}
/**
* Create a new error value.
*
* @template F
*
* @param F $value
*
* @return \GrahamCampbell\ResultType\Result<T,F>
*/
public static function create($value)
{
return new self($value);
}
/**
* Get the success option value.
*
* @return \PhpOption\Option<T>
*/
public function success()
{
return None::create();
}
/**
* Map over the success value.
*
* @template S
*
* @param callable(T):S $f
*
* @return \GrahamCampbell\ResultType\Result<S,E>
*/
public function map(callable $f)
{
return self::create($this->value);
}
/**
* Flat map over the success value.
*
* @template S
* @template F
*
* @param callable(T):\GrahamCampbell\ResultType\Result<S,F> $f
*
* @return \GrahamCampbell\ResultType\Result<S,F>
*/
public function flatMap(callable $f)
{
/** @var \GrahamCampbell\ResultType\Result<S,F> */
return self::create($this->value);
}
/**
* Get the error option value.
*
* @return \PhpOption\Option<E>
*/
public function error()
{
return Some::create($this->value);
}
/**
* Map over the error value.
*
* @template F
*
* @param callable(E):F $f
*
* @return \GrahamCampbell\ResultType\Result<T,F>
*/
public function mapError(callable $f)
{
return self::create($f($this->value));
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* This file is part of Result Type.
*
* (c) Graham Campbell <hello@gjcampbell.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace GrahamCampbell\ResultType;
/**
* @template T
* @template E
*/
abstract class Result
{
/**
* Get the success option value.
*
* @return \PhpOption\Option<T>
*/
abstract public function success();
/**
* Map over the success value.
*
* @template S
*
* @param callable(T):S $f
*
* @return \GrahamCampbell\ResultType\Result<S,E>
*/
abstract public function map(callable $f);
/**
* Flat map over the success value.
*
* @template S
* @template F
*
* @param callable(T):\GrahamCampbell\ResultType\Result<S,F> $f
*
* @return \GrahamCampbell\ResultType\Result<S,F>
*/
abstract public function flatMap(callable $f);
/**
* Get the error option value.
*
* @return \PhpOption\Option<E>
*/
abstract public function error();
/**
* Map over the error value.
*
* @template F
*
* @param callable(E):F $f
*
* @return \GrahamCampbell\ResultType\Result<T,F>
*/
abstract public function mapError(callable $f);
}
@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
/*
* This file is part of Result Type.
*
* (c) Graham Campbell <hello@gjcampbell.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace GrahamCampbell\ResultType;
use PhpOption\None;
use PhpOption\Some;
/**
* @template T
* @template E
*
* @extends \GrahamCampbell\ResultType\Result<T,E>
*/
final class Success extends Result
{
/**
* @var T
*/
private $value;
/**
* Internal constructor for a success value.
*
* @param T $value
*
* @return void
*/
private function __construct($value)
{
$this->value = $value;
}
/**
* Create a new error value.
*
* @template S
*
* @param S $value
*
* @return \GrahamCampbell\ResultType\Result<S,E>
*/
public static function create($value)
{
return new self($value);
}
/**
* Get the success option value.
*
* @return \PhpOption\Option<T>
*/
public function success()
{
return Some::create($this->value);
}
/**
* Map over the success value.
*
* @template S
*
* @param callable(T):S $f
*
* @return \GrahamCampbell\ResultType\Result<S,E>
*/
public function map(callable $f)
{
return self::create($f($this->value));
}
/**
* Flat map over the success value.
*
* @template S
* @template F
*
* @param callable(T):\GrahamCampbell\ResultType\Result<S,F> $f
*
* @return \GrahamCampbell\ResultType\Result<S,F>
*/
public function flatMap(callable $f)
{
return $f($this->value);
}
/**
* Get the error option value.
*
* @return \PhpOption\Option<E>
*/
public function error()
{
return None::create();
}
/**
* Map over the error value.
*
* @template F
*
* @param callable(E):F $f
*
* @return \GrahamCampbell\ResultType\Result<T,F>
*/
public function mapError(callable $f)
{
return self::create($this->value);
}
}
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,50 @@
{
"name": "phpoption/phpoption",
"description": "Option Type for PHP",
"keywords": ["php", "option", "language", "type"],
"license": "Apache-2.0",
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"autoload-dev": {
"psr-4": {
"PhpOption\\Tests\\": "tests/PhpOption/Tests/"
}
},
"config": {
"allow-plugins": {
"bamarni/composer-bin-plugin": true
},
"preferred-install": "dist"
},
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "1.9-dev"
}
}
}
@@ -0,0 +1,175 @@
<?php
/*
* Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace PhpOption;
use Traversable;
/**
* @template T
*
* @extends Option<T>
*/
final class LazyOption extends Option
{
/** @var callable(mixed...):(Option<T>) */
private $callback;
/** @var array<int, mixed> */
private $arguments;
/** @var Option<T>|null */
private $option;
/**
* @template S
* @param callable(mixed...):(Option<S>) $callback
* @param array<int, mixed> $arguments
*
* @return LazyOption<S>
*/
public static function create($callback, array $arguments = []): self
{
return new self($callback, $arguments);
}
/**
* @param callable(mixed...):(Option<T>) $callback
* @param array<int, mixed> $arguments
*/
public function __construct($callback, array $arguments = [])
{
if (!is_callable($callback)) {
throw new \InvalidArgumentException('Invalid callback given');
}
$this->callback = $callback;
$this->arguments = $arguments;
}
public function isDefined(): bool
{
return $this->option()->isDefined();
}
public function isEmpty(): bool
{
return $this->option()->isEmpty();
}
public function get()
{
return $this->option()->get();
}
public function getOrElse($default)
{
return $this->option()->getOrElse($default);
}
public function getOrCall($callable)
{
return $this->option()->getOrCall($callable);
}
public function getOrThrow(\Exception $ex)
{
return $this->option()->getOrThrow($ex);
}
public function orElse(Option $else)
{
return $this->option()->orElse($else);
}
public function ifDefined($callable)
{
$this->option()->forAll($callable);
}
public function forAll($callable)
{
return $this->option()->forAll($callable);
}
public function map($callable)
{
return $this->option()->map($callable);
}
public function flatMap($callable)
{
return $this->option()->flatMap($callable);
}
public function filter($callable)
{
return $this->option()->filter($callable);
}
public function filterNot($callable)
{
return $this->option()->filterNot($callable);
}
public function select($value)
{
return $this->option()->select($value);
}
public function reject($value)
{
return $this->option()->reject($value);
}
/**
* @return Traversable<T>
*/
public function getIterator(): Traversable
{
return $this->option()->getIterator();
}
public function foldLeft($initialValue, $callable)
{
return $this->option()->foldLeft($initialValue, $callable);
}
public function foldRight($initialValue, $callable)
{
return $this->option()->foldRight($initialValue, $callable);
}
/**
* @return Option<T>
*/
private function option(): Option
{
if (null === $this->option) {
/** @var mixed */
$option = call_user_func_array($this->callback, $this->arguments);
if ($option instanceof Option) {
$this->option = $option;
} else {
throw new \RuntimeException(sprintf('Expected instance of %s', Option::class));
}
}
return $this->option;
}
}
@@ -0,0 +1,136 @@
<?php
/*
* Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace PhpOption;
use EmptyIterator;
/**
* @extends Option<mixed>
*/
final class None extends Option
{
/** @var None|null */
private static $instance;
/**
* @return None
*/
public static function create(): self
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function get()
{
throw new \RuntimeException('None has no value.');
}
public function getOrCall($callable)
{
return $callable();
}
public function getOrElse($default)
{
return $default;
}
public function getOrThrow(\Exception $ex)
{
throw $ex;
}
public function isEmpty(): bool
{
return true;
}
public function isDefined(): bool
{
return false;
}
public function orElse(Option $else)
{
return $else;
}
public function ifDefined($callable)
{
// Just do nothing in that case.
}
public function forAll($callable)
{
return $this;
}
public function map($callable)
{
return $this;
}
public function flatMap($callable)
{
return $this;
}
public function filter($callable)
{
return $this;
}
public function filterNot($callable)
{
return $this;
}
public function select($value)
{
return $this;
}
public function reject($value)
{
return $this;
}
public function getIterator(): EmptyIterator
{
return new EmptyIterator();
}
public function foldLeft($initialValue, $callable)
{
return $initialValue;
}
public function foldRight($initialValue, $callable)
{
return $initialValue;
}
private function __construct()
{
}
}
@@ -0,0 +1,434 @@
<?php
/*
* Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace PhpOption;
use ArrayAccess;
use IteratorAggregate;
/**
* @template T
*
* @implements IteratorAggregate<T>
*/
abstract class Option implements IteratorAggregate
{
/**
* Creates an option given a return value.
*
* This is intended for consuming existing APIs and allows you to easily
* convert them to an option. By default, we treat ``null`` as the None
* case, and everything else as Some.
*
* @template S
*
* @param S $value The actual return value.
* @param S $noneValue The value which should be considered "None"; null by
* default.
*
* @return Option<S>
*/
public static function fromValue($value, $noneValue = null)
{
if ($value === $noneValue) {
return None::create();
}
return new Some($value);
}
/**
* Creates an option from an array's value.
*
* If the key does not exist in the array, the array is not actually an
* array, or the array's value at the given key is null, None is returned.
* Otherwise, Some is returned wrapping the value at the given key.
*
* @template S
*
* @param array<string|int,S>|ArrayAccess<string|int,S>|null $array A potential array or \ArrayAccess value.
* @param string|int|null $key The key to check.
*
* @return Option<S>
*/
public static function fromArraysValue($array, $key)
{
if ($key === null || !(is_array($array) || $array instanceof ArrayAccess) || !isset($array[$key])) {
return None::create();
}
return new Some($array[$key]);
}
/**
* Creates a lazy-option with the given callback.
*
* This is also a helper constructor for lazy-consuming existing APIs where
* the return value is not yet an option. By default, we treat ``null`` as
* None case, and everything else as Some.
*
* @template S
*
* @param callable $callback The callback to evaluate.
* @param array $arguments The arguments for the callback.
* @param S $noneValue The value which should be considered "None";
* null by default.
*
* @return LazyOption<S>
*/
public static function fromReturn($callback, array $arguments = [], $noneValue = null)
{
return new LazyOption(static function () use ($callback, $arguments, $noneValue) {
/** @var mixed */
$return = call_user_func_array($callback, $arguments);
if ($return === $noneValue) {
return None::create();
}
return new Some($return);
});
}
/**
* Option factory, which creates new option based on passed value.
*
* If value is already an option, it simply returns. If value is callable,
* LazyOption with passed callback created and returned. If Option
* returned from callback, it returns directly. On other case value passed
* to Option::fromValue() method.
*
* @template S
*
* @param Option<S>|callable|S $value
* @param S $noneValue Used when $value is mixed or
* callable, for None-check.
*
* @return Option<S>|LazyOption<S>
*/
public static function ensure($value, $noneValue = null)
{
if ($value instanceof self) {
return $value;
} elseif (is_callable($value)) {
return new LazyOption(static function () use ($value, $noneValue) {
/** @var mixed */
$return = $value();
if ($return instanceof self) {
return $return;
} else {
return self::fromValue($return, $noneValue);
}
});
} else {
return self::fromValue($value, $noneValue);
}
}
/**
* Lift a function so that it accepts Option as parameters.
*
* We return a new closure that wraps the original callback. If any of the
* parameters passed to the lifted function is empty, the function will
* return a value of None. Otherwise, we will pass all parameters to the
* original callback and return the value inside a new Option, unless an
* Option is returned from the function, in which case, we use that.
*
* @template S
*
* @param callable $callback
* @param mixed $noneValue
*
* @return callable
*/
public static function lift($callback, $noneValue = null)
{
return static function () use ($callback, $noneValue) {
/** @var array<int, mixed> */
$args = func_get_args();
$reduced_args = array_reduce(
$args,
/** @param bool $status */
static function ($status, self $o) {
return $o->isEmpty() ? true : $status;
},
false
);
// if at least one parameter is empty, return None
if ($reduced_args) {
return None::create();
}
$args = array_map(
/** @return T */
static function (self $o) {
// it is safe to do so because the fold above checked
// that all arguments are of type Some
/** @var T */
return $o->get();
},
$args
);
return self::ensure(call_user_func_array($callback, $args), $noneValue);
};
}
/**
* Returns the value if available, or throws an exception otherwise.
*
* @throws \RuntimeException If value is not available.
*
* @return T
*/
abstract public function get();
/**
* Returns the value if available, or the default value if not.
*
* @template S
*
* @param S $default
*
* @return T|S
*/
abstract public function getOrElse($default);
/**
* Returns the value if available, or the results of the callable.
*
* This is preferable over ``getOrElse`` if the computation of the default
* value is expensive.
*
* @template S
*
* @param callable():S $callable
*
* @return T|S
*/
abstract public function getOrCall($callable);
/**
* Returns the value if available, or throws the passed exception.
*
* @param \Exception $ex
*
* @return T
*/
abstract public function getOrThrow(\Exception $ex);
/**
* Returns true if no value is available, false otherwise.
*
* @return bool
*/
abstract public function isEmpty();
/**
* Returns true if a value is available, false otherwise.
*
* @return bool
*/
abstract public function isDefined();
/**
* Returns this option if non-empty, or the passed option otherwise.
*
* This can be used to try multiple alternatives, and is especially useful
* with lazy evaluating options:
*
* ```php
* $repo->findSomething()
* ->orElse(new LazyOption(array($repo, 'findSomethingElse')))
* ->orElse(new LazyOption(array($repo, 'createSomething')));
* ```
*
* @param Option<T> $else
*
* @return Option<T>
*/
abstract public function orElse(self $else);
/**
* This is similar to map() below except that the return value has no meaning;
* the passed callable is simply executed if the option is non-empty, and
* ignored if the option is empty.
*
* In all cases, the return value of the callable is discarded.
*
* ```php
* $comment->getMaybeFile()->ifDefined(function($file) {
* // Do something with $file here.
* });
* ```
*
* If you're looking for something like ``ifEmpty``, you can use ``getOrCall``
* and ``getOrElse`` in these cases.
*
* @deprecated Use forAll() instead.
*
* @param callable(T):mixed $callable
*
* @return void
*/
abstract public function ifDefined($callable);
/**
* This is similar to map() except that the return value of the callable has no meaning.
*
* The passed callable is simply executed if the option is non-empty, and ignored if the
* option is empty. This method is preferred for callables with side-effects, while map()
* is intended for callables without side-effects.
*
* @param callable(T):mixed $callable
*
* @return Option<T>
*/
abstract public function forAll($callable);
/**
* Applies the callable to the value of the option if it is non-empty,
* and returns the return value of the callable wrapped in Some().
*
* If the option is empty, then the callable is not applied.
*
* ```php
* (new Some("foo"))->map('strtoupper')->get(); // "FOO"
* ```
*
* @template S
*
* @param callable(T):S $callable
*
* @return Option<S>
*/
abstract public function map($callable);
/**
* Applies the callable to the value of the option if it is non-empty, and
* returns the return value of the callable directly.
*
* In contrast to ``map``, the return value of the callable is expected to
* be an Option itself; it is not automatically wrapped in Some().
*
* @template S
*
* @param callable(T):Option<S> $callable must return an Option
*
* @return Option<S>
*/
abstract public function flatMap($callable);
/**
* If the option is empty, it is returned immediately without applying the callable.
*
* If the option is non-empty, the callable is applied, and if it returns true,
* the option itself is returned; otherwise, None is returned.
*
* @param callable(T):bool $callable
*
* @return Option<T>
*/
abstract public function filter($callable);
/**
* If the option is empty, it is returned immediately without applying the callable.
*
* If the option is non-empty, the callable is applied, and if it returns false,
* the option itself is returned; otherwise, None is returned.
*
* @param callable(T):bool $callable
*
* @return Option<T>
*/
abstract public function filterNot($callable);
/**
* If the option is empty, it is returned immediately.
*
* If the option is non-empty, and its value does not equal the passed value
* (via a shallow comparison ===), then None is returned. Otherwise, the
* Option is returned.
*
* In other words, this will filter all but the passed value.
*
* @param T $value
*
* @return Option<T>
*/
abstract public function select($value);
/**
* If the option is empty, it is returned immediately.
*
* If the option is non-empty, and its value does equal the passed value (via
* a shallow comparison ===), then None is returned; otherwise, the Option is
* returned.
*
* In other words, this will let all values through except the passed value.
*
* @param T $value
*
* @return Option<T>
*/
abstract public function reject($value);
/**
* Binary operator for the initial value and the option's value.
*
* If empty, the initial value is returned. If non-empty, the callable
* receives the initial value and the option's value as arguments.
*
* ```php
*
* $some = new Some(5);
* $none = None::create();
* $result = $some->foldLeft(1, function($a, $b) { return $a + $b; }); // int(6)
* $result = $none->foldLeft(1, function($a, $b) { return $a + $b; }); // int(1)
*
* // This can be used instead of something like the following:
* $option = Option::fromValue($integerOrNull);
* $result = 1;
* if ( ! $option->isEmpty()) {
* $result += $option->get();
* }
* ```
*
* @template S
*
* @param S $initialValue
* @param callable(S, T):S $callable
*
* @return S
*/
abstract public function foldLeft($initialValue, $callable);
/**
* foldLeft() but with reversed arguments for the callable.
*
* @template S
*
* @param S $initialValue
* @param callable(T, S):S $callable
*
* @return S
*/
abstract public function foldRight($initialValue, $callable);
}
@@ -0,0 +1,169 @@
<?php
/*
* Copyright 2012 Johannes M. Schmitt <schmittjoh@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace PhpOption;
use ArrayIterator;
/**
* @template T
*
* @extends Option<T>
*/
final class Some extends Option
{
/** @var T */
private $value;
/**
* @param T $value
*/
public function __construct($value)
{
$this->value = $value;
}
/**
* @template U
*
* @param U $value
*
* @return Some<U>
*/
public static function create($value): self
{
return new self($value);
}
public function isDefined(): bool
{
return true;
}
public function isEmpty(): bool
{
return false;
}
public function get()
{
return $this->value;
}
public function getOrElse($default)
{
return $this->value;
}
public function getOrCall($callable)
{
return $this->value;
}
public function getOrThrow(\Exception $ex)
{
return $this->value;
}
public function orElse(Option $else)
{
return $this;
}
public function ifDefined($callable)
{
$this->forAll($callable);
}
public function forAll($callable)
{
$callable($this->value);
return $this;
}
public function map($callable)
{
return new self($callable($this->value));
}
public function flatMap($callable)
{
/** @var mixed */
$rs = $callable($this->value);
if (!$rs instanceof Option) {
throw new \RuntimeException('Callables passed to flatMap() must return an Option. Maybe you should use map() instead?');
}
return $rs;
}
public function filter($callable)
{
if (true === $callable($this->value)) {
return $this;
}
return None::create();
}
public function filterNot($callable)
{
if (false === $callable($this->value)) {
return $this;
}
return None::create();
}
public function select($value)
{
if ($this->value === $value) {
return $this;
}
return None::create();
}
public function reject($value)
{
if ($this->value === $value) {
return None::create();
}
return $this;
}
/**
* @return ArrayIterator<int, T>
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator([$this->value]);
}
public function foldLeft($initialValue, $callable)
{
return $callable($initialValue, $this->value);
}
public function foldRight($initialValue, $callable)
{
return $callable($this->value, $initialValue);
}
}
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2009-2020 Daniele Alessandri (original work)
Copyright (c) 2021-2024 Till Krüss (modified work)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,763 @@
# Predis #
[![Software license][ico-license]](LICENSE)
[![Latest stable][ico-version-stable]][link-releases]
[![Latest development][ico-version-dev]][link-releases]
[![Monthly installs][ico-downloads-monthly]][link-downloads]
[![Build status][ico-build]][link-actions]
[![Coverage Status][ico-coverage]][link-coverage]
A flexible and feature-complete [Redis](http://redis.io) / [Valkey](https://github.com/valkey-io/valkey) client for PHP 7.2 and newer.
More details about this project can be found on the [frequently asked questions](FAQ.md).
## Main features ##
- Support for Redis from __3.0__ to __8.0__.
- Support for clustering using client-side sharding and pluggable keyspace distributors.
- Support for [redis-cluster](http://redis.io/topics/cluster-tutorial) (Redis >= 3.0).
- Support for master-slave replication setups and [redis-sentinel](http://redis.io/topics/sentinel).
- Transparent key prefixing of keys using a customizable prefix strategy.
- Command pipelining on both single nodes and clusters (client-side sharding only).
- Abstraction for Redis transactions (Redis >= 2.0) and CAS operations (Redis >= 2.2).
- Abstraction for Lua scripting (Redis >= 2.6) and automatic switching between `EVALSHA` or `EVAL`.
- Abstraction for `SCAN`, `SSCAN`, `ZSCAN` and `HSCAN` (Redis >= 2.8) based on PHP iterators.
- Connections are established lazily by the client upon the first command and can be persisted.
- Connections can be established via TCP/IP (also TLS/SSL-encrypted) or UNIX domain sockets.
- Support for custom connection classes for providing different network or protocol backends.
- Flexible system for defining custom commands and override the default ones.
## How to _install_ and use Predis ##
This library can be found on [Packagist](http://packagist.org/packages/predis/predis) for an easier
management of projects dependencies using [Composer](http://packagist.org/about-composer).
Compressed archives of each release are [available on GitHub](https://github.com/predis/predis/releases).
```shell
composer require predis/predis
```
### Loading the library ###
Predis relies on the autoloading features of PHP to load its files when needed and complies with the
[PSR-4 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md).
Autoloading is handled automatically when dependencies are managed through Composer, but it is also
possible to leverage its own autoloader in projects or scripts lacking any autoload facility:
```php
// Prepend a base path if Predis is not available in your "include_path".
require 'Predis/Autoloader.php';
Predis\Autoloader::register();
```
### Connecting to Redis ###
When creating a client instance without passing any connection parameter, Predis assumes `127.0.0.1`
and `6379` as default host and port. The default timeout for the `connect()` operation is 5 seconds:
```php
$client = new Predis\Client();
$client->set('foo', 'bar');
$value = $client->get('foo');
```
Connection parameters can be supplied either in the form of URI strings or named arrays. The latter
is the preferred way to supply parameters, but URI strings can be useful when parameters are read
from non-structured or partially-structured sources:
```php
// Parameters passed using a named array:
$client = new Predis\Client([
'scheme' => 'tcp',
'host' => '10.0.0.1',
'port' => 6379,
]);
// Same set of parameters, passed using an URI string:
$client = new Predis\Client('tcp://10.0.0.1:6379');
```
Password protected servers can be accessed by adding `password` to the parameters set. When ACLs are
enabled on Redis >= 6.0, both `username` and `password` are required for user authentication.
It is also possible to connect to local instances of Redis using UNIX domain sockets, in this case
the parameters must use the `unix` scheme and specify a path for the socket file:
```php
$client = new Predis\Client(['scheme' => 'unix', 'path' => '/path/to/redis.sock']);
$client = new Predis\Client('unix:/path/to/redis.sock');
```
The client can leverage TLS/SSL encryption to connect to secured remote Redis instances without the
need to configure an SSL proxy like stunnel. This can be useful when connecting to nodes running on
various cloud hosting providers. Encryption can be enabled with using the `tls` scheme and an array
of suitable [options](http://php.net/manual/context.ssl.php) passed via the `ssl` parameter:
```php
// Named array of connection parameters:
$client = new Predis\Client([
'scheme' => 'tls',
'ssl' => ['cafile' => 'private.pem', 'verify_peer' => true],
]);
// Same set of parameters, but using an URI string:
$client = new Predis\Client('tls://127.0.0.1?ssl[cafile]=private.pem&ssl[verify_peer]=1');
```
The connection schemes [`redis`](http://www.iana.org/assignments/uri-schemes/prov/redis) (alias of
`tcp`) and [`rediss`](http://www.iana.org/assignments/uri-schemes/prov/rediss) (alias of `tls`) are
also supported, with the difference that URI strings containing these schemes are parsed following
the rules described on their respective IANA provisional registration documents.
Since Redis 8.6, you can authenticate a client using the Subject CN from its TLS client certificate (mTLS).
When this is enabled on the server, the client is authenticated during the TLS handshake, so you dont need
to send an AUTH command.
To use this, configure:
- a CA certificate used to verify the server certificate (cafile),
- a client certificate (local_cert) signed by a CA trusted by the Redis server for client authentication,
- the corresponding private key (local_pk).
Make sure:
- the Redis server certificate is signed by a CA trusted by the client, and
- the client certificate is signed by a CA trusted by the Redis server (mTLS).
```php
// Named array of connection parameters:
$client = new Predis\Client([
'scheme' => 'tls',
'ssl' => [
'cafile' => 'ca.pem', // CA used to verify the server certificate
'local_cert' => 'client.crt', // client certificate (Subject CN maps to ACL user)
'local_pk' => 'client.key', // client private key
'verify_peer' => true,
],
]);
// ACL user must exist and match the certificate Subject CN (example: CN=CN_NAME).
// Enable the user and grant permissions as needed:
$client->acl->setUser('CN_NAME', 'on', '>clientpass', 'allcommands', 'allkeys')
echo $client->acl->whoami() // CN_NAME
```
The actual list of supported connection parameters can vary depending on each connection backend so
it is recommended to refer to their specific documentation or implementation for details.
Predis can aggregate multiple connections when providing an array of connection parameters and the
appropriate option to instruct the client about how to aggregate them (clustering, replication or a
custom aggregation logic). Named arrays and URI strings can be mixed when providing configurations
for each node:
```php
$client = new Predis\Client([
'tcp://10.0.0.1?alias=first-node', ['host' => '10.0.0.2', 'alias' => 'second-node'],
], [
'cluster' => 'predis',
]);
```
See the [aggregate connections](#aggregate-connections) section of this document for more details.
Connections to Redis are lazy meaning that the client connects to a server only if and when needed.
While it is recommended to let the client do its own stuff under the hood, there may be times when
it is still desired to have control of when the connection is opened or closed: this can easily be
achieved by invoking `$client->connect()` and `$client->disconnect()`. Please note that the effect
of these methods on aggregate connections may differ depending on each specific implementation.
#### Persistent connections ####
To increase a performance of your application you may set up a client to use persistent TCP connection, this way
client saves a time on socket creation and connection handshake. By default, connection is created on first-command
execution and will be automatically closed by GC before the process is being killed.
However, if your application is backed by PHP-FPM the processes are idle, and you may set up it to be persistent and
reusable across multiple script execution within the same process.
To enable the persistent connection mode you should provide following configuration:
```php
// Standalone
$client = new Predis\Client(['persistent' => true]);
// Cluster
$client = new Predis\Client(
['tcp://host:port', 'tcp://host:port', 'tcp://host:port'],
['cluster' => 'redis', 'parameters' => ['persistent' => true]]
);
```
**Important**
If you operate on multiple clients within the same application, and they communicate with the same resource, by default
they will share the same socket (that's the default behaviour of persistent sockets). So in this case you would need
to additionally provide a `conn_uid` identifier for each client, this way each client will create its own socket so
the connection context won't be shared across clients. This socket behaviour explained
[here](https://www.php.net/manual/en/function.stream-socket-client.php#105393)
```php
// Standalone
$client1 = new Predis\Client(['persistent' => true, 'conn_uid' => 'id_1']);
$client2 = new Predis\Client(['persistent' => true, 'conn_uid' => 'id_2']);
// Cluster
$client1 = new Predis\Client(
['tcp://host:port', 'tcp://host:port', 'tcp://host:port'],
['cluster' => 'redis', 'parameters' => ['persistent' => true, 'conn_uid' => 'id_1']]
);
$client2 = new Predis\Client(
['tcp://host:port', 'tcp://host:port', 'tcp://host:port'],
['cluster' => 'redis', 'parameters' => ['persistent' => true, 'conn_uid' => 'id_2']]
);
```
### Client configuration ###
Many aspects and behaviors of the client can be configured by passing specific client options to the
second argument of `Predis\Client::__construct()`:
```php
$client = new Predis\Client($parameters, ['prefix' => 'sample:']);
```
Options are managed using a mini DI-alike container and their values can be lazily initialized only
when needed. The client options supported by default in Predis are:
- `prefix`: prefix string applied to every key found in commands.
- `exceptions`: whether the client should throw or return responses upon Redis errors.
- `connections`: list of connection backends or a connection factory instance.
- `cluster`: specifies a cluster backend (`predis`, `redis` or callable).
- `replication`: specifies a replication backend (`predis`, `sentinel` or callable).
- `aggregate`: configures the client with a custom aggregate connection (callable).
- `parameters`: list of default connection parameters for aggregate connections.
- `commands`: specifies a command factory instance to use through the library.
- `readTimeout`: (cluster only) Timeout between read operations while loop over connections.
Users can also provide custom options with values or callable objects (for lazy initialization) that
are stored in the options container for later use through the library.
### Aggregate connections ###
Aggregate connections are the foundation upon which Predis implements clustering and replication and
they are used to group multiple connections to single Redis nodes and hide the specific logic needed
to handle them properly depending on the context. Aggregate connections usually require an array of
connection parameters along with the appropriate client option when creating a new client instance.
#### Cluster ####
Predis can be configured to work in clustering mode with a traditional client-side sharding approach
to create a cluster of independent nodes and distribute the keyspace among them. This approach needs
some sort of external health monitoring of nodes and requires the keyspace to be rebalanced manually
when nodes are added or removed:
```php
$parameters = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
$options = ['cluster' => 'predis'];
$client = new Predis\Client($parameters);
```
Along with Redis 3.0, a new supervised and coordinated type of clustering was introduced in the form
of [redis-cluster](http://redis.io/topics/cluster-tutorial). This kind of approach uses a different
algorithm to distribute the keyspaces, with Redis nodes coordinating themselves by communicating via
a gossip protocol to handle health status, rebalancing, nodes discovery and request redirection. In
order to connect to a cluster managed by redis-cluster, the client requires a list of its nodes (not
necessarily complete since it will automatically discover new nodes if necessary) and the `cluster`
client options set to `redis`:
```php
$parameters = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
$options = ['cluster' => 'redis'];
$client = new Predis\Client($parameters, $options);
```
#### Redis Gears with cluster ####
Since Redis v7.2, Redis Gears module is a part of Redis Stack bundle. Client supports a variety of
Redis Gears commands that can be used with OSS cluster API. Currently, before using any Redis
Gears commands against OSS cluster Redis server needs to be aware of cluster topology.
`REDISGEARS_2.REFRESHCLUSTER` command should be called against **each master node** (read replicas
should be ignored) **on cluster creation and each time cluster topology changes**.
In most cases this actions should be performed from the CLI interface by the administrator, DevOPS
or even Kubernetes, depends on your infrastructure managing process. However, client provides an API
to do this programmatically.
```php
/** @var \Predis\Connection\Cluster\ClusterInterface $connection */
$connection->executeCommandOnEachNode(
new \Predis\Command\RawCommand('REDISGEARS_2.REFRESHCLUSTER')
);
```
#### Replication ####
The client can be configured to operate in a single master / multiple slaves setup to provide better
service availability. When using replication, Predis recognizes read-only commands and sends them to
a random slave in order to provide some sort of load-balancing and switches to the master as soon as
it detects a command that performs any kind of operation that would end up modifying the keyspace or
the value of a key. Instead of raising a connection error when a slave fails, the client attempts to
fall back to a different slave among the ones provided in the configuration.
The basic configuration needed to use the client in replication mode requires one Redis server to be
identified as the master (this can be done via connection parameters by setting the `role` parameter
to `master`) and one or more slaves (in this case setting `role` to `slave` for slaves is optional):
```php
$parameters = ['tcp://10.0.0.1?role=master', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
$options = ['replication' => 'predis'];
$client = new Predis\Client($parameters, $options);
```
The above configuration has a static list of servers and relies entirely on the client's logic, but
it is possible to rely on [`redis-sentinel`](http://redis.io/topics/sentinel) for a more robust HA
environment with sentinel servers acting as a source of authority for clients for service discovery.
The minimum configuration required by the client to work with redis-sentinel is a list of connection
parameters pointing to a bunch of sentinel instances, the `replication` option set to `sentinel` and
the `service` option set to the name of the service:
```php
$sentinels = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
$options = ['replication' => 'sentinel', 'service' => 'mymaster'];
$client = new Predis\Client($sentinels, $options);
```
If the master and slave nodes are configured to require an authentication from clients, a password
must be provided via the global `parameters` client option. This option can also be used to specify
a different database index. The client options array would then look like this:
```php
$options = [
'replication' => 'sentinel',
'service' => 'mymaster',
'parameters' => [
'password' => $secretpassword,
'database' => 10,
],
];
```
While Predis is able to distinguish commands performing write and read-only operations, `EVAL` and
`EVALSHA` represent a corner case in which the client switches to the master node because it cannot
tell when a Lua script is safe to be executed on slaves. While this is indeed the default behavior,
when certain Lua scripts do not perform write operations it is possible to provide an hint to tell
the client to stick with slaves for their execution:
```php
$parameters = ['tcp://10.0.0.1?role=master', 'tcp://10.0.0.2', 'tcp://10.0.0.3'];
$options = ['replication' => function () {
// Set scripts that won't trigger a switch from a slave to the master node.
$strategy = new Predis\Replication\ReplicationStrategy();
$strategy->setScriptReadOnly($LUA_SCRIPT);
return new Predis\Connection\Replication\MasterSlaveReplication($strategy);
}];
$client = new Predis\Client($parameters, $options);
$client->eval($LUA_SCRIPT, 0); // Sticks to slave using `eval`...
$client->evalsha(sha1($LUA_SCRIPT), 0); // ... and `evalsha`, too.
```
The [`examples`](examples/) directory contains a few scripts that demonstrate how the client can be
configured and used to leverage replication in both basic and complex scenarios.
### Command pipelines ###
Pipelining can help with performances when many commands need to be sent to a server by reducing the
latency introduced by network round-trip timings. Pipelining also works with aggregate connections.
The client can execute the pipeline inside a callable block or return a pipeline instance with the
ability to chain commands thanks to its fluent interface:
```php
// Executes a pipeline inside the given callable block:
$responses = $client->pipeline(function ($pipe) {
for ($i = 0; $i < 1000; $i++) {
$pipe->set("key:$i", str_pad($i, 4, '0', 0));
$pipe->get("key:$i");
}
});
// Returns a pipeline that can be chained thanks to its fluent interface:
$responses = $client->pipeline()->set('foo', 'bar')->get('foo')->execute();
```
### Transactions ###
The client provides an abstraction for Redis transactions based on `MULTI` and `EXEC` with a similar
interface to command pipelines:
```php
// Executes a transaction inside the given callable block:
$responses = $client->transaction(function ($tx) {
$tx->set('foo', 'bar');
$tx->get('foo');
});
// Returns a transaction that can be chained thanks to its fluent interface:
$responses = $client->transaction()->set('foo', 'bar')->get('foo')->execute();
```
This abstraction can perform check-and-set operations thanks to `WATCH` and `UNWATCH` and provides
automatic retries of transactions aborted by Redis when `WATCH`ed keys are touched. For an example
of a transaction using CAS you can see [the following example](examples/transaction_using_cas.php).
#### Support for clustered connections ####
Since Predis v3.0 transactions could be used with clustered connections. However, it has some limitations due to the
fact that Redis doesn't support distributed transactions. All keys in the transaction context should operate on the same
hash slot, due to this limitation it's recommended to use `{}` syntax to make sure that all keys will be mapped to the same hash
slot. Apart from it no additional configuration needed on a client side.
```php
$redis = $this->getClient();
$response = $redis->transaction(function (MultiExec $tx) {
$tx->set('{foo}foo', 'value');
$tx->set('{foo}bar', 'value');
$tx->set('{foo}baz', 'value');
});
// ['OK', 'OK', 'OK']
```
### Adding new commands ###
While we try to update Predis to stay up to date with all the commands available in Redis, you might
prefer to stick with an old version of the library or provide a different way to filter arguments or
parse responses for specific commands. To achieve that, Predis provides the ability to implement new
command classes to define or override commands in the default command factory used by the client:
```php
// Define a new command by extending Predis\Command\Command:
class BrandNewRedisCommand extends Predis\Command\Command
{
public function getId()
{
return 'NEWCMD';
}
}
// Inject your command in the current command factory:
$client = new Predis\Client($parameters, [
'commands' => [
'newcmd' => 'BrandNewRedisCommand',
],
]);
$response = $client->newcmd();
```
There is also a method to send raw commands without filtering their arguments or parsing responses.
Users must provide the list of arguments for the command as an array, following the signatures as
defined by the [Redis documentation for commands](http://redis.io/commands):
```php
$response = $client->executeRaw(['SET', 'foo', 'bar']);
```
### Script commands ###
While it is possible to leverage [Lua scripting](http://redis.io/commands/eval) on Redis 2.6+ using
directly [`EVAL`](http://redis.io/commands/eval) and [`EVALSHA`](http://redis.io/commands/evalsha),
Predis offers script commands as an higher level abstraction built upon them to make things simple.
Script commands can be registered in the command factory used by the client and are accessible as if
they were plain Redis commands, but they define Lua scripts that get transmitted to the server for
remote execution. Internally they use [`EVALSHA`](http://redis.io/commands/evalsha) by default and
identify a script by its SHA1 hash to save bandwidth, but [`EVAL`](http://redis.io/commands/eval)
is used as a fall back when needed:
```php
// Define a new script command by extending Predis\Command\ScriptCommand:
class ListPushRandomValue extends Predis\Command\ScriptCommand
{
public function getKeysCount()
{
return 1;
}
public function getScript()
{
return <<<LUA
math.randomseed(ARGV[1])
local rnd = tostring(math.random())
redis.call('lpush', KEYS[1], rnd)
return rnd
LUA;
}
}
// Inject the script command in the current command factory:
$client = new Predis\Client($parameters, [
'commands' => [
'lpushrand' => 'ListPushRandomValue',
],
]);
$response = $client->lpushrand('random_values', $seed = mt_rand());
```
### Customizable connection backends ###
Predis can use different connection backends to connect to Redis. The builtin Relay integration
leverages the [Relay](https://github.com/cachewerk/relay) extension for PHP for major performance
gains, by caching a partial replica of the Redis dataset in PHP shared runtime memory.
```php
$client = new Predis\Client('tcp://127.0.0.1', [
'connections' => 'relay',
]);
```
Developers can create their own connection classes to support whole new network backends, extend
existing classes or provide completely different implementations. Connection classes must implement
`Predis\Connection\NodeConnectionInterface` or extend `Predis\Connection\AbstractConnection`:
```php
class MyConnectionClass implements Predis\Connection\NodeConnectionInterface
{
// Implementation goes here...
}
// Use MyConnectionClass to handle connections for the `tcp` scheme:
$client = new Predis\Client('tcp://127.0.0.1', [
'connections' => ['tcp' => 'MyConnectionClass'],
]);
```
For a more in-depth insight on how to create new connection backends you can refer to the actual
implementation of the standard connection classes available in the `Predis\Connection` namespace.
### Retry exceptions
You can enable automatic retry that is disabled by default, to be able to reduce the amount of
false-positives in case of network issues. By default, we're retrying on any connection,
timeout or socket initialization exception, but you can update the list of retry
exceptions. For now `EqualBackoff` and `ExponentialBackoff` strategies are available,
but you may provide your custom one. Retry may be configured with any type of communication
(standalone node, cluster, pipeline, transaction, replication). Here's an example of
configuration:
```php
// Standalone client
$client = new Predis\Client([
'retry' => new \Predis\Retry\Retry(
new \Predis\Retry\Strategy\ExponentialBackoff(1000, 10000), // Base and cap configuration in microseconds
3 // Number of retries
),
]);
// Cluster configuration
$options = [
'parameters' => [
'retry' => new \Predis\Retry\Retry(new \Predis\Retry\Strategy\ExponentialBackoff(1000, 10000), 3),
],
];
$client = new Predis\Client(['tcp://host:port', 'tcp://host:port', 'tcp://host:port'], $options);
$retry = new \Predis\Retry\Retry(
new \Predis\Retry\Strategy\ExponentialBackoff(1000, 10000),
3
);
// Update a list of exceptions to catch
$retry->updateCatchableExceptions([Exception::class]);
```
## RESP3 ##
### Connection ###
To establish the connection using the [RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md) protocol, you need to set parameter `protocol => 3`. The default protocol is RESP2.
You can pass parameter as configuration option in array or as a query parameter in `redis_url`
```php
// Configuration option
$client = new \Predis\Client(['protocol' => 3]);
// Redis URL
$client = new \Predis\Client('redis://localhost:6379?protocol=3');
// ["proto" => "3"]
$client->executeRaw(['HELLO']);
```
### Command responses ###
RESP3 protocol introduce a variety of new [response types](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#resp3-types),
so on the client-side we have more explicit understanding on data types we retrieve from server. Here's some examples to show the difference
between RESP2 and RESP3 responses.
#### Float responses ####
``` php
// RESP2 connection
$client = new \Predis\Client();
$client->geoadd('my_geo', 11.111, 22.222, 'member1');
// [[0 => string(20) "11.11099988222122192", 1 => string(20) "22.22200052541037252"]]
// RESP2 returns float values as simple strings.
var_dump($client->geopos('my_geo', ['member1']));
// RESP3 connection
$client = new \Predis\Client(['protocol' => 3]);
// [[0 => float(11.110999882221222), 1 => float(22.222000525410373)]]
// RESP3 introduces new double type, that corresponds to PHP float.
var_dump($client->geopos('my_geo', ['member1']));
```
#### Aggregate types ####
In RESP3 new aggregate type [Map](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#map-type)
was introduced, that represents the sequence of field-value pairs. So it simplifies parsing, since we don't need to specify
parsing strategy per command (RESP2) and instead relies on the type defined by protocol (RESP3).
In most cases RESP2 responses shouldn't differ from RESP3, since we added additional parsing for those
command that return field-value pairs. However, since RESP2 requires additional parsing, it could be that some commands
had lack of it and return unhandled responses. In this case there would be difference like this:
```php
$client = new \Predis\Client();
// RESP2: ['field', 'value]
$client->commandThatReturnsFieldValuePair('key');
$client = new \Predis\Client(['protocol' => 3]);
// RESP3: ['field' => 'value]
$client->commandThatReturnsFieldValuePair('key');
```
Feel free to open PR or GitHub issue if you face those protocol mismatching.
### Push notifications ###
RESP3 introduce a concept of [push connection](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#push-type),
is the one where server could send asynchronous data to client which was not explicitly requested. Predis 3.0 provides
an API to establish this kind of connection as separate blocking process (worker) and invoke callbacks depends on push
notification message type.
#### Consumer ####
First of all, you need to set up a consumer connection and provide an optional callback that will be executed before
event loop will be started. It allows you to subscribe on channels, enable keys invalidations tracking or enable monitor
connection, any Redis command to let server know that you want to receive push notification within this connection.
```php
// Make sure that RESP3 protocol enabled and read_write_timeout set 0,
// so connection won't be killed by timeout.
$client = new Predis\Client(['read_write_timeout' => 0, 'protocol' => 3]);
// Create push notifications consumer.
// Provides callback where current consumer subscribes to few channels before
// enter the loop.
$push = $client->push(static function (ClientInterface $client) {
$response = $client->subscribe('channel', 'control');
$status = ($response[2] === 1) ? 'OK' : 'FAILED';
echo "Channel subscription status: {$status}\n";
});
```
#### Dispatcher loop ####
Dispatcher object allows you to attach a callback to given push notification type and run the actual worker process that
listen for incoming push notifications. To be able to stop blocking process in runtime you can specify a condition and
call `$dispatcher->stop()` method from given callback. In this example we're waiting for specific message `terminate`
within `control` channel that we subscribed to before entering the loop.
```php
// Storage for incoming notifications.
$messages = [];
// Create dispatcher for push notifications.
$dispatcher = new Predis\Consumer\Push\DispatcherLoop($push);
$dispatcher->attachCallback(
PushResponseInterface::MESSAGE_DATA_TYPE,
static function (array $payload, DispatcherLoopInterface $dispatcher) {
global $messages;
[$channel, $message] = $payload;
if ($channel === 'control' && $message === 'terminate') {
echo "Terminating notification consumer.\n";
$dispatcher->stop();
return;
}
$messages[] = $message;
echo "Received message: {$message}\n";
}
);
// Run consumer loop with attached callbacks.
$dispatcher->run();
// Count all messages that were received during consumer loop.
$messagesCount = count($messages);
echo "We received: {$messagesCount} messages\n";
```
This example shows a simple script to count all incoming messages from push notifications that we receive from
subscribed channels until stop condition will be met. Examples available in `examples/` folder.
### Sharded pub/sub ###
From Redis 7.0, sharded Pub/Sub is introduced in which shard channels are assigned to slots by the same algorithm used
to assign keys to slots.
Predis 3.0 provides an API that allows to use pub/sub for Cluster connections using sharded pub/sub from Redis.
You don't need to specify any additional configuration to enable sharded pub/sub, it will be automatically enabled if
Cluster connection is using.
Implementation looks pretty much the same as Push notification, so you need to set up consumer
and run it over Dispatcher loop object. All examples available in `examples/` folder.
## Development ##
### Reporting bugs and contributing code ###
Contributions to Predis are highly appreciated either in the form of pull requests for new features,
bug fixes, or just bug reports. We only ask you to adhere to issue and pull request templates.
### Test suite ###
__ATTENTION__: Do not ever run the test suite shipped with Predis against instances of Redis running
in production environments or containing data you are interested in!
Predis has a comprehensive test suite covering every aspect of the library and that can optionally
perform integration tests against a running instance of Redis (required >= 2.4.0 in order to verify
the correct behavior of the implementation of each command. Integration tests for unsupported Redis
commands are automatically skipped. If you do not have Redis up and running, integration tests can
be disabled. See [the tests README](tests/README.md) for more details about testing this library.
Predis uses GitHub Actions for continuous integration and the history for past and current builds can be
found [on its actions page](https://github.com/predis/predis/actions).
### License ###
The code for Predis is distributed under the terms of the MIT license (see [LICENSE](LICENSE)).
[ico-license]: https://img.shields.io/github/license/predis/predis.svg?style=flat-square
[ico-version-stable]: https://img.shields.io/github/v/tag/predis/predis?label=stable&style=flat-square
[ico-version-dev]: https://img.shields.io/github/v/tag/predis/predis?include_prereleases&label=pre-release&style=flat-square
[ico-downloads-monthly]: https://img.shields.io/packagist/dm/predis/predis.svg?style=flat-square
[ico-build]: https://img.shields.io/github/actions/workflow/status/predis/predis/tests.yml?branch=main&style=flat-square
[ico-coverage]: https://img.shields.io/coverallsCoverage/github/predis/predis?style=flat-square
[link-releases]: https://github.com/predis/predis/releases
[link-actions]: https://github.com/predis/predis/actions
[link-downloads]: https://packagist.org/packages/predis/predis/stats
[link-coverage]: https://coveralls.io/github/predis/predis
@@ -0,0 +1,12 @@
<?php
/*
* This file is part of the Predis package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
require __DIR__.'/src/Autoloader.php';
Predis\Autoloader::register();
@@ -0,0 +1,59 @@
{
"name": "predis/predis",
"type": "library",
"description": "A flexible and feature-complete Redis/Valkey client for PHP.",
"keywords": ["nosql", "redis", "predis"],
"homepage": "http://github.com/predis/predis",
"license": "MIT",
"support": {
"issues": "https://github.com/predis/predis/issues"
},
"authors": [
{
"name": "Till Krüss",
"homepage": "https://till.im",
"role": "Maintainer"
}
],
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/tillkruss"
}
],
"require": {
"php": "^7.2 || ^8.0",
"psr/http-message": "^1.0|^2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.3",
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^8.0 || ~9.4.4",
"phpunit/phpcov": "^6.0 || ^8.0"
},
"suggest": {
"ext-relay": "Faster connection with in-memory caching (>=0.6.2)"
},
"scripts": {
"phpstan": "phpstan analyse",
"style": "php-cs-fixer fix --diff --dry-run",
"style:fix": "php-cs-fixer fix"
},
"autoload": {
"psr-4": {
"Predis\\": "src/"
}
},
"config": {
"sort-packages": true,
"preferred-install": "dist",
"audit": {
"ignore": [
"GHSA-vvj3-c3rp-c85p",
"PKSA-z3gr-8qht-p93v"
]
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis;
/**
* Implements a lightweight PSR-0 compliant autoloader for Predis.
*
* @author Eric Naeseth <eric@thumbtack.com>
* @author Daniele Alessandri <suppakilla@gmail.com>
* @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;
}
}
}
}
@@ -0,0 +1,642 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis;
use ArrayIterator;
use InvalidArgumentException;
use IteratorAggregate;
use Predis\Command\CommandInterface;
use Predis\Command\Container\ContainerFactory;
use Predis\Command\Container\ContainerInterface;
use Predis\Command\RawCommand;
use Predis\Command\ScriptCommand;
use Predis\Configuration\Options;
use Predis\Configuration\OptionsInterface;
use Predis\Connection\AggregateConnectionInterface;
use Predis\Connection\ConnectionInterface;
use Predis\Connection\Parameters;
use Predis\Connection\ParametersInterface;
use Predis\Connection\RelayConnection;
use Predis\Consumer\PubSub\Consumer as PubSubConsumer;
use Predis\Consumer\PubSub\RelayConsumer as RelayPubSubConsumer;
use Predis\Consumer\Push\Consumer as PushConsumer;
use Predis\Monitor\Consumer as MonitorConsumer;
use Predis\Pipeline\Atomic;
use Predis\Pipeline\FireAndForget;
use Predis\Pipeline\Pipeline;
use Predis\Pipeline\RelayAtomic;
use Predis\Pipeline\RelayPipeline;
use Predis\Response\ErrorInterface as ErrorResponseInterface;
use Predis\Response\ResponseInterface;
use Predis\Response\ServerException;
use Predis\Transaction\MultiExec as MultiExecTransaction;
use ReturnTypeWillChange;
use RuntimeException;
use Throwable;
use Traversable;
/**
* Client class used for connecting and executing commands on Redis.
*
* This is the main high-level abstraction of Predis upon which various other
* abstractions are built. Internally it aggregates various other classes each
* one with its own responsibility and scope.
*
* @template-implements \IteratorAggregate<string, static>
*/
class Client implements ClientInterface, IteratorAggregate
{
public const VERSION = '3.4.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;
}
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);
}
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}
* @throws Throwable
*/
public function executeCommand(CommandInterface $command)
{
$parameters = $this->connection->getParameters();
if ($this->connection instanceof AggregateConnectionInterface || $this->connection instanceof RelayConnection) {
$response = $this->connection->executeCommand($command);
} else {
$response = $parameters->retry->callWithRetry(
function () use ($command) {
return $this->connection->executeCommand($command);
},
function () {
$this->connection->disconnect();
}
);
}
if ($response instanceof ResponseInterface) {
if ($response instanceof ErrorResponseInterface) {
$response = $this->onErrorResponse($command, $response);
}
return $response;
}
if ($parameters->protocol === 2) {
return $command->parseResponse($response);
}
return $command->parseResp3Response($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());
}
/**
* Creates new push notifications consumer.
*
* @param callable|null $preLoopCallback Callback that should be called on client before enter a loop.
* @return PushConsumer
*/
public function push(?callable $preLoopCallback = null): PushConsumer
{
return new PushConsumer($this, $preLoopCallback);
}
/**
* 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<string, static>
*/
#[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);
}
}
@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis;
class ClientConfiguration
{
/**
* @var array{modules: array}|string[][]
*/
private static $config = [
'modules' => [
['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'];
}
}
@@ -0,0 +1,437 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis;
use Predis\Command\Argument\Geospatial\ByInterface;
use Predis\Command\Argument\Geospatial\FromInterface;
use Predis\Command\Argument\Search\AggregateArguments;
use Predis\Command\Argument\Search\AlterArguments;
use Predis\Command\Argument\Search\CreateArguments;
use Predis\Command\Argument\Search\DropArguments;
use Predis\Command\Argument\Search\ExplainArguments;
use Predis\Command\Argument\Search\HybridSearch\HybridSearchQuery;
use Predis\Command\Argument\Search\ProfileArguments;
use Predis\Command\Argument\Search\SchemaFields\FieldInterface;
use Predis\Command\Argument\Search\SearchArguments;
use Predis\Command\Argument\Search\SugAddArguments;
use Predis\Command\Argument\Search\SugGetArguments;
use Predis\Command\Argument\Search\SynUpdateArguments;
use Predis\Command\Argument\Server\LimitOffsetCount;
use Predis\Command\Argument\Server\To;
use Predis\Command\Argument\TimeSeries\AddArguments;
use Predis\Command\Argument\TimeSeries\AlterArguments as TSAlterArguments;
use Predis\Command\Argument\TimeSeries\CreateArguments as TSCreateArguments;
use Predis\Command\Argument\TimeSeries\DecrByArguments;
use Predis\Command\Argument\TimeSeries\GetArguments;
use Predis\Command\Argument\TimeSeries\IncrByArguments;
use Predis\Command\Argument\TimeSeries\InfoArguments;
use Predis\Command\Argument\TimeSeries\MGetArguments;
use Predis\Command\Argument\TimeSeries\MRangeArguments;
use Predis\Command\Argument\TimeSeries\RangeArguments;
use Predis\Command\CommandInterface;
use Predis\Command\Container\ACL;
use Predis\Command\Container\CLIENT;
use Predis\Command\Container\FUNCTIONS;
use Predis\Command\Container\HOTKEYS;
use Predis\Command\Container\Json\JSONDEBUG;
use Predis\Command\Container\Search\FTCONFIG;
use Predis\Command\Container\Search\FTCURSOR;
use Predis\Command\Container\XGROUP;
use Predis\Command\Redis\VADD;
/**
* Interface defining a client-side context such as a pipeline or transaction.
*
* @method $this copy(string $source, string $destination, int $db = -1, bool $replace = false)
* @method $this del(array|string $keys)
* @method $this delex(string $key, string $flag, $flagValue)
* @method $this digest(string $key)
* @method $this dump($key)
* @method $this exists($key)
* @method $this expire($key, $seconds, string $expireOption = '')
* @method $this expireat($key, $timestamp, string $expireOption = '')
* @method $this expiretime(string $key)
* @method $this keys($pattern)
* @method $this move($key, $db)
* @method $this object($subcommand, $key)
* @method $this persist($key)
* @method $this pexpire($key, $milliseconds, string $option = null)
* @method $this pexpireat($key, $timestamp, string $option = null)
* @method $this pttl($key)
* @method $this randomkey()
* @method $this rename($key, $target)
* @method $this renamenx($key, $target)
* @method $this scan($cursor, ?array $options = null)
* @method $this sort($key, ?array $options = null)
* @method $this sort_ro(string $key, ?string $byPattern = null, ?LimitOffsetCount $limit = null, array $getPatterns = [], ?string $sorting = null, bool $alpha = false)
* @method $this ttl($key)
* @method $this type($key)
* @method $this append($key, $value)
* @method $this bfadd(string $key, $item)
* @method $this bfexists(string $key, $item)
* @method $this bfinfo(string $key, string $modifier = '')
* @method $this bfinsert(string $key, int $capacity = -1, float $error = -1, int $expansion = -1, bool $noCreate = false, bool $nonScaling = false, string ...$item)
* @method $this bfloadchunk(string $key, int $iterator, $data)
* @method $this bfmadd(string $key, ...$item)
* @method $this bfmexists(string $key, ...$item)
* @method $this bfreserve(string $key, float $errorRate, int $capacity, int $expansion = -1, bool $nonScaling = false)
* @method $this bfscandump(string $key, int $iterator)
* @method $this bitcount(string $key, $start = null, $end = null, string $index = 'byte')
* @method $this bitop($operation, $destkey, $key)
* @method $this bitfield($key, $subcommand, ...$subcommandArg)
* @method $this bitfield_ro(string $key, ?array $encodingOffsetMap = null)
* @method $this bitpos($key, $bit, $start = null, $end = null, string $index = 'byte')
* @method $this blmpop(int $timeout, array $keys, string $modifier = 'left', int $count = 1)
* @method $this bzpopmax(array $keys, int $timeout)
* @method $this bzpopmin(array $keys, int $timeout)
* @method $this bzmpop(int $timeout, array $keys, string $modifier = 'min', int $count = 1)
* @method $this cfadd(string $key, $item)
* @method $this cfaddnx(string $key, $item)
* @method $this cfcount(string $key, $item)
* @method $this cfdel(string $key, $item)
* @method $this cfexists(string $key, $item)
* @method $this cfloadchunk(string $key, int $iterator, $data)
* @method $this cfmexists(string $key, ...$item)
* @method $this cfinfo(string $key)
* @method $this cfinsert(string $key, int $capacity = -1, bool $noCreate = false, string ...$item)
* @method $this cfinsertnx(string $key, int $capacity = -1, bool $noCreate = false, string ...$item)
* @method $this cfreserve(string $key, int $capacity, int $bucketSize = -1, int $maxIterations = -1, int $expansion = -1)
* @method $this cfscandump(string $key, int $iterator)
* @method $this cmsincrby(string $key, string|int...$itemIncrementDictionary)
* @method $this cmsinfo(string $key)
* @method $this cmsinitbydim(string $key, int $width, int $depth)
* @method $this cmsinitbyprob(string $key, float $errorRate, float $probability)
* @method $this cmsmerge(string $destination, array $sources, array $weights = [])
* @method $this cmsquery(string $key, string ...$item)
* @method $this decr($key)
* @method $this decrby($key, $decrement)
* @method $this failover(?To $to = null, bool $abort = false, int $timeout = -1)
* @method $this fcall(string $function, array $keys, ...$args)
* @method $this fcall_ro(string $function, array $keys, ...$args)
* @method $this ft_list()
* @method $this ftaggregate(string $index, string $query, ?AggregateArguments $arguments = null)
* @method $this ftaliasadd(string $alias, string $index)
* @method $this ftaliasdel(string $alias)
* @method $this ftaliasupdate(string $alias, string $index)
* @method $this ftalter(string $index, FieldInterface[] $schema, ?AlterArguments $arguments = null)
* @method $this ftcreate(string $index, FieldInterface[] $schema, ?CreateArguments $arguments = null)
* @method $this ftdictadd(string $dict, ...$term)
* @method $this ftdictdel(string $dict, ...$term)
* @method $this ftdictdump(string $dict)
* @method $this ftdropindex(string $index, ?DropArguments $arguments = null)
* @method $this ftexplain(string $index, string $query, ?ExplainArguments $arguments = null)
* @method $this fthybrid(string $index, HybridSearchQuery $query)
* @method $this ftinfo(string $index)
* @method $this ftprofile(string $index, ProfileArguments $arguments)
* @method $this ftsearch(string $index, string $query, ?SearchArguments $arguments = null)
* @method $this ftspellcheck(string $index, string $query, ?SearchArguments $arguments = null)
* @method $this ftsugadd(string $key, string $string, float $score, ?SugAddArguments $arguments = null)
* @method $this ftsugdel(string $key, string $string)
* @method $this ftsugget(string $key, string $prefix, ?SugGetArguments $arguments = null)
* @method $this ftsuglen(string $key)
* @method $this ftsyndump(string $index)
* @method $this ftsynupdate(string $index, string $synonymGroupId, ?SynUpdateArguments $arguments = null, string ...$terms)
* @method $this fttagvals(string $index, string $fieldName)
* @method $this get($key)
* @method $this getbit($key, $offset)
* @method $this getex(string $key, $modifier = '', $value = false)
* @method $this getrange($key, $start, $end)
* @method $this getdel(string $key)
* @method $this getset($key, $value)
* @method $this incr($key)
* @method $this incrby($key, $increment)
* @method $this incrbyfloat($key, $increment)
* @method $this mget(array $keys)
* @method $this mset(array $dictionary)
* @method $this msetex(array $dictionary, ?string $existModifier = null, ?string $expireResolution = null, ?int $expireTTL = null)
* @method $this msetnx(array $dictionary)
* @method $this psetex($key, $milliseconds, $value)
* @method $this set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null, $flagValue = null)
* @method $this setbit($key, $offset, $value)
* @method $this setex($key, $seconds, $value)
* @method $this setnx($key, $value)
* @method $this setrange($key, $offset, $value)
* @method $this strlen($key)
* @method $this hdel($key, array $fields)
* @method $this hexists($key, $field)
* @method $this hexpire(string $key, int $seconds, array $fields, string $flag = null)
* @method $this hexpireat(string $key, int $unixTimeSeconds, array $fields, string $flag = null)
* @method $this hexpiretime(string $key, array $fields)
* @method $this hpersist(string $key, array $fields)
* @method $this hpexpire(string $key, int $milliseconds, array $fields, string $flag = null)
* @method $this hpexpireat(string $key, int $unixTimeMilliseconds, array $fields, string $flag = null)
* @method $this hpexpiretime(string $key, array $fields)
* @method $this hget($key, $field)
* @method $this hgetex(string $key, array $fields, string $modifier = HGETEX::NULL)
* @method $this hgetall($key)
* @method $this hgetdel(string $key, array $fields)
* @method $this hincrby($key, $field, $increment)
* @method $this hincrbyfloat($key, $field, $increment)
* @method $this hkeys($key)
* @method $this hlen($key)
* @method $this hmget($key, array $fields)
* @method $this hmset($key, array $dictionary)
* @method $this hrandfield(string $key, int $count = 1, bool $withValues = false)
* @method $this hscan($key, $cursor, ?array $options = null)
* @method $this hset($key, $field, $value)
* @method $this hsetex(string $key, array $fieldValueMap, string $setModifier = HSETEX::SET_NULL, string $ttlModifier = HSETEX::TTL_NULL, int|bool $ttlModifierValue = false)
* @method $this hsetnx($key, $field, $value)
* @method $this httl(string $key, array $fields)
* @method $this hpttl(string $key, array $fields)
* @method $this hvals($key)
* @method $this hstrlen($key, $field)
* @method $this jsonarrappend(string $key, string $path = '$', ...$value)
* @method $this jsonarrindex(string $key, string $path, string $value, int $start = 0, int $stop = 0)
* @method $this jsonarrinsert(string $key, string $path, int $index, string ...$value)
* @method $this jsonarrlen(string $key, string $path = '$')
* @method $this jsonarrpop(string $key, string $path = '$', int $index = -1)
* @method $this jsonarrtrim(string $key, string $path, int $start, int $stop)
* @method $this jsonclear(string $key, string $path = '$')
* @method $this jsondel(string $key, string $path = '$')
* @method $this jsonforget(string $key, string $path = '$')
* @method $this jsonget(string $key, string $indent = '', string $newline = '', string $space = '', string ...$paths)
* @method $this jsonnumincrby(string $key, string $path, int $value)
* @method $this jsonmerge(string $key, string $path, string $value)
* @method $this jsonmget(array $keys, string $path)
* @method $this jsonmset(string ...$keyPathValue)
* @method $this jsonobjkeys(string $key, string $path = '$')
* @method $this jsonobjlen(string $key, string $path = '$')
* @method $this jsonresp(string $key, string $path = '$')
* @method $this jsonset(string $key, string $path, string $value, ?string $subcommand = null)
* @method $this jsonstrappend(string $key, string $path, string $value)
* @method $this jsonstrlen(string $key, string $path = '$')
* @method $this jsontoggle(string $key, string $path)
* @method $this jsontype(string $key, string $path = '$')
* @method $this blmove(string $source, string $destination, string $where, string $to, int $timeout)
* @method $this blpop(array|string $keys, $timeout)
* @method $this brpop(array|string $keys, $timeout)
* @method $this brpoplpush($source, $destination, $timeout)
* @method $this lcs(string $key1, string $key2, bool $len = false, bool $idx = false, int $minMatchLen = 0, bool $withMatchLen = false)
* @method $this lindex($key, $index)
* @method $this linsert($key, $whence, $pivot, $value)
* @method $this llen($key)
* @method $this lmove(string $source, string $destination, string $where, string $to)
* @method $this lmpop(array $keys, string $modifier = 'left', int $count = 1)
* @method $this lpop($key)
* @method $this lpush($key, array $values)
* @method $this lpushx($key, array $values)
* @method $this lrange($key, $start, $stop)
* @method $this lrem($key, $count, $value)
* @method $this lset($key, $index, $value)
* @method $this ltrim($key, $start, $stop)
* @method $this rpop($key)
* @method $this rpoplpush($source, $destination)
* @method $this rpush($key, array $values)
* @method $this rpushx($key, array $values)
* @method $this sadd($key, array $members)
* @method $this scard($key)
* @method $this sdiff(array|string $keys)
* @method $this sdiffstore($destination, array|string $keys)
* @method $this sinter(array|string $keys)
* @method $this sintercard(array $keys, int $limit = 0)
* @method $this sinterstore($destination, array|string $keys)
* @method $this sismember($key, $member)
* @method $this smembers($key)
* @method $this smismember(string $key, string ...$members)
* @method $this smove($source, $destination, $member)
* @method $this spop($key, $count = null)
* @method $this srandmember($key, $count = null)
* @method $this srem($key, $member)
* @method $this sscan($key, $cursor, ?array $options = null)
* @method $this ssubscribe(string ...$shardChannels)
* @method $this subscribe(string ...$channels)
* @method $this sunsubscribe(?string ...$shardChannels = null)
* @method $this sunion(array|string $keys)
* @method $this sunionstore($destination, array|string $keys)
* @method $this tdigestadd(string $key, float ...$value)
* @method $this tdigestbyrank(string $key, int ...$rank)
* @method $this tdigestbyrevrank(string $key, int ...$reverseRank)
* @method $this tdigestcdf(string $key, int ...$value)
* @method $this tdigestcreate(string $key, int $compression = 0)
* @method $this tdigestinfo(string $key)
* @method $this tdigestmax(string $key)
* @method $this tdigestmerge(string $destinationKey, array $sourceKeys, int $compression = 0, bool $override = false)
* @method $this tdigestquantile(string $key, float ...$quantile)
* @method $this tdigestmin(string $key)
* @method $this tdigestrank(string $key, ...$value)
* @method $this tdigestreset(string $key)
* @method $this tdigestrevrank(string $key, float ...$value)
* @method $this tdigesttrimmed_mean(string $key, float $lowCutQuantile, float $highCutQuantile)
* @method $this topkadd(string $key, ...$items)
* @method $this topkincrby(string $key, ...$itemIncrement)
* @method $this topkinfo(string $key)
* @method $this topklist(string $key, bool $withCount = false)
* @method $this topkquery(string $key, ...$items)
* @method $this topkreserve(string $key, int $topK, int $width = 8, int $depth = 7, float $decay = 0.9)
* @method $this tsadd(string $key, int $timestamp, string|float $value, ?AddArguments $arguments = null)
* @method $this tsalter(string $key, ?TSAlterArguments $arguments = null)
* @method $this tscreate(string $key, ?TSCreateArguments $arguments = null)
* @method $this tscreaterule(string $sourceKey, string $destKey, string $aggregator, int $bucketDuration, int $alignTimestamp = 0)
* @method $this tsdecrby(string $key, float $value, ?DecrByArguments $arguments = null)
* @method $this tsdel(string $key, int $fromTimestamp, int $toTimestamp)
* @method $this tsdeleterule(string $sourceKey, string $destKey)
* @method $this tsget(string $key, ?GetArguments $arguments = null)
* @method $this tsincrby(string $key, float $value, ?IncrByArguments $arguments = null)
* @method $this tsinfo(string $key, ?InfoArguments $arguments = null)
* @method $this tsmadd(mixed ...$keyTimestampValue)
* @method $this tsmget(MGetArguments $arguments, string ...$filterExpression)
* @method $this tsmrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments)
* @method $this tsmrevrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments)
* @method $this tsqueryindex(string ...$filterExpression)
* @method $this tsrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null)
* @method $this tsrevrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null)
* @method $this xack(string $key, string $group, string ...$id)
* @method $this xackdel(string $key, string $group, string $mode, array $ids)
* @method $this xadd(string $key, array $dictionary, string $id = '*', array $options = null)
* @method $this xautoclaim(string $key, string $group, string $consumer, int $minIdleTime, string $start, ?int $count = null, bool $justId = false)
* @method $this xclaim(string $key, string $group, string $consumer, int $minIdleTime, string|array $ids, ?int $idle = null, ?int $time = null, ?int $retryCount = null, bool $force = false, bool $justId = false, ?string $lastId = null)
* @method $this xcfgset(string $key, ?int $duration = null, ?int $maxsize = null)
* @method $this xdel(string $key, string ...$id)
* @method $this xdelex(string $key, string $mode, array $ids)
* @method $this xlen(string $key)
* @method $this xpending(string $key, string $group, ?int $minIdleTime = null, ?string $start = null, ?string $end = null, ?int $count = null, ?string $consumer = null)
* @method $this xrevrange(string $key, string $end, string $start, ?int $count = null)
* @method $this xrange(string $key, string $start, string $end, ?int $count = null)
* @method $this xread(int $count = null, int $block = null, array $streams = null, string ...$id)
* @method $this xreadgroup(string $group, string $consumer, ?int $count = null, ?int $blockMs = null, bool $noAck = false, string ...$keyOrId)
* @method $this xreadgroup_claim(string $group, string $consumer, array $keyIdDict, ?int $count = null, ?int $blockMs = null, bool $noAck = false, ?int $claim = null)
* @method $this xsetid(string $key, string $lastId, ?int $entriesAdded = null, ?string $maxDeleteId = null)
* @method $this xtrim(string $key, array|string $strategy, string $threshold, array $options = null)
* @method $this zadd($key, array $membersAndScoresDictionary)
* @method $this zcard($key)
* @method $this zcount($key, $min, $max)
* @method $this zdiff(array $keys, bool $withScores = false)
* @method $this zdiffstore(string $destination, array $keys)
* @method $this zincrby($key, $increment, $member)
* @method $this zintercard(array $keys, int $limit = 0)
* @method $this zinterstore(string $destination, array $keys, int[] $weights = [], string $aggregate = 'sum')
* @method $this zinter(array $keys, int[] $weights = [], string $aggregate = 'sum', bool $withScores = false)
* @method $this zmpop(array $keys, string $modifier = 'min', int $count = 1)
* @method $this zmscore(string $key, string ...$member)
* @method $this zrandmember(string $key, int $count = 1, bool $withScores = false)
* @method $this zrange($key, $start, $stop, ?array $options = null)
* @method $this zrangebyscore($key, $min, $max, ?array $options = null)
* @method $this zrangestore(string $destination, string $source, int|string $min, string|int $max, string|bool $by = false, bool $reversed = false, bool $limit = false, int $offset = 0, int $count = 0)
* @method $this zrank($key, $member)
* @method $this zrem($key, $member)
* @method $this zremrangebyrank($key, $start, $stop)
* @method $this zremrangebyscore($key, $min, $max)
* @method $this zrevrange($key, $start, $stop, ?array $options = null)
* @method $this zrevrangebyscore($key, $max, $min, ?array $options = null)
* @method $this zrevrank($key, $member)
* @method $this zunion(array $keys, int[] $weights = [], string $aggregate = 'sum', bool $withScores = false)
* @method $this zunionstore(string $destination, array $keys, int[] $weights = [], string $aggregate = 'sum')
* @method $this zscore($key, $member)
* @method $this zscan($key, $cursor, ?array $options = null)
* @method $this zrangebylex($key, $start, $stop, ?array $options = null)
* @method $this zrevrangebylex($key, $start, $stop, ?array $options = null)
* @method $this zremrangebylex($key, $min, $max)
* @method $this zlexcount($key, $min, $max)
* @method $this pexpiretime(string $key)
* @method $this pfadd($key, array $elements)
* @method $this pfmerge($destinationKey, array|string $sourceKeys)
* @method $this pfcount(array|string $keys)
* @method $this pubsub($subcommand, $argument)
* @method $this publish($channel, $message)
* @method $this discard()
* @method $this exec()
* @method $this multi()
* @method $this unwatch()
* @method $this waitaof(int $numLocal, int $numReplicas, int $timeout)
* @method $this unsubscribe(string ...$channels)
* @method $this vadd(string $key, string|array $vector, string $elem, int $dim = null, bool $cas = false, string $quant = VADD::QUANT_DEFAULT, ?int $BEF = null, string|array $attributes = null, int $numlinks = null)
* @method $this vcard(string $key)
* @method $this vdim(int $key)
* @method $this vemb(string $key, string $elem, bool $raw = false)
* @method $this vgetattr(string $key, string $elem, bool $asJson = false)
* @method $this vinfo(string $key)
* @method $this vlinks(string $key, string $elem, bool $withScores = false)
* @method $this vrandmember(string $key, int $count = null)
* @method $this vrange(string $key, string $start, string $end, int $count = null)
* @method $this vrem(string $key, string $elem)
* @method $this vsetattr(string $key, string $elem, string|array $attributes)
* @method $this vsim(string $key, string|array $vectorOrElem, bool $isElem = false, bool $withScores = false, int $count = null, float $epsilon = null, int $ef = null, string $filter = null, int $filterEf = null, bool $truth = false, bool $noThread = false)
* @method $this watch($key)
* @method $this eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
* @method $this eval_ro(string $script, array $keys, ...$argument)
* @method $this evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
* @method $this evalsha_ro(string $sha1, array $keys, ...$argument)
* @method $this script($subcommand, $argument = null)
* @method $this shutdown(?bool $noSave = null, bool $now = false, bool $force = false, bool $abort = false)
* @method $this auth($password)
* @method $this echo($message)
* @method $this ping($message = null)
* @method $this select($database)
* @method $this bgrewriteaof()
* @method $this bgsave()
* @method $this config($subcommand, $argument = null)
* @method $this dbsize()
* @method $this flushall()
* @method $this flushdb()
* @method $this info(string ...$section = null)
* @method $this lastsave()
* @method $this save()
* @method $this slaveof($host, $port)
* @method $this slowlog($subcommand, $argument = null)
* @method $this spublish(string $shardChannel, string $message)
* @method $this time()
* @method $this command($subcommand, $argument = null)
* @method $this geoadd($key, $longitude, $latitude, $member)
* @method $this geohash($key, array $members)
* @method $this geopos($key, array $members)
* @method $this geodist($key, $member1, $member2, $unit = null)
* @method $this georadius($key, $longitude, $latitude, $radius, $unit, ?array $options = null)
* @method $this georadiusbymember($key, $member, $radius, $unit, ?array $options = null)
* @method $this geosearch(string $key, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $withCoord = false, bool $withDist = false, bool $withHash = false)
* @method $this geosearchstore(string $destination, string $source, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $storeDist = false)
*
* Container commands
* @property CLIENT $client
* @property HOTKEYS $hotkeys
* @property FUNCTIONS $function
* @property FTCONFIG $ftconfig
* @property FTCURSOR $ftcursor
* @property JSONDEBUG $jsondebug
* @property ACL $acl
* @property XGROUP $xgroup
*/
interface ClientContextInterface
{
/**
* Sends the specified command instance to Redis.
*
* @param CommandInterface $command Command instance.
*
* @return mixed
*/
public function executeCommand(CommandInterface $command);
/**
* Sends the specified command with its arguments to Redis.
*
* @param string $method Command ID.
* @param array $arguments Arguments for the command.
*
* @return mixed
*/
public function __call($method, $arguments);
/**
* Starts the execution of the context.
*
* @param mixed $callable Optional callback for execution.
*
* @return array
*/
public function execute($callable = null);
}
@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis;
/**
* Exception class that identifies client-side errors.
*/
class ClientException extends PredisException
{
}
@@ -0,0 +1,484 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis;
use Predis\Command\Argument\Geospatial\ByInterface;
use Predis\Command\Argument\Geospatial\FromInterface;
use Predis\Command\Argument\Search\AggregateArguments;
use Predis\Command\Argument\Search\AlterArguments;
use Predis\Command\Argument\Search\CreateArguments;
use Predis\Command\Argument\Search\DropArguments;
use Predis\Command\Argument\Search\ExplainArguments;
use Predis\Command\Argument\Search\HybridSearch\HybridSearchQuery;
use Predis\Command\Argument\Search\ProfileArguments;
use Predis\Command\Argument\Search\SchemaFields\FieldInterface;
use Predis\Command\Argument\Search\SearchArguments;
use Predis\Command\Argument\Search\SugAddArguments;
use Predis\Command\Argument\Search\SugGetArguments;
use Predis\Command\Argument\Search\SynUpdateArguments;
use Predis\Command\Argument\Server\LimitOffsetCount;
use Predis\Command\Argument\Server\To;
use Predis\Command\Argument\TimeSeries\AddArguments;
use Predis\Command\Argument\TimeSeries\AlterArguments as TSAlterArguments;
use Predis\Command\Argument\TimeSeries\CreateArguments as TSCreateArguments;
use Predis\Command\Argument\TimeSeries\DecrByArguments;
use Predis\Command\Argument\TimeSeries\GetArguments;
use Predis\Command\Argument\TimeSeries\IncrByArguments;
use Predis\Command\Argument\TimeSeries\InfoArguments;
use Predis\Command\Argument\TimeSeries\MGetArguments;
use Predis\Command\Argument\TimeSeries\MRangeArguments;
use Predis\Command\Argument\TimeSeries\RangeArguments;
use Predis\Command\CommandInterface;
use Predis\Command\Container\ACL;
use Predis\Command\Container\CLIENT;
use Predis\Command\Container\FUNCTIONS;
use Predis\Command\Container\HOTKEYS;
use Predis\Command\Container\Json\JSONDEBUG;
use Predis\Command\Container\Search\FTCONFIG;
use Predis\Command\Container\Search\FTCURSOR;
use Predis\Command\Container\XGROUP;
use Predis\Command\Container\XINFO;
use Predis\Command\FactoryInterface;
use Predis\Command\Redis\VADD;
use Predis\Configuration\OptionsInterface;
use Predis\Connection\ConnectionInterface;
use Predis\Response\Status;
/**
* Interface defining a client able to execute commands against Redis.
*
* All the commands exposed by the client generally have the same signature as
* described by the Redis documentation, but some of them offer an additional
* and more friendly interface to ease programming which is described in the
* following list of methods:
*
* @method int copy(string $source, string $destination, int $db = -1, bool $replace = false)
* @method int del(string[]|string $keyOrKeys, string ...$keys = null)
* @method int delex(string $key, string $flag, $flagValue)
* @method string digest(string $key)
* @method string|null dump(string $key)
* @method int exists(string $key)
* @method int expire(string $key, int $seconds, string $expireOption = '')
* @method int expireat(string $key, int $timestamp, string $expireOption = '')
* @method int expiretime(string $key)
* @method array keys(string $pattern)
* @method int move(string $key, int $db)
* @method mixed object($subcommand, string $key)
* @method int persist(string $key)
* @method int pexpire(string $key, int $milliseconds, string $option = null)
* @method int pexpireat(string $key, int $timestamp, string $option = null)
* @method int pttl(string $key)
* @method string|null randomkey()
* @method mixed rename(string $key, string $target)
* @method int renamenx(string $key, string $target)
* @method array scan($cursor, ?array $options = null)
* @method array sort(string $key, ?array $options = null)
* @method array sort_ro(string $key, ?string $byPattern = null, ?LimitOffsetCount $limit = null, array $getPatterns = [], ?string $sorting = null, bool $alpha = false)
* @method int ttl(string $key)
* @method mixed type(string $key)
* @method int append(string $key, $value)
* @method mixed bfadd(string $key, $item)
* @method mixed bfexists(string $key, $item)
* @method array bfinfo(string $key, string $modifier = '')
* @method array bfinsert(string $key, int $capacity = -1, float $error = -1, int $expansion = -1, bool $noCreate = false, bool $nonScaling = false, string ...$item)
* @method Status bfloadchunk(string $key, int $iterator, $data)
* @method array bfmadd(string $key, ...$item)
* @method array bfmexists(string $key, ...$item)
* @method Status bfreserve(string $key, float $errorRate, int $capacity, int $expansion = -1, bool $nonScaling = false)
* @method array bfscandump(string $key, int $iterator)
* @method int bitcount(string $key, $start = null, $end = null, string $index = 'byte')
* @method int bitop($operation, $destkey, $key)
* @method array|null bitfield(string $key, $subcommand, ...$subcommandArg)
* @method array|null bitfield_ro(string $key, ?array $encodingOffsetMap = null)
* @method int bitpos(string $key, $bit, $start = null, $end = null, string $index = 'byte')
* @method array blmpop(int $timeout, array $keys, string $modifier = 'left', int $count = 1)
* @method array bzpopmax(array $keys, int $timeout)
* @method array bzpopmin(array $keys, int $timeout)
* @method array bzmpop(int $timeout, array $keys, string $modifier = 'min', int $count = 1)
* @method mixed cfadd(string $key, $item)
* @method mixed cfaddnx(string $key, $item)
* @method int cfcount(string $key, $item)
* @method mixed cfdel(string $key, $item)
* @method mixed cfexists(string $key, $item)
* @method Status cfloadchunk(string $key, int $iterator, $data)
* @method int cfmexists(string $key, ...$item)
* @method array cfinfo(string $key)
* @method array cfinsert(string $key, int $capacity = -1, bool $noCreate = false, string ...$item)
* @method array cfinsertnx(string $key, int $capacity = -1, bool $noCreate = false, string ...$item)
* @method Status cfreserve(string $key, int $capacity, int $bucketSize = -1, int $maxIterations = -1, int $expansion = -1)
* @method array cfscandump(string $key, int $iterator)
* @method array cmsincrby(string $key, string|int ...$itemIncrementDictionary)
* @method array cmsinfo(string $key)
* @method Status cmsinitbydim(string $key, int $width, int $depth)
* @method Status cmsinitbyprob(string $key, float $errorRate, float $probability)
* @method Status cmsmerge(string $destination, array $sources, array $weights = [])
* @method array cmsquery(string $key, string ...$item)
* @method int decr(string $key)
* @method int decrby(string $key, int $decrement)
* @method Status failover(?To $to = null, bool $abort = false, int $timeout = -1)
* @method mixed fcall(string $function, array $keys, ...$args)
* @method mixed fcall_ro(string $function, array $keys, ...$args)
* @method array ft_list()
* @method array ftaggregate(string $index, string $query, ?AggregateArguments $arguments = null)
* @method Status ftaliasadd(string $alias, string $index)
* @method Status ftaliasdel(string $alias)
* @method Status ftaliasupdate(string $alias, string $index)
* @method Status ftalter(string $index, FieldInterface[] $schema, ?AlterArguments $arguments = null)
* @method Status ftcreate(string $index, FieldInterface[] $schema, ?CreateArguments $arguments = null)
* @method int ftdictadd(string $dict, ...$term)
* @method int ftdictdel(string $dict, ...$term)
* @method array ftdictdump(string $dict)
* @method Status ftdropindex(string $index, ?DropArguments $arguments = null)
* @method string ftexplain(string $index, string $query, ?ExplainArguments $arguments = null)
* @method array fthybrid(string $index, HybridSearchQuery $query)
* @method array ftinfo(string $index)
* @method array ftprofile(string $index, ProfileArguments $arguments)
* @method array ftsearch(string $index, string $query, ?SearchArguments $arguments = null)
* @method array ftspellcheck(string $index, string $query, ?SearchArguments $arguments = null)
* @method int ftsugadd(string $key, string $string, float $score, ?SugAddArguments $arguments = null)
* @method int ftsugdel(string $key, string $string)
* @method array ftsugget(string $key, string $prefix, ?SugGetArguments $arguments = null)
* @method int ftsuglen(string $key)
* @method array ftsyndump(string $index)
* @method Status ftsynupdate(string $index, string $synonymGroupId, ?SynUpdateArguments $arguments = null, string ...$terms)
* @method array fttagvals(string $index, string $fieldName)
* @method string|null get(string $key)
* @method int getbit(string $key, $offset)
* @method int|null getex(string $key, $modifier = '', $value = false)
* @method string getrange(string $key, $start, $end)
* @method string getdel(string $key)
* @method string|null getset(string $key, $value)
* @method int incr(string $key)
* @method int incrby(string $key, int $increment)
* @method string incrbyfloat(string $key, int|float $increment)
* @method array mget(string[]|string $keyOrKeys, string ...$keys = null)
* @method mixed mset(array $dictionary)
* @method array msetex(array $dictionary, ?string $existModifier = null, ?string $expireResolution = null, ?int $expireTTL = null)
* @method int msetnx(array $dictionary)
* @method Status psetex(string $key, $milliseconds, $value)
* @method Status|null set(string $key, $value, $expireResolution = null, $expireTTL = null, $flag = null, $flagValue = null)
* @method int setbit(string $key, $offset, $value)
* @method Status setex(string $key, $seconds, $value)
* @method int setnx(string $key, $value)
* @method int setrange(string $key, $offset, $value)
* @method int strlen(string $key)
* @method int hdel(string $key, array $fields)
* @method int hexists(string $key, string $field)
* @method array|null hexpire(string $key, int $seconds, array $fields, string $flag = null)
* @method array|null hexpireat(string $key, int $unixTimeSeconds, array $fields, string $flag = null)
* @method array|null hexpiretime(string $key, array $fields)
* @method array|null hpersist(string $key, array $fields)
* @method array|null hpexpire(string $key, int $milliseconds, array $fields, string $flag = null)
* @method array|null hpexpireat(string $key, int $unixTimeMilliseconds, array $fields, string $flag = null)
* @method array|null hpexpiretime(string $key, array $fields)
* @method string|null hget(string $key, string $field)
* @method array|null hgetex(string $key, array $fields, string $modifier = HGETEX::NULL, int|bool $modifierValue = false)
* @method array hgetall(string $key)
* @method array hgetdel(string $key, array $fields)
* @method int hincrby(string $key, string $field, int $increment)
* @method string hincrbyfloat(string $key, string $field, int|float $increment)
* @method array hkeys(string $key)
* @method int hlen(string $key)
* @method array hmget(string $key, array $fields)
* @method mixed hmset(string $key, array $dictionary)
* @method array hrandfield(string $key, int $count = 1, bool $withValues = false)
* @method array hscan(string $key, $cursor, ?array $options = null)
* @method int hset(string $key, string $field, string $value)
* @method int hsetex(string $key, array $fieldValueMap, string $setModifier = HSETEX::SET_NULL, string $ttlModifier = HSETEX::TTL_NULL, int|bool $ttlModifierValue = false)
* @method int hsetnx(string $key, string $field, string $value)
* @method array|null httl(string $key, array $fields)
* @method array|null hpttl(string $key, array $fields)
* @method array hvals(string $key)
* @method int hstrlen(string $key, string $field)
* @method array jsonarrappend(string $key, string $path = '$', ...$value)
* @method array jsonarrindex(string $key, string $path, string $value, int $start = 0, int $stop = 0)
* @method array jsonarrinsert(string $key, string $path, int $index, string ...$value)
* @method array jsonarrlen(string $key, string $path = '$')
* @method array jsonarrpop(string $key, string $path = '$', int $index = -1)
* @method int jsonclear(string $key, string $path = '$')
* @method array jsonarrtrim(string $key, string $path, int $start, int $stop)
* @method int jsondel(string $key, string $path = '$')
* @method int jsonforget(string $key, string $path = '$')
* @method mixed jsonget(string $key, string $indent = '', string $newline = '', string $space = '', string ...$paths)
* @method mixed jsonnumincrby(string $key, string $path, int $value)
* @method Status jsonmerge(string $key, string $path, string $value)
* @method array jsonmget(array $keys, string $path)
* @method Status jsonmset(string ...$keyPathValue)
* @method array jsonobjkeys(string $key, string $path = '$')
* @method array jsonobjlen(string $key, string $path = '$')
* @method array jsonresp(string $key, string $path = '$')
* @method string jsonset(string $key, string $path, string $value, ?string $subcommand = null)
* @method array jsonstrappend(string $key, string $path, string $value)
* @method array jsonstrlen(string $key, string $path = '$')
* @method array jsontoggle(string $key, string $path)
* @method array jsontype(string $key, string $path = '$')
* @method string blmove(string $source, string $destination, string $where, string $to, int $timeout)
* @method array|null blpop(array|string $keys, int|float $timeout)
* @method array|null brpop(array|string $keys, int|float $timeout)
* @method string|null brpoplpush(string $source, string $destination, int|float $timeout)
* @method mixed lcs(string $key1, string $key2, bool $len = false, bool $idx = false, int $minMatchLen = 0, bool $withMatchLen = false)
* @method string|null lindex(string $key, int $index)
* @method int linsert(string $key, $whence, $pivot, $value)
* @method int llen(string $key)
* @method string lmove(string $source, string $destination, string $where, string $to)
* @method array|null lmpop(array $keys, string $modifier = 'left', int $count = 1)
* @method string|null lpop(string $key)
* @method int lpush(string $key, array $values)
* @method int lpushx(string $key, array $values)
* @method string[] lrange(string $key, int $start, int $stop)
* @method int lrem(string $key, int $count, string $value)
* @method mixed lset(string $key, int $index, string $value)
* @method mixed ltrim(string $key, int $start, int $stop)
* @method string|null rpop(string $key)
* @method string|null rpoplpush(string $source, string $destination)
* @method int rpush(string $key, array $values)
* @method int rpushx(string $key, array $values)
* @method int sadd(string $key, array $members)
* @method int scard(string $key)
* @method string[] sdiff(array|string $keys)
* @method int sdiffstore(string $destination, array|string $keys)
* @method string[] sinter(array|string $keys)
* @method int sintercard(array $keys, int $limit = 0)
* @method int sinterstore(string $destination, array|string $keys)
* @method int sismember(string $key, string $member)
* @method string[] smembers(string $key)
* @method array smismember(string $key, string ...$members)
* @method int smove(string $source, string $destination, string $member)
* @method string|array|null spop(string $key, ?int $count = null)
* @method string|null srandmember(string $key, ?int $count = null)
* @method int srem(string $key, array|string $member)
* @method array sscan(string $key, int $cursor, array $options = null)
* @method array ssubscribe(string ...$shardChannels)
* @method array subscribe(string ...$channels)
* @method string[] sunion(array|string $keys)
* @method int sunionstore(string $destination, array|string $keys)
* @method array sunsubscribe(?string ...$shardChannels = null)
* @method int touch(string[]|string $keyOrKeys, string ...$keys = null)
* @method Status tdigestadd(string $key, float ...$value)
* @method array tdigestbyrank(string $key, int ...$rank)
* @method array tdigestbyrevrank(string $key, int ...$reverseRank)
* @method array tdigestcdf(string $key, int ...$value)
* @method Status tdigestcreate(string $key, int $compression = 0)
* @method array tdigestinfo(string $key)
* @method mixed tdigestmax(string $key)
* @method Status tdigestmerge(string $destinationKey, array $sourceKeys, int $compression = 0, bool $override = false)
* @method string[] tdigestquantile(string $key, float ...$quantile)
* @method mixed tdigestmin(string $key)
* @method array tdigestrank(string $key, float ...$value)
* @method Status tdigestreset(string $key)
* @method array tdigestrevrank(string $key, float ...$value)
* @method string tdigesttrimmed_mean(string $key, float $lowCutQuantile, float $highCutQuantile)
* @method array topkadd(string $key, ...$items)
* @method array topkincrby(string $key, ...$itemIncrement)
* @method array topkinfo(string $key)
* @method array topklist(string $key, bool $withCount = false)
* @method array topkquery(string $key, ...$items)
* @method Status topkreserve(string $key, int $topK, int $width = 8, int $depth = 7, float $decay = 0.9)
* @method int tsadd(string $key, int $timestamp, string|float $value, ?AddArguments $arguments = null)
* @method Status tsalter(string $key, ?TSAlterArguments $arguments = null)
* @method Status tscreate(string $key, ?TSCreateArguments $arguments = null)
* @method Status tscreaterule(string $sourceKey, string $destKey, string $aggregator, int $bucketDuration, int $alignTimestamp = 0)
* @method int tsdecrby(string $key, float $value, ?DecrByArguments $arguments = null)
* @method int tsdel(string $key, int $fromTimestamp, int $toTimestamp)
* @method Status tsdeleterule(string $sourceKey, string $destKey)
* @method array tsget(string $key, ?GetArguments $arguments = null)
* @method int tsincrby(string $key, float $value, ?IncrByArguments $arguments = null)
* @method array tsinfo(string $key, ?InfoArguments $arguments = null)
* @method array tsmadd(mixed ...$keyTimestampValue)
* @method array tsmget(MGetArguments $arguments, string ...$filterExpression)
* @method array tsmrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments)
* @method array tsmrevrange($fromTimestamp, $toTimestamp, MRangeArguments $arguments)
* @method array tsqueryindex(string ...$filterExpression)
* @method array tsrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null)
* @method array tsrevrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null)
* @method int xack(string $key, string $group, string ...$id)
* @method array xackdel(string $key, string $group, string $mode, array $ids)
* @method string xadd(string $key, array $dictionary, string $id = '*', array $options = null)
* @method array xautoclaim(string $key, string $group, string $consumer, int $minIdleTime, string $start, ?int $count = null, bool $justId = false)
* @method array xclaim(string $key, string $group, string $consumer, int $minIdleTime, string|array $ids, ?int $idle = null, ?int $time = null, ?int $retryCount = null, bool $force = false, bool $justId = false, ?string $lastId = null)
* @method Status xcfgset(string $key, ?int $duration = null, ?int $maxsize = null)
* @method int xdel(string $key, string ...$id)
* @method array xdelex(string $key, string $mode, array $ids)
* @method int xlen(string $key)
* @method array xpending(string $key, string $group, ?int $minIdleTime = null, ?string $start = null, ?string $end = null, ?int $count = null, ?string $consumer = null)
* @method array xrevrange(string $key, string $end, string $start, ?int $count = null)
* @method array xrange(string $key, string $start, string $end, ?int $count = null)
* @method array|null xread(int $count = null, int $block = null, array $streams = null, string ...$id)
* @method array xreadgroup(string $group, string $consumer, ?int $count = null, ?int $blockMs = null, bool $noAck = false, string ...$keyOrId)
* @method array xreadgroup_claim(string $group, string $consumer, array $keyIdDict, ?int $count = null, ?int $blockMs = null, bool $noAck = false, ?int $claim = null)
* @method Status xsetid(string $key, string $lastId, ?int $entriesAdded = null, ?string $maxDeleteId = null)
* @method string xtrim(string $key, array|string $strategy, string $threshold, array $options = null)
* @method int zadd(string $key, array $membersAndScoresDictionary)
* @method int zcard(string $key)
* @method int zcount(string $key, int|string $min, int|string $max)
* @method array zdiff(array $keys, bool $withScores = false)
* @method int zdiffstore(string $destination, array $keys)
* @method string zincrby(string $key, int $increment, string $member)
* @method int zintercard(array $keys, int $limit = 0)
* @method int zinterstore(string $destination, array $keys, int[] $weights = [], string $aggregate = 'sum')
* @method array zinter(array $keys, int[] $weights = [], string $aggregate = 'sum', bool $withScores = false)
* @method array zmpop(array $keys, string $modifier = 'min', int $count = 1)
* @method array zmscore(string $key, string ...$member)
* @method array zpopmin(string $key, int $count = 1)
* @method array zpopmax(string $key, int $count = 1)
* @method mixed zrandmember(string $key, int $count = 1, bool $withScores = false)
* @method array zrange(string $key, int|string $start, int|string $stop, ?array $options = null)
* @method array zrangebyscore(string $key, int|string $min, int|string $max, ?array $options = null)
* @method int zrangestore(string $destination, string $source, int|string $min, int|string $max, string|bool $by = false, bool $reversed = false, bool $limit = false, int $offset = 0, int $count = 0)
* @method int|null zrank(string $key, string $member)
* @method int zrem(string $key, string ...$member)
* @method int zremrangebyrank(string $key, int|string $start, int|string $stop)
* @method int zremrangebyscore(string $key, int|string $min, int|string $max)
* @method array zrevrange(string $key, int|string $start, int|string $stop, ?array $options = null)
* @method array zrevrangebyscore(string $key, int|string $max, int|string $min, ?array $options = null)
* @method int|null zrevrank(string $key, string $member)
* @method array zunion(array $keys, int[] $weights = [], string $aggregate = 'sum', bool $withScores = false)
* @method int zunionstore(string $destination, array $keys, int[] $weights = [], string $aggregate = 'sum')
* @method string|null zscore(string $key, string $member)
* @method array zscan(string $key, int $cursor, ?array $options = null)
* @method array zrangebylex(string $key, string $start, string $stop, ?array $options = null)
* @method array zrevrangebylex(string $key, string $start, string $stop, ?array $options = null)
* @method int zremrangebylex(string $key, string $min, string $max)
* @method int zlexcount(string $key, string $min, string $max)
* @method int pexpiretime(string $key)
* @method int pfadd(string $key, array $elements)
* @method mixed pfmerge(string $destinationKey, array|string $sourceKeys)
* @method int pfcount(string[]|string $keyOrKeys, string ...$keys = null)
* @method mixed pubsub($subcommand, $argument)
* @method int publish($channel, $message)
* @method mixed discard()
* @method array|null exec()
* @method mixed multi()
* @method mixed unwatch()
* @method array unsubscribe(string ...$channels)
* @method bool vadd(string $key, string|array $vector, string $elem, int $dim = null, bool $cas = false, string $quant = VADD::QUANT_DEFAULT, int $bef = null, string|array $attributes = null, int $numlinks = null)
* @method int vcard(string $key)
* @method int vdim(string $key)
* @method array vemb(string $key, string $elem, bool $raw = false)
* @method string|array|null vgetattr(string $key, string $elem, bool $asJson = false)
* @method array|null vinfo(string $key)
* @method array|null vlinks(string $key, string $elem, bool $withScores = false)
* @method string|array|null vrandmember(string $key, int $count = null)
* @method array vrange(string $key, string $start, string $end, int $count = null)
* @method bool vrem(string $key, string $elem)
* @method array vsim(string $key, string|array $vectorOrElem, bool $isElem = false, bool $withScores = false, int $count = null, float $epsilon = null, int $ef = null, string $filter = null, int $filterEf = null, bool $truth = false, bool $noThread = false)
* @method bool vsetattr(string $key, string $elem, string|array $attributes)
* @method array waitaof(int $numLocal, int $numReplicas, int $timeout)
* @method mixed watch(string[]|string $keyOrKeys)
* @method mixed eval(string $script, int $numkeys, string ...$keyOrArg = null)
* @method mixed eval_ro(string $script, array $keys, ...$argument)
* @method mixed evalsha(string $script, int $numkeys, string ...$keyOrArg = null)
* @method mixed evalsha_ro(string $sha1, array $keys, ...$argument)
* @method mixed script($subcommand, $argument = null)
* @method Status shutdown(?bool $noSave = null, bool $now = false, bool $force = false, bool $abort = false)
* @method mixed auth(string $password)
* @method string echo(string $message)
* @method mixed ping(?string $message = null)
* @method mixed select(int $database)
* @method mixed bgrewriteaof()
* @method mixed bgsave()
* @method mixed config($subcommand, $argument = null)
* @method int dbsize()
* @method mixed flushall()
* @method mixed flushdb()
* @method array info(string ...$section = null)
* @method int lastsave()
* @method mixed save()
* @method mixed slaveof(string $host, int $port)
* @method mixed slowlog($subcommand, $argument = null)
* @method int spublish(string $shardChannel, string $message)
* @method array time()
* @method array command($subcommand, $argument = null)
* @method int geoadd(string $key, $longitude, $latitude, $member)
* @method array geohash(string $key, array $members)
* @method array geopos(string $key, array $members)
* @method string|null geodist(string $key, $member1, $member2, $unit = null)
* @method array georadius(string $key, $longitude, $latitude, $radius, $unit, ?array $options = null)
* @method array georadiusbymember(string $key, $member, $radius, $unit, ?array $options = null)
* @method array geosearch(string $key, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $withCoord = false, bool $withDist = false, bool $withHash = false)
* @method int geosearchstore(string $destination, string $source, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $storeDist = false)
*
* Container commands
* @property CLIENT $client
* @property HOTKEYS $hotkeys
* @property FUNCTIONS $function
* @property FTCONFIG $ftconfig
* @property FTCURSOR $ftcursor
* @property JSONDEBUG $jsondebug
* @property ACL $acl
* @property XGROUP $xgroup
* @property XINFO $xinfo
*/
interface ClientInterface
{
/**
* Returns the command factory used by the client.
*
* @return FactoryInterface
*/
public function getCommandFactory();
/**
* Returns the client options specified upon initialization.
*
* @return OptionsInterface
*/
public function getOptions();
/**
* Opens the underlying connection to the server.
*/
public function connect();
/**
* Closes the underlying connection from the server.
*/
public function disconnect();
/**
* Returns the underlying connection instance.
*
* @return ConnectionInterface
*/
public function getConnection();
/**
* Creates a new instance of the specified Redis command.
*
* @param string $method Command ID.
* @param array $arguments Arguments for the command.
*
* @return CommandInterface
*/
public function createCommand($method, $arguments = []);
/**
* Executes the specified Redis command.
*
* @param CommandInterface $command Command instance.
*
* @return mixed
*/
public function executeCommand(CommandInterface $command);
/**
* Creates a Redis command with the specified arguments and sends a request
* to the server.
*
* @param string $method Command ID.
* @param array $arguments Arguments for the command.
*
* @return mixed
*/
public function __call($method, $arguments);
}
@@ -0,0 +1,520 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
use InvalidArgumentException;
use Predis\Command\CommandInterface;
use Predis\Command\ScriptCommand;
/**
* Common class implementing the logic needed to support clustering strategies.
*/
abstract class ClusterStrategy implements StrategyInterface
{
protected $commands;
public function __construct()
{
$this->commands = $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 streams */
'XADD' => $getKeyFromFirstArgument,
'XDEL' => $getKeyFromFirstArgument,
'XRANGE' => $getKeyFromFirstArgument,
/* commands operating on HyperLogLog */
'PFADD' => $getKeyFromFirstArgument,
'PFCOUNT' => $getKeyFromAllArguments,
'PFMERGE' => $getKeyFromAllArguments,
/* scripting */
'EVAL' => [$this, 'getKeyFromScriptingCommands'],
'EVALSHA' => [$this, 'getKeyFromScriptingCommands'],
'EVAL_RO' => [$this, 'getKeyFromScriptingCommands'],
'EVALSHA_RO' => [$this, 'getKeyFromScriptingCommands'],
/* server */
'INFO' => [$this, 'getFakeKey'],
/* commands performing geospatial operations */
'GEOADD' => $getKeyFromFirstArgument,
'GEOHASH' => $getKeyFromFirstArgument,
'GEOPOS' => $getKeyFromFirstArgument,
'GEODIST' => $getKeyFromFirstArgument,
'GEORADIUS' => [$this, 'getKeyFromGeoradiusCommands'],
'GEORADIUSBYMEMBER' => [$this, 'getKeyFromGeoradiusCommands'],
/* sharded pubsub */
'SSUBSCRIBE' => $getKeyFromAllArguments,
'SUNSUBSCRIBE' => [$this, 'getKeyFromSUnsubscribeCommand'],
'SPUBLISH' => $getKeyFromFirstArgument,
/* cluster */
'CLUSTER' => [$this, 'getFakeKey'],
/* control */
'ACL' => [$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 key from SUNSUBSCRIBE command if it's given.
*
* @param CommandInterface $command
* @return string
*/
protected function getKeyFromSUnsubscribeCommand(CommandInterface $command): ?string
{
$arguments = $command->getArguments();
// SUNSUBSCRIBE command could be called without arguments, so it doesn't matter on each node it will be called.
if (empty($arguments)) {
return 'fake';
}
return $this->getKeyFromAllArguments($command);
}
/**
* 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;
}
/**
* {@inheritdoc}
*/
public function checkSameSlotForKeys(array $keys): bool
{
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;
}
}
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;
}
}
@@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster\Distributor;
use Predis\Cluster\Hash\HashGeneratorInterface;
/**
* A distributor implements the logic to automatically distribute keys among
* several nodes for client-side sharding.
*/
interface DistributorInterface
{
/**
* Adds a node to the distributor with an optional weight.
*
* @param mixed $node Node object.
* @param int $weight Weight for the node.
*/
public function add($node, $weight = null);
/**
* Removes a node from the distributor.
*
* @param mixed $node Node object.
*/
public function remove($node);
/**
* Returns the corresponding slot of a node from the distributor using the
* computed hash of a key.
*
* @param mixed $hash
*
* @return mixed
*/
public function getSlot($hash);
/**
* Returns a node from the distributor using its assigned slot ID.
*
* @param mixed $slot
*
* @return mixed|null
*/
public function getBySlot($slot);
/**
* Returns a node from the distributor using the computed hash of a key.
*
* @param mixed $hash
*
* @return mixed
*/
public function getByHash($hash);
/**
* Returns a node from the distributor mapping to the specified value.
*
* @param string $value
*
* @return mixed
*/
public function get($value);
/**
* Returns the underlying hash generator instance.
*
* @return HashGeneratorInterface
*/
public function getHashGenerator();
}
@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster\Distributor;
use Exception;
/**
* Exception class that identifies empty rings.
*/
class EmptyRingException extends Exception
{
}
@@ -0,0 +1,268 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster\Distributor;
use Predis\Cluster\Hash\HashGeneratorInterface;
/**
* This class implements an hashring-based distributor that uses the same
* algorithm of memcache to distribute keys in a cluster using client-side
* sharding.
* @author Lorenzo Castelli <lcastelli@gmail.com>
*/
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;
}
}
@@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster\Distributor;
/**
* This class implements an hashring-based distributor that uses the same
* algorithm of libketama to distribute keys in a cluster using client-side
* sharding.
* @author Lorenzo Castelli <lcastelli@gmail.com>
*/
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;
}
}
@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster\Hash;
/**
* Hash generator implementing the CRC-CCITT-16 algorithm used by redis-cluster.
*/
class CRC16 implements HashGeneratorInterface
{
private static $CCITT_16 = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0,
];
/**
* {@inheritdoc}
*/
public function hash($value)
{
// CRC-CCITT-16 algorithm
$crc = 0;
$CCITT_16 = self::$CCITT_16;
$value = (string) $value;
$strlen = strlen($value);
for ($i = 0; $i < $strlen; ++$i) {
$crc = (($crc << 8) ^ $CCITT_16[($crc >> 8) ^ ord($value[$i])]) & 0xFFFF;
}
return $crc;
}
}
@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster\Hash;
/**
* An hash generator implements the logic used to calculate the hash of a key to
* distribute operations among Redis nodes.
*/
interface HashGeneratorInterface
{
/**
* Generates an hash from a string to be used for distribution.
*
* @param string $value String value.
*
* @return int
*/
public function hash($value);
}
@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
/**
* Represents the gap between slot ranges.
*/
class NullSlotRange extends SlotRange
{
public function __construct(int $start, int $end)
{
parent::__construct($start, $end, '');
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
return [];
}
/**
* {@inheritDoc}
*/
public function count(): int
{
return 0;
}
}
@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
use Predis\Cluster\Distributor\DistributorInterface;
use Predis\Cluster\Distributor\HashRing;
/**
* Default cluster strategy used by Predis to handle client-side sharding.
*/
class PredisStrategy extends ClusterStrategy
{
protected $distributor;
/**
* @param DistributorInterface|null $distributor Optional distributor instance.
*/
public function __construct(?DistributorInterface $distributor = null)
{
parent::__construct();
$this->distributor = $distributor ?: new HashRing();
}
/**
* {@inheritdoc}
*/
public function getSlotByKey($key)
{
$key = $this->extractKeyTag($key);
$hash = $this->distributor->hash($key);
return $this->distributor->getSlot($hash);
}
/**
* {@inheritdoc}
*/
public function checkSameSlotForKeys(array $keys): bool
{
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;
}
}
return true;
}
/**
* {@inheritdoc}
*/
public function getDistributor()
{
return $this->distributor;
}
}
@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
use Predis\Cluster\Hash\CRC16;
use Predis\Cluster\Hash\HashGeneratorInterface;
use Predis\NotSupportedException;
/**
* Default class used by Predis to calculate hashes out of keys of
* commands supported by redis-cluster.
*/
class RedisStrategy extends ClusterStrategy
{
protected $hashGenerator;
/**
* @param HashGeneratorInterface|null $hashGenerator Hash generator instance.
*/
public function __construct(?HashGeneratorInterface $hashGenerator = null)
{
parent::__construct();
$this->hashGenerator = $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");
}
}
@@ -0,0 +1,209 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
use ArrayAccess;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use OutOfBoundsException;
use Predis\Connection\NodeConnectionInterface;
use ReturnTypeWillChange;
use Traversable;
/**
* Slot map for redis-cluster.
*/
class SimpleSlotMap implements ArrayAccess, IteratorAggregate, Countable
{
private $slots = [];
/**
* Checks if the given slot is valid.
*
* @param int $slot Slot index.
*
* @return bool
*/
public static function isValid($slot)
{
return $slot >= 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<int, string>
*/
#[ReturnTypeWillChange]
public function getIterator()
{
return new ArrayIterator($this->slots);
}
}
@@ -0,0 +1,417 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
use ArrayAccess;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use OutOfBoundsException;
use Predis\Connection\NodeConnectionInterface;
use ReturnTypeWillChange;
use Traversable;
/**
* Compact slot map for redis-cluster.
*/
class SlotMap implements ArrayAccess, IteratorAggregate, Countable
{
/**
* Slot ranges list.
*
* @var SlotRange[]
*/
private $slotRanges = [];
/**
* Checks if the given slot is valid.
*
* @param int $slot Slot index.
*
* @return bool
*/
public static function isValid($slot)
{
return $slot >= 0 && $slot <= SlotRange::MAX_SLOTS;
}
/**
* 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 SlotRange::isValidRange($first, $last);
}
/**
* Resets the slot map.
*/
public function reset()
{
$this->slotRanges = [];
}
/**
* Checks if the slot map is empty.
*
* @return bool
*/
public function isEmpty()
{
return empty($this->slotRanges);
}
/**
* 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 array_reduce(
$this->slotRanges,
function ($carry, $slotRange) {
return $carry + $slotRange->toArray();
},
[]
);
}
/**
* Returns the list of unique nodes in the slot map.
*
* @return array
*/
public function getNodes()
{
return array_unique(array_map(
function ($slotRange) {
return $slotRange->getConnection();
},
$this->slotRanges
));
}
/**
* Returns the list of slot ranges.
*
* @return SlotRange[]
*/
public function getSlotRanges()
{
return $this->slotRanges;
}
/**
* 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`");
}
$targetSlotRange = new SlotRange($first, $last, (string) $connection);
// Get gaps of slot ranges list.
$gaps = $this->getGaps($this->slotRanges);
$results = $this->slotRanges;
foreach ($gaps as $gap) {
if (!$gap->hasIntersectionWith($targetSlotRange)) {
continue;
}
// Get intersection of the gap and target slot range.
$results[] = new SlotRange(
max($gap->getStart(), $targetSlotRange->getStart()),
min($gap->getEnd(), $targetSlotRange->getEnd()),
$targetSlotRange->getConnection()
);
}
$this->sortSlotRanges($results);
$results = $this->compactSlotRanges($results);
$this->slotRanges = $results;
}
/**
* Returns the specified slot range.
*
* @param int $first Initial slot of the range.
* @param int $last Last slot of the range.
*
* @return array<int, string>
*/
public function getSlots($first, $last)
{
if (!static::isValidRange($first, $last)) {
throw new OutOfBoundsException("Invalid slot range $first-$last");
}
$placeHolder = new NullSlotRange($first, $last);
$intersections = [];
foreach ($this->slotRanges as $slotRange) {
if (!$placeHolder->hasIntersectionWith($slotRange)) {
continue;
}
$intersections[] = new SlotRange(
max($placeHolder->getStart(), $slotRange->getStart()),
min($placeHolder->getEnd(), $slotRange->getEnd()),
$slotRange->getConnection()
);
}
return array_reduce(
$intersections,
function ($carry, $slotRange) {
return $carry + $slotRange->toArray();
},
[]
);
}
/**
* Checks if the specified slot is assigned.
*
* @param int $slot Slot index.
*
* @return bool
*/
#[ReturnTypeWillChange]
public function offsetExists($slot)
{
return $this->findRangeBySlot($slot) !== false;
}
/**
* Returns the node assigned to the specified slot.
*
* @param int $slot Slot index.
*
* @return string|null
*/
#[ReturnTypeWillChange]
public function offsetGet($slot)
{
$found = $this->findRangeBySlot($slot);
return $found ? $found->getConnection() : 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->offsetUnset($slot);
$this->setSlots($slot, $slot, $connection);
}
/**
* Returns the node assigned to the specified slot.
*
* @param int $slot Slot index.
*
* @return void
*/
#[ReturnTypeWillChange]
public function offsetUnset($slot)
{
if (!static::isValid($slot)) {
throw new OutOfBoundsException("Invalid slot $slot");
}
$results = [];
foreach ($this->slotRanges as $slotRange) {
if (!$slotRange->hasSlot($slot)) {
$results[] = $slotRange;
}
if (static::isValidRange($slotRange->getStart(), $slot - 1)) {
$results[] = new SlotRange($slotRange->getStart(), $slot - 1, $slotRange->getConnection());
}
if (static::isValidRange($slot + 1, $slotRange->getEnd())) {
$results[] = new SlotRange($slot + 1, $slotRange->getEnd(), $slotRange->getConnection());
}
}
$this->slotRanges = $results;
}
/**
* Returns the current number of assigned slots.
*
* @return int
*/
#[ReturnTypeWillChange]
public function count()
{
return array_sum(array_map(
function ($slotRange) {
return $slotRange->count();
},
$this->slotRanges
));
}
/**
* Returns an iterator over the slot map.
*
* @return Traversable<int, string>
*/
#[ReturnTypeWillChange]
public function getIterator()
{
return new ArrayIterator($this->toArray());
}
/**
* Find the slot range which contains the specific slot index.
*
* @param int $slot Slot index.
*
* @return SlotRange|false The slot range object or false if not found.
*/
protected function findRangeBySlot(int $slot)
{
foreach ($this->slotRanges as $slotRange) {
if ($slotRange->hasSlot($slot)) {
return $slotRange;
}
}
return false;
}
/**
* Get gaps between sorted slot ranges with NullSlotRange object.
*
* @param SlotRange[] $slotRanges
*
* @return SlotRange[]
*/
protected function getGaps(array $slotRanges)
{
if (empty($slotRanges)) {
return [
new NullSlotRange(0, SlotRange::MAX_SLOTS),
];
}
$gaps = [];
$count = count($slotRanges);
$i = 0;
foreach ($slotRanges as $key => $slotRange) {
$start = $slotRange->getStart();
$end = $slotRange->getEnd();
if (static::isValidRange($i, $start - 1)) {
$gaps[] = new NullSlotRange($i, $start - 1);
}
$i = $end + 1;
if ($key === $count - 1) {
if (static::isValidRange($i, SlotRange::MAX_SLOTS)) {
$gaps[] = new NullSlotRange($i, SlotRange::MAX_SLOTS);
}
}
}
return $gaps;
}
/**
* Sort slot ranges by start index.
*
* @param SlotRange[] $slotRanges
*
* @return void
*/
protected function sortSlotRanges(array &$slotRanges)
{
usort(
$slotRanges,
function (SlotRange $a, SlotRange $b) {
if ($a->getStart() == $b->getStart()) {
return 0;
}
return $a->getStart() < $b->getStart() ? -1 : 1;
}
);
}
/**
* Compact adjacent slot ranges with the same connection.
*
* @param SlotRange[] $slotRanges
*
* @return SlotRange[]
*/
protected function compactSlotRanges(array $slotRanges)
{
if (empty($slotRanges)) {
return [];
}
$compacted = [];
$count = count($slotRanges);
$i = 0;
$carry = $slotRanges[0];
while ($i < $count) {
$next = $slotRanges[$i + 1] ?? null;
if (
!is_null($next)
&& ($carry->getEnd() + 1) === $next->getStart()
&& $carry->getConnection() === $next->getConnection()
) {
$carry = new SlotRange($carry->getStart(), $next->getEnd(), $carry->getConnection());
} else {
$compacted[] = $carry;
$carry = $next;
}
$i++;
}
return array_values($compacted);
}
}
@@ -0,0 +1,145 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
use Countable;
use OutOfBoundsException;
/**
* Represents a range of slots in a Redis cluster.
*/
class SlotRange implements Countable
{
/**
* Maximum number of slots in a Redis cluster is 16384.
*/
public const MAX_SLOTS = 0x3FFF;
/**
* Starting slot of the range.
*
* @var int
*/
protected $start;
/**
* Ending slot of the range.
*
* @var int
*/
protected $end;
/**
* Connection to the server hosting this slot range.
*
* @var string
*/
protected $connection;
public function __construct(int $start, int $end, string $connection)
{
if (!static::isValidRange($start, $end)) {
throw new OutOfBoundsException("Invalid slot range $start-$end for `$connection`");
}
$this->start = $start;
$this->end = $end;
$this->connection = $connection;
}
/**
* Checks if a slot range is valid.
*
* @param int $first
* @param int $last
*
* @return bool
*/
public static function isValidRange($first, $last)
{
return $first >= 0x0000 && $first <= self::MAX_SLOTS && $last >= 0x0000 && $last <= self::MAX_SLOTS && $first <= $last;
}
/**
* Returns the start slot index of this range.
*
* @return int
*/
public function getStart()
{
return $this->start;
}
/**
* Returns the end slot index of this range.
*
* @return int
*/
public function getEnd()
{
return $this->end;
}
/**
* Returns the connection to the server hosting this slot range.
*
* @return string
*/
public function getConnection()
{
return $this->connection;
}
/**
* Checks if the specific slot is contained in this range.
*
* @param int $slot
*
* @return bool
*/
public function hasSlot(int $slot)
{
return $this->start <= $slot && $this->end >= $slot;
}
/**
* Returns an array of connection strings for each slot in this range.
*
* @return string[]
*/
public function toArray(): array
{
return array_fill($this->start, $this->end - $this->start + 1, $this->connection);
}
/**
* Returns the number of slots in this range.
*
* @return int
*/
public function count(): int
{
return $this->end - $this->start + 1;
}
/**
* Checks if this range has an intersection with the given slot range.
*
* @param SlotRange $slotRange
*
* @return bool
*/
public function hasIntersectionWith(SlotRange $slotRange): bool
{
return $this->start <= $slotRange->getEnd() && $this->end >= $slotRange->getStart();
}
}
@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
use Predis\Cluster\Distributor\DistributorInterface;
use Predis\Command\CommandInterface;
/**
* Interface for classes defining the strategy used to calculate an hash out of
* keys extracted from supported commands.
*
* This is mostly useful to support clustering via client-side sharding.
*/
interface StrategyInterface
{
/**
* Returns a slot for the given command used for clustering distribution or
* NULL when this is not possible.
*
* @param CommandInterface $command Command instance.
*
* @return int|null
*/
public function getSlot(CommandInterface $command);
/**
* Returns a slot for the given key used for clustering distribution or NULL
* when this is not possible.
*
* @param string $key Key string.
*
* @return int|null
*/
public function getSlotByKey($key);
/**
* Returns a distributor instance to be used by the cluster.
*
* @return DistributorInterface
*/
public function getDistributor();
/**
* Checks if the specified array of keys will generate the same hash.
*
* @param array $keys
* @return bool
*/
public function checkSameSlotForKeys(array $keys): bool;
}
@@ -0,0 +1,196 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Collection\Iterator;
use Iterator;
use Predis\ClientInterface;
use Predis\NotSupportedException;
use ReturnTypeWillChange;
/**
* Provides the base implementation for a fully-rewindable PHP iterator that can
* incrementally iterate over cursor-based collections stored on Redis using the
* commands in the `SCAN` family.
*
* Given their incremental nature with multiple fetches, these kind of iterators
* offer limited guarantees about the returned elements because the collection
* can change several times during the iteration process.
*
* @see http://redis.io/commands/scan
*/
abstract class CursorBasedIterator implements Iterator
{
protected $client;
protected $match;
protected $count;
protected $valid;
protected $fetchmore;
protected $elements;
protected $cursor;
protected $position;
protected $current;
/**
* @param ClientInterface $client Client connected to Redis.
* @param string $match Pattern to match during the server-side iteration.
* @param int $count Hint used by Redis to compute the number of results per iteration.
*/
public function __construct(ClientInterface $client, $match = null, $count = null)
{
$this->client = $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;
}
}
@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Collection\Iterator;
use Predis\ClientInterface;
/**
* Abstracts the iteration of fields and values of an hash by leveraging the
* HSCAN command (Redis >= 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]);
}
}
@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Collection\Iterator;
use Predis\ClientInterface;
/**
* Abstracts the iteration of the keyspace on a Redis instance by leveraging the
* SCAN command (Redis >= 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());
}
}
@@ -0,0 +1,183 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Collection\Iterator;
use InvalidArgumentException;
use Iterator;
use Predis\ClientInterface;
use Predis\NotSupportedException;
use ReturnTypeWillChange;
/**
* Abstracts the iteration of items stored in a list by leveraging the LRANGE
* command wrapped in a fully-rewindable PHP iterator.
*
* This iterator tries to emulate the behaviour of cursor-based iterators based
* on the SCAN-family of commands introduced in Redis <= 2.8, meaning that due
* to its incremental nature with multiple fetches it can only offer limited
* guarantees on the returned elements because the collection can change several
* times (trimmed, deleted, overwritten) during the iteration process.
*
* @see http://redis.io/commands/lrange
*/
class ListKey implements Iterator
{
protected $client;
protected $count;
protected $key;
protected $valid;
protected $fetchmore;
protected $elements;
protected $position;
protected $current;
/**
* @param ClientInterface $client Client connected to Redis.
* @param string $key Redis list key.
* @param int $count Number of items retrieved on each fetch operation.
*
* @throws InvalidArgumentException
*/
public function __construct(ClientInterface $client, $key, $count = 10)
{
$this->requiredCommand($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;
}
}
@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Collection\Iterator;
use Predis\ClientInterface;
/**
* Abstracts the iteration of members stored in a set by leveraging the SSCAN
* command (Redis >= 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());
}
}
@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Collection\Iterator;
use Predis\ClientInterface;
/**
* Abstracts the iteration of members stored in a sorted set by leveraging the
* ZSCAN command (Redis >= 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]);
}
}
@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument;
/**
* Allows to use object-oriented approach to handle complex conditional arguments.
*/
interface ArrayableArgument
{
/**
* Get the instance as an array.
*
* @return array
*/
public function toArray(): array;
}
@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Geospatial;
use UnexpectedValueException;
abstract class AbstractBy implements ByInterface
{
/**
* @var string[]
*/
private static $unitEnum = ['m', 'km', 'ft', 'mi'];
/**
* @var string
*/
protected $unit;
/**
* {@inheritDoc}
*/
abstract public function toArray(): array;
/**
* @param string $unit
* @return void
*/
protected function setUnit(string $unit): void
{
if (!in_array($unit, self::$unitEnum, true)) {
throw new UnexpectedValueException('Wrong value given for unit');
}
$this->unit = $unit;
}
}
@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Geospatial;
class ByBox extends AbstractBy
{
private const KEYWORD = 'BYBOX';
/**
* @var int
*/
private $width;
/**
* @var int
*/
private $height;
public function __construct(int $width, int $height, string $unit)
{
$this->width = $width;
$this->height = $height;
$this->setUnit($unit);
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
return [self::KEYWORD, $this->width, $this->height, $this->unit];
}
}
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Geospatial;
use Predis\Command\Argument\ArrayableArgument;
interface ByInterface extends ArrayableArgument
{
}
@@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Geospatial;
class ByRadius extends AbstractBy
{
private const KEYWORD = 'BYRADIUS';
/**
* @var int
*/
private $radius;
public function __construct(int $radius, string $unit)
{
$this->radius = $radius;
$this->setUnit($unit);
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
return [self::KEYWORD, $this->radius, $this->unit];
}
}
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Geospatial;
use Predis\Command\Argument\ArrayableArgument;
interface FromInterface extends ArrayableArgument
{
}
@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Geospatial;
class FromLonLat implements FromInterface
{
private const KEYWORD = 'FROMLONLAT';
/**
* @var float
*/
private $longitude;
/**
* @var float
*/
private $latitude;
public function __construct(float $longitude, float $latitude)
{
$this->longitude = $longitude;
$this->latitude = $latitude;
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
return [self::KEYWORD, $this->longitude, $this->latitude];
}
}
@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Geospatial;
class FromMember implements FromInterface
{
private const KEYWORD = 'FROMMEMBER';
/**
* @var string
*/
private $member;
public function __construct(string $member)
{
$this->member = $member;
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
return [self::KEYWORD, $this->member];
}
}
@@ -0,0 +1,161 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search;
class AggregateArguments extends CommonArguments
{
/**
* @var string[]
*/
private $sortingEnum = [
'asc' => '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;
}
}
@@ -0,0 +1,17 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search;
class AlterArguments extends CommonArguments
{
}
@@ -0,0 +1,182 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search;
use Predis\Command\Argument\ArrayableArgument;
class CommonArguments implements ArrayableArgument
{
/**
* @var array
*/
protected $arguments = [];
/**
* Adds default language for documents within an index.
*
* @param string $defaultLanguage
* @return $this
*/
public function language(string $defaultLanguage = 'english'): self
{
$this->arguments[] = 'LANGUAGE';
$this->arguments[] = $defaultLanguage;
return $this;
}
/**
* Selects the dialect version under which to execute the query.
* If not specified, the query will execute under the default dialect version
* set during module initial loading or via FT.CONFIG SET command.
*
* @param string $dialect
* @return $this
*/
public function dialect(string $dialect): self
{
$this->arguments[] = 'DIALECT';
$this->arguments[] = $dialect;
return $this;
}
/**
* If set, does not scan and index.
*
* @return $this
*/
public function skipInitialScan(): self
{
$this->arguments[] = 'SKIPINITIALSCAN';
return $this;
}
/**
* Adds an arbitrary, binary safe payload that is exposed to custom scoring functions.
*
* @param string $payload
* @return $this
*/
public function payload(string $payload): self
{
$this->arguments[] = 'PAYLOAD';
$this->arguments[] = $payload;
return $this;
}
/**
* Also returns the relative internal score of each document.
*
* @return $this
*/
public function withScores(): self
{
$this->arguments[] = 'WITHSCORES';
return $this;
}
/**
* Retrieves optional document payloads.
*
* @return $this
*/
public function withPayloads(): self
{
$this->arguments[] = 'WITHPAYLOADS';
return $this;
}
/**
* Does not try to use stemming for query expansion but searches the query terms verbatim.
*
* @return $this
*/
public function verbatim(): self
{
$this->arguments[] = 'VERBATIM';
return $this;
}
/**
* Overrides the timeout parameter of the module.
*
* @param int $timeout
* @return $this
*/
public function timeout(int $timeout): self
{
$this->arguments[] = 'TIMEOUT';
$this->arguments[] = $timeout;
return $this;
}
/**
* Adds an arbitrary, binary safe payload that is exposed to custom scoring functions.
*
* @param int $offset
* @param int $num
* @return $this
*/
public function limit(int $offset, int $num): self
{
array_push($this->arguments, 'LIMIT', $offset, $num);
return $this;
}
/**
* Adds filter expression into index.
*
* @param string $filter
* @return $this
*/
public function filter(string $filter): self
{
$this->arguments[] = 'FILTER';
$this->arguments[] = $filter;
return $this;
}
/**
* Defines one or more value parameters. Each parameter has a name and a value.
*
* Example: ['name1', 'value1', 'name2', 'value2'...]
*
* @param array $nameValuesDictionary
* @return $this
*/
public function params(array $nameValuesDictionary): self
{
$this->arguments[] = 'PARAMS';
$this->arguments[] = count($nameValuesDictionary);
$this->arguments = array_merge($this->arguments, $nameValuesDictionary);
return $this;
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
return $this->arguments;
}
}
@@ -0,0 +1,191 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search;
use InvalidArgumentException;
class CreateArguments extends CommonArguments
{
/**
* @var string[]
*/
private $supportedDataTypesEnum = [
'hash' => '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;
}
}
@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search;
use Predis\Command\Argument\ArrayableArgument;
class CursorArguments implements ArrayableArgument
{
/**
* @var array
*/
protected $arguments = [];
/**
* Is number of results to read. This parameter overrides COUNT specified in FT.AGGREGATE.
*
* @param int $readSize
* @return $this
*/
public function count(int $readSize): self
{
array_push($this->arguments, 'COUNT', $readSize);
return $this;
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
return $this->arguments;
}
}
@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search;
use Predis\Command\Argument\ArrayableArgument;
class DropArguments implements ArrayableArgument
{
/**
* @var array
*/
protected $arguments = [];
/**
* Drop operation that, if set, deletes the actual document hashes.
*
* @return $this
*/
public function dd(): self
{
$this->arguments[] = 'DD';
return $this;
}
/**
* @return array
*/
public function toArray(): array
{
return $this->arguments;
}
}
@@ -0,0 +1,17 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search;
class ExplainArguments extends CommonArguments
{
}
@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search\HybridSearch\Combine;
use Predis\Command\Argument\ArrayableArgument;
abstract class BaseCombine implements ArrayableArgument
{
/**
* @var array
*/
protected $arguments = ['COMBINE'];
/**
* @var array
*/
protected $as = [];
/**
* @param string $alias
* @return $this
*/
public function as(string $alias): self
{
array_push($this->as, 'YIELD_SCORE_AS', $alias);
return $this;
}
/**
* {@inheritDoc}
*/
abstract public function toArray(): array;
}
@@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search\HybridSearch\Combine;
class LinearCombineConfig extends BaseCombine
{
/**
* @var float
*/
protected $alpha;
/**
* @var float
*/
protected $beta;
/**
* The weight for the text score (a value between 0 and 1).
*
* @param float $alpha
* @return $this
*/
public function alpha(float $alpha): self
{
$this->alpha = $alpha;
return $this;
}
/**
* The weight for the vector score (a value between 0 and 1).
*
* @param float $beta
* @return $this
*/
public function beta(float $beta): self
{
$this->beta = $beta;
return $this;
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
$this->arguments[] = 'LINEAR';
$tokens = [];
if ($this->alpha !== null) {
array_push($tokens, 'ALPHA', $this->alpha);
}
if ($this->beta !== null) {
array_push($tokens, 'BETA', $this->beta);
}
if ($this->as) {
array_push($tokens, ...$this->as);
}
if (!empty($tokens)) {
array_push($this->arguments, count($tokens), ...$tokens);
}
return $this->arguments;
}
}
@@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search\HybridSearch\Combine;
class RRFCombineConfig extends BaseCombine
{
/**
* @var int
*/
protected $window;
/**
* @var int
*/
protected $rrfConstant;
/**
* The number of top results from each search type to consider for fusion. Defaults to 50.
*
* @param int $window
* @return $this
*/
public function window(int $window): self
{
$this->window = $window;
return $this;
}
/**
* The RRF ranking constant. A smaller value gives more weight to top-ranked items. Defaults to 60.
*
* @param int $constant
* @return $this
*/
public function rrfConstant(int $constant): self
{
$this->rrfConstant = $constant;
return $this;
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
$this->arguments[] = 'RRF';
$tokens = [];
if ($this->window !== null) {
array_push($tokens, 'WINDOW', $this->window);
}
if ($this->rrfConstant !== null) {
array_push($tokens, 'CONSTANT', $this->rrfConstant);
}
if ($this->as) {
array_push($tokens, ...$this->as);
}
if (!empty($tokens)) {
array_push($this->arguments, count($tokens), ...$tokens);
}
return $this->arguments;
}
}
@@ -0,0 +1,352 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search\HybridSearch;
use Predis\Command\Argument\ArrayableArgument;
use Predis\Command\Argument\Search\HybridSearch\Combine\LinearCombineConfig;
use Predis\Command\Argument\Search\HybridSearch\Combine\RRFCombineConfig;
use Predis\Command\Argument\Search\HybridSearch\VectorSearch\KNNVectorSearchConfig;
use Predis\Command\Argument\Search\HybridSearch\VectorSearch\RangeVectorSearchConfig;
use Predis\Command\Redis\Utils\CommandUtility;
use ValueError;
class HybridSearchQuery implements ArrayableArgument
{
public const SORT_ASC = 'ASC';
public const SORT_DESC = 'DESC';
/**
* @var SearchConfig
*/
protected $searchConfig;
/**
* The vector search portion of the query.
*
* @var KNNVectorSearchConfig|RangeVectorSearchConfig
*/
protected $vectorSearchConfig;
/**
* Configuration for the score fusion method (optional).
* If not provided, Reciprocal Rank Fusion (RRF) is used with server-side default parameters.
*
* @var RRFCombineConfig|LinearCombineConfig
*/
protected $combineConfig;
/**
* @var array
*/
protected $load = [];
/**
* @var array
*/
protected $groupBy = [];
/**
* @var array
*/
protected $apply = [];
/**
* @var array
*/
protected $sortBy = [];
/**
* @var string
*/
protected $filter;
/**
* @var array
*/
protected $limit = [];
/**
* @var array
*/
protected $params = [];
/**
* @var bool
*/
protected $explainScore = false;
/**
* @var bool
*/
protected $timeout = false;
/**
* @var array
*/
protected $withCursor = [];
/**
* @var array
*/
protected $arguments = [];
/**
* @param string $vectorSearchMethod Class type of desired vector search method
* @param string $combineMethod Class type of desired combine method
*/
public function __construct(
string $vectorSearchMethod = KNNVectorSearchConfig::class,
string $combineMethod = RRFCombineConfig::class
) {
$this->searchConfig = new SearchConfig();
$this->vectorSearchConfig = new $vectorSearchMethod();
$this->combineConfig = new $combineMethod();
}
/**
* @param callable(SearchConfig): void $callable
* @return $this
*/
public function buildSearchConfig(callable $callable): self
{
$callable($this->searchConfig);
return $this;
}
/**
* @param callable(KNNVectorSearchConfig|RangeVectorSearchConfig): void $callable
* @return $this
*/
public function buildVectorSearchConfig(callable $callable): self
{
$callable($this->vectorSearchConfig);
return $this;
}
/**
* @param callable(RRFCombineConfig|LinearCombineConfig): void $callable
* @return $this
*/
public function buildCombineConfig(callable $callable): self
{
$callable($this->combineConfig);
return $this;
}
/**
* The list of fields to return in the results.
*
* @param array $fields
* @return $this
*/
public function load(array $fields): self
{
array_push($this->load, 'LOAD', count($fields), ...$fields);
return $this;
}
/**
* @param array $fields
* @param Reducer[] $reducers
* @return $this
*/
public function groupBy(array $fields, array $reducers): self
{
array_push($this->groupBy, 'GROUPBY', count($fields), ...$fields);
foreach ($reducers as $reducer) {
array_push($this->groupBy, 'REDUCE', ...$reducer->toArray());
}
return $this;
}
/**
* @param array $expressionFieldDict field => function dictionary
* @return $this
*/
public function apply(array $expressionFieldDict): self
{
foreach ($expressionFieldDict as $field => $function) {
array_push($this->apply, 'APPLY', $function, 'AS', $field);
}
return $this;
}
/**
* Sorts the final results by a specific field.
*
* @param array<string, string> $fields Dictionary with fields and sort direction. Check class constants.
* @return $this
*/
public function sortBy(array $fields): self
{
$fieldsArray = [];
foreach ($fields as $field => $direction) {
if (!in_array(strtoupper($direction), [self::SORT_ASC, self::SORT_DESC])) {
throw new ValueError('Sort direction must be one of "ASC" or "DESC".');
}
array_push($fieldsArray, $field, $direction);
}
array_push($this->sortBy, 'SORTBY', count($fieldsArray), ...$fieldsArray);
return $this;
}
/**
* Final result filtering.
*
* @param string $expression
* @return $this
*/
public function filter(string $expression): self
{
$this->filter = $expression;
return $this;
}
/**
* @param int $offset
* @param int $num
* @return $this
*/
public function limit(int $offset, int $num): self
{
array_push($this->limit, 'LIMIT', $offset, $num);
return $this;
}
/**
* Binds values to named parameters in the query string.
*
* @param array $params
* @return $this
*/
public function params(array $params): self
{
$arrayParams = CommandUtility::dictionaryToArray($params);
array_push($this->params, 'PARAMS', count($arrayParams), ...$arrayParams);
return $this;
}
/**
* @return $this
*/
public function explainScore(): self
{
$this->explainScore = true;
return $this;
}
/**
* @return $this
*/
public function timeout(): self
{
$this->timeout = true;
return $this;
}
/**
* @param int|null $readSize
* @param int|null $idleTime
* @return $this
*/
public function withCursor(?int $readSize = null, ?int $idleTime = null): self
{
$this->withCursor[] = 'WITHCURSOR';
if ($readSize) {
array_push($this->withCursor, 'COUNT', $readSize);
}
if ($idleTime) {
array_push($this->withCursor, 'MAXIDLE', $idleTime);
}
return $this;
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
$this->arguments = array_merge(
$this->arguments,
$this->searchConfig->toArray(),
$this->vectorSearchConfig->toArray()
);
$combineConfig = $this->combineConfig->toArray();
// Only add if any configuration was applied
if (count($combineConfig) > 2) {
$this->arguments = array_merge($this->arguments, $combineConfig);
}
if ($this->load) {
$this->arguments = array_merge($this->arguments, $this->load);
}
if ($this->groupBy) {
$this->arguments = array_merge($this->arguments, $this->groupBy);
}
if ($this->apply) {
$this->arguments = array_merge($this->arguments, $this->apply);
}
if ($this->sortBy) {
$this->arguments = array_merge($this->arguments, $this->sortBy);
}
if ($this->filter) {
array_push($this->arguments, 'FILTER', $this->filter);
}
if ($this->limit) {
$this->arguments = array_merge($this->arguments, $this->limit);
}
if ($this->params) {
$this->arguments = array_merge($this->arguments, $this->params);
}
if ($this->explainScore) {
$this->arguments[] = 'EXPLAINSCORE';
}
if ($this->timeout) {
$this->arguments[] = 'TIMEOUT';
}
if ($this->withCursor) {
$this->arguments = array_merge($this->arguments, $this->withCursor);
}
return $this->arguments;
}
}
@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search\HybridSearch;
use Predis\Command\Argument\ArrayableArgument;
class Reducer implements ArrayableArgument
{
public const REDUCE_COUNT = 'COUNT';
public const REDUCE_COUNT_DISTINCT = 'COUNT_DISTINCT';
public const REDUCE_COUNT_DISTINCTISH = 'COUNT_DISTINCTISH';
public const REDUCE_SUM = 'SUM';
public const REDUCE_MIN = 'MIN';
public const REDUCE_MAX = 'MAX';
public const REDUCE_AVG = 'AVG';
public const REDUCE_STDDEV = 'STDDEV';
public const REDUCE_QUANTILE = 'QUANTILE';
/**
* @var array
*/
protected $arguments = [];
/**
* @param string $function One of the available functions. Check class constants.
* @param array $arguments List of properties
*/
public function __construct(string $function = self::REDUCE_COUNT, array $arguments = [], ?string $alias = null)
{
array_push($this->arguments, $function, count($arguments), ...$arguments);
if ($alias) {
array_push($this->arguments, 'AS', $alias);
}
}
public function toArray(): array
{
return $this->arguments;
}
}
@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search\HybridSearch;
use Predis\Command\Argument\ArrayableArgument;
class ScorerConfig implements ArrayableArgument
{
public const TYPE_BM25 = 'BM25';
public const TYPE_TFIDF = 'TFIDF';
public const TYPE_DISMAX = 'DISMAX';
public const TYPE_DOCSCORE = 'DOCSCORE';
/**
* @var array
*/
protected $arguments = [];
/**
* The text scoring algorithm. Defaults to BM25.
*
* @param string $type
* @return $this
*/
public function type(string $type = self::TYPE_BM25): self
{
$this->arguments[] = $type;
return $this;
}
/**
* An alias for the text score field in the results.
* The aliased field will be included in the `value` object of each returned document.
*
* @param string $alias
* @return $this
*/
public function as(string $alias): self
{
array_push($this->arguments, 'YIELD_SCORE_AS', $alias);
return $this;
}
public function toArray(): array
{
return $this->arguments;
}
}
@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search\HybridSearch;
use Predis\Command\Argument\ArrayableArgument;
class SearchConfig implements ArrayableArgument
{
/**
* @var array
*/
protected $arguments = ['SEARCH'];
/**
* @var ScorerConfig
*/
protected $scorerConfig;
public function __construct()
{
$this->scorerConfig = new ScorerConfig();
}
/**
* Search query.
*
* @param string $query
* @return $this
*/
public function query(string $query): self
{
$this->arguments[] = $query;
return $this;
}
/**
* @param string $alias
* @return $this
*/
public function as(string $alias): self
{
array_push($this->arguments, 'YIELD_SCORE_AS', $alias);
return $this;
}
/**
* @param callable(ScorerConfig): void $callable
* @return $this
*/
public function buildScorerConfig(callable $callable): self
{
$callable($this->scorerConfig);
return $this;
}
public function toArray(): array
{
$scorerConfig = $this->scorerConfig->toArray();
if (!empty($scorerConfig)) {
$this->arguments[] = 'SCORER';
$this->arguments = array_merge($this->arguments, $scorerConfig);
}
return $this->arguments;
}
}
@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Search\HybridSearch\VectorSearch;
use Predis\Command\Argument\ArrayableArgument;
abstract class BaseVectorSearchConfig implements ArrayableArgument
{
public const POLICY_ADHOC = 'ADHOC';
public const POLICY_BATCHES = 'BATCHES';
public const POLICY_ACORN = 'ACORN';
/**
* @var array
*/
protected $vector = [];
/**
* @var array
*/
protected $filter = [];
/**
* @var array
*/
protected $as = [];
/**
* @var array
*/
protected $arguments = ['VSIM'];
/**
* Vector to perform search against.
*
* @param string $field The vector field name to search against. Must start with "@".
* @param string $value Name of the parameter to use in the query. Must start with "$".
* @return self
*/
public function vector(string $field, string $value): self
{
array_push($this->vector, $field, $value);
return $this;
}
/**
* @param string $expression
* @return $this
*/
public function filter(string $expression): self
{
array_push($this->filter, 'FILTER', $expression);
return $this;
}
/**
* @param string $alias
* @return $this
*/
public function as(string $alias): self
{
array_push($this->as, 'YIELD_SCORE_AS', $alias);
return $this;
}
abstract public function toArray(): array;
}

Some files were not shown because too many files have changed in this diff Show More