├── Connections ├── Connection.php ├── PacksPhpRedisValues.php ├── PhpRedisClusterConnection.php ├── PhpRedisConnection.php ├── PredisClusterConnection.php └── PredisConnection.php ├── Connectors ├── PhpRedisConnector.php └── PredisConnector.php ├── Events └── CommandExecuted.php ├── LICENSE.md ├── Limiters ├── ConcurrencyLimiter.php ├── ConcurrencyLimiterBuilder.php ├── DurationLimiter.php └── DurationLimiterBuilder.php ├── RedisManager.php ├── RedisServiceProvider.php └── composer.json /Connections/Connection.php: -------------------------------------------------------------------------------- 1 | client; 79 | } 80 | 81 | /** 82 | * Subscribe to a set of given channels for messages. 83 | * 84 | * @param array|string $channels 85 | * @param \Closure $callback 86 | * @return void 87 | */ 88 | public function subscribe($channels, Closure $callback) 89 | { 90 | $this->createSubscription($channels, $callback, __FUNCTION__); 91 | } 92 | 93 | /** 94 | * Subscribe to a set of given channels with wildcards. 95 | * 96 | * @param array|string $channels 97 | * @param \Closure $callback 98 | * @return void 99 | */ 100 | public function psubscribe($channels, Closure $callback) 101 | { 102 | $this->createSubscription($channels, $callback, __FUNCTION__); 103 | } 104 | 105 | /** 106 | * Run a command against the Redis database. 107 | * 108 | * @param string $method 109 | * @param array $parameters 110 | * @return mixed 111 | */ 112 | public function command($method, array $parameters = []) 113 | { 114 | $start = microtime(true); 115 | 116 | $result = $this->client->{$method}(...$parameters); 117 | 118 | $time = round((microtime(true) - $start) * 1000, 2); 119 | 120 | $this->events?->dispatch(new CommandExecuted( 121 | $method, $this->parseParametersForEvent($parameters), $time, $this 122 | )); 123 | 124 | return $result; 125 | } 126 | 127 | /** 128 | * Parse the command's parameters for event dispatching. 129 | * 130 | * @param array $parameters 131 | * @return array 132 | */ 133 | protected function parseParametersForEvent(array $parameters) 134 | { 135 | return $parameters; 136 | } 137 | 138 | /** 139 | * Fire the given event if possible. 140 | * 141 | * @param mixed $event 142 | * @return void 143 | * 144 | * @deprecated since Laravel 11.x 145 | */ 146 | protected function event($event) 147 | { 148 | $this->events?->dispatch($event); 149 | } 150 | 151 | /** 152 | * Register a Redis command listener with the connection. 153 | * 154 | * @param \Closure $callback 155 | * @return void 156 | */ 157 | public function listen(Closure $callback) 158 | { 159 | $this->events?->listen(CommandExecuted::class, $callback); 160 | } 161 | 162 | /** 163 | * Get the connection name. 164 | * 165 | * @return string|null 166 | */ 167 | public function getName() 168 | { 169 | return $this->name; 170 | } 171 | 172 | /** 173 | * Set the connections name. 174 | * 175 | * @param string $name 176 | * @return $this 177 | */ 178 | public function setName($name) 179 | { 180 | $this->name = $name; 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * Get the event dispatcher used by the connection. 187 | * 188 | * @return \Illuminate\Contracts\Events\Dispatcher 189 | */ 190 | public function getEventDispatcher() 191 | { 192 | return $this->events; 193 | } 194 | 195 | /** 196 | * Set the event dispatcher instance on the connection. 197 | * 198 | * @param \Illuminate\Contracts\Events\Dispatcher $events 199 | * @return void 200 | */ 201 | public function setEventDispatcher(Dispatcher $events) 202 | { 203 | $this->events = $events; 204 | } 205 | 206 | /** 207 | * Unset the event dispatcher instance on the connection. 208 | * 209 | * @return void 210 | */ 211 | public function unsetEventDispatcher() 212 | { 213 | $this->events = null; 214 | } 215 | 216 | /** 217 | * Pass other method calls down to the underlying client. 218 | * 219 | * @param string $method 220 | * @param array $parameters 221 | * @return mixed 222 | */ 223 | public function __call($method, $parameters) 224 | { 225 | if (static::hasMacro($method)) { 226 | return $this->macroCall($method, $parameters); 227 | } 228 | 229 | return $this->command($method, $parameters); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /Connections/PacksPhpRedisValues.php: -------------------------------------------------------------------------------- 1 | $values 36 | * @return array 37 | */ 38 | public function pack(array $values): array 39 | { 40 | if (empty($values)) { 41 | return $values; 42 | } 43 | 44 | if ($this->supportsPacking()) { 45 | return array_map($this->client->_pack(...), $values); 46 | } 47 | 48 | if ($this->compressed()) { 49 | if ($this->supportsLzf() && $this->lzfCompressed()) { 50 | if (! function_exists('lzf_compress')) { 51 | throw new RuntimeException("'lzf' extension required to call 'lzf_compress'."); 52 | } 53 | 54 | $processor = function ($value) { 55 | return \lzf_compress($this->client->_serialize($value)); 56 | }; 57 | } elseif ($this->supportsZstd() && $this->zstdCompressed()) { 58 | if (! function_exists('zstd_compress')) { 59 | throw new RuntimeException("'zstd' extension required to call 'zstd_compress'."); 60 | } 61 | 62 | $compressionLevel = $this->client->getOption(Redis::OPT_COMPRESSION_LEVEL); 63 | 64 | $processor = function ($value) use ($compressionLevel) { 65 | return \zstd_compress( 66 | $this->client->_serialize($value), 67 | $compressionLevel === 0 ? Redis::COMPRESSION_ZSTD_DEFAULT : $compressionLevel 68 | ); 69 | }; 70 | } else { 71 | throw new UnexpectedValueException(sprintf( 72 | 'Unsupported phpredis compression in use [%d].', 73 | $this->client->getOption(Redis::OPT_COMPRESSION) 74 | )); 75 | } 76 | } else { 77 | $processor = function ($value) { 78 | return $this->client->_serialize($value); 79 | }; 80 | } 81 | 82 | return array_map($processor, $values); 83 | } 84 | 85 | /** 86 | * Execute the given callback without serialization or compression when applicable. 87 | * 88 | * @param callable $callback 89 | * @return mixed 90 | */ 91 | public function withoutSerializationOrCompression(callable $callback) 92 | { 93 | $client = $this->client; 94 | 95 | $oldSerializer = null; 96 | 97 | if ($this->serialized()) { 98 | $oldSerializer = $client->getOption(Redis::OPT_SERIALIZER); 99 | $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); 100 | } 101 | 102 | $oldCompressor = null; 103 | 104 | if ($this->compressed()) { 105 | $oldCompressor = $client->getOption(Redis::OPT_COMPRESSION); 106 | $client->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_NONE); 107 | } 108 | 109 | try { 110 | return $callback(); 111 | } finally { 112 | if ($oldSerializer !== null) { 113 | $client->setOption(Redis::OPT_SERIALIZER, $oldSerializer); 114 | } 115 | 116 | if ($oldCompressor !== null) { 117 | $client->setOption(Redis::OPT_COMPRESSION, $oldCompressor); 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * Determine if serialization is enabled. 124 | * 125 | * @return bool 126 | */ 127 | public function serialized(): bool 128 | { 129 | return defined('Redis::OPT_SERIALIZER') && 130 | $this->client->getOption(Redis::OPT_SERIALIZER) !== Redis::SERIALIZER_NONE; 131 | } 132 | 133 | /** 134 | * Determine if compression is enabled. 135 | * 136 | * @return bool 137 | */ 138 | public function compressed(): bool 139 | { 140 | return defined('Redis::OPT_COMPRESSION') && 141 | $this->client->getOption(Redis::OPT_COMPRESSION) !== Redis::COMPRESSION_NONE; 142 | } 143 | 144 | /** 145 | * Determine if LZF compression is enabled. 146 | * 147 | * @return bool 148 | */ 149 | public function lzfCompressed(): bool 150 | { 151 | return defined('Redis::COMPRESSION_LZF') && 152 | $this->client->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZF; 153 | } 154 | 155 | /** 156 | * Determine if ZSTD compression is enabled. 157 | * 158 | * @return bool 159 | */ 160 | public function zstdCompressed(): bool 161 | { 162 | return defined('Redis::COMPRESSION_ZSTD') && 163 | $this->client->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_ZSTD; 164 | } 165 | 166 | /** 167 | * Determine if LZ4 compression is enabled. 168 | * 169 | * @return bool 170 | */ 171 | public function lz4Compressed(): bool 172 | { 173 | return defined('Redis::COMPRESSION_LZ4') && 174 | $this->client->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZ4; 175 | } 176 | 177 | /** 178 | * Determine if the current PhpRedis extension version supports packing. 179 | * 180 | * @return bool 181 | */ 182 | protected function supportsPacking(): bool 183 | { 184 | if ($this->supportsPacking === null) { 185 | $this->supportsPacking = $this->phpRedisVersionAtLeast('5.3.5'); 186 | } 187 | 188 | return $this->supportsPacking; 189 | } 190 | 191 | /** 192 | * Determine if the current PhpRedis extension version supports LZF compression. 193 | * 194 | * @return bool 195 | */ 196 | protected function supportsLzf(): bool 197 | { 198 | if ($this->supportsLzf === null) { 199 | $this->supportsLzf = $this->phpRedisVersionAtLeast('4.3.0'); 200 | } 201 | 202 | return $this->supportsLzf; 203 | } 204 | 205 | /** 206 | * Determine if the current PhpRedis extension version supports Zstd compression. 207 | * 208 | * @return bool 209 | */ 210 | protected function supportsZstd(): bool 211 | { 212 | if ($this->supportsZstd === null) { 213 | $this->supportsZstd = $this->phpRedisVersionAtLeast('5.1.0'); 214 | } 215 | 216 | return $this->supportsZstd; 217 | } 218 | 219 | /** 220 | * Determine if the PhpRedis extension version is at least the given version. 221 | * 222 | * @param string $version 223 | * @return bool 224 | */ 225 | protected function phpRedisVersionAtLeast(string $version): bool 226 | { 227 | $phpredisVersion = phpversion('redis'); 228 | 229 | return $phpredisVersion !== false && version_compare($phpredisVersion, $version, '>='); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /Connections/PhpRedisClusterConnection.php: -------------------------------------------------------------------------------- 1 | client->scan($cursor, 36 | $options['node'] ?? $this->defaultNode(), 37 | $options['match'] ?? '*', 38 | $options['count'] ?? 10 39 | ); 40 | 41 | if ($result === false) { 42 | $result = []; 43 | } 44 | 45 | return $cursor === 0 && empty($result) ? false : [$cursor, $result]; 46 | } 47 | 48 | /** 49 | * Flush the selected Redis database on all master nodes. 50 | * 51 | * @return void 52 | */ 53 | public function flushdb() 54 | { 55 | $arguments = func_get_args(); 56 | 57 | $async = strtoupper((string) ($arguments[0] ?? null)) === 'ASYNC'; 58 | 59 | foreach ($this->client->_masters() as $master) { 60 | $async 61 | ? $this->command('rawCommand', [$master, 'flushdb', 'async']) 62 | : $this->command('flushdb', [$master]); 63 | } 64 | } 65 | 66 | /** 67 | * Return default node to use for cluster. 68 | * 69 | * @return string|array 70 | * 71 | * @throws \InvalidArgumentException 72 | */ 73 | private function defaultNode() 74 | { 75 | if (! isset($this->defaultNode)) { 76 | $this->defaultNode = $this->client->_masters()[0] ?? throw new InvalidArgumentException('Unable to determine default node. No master nodes found in the cluster.'); 77 | } 78 | 79 | return $this->defaultNode; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Connections/PhpRedisConnection.php: -------------------------------------------------------------------------------- 1 | client = $client; 42 | $this->config = $config; 43 | $this->connector = $connector; 44 | } 45 | 46 | /** 47 | * Returns the value of the given key. 48 | * 49 | * @param string $key 50 | * @return string|null 51 | */ 52 | public function get($key) 53 | { 54 | $result = $this->command('get', [$key]); 55 | 56 | return $result !== false ? $result : null; 57 | } 58 | 59 | /** 60 | * Get the values of all the given keys. 61 | * 62 | * @param array $keys 63 | * @return array 64 | */ 65 | public function mget(array $keys) 66 | { 67 | return array_map(function ($value) { 68 | return $value !== false ? $value : null; 69 | }, $this->command('mget', [$keys])); 70 | } 71 | 72 | /** 73 | * Set the string value in the argument as the value of the key. 74 | * 75 | * @param string $key 76 | * @param mixed $value 77 | * @param string|null $expireResolution 78 | * @param int|null $expireTTL 79 | * @param string|null $flag 80 | * @return bool 81 | */ 82 | public function set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null) 83 | { 84 | return $this->command('set', [ 85 | $key, 86 | $value, 87 | $expireResolution ? [$flag, $expireResolution => $expireTTL] : null, 88 | ]); 89 | } 90 | 91 | /** 92 | * Set the given key if it doesn't exist. 93 | * 94 | * @param string $key 95 | * @param string $value 96 | * @return int 97 | */ 98 | public function setnx($key, $value) 99 | { 100 | return (int) $this->command('setnx', [$key, $value]); 101 | } 102 | 103 | /** 104 | * Get the value of the given hash fields. 105 | * 106 | * @param string $key 107 | * @param mixed ...$dictionary 108 | * @return array 109 | */ 110 | public function hmget($key, ...$dictionary) 111 | { 112 | if (count($dictionary) === 1) { 113 | $dictionary = $dictionary[0]; 114 | } 115 | 116 | return array_values($this->command('hmget', [$key, $dictionary])); 117 | } 118 | 119 | /** 120 | * Set the given hash fields to their respective values. 121 | * 122 | * @param string $key 123 | * @param mixed ...$dictionary 124 | * @return int 125 | */ 126 | public function hmset($key, ...$dictionary) 127 | { 128 | if (count($dictionary) === 1) { 129 | $dictionary = $dictionary[0]; 130 | } else { 131 | $input = new Collection($dictionary); 132 | 133 | $dictionary = $input->nth(2)->combine($input->nth(2, 1))->toArray(); 134 | } 135 | 136 | return $this->command('hmset', [$key, $dictionary]); 137 | } 138 | 139 | /** 140 | * Set the given hash field if it doesn't exist. 141 | * 142 | * @param string $hash 143 | * @param string $key 144 | * @param string $value 145 | * @return int 146 | */ 147 | public function hsetnx($hash, $key, $value) 148 | { 149 | return (int) $this->command('hsetnx', [$hash, $key, $value]); 150 | } 151 | 152 | /** 153 | * Removes the first count occurrences of the value element from the list. 154 | * 155 | * @param string $key 156 | * @param int $count 157 | * @param mixed $value 158 | * @return int|false 159 | */ 160 | public function lrem($key, $count, $value) 161 | { 162 | return $this->command('lrem', [$key, $value, $count]); 163 | } 164 | 165 | /** 166 | * Removes and returns the first element of the list stored at key. 167 | * 168 | * @param mixed ...$arguments 169 | * @return array|null 170 | */ 171 | public function blpop(...$arguments) 172 | { 173 | $result = $this->command('blpop', $arguments); 174 | 175 | return empty($result) ? null : $result; 176 | } 177 | 178 | /** 179 | * Removes and returns the last element of the list stored at key. 180 | * 181 | * @param mixed ...$arguments 182 | * @return array|null 183 | */ 184 | public function brpop(...$arguments) 185 | { 186 | $result = $this->command('brpop', $arguments); 187 | 188 | return empty($result) ? null : $result; 189 | } 190 | 191 | /** 192 | * Removes and returns a random element from the set value at key. 193 | * 194 | * @param string $key 195 | * @param int|null $count 196 | * @return mixed|false 197 | */ 198 | public function spop($key, $count = 1) 199 | { 200 | return $this->command('spop', func_get_args()); 201 | } 202 | 203 | /** 204 | * Add one or more members to a sorted set or update its score if it already exists. 205 | * 206 | * @param string $key 207 | * @param mixed ...$dictionary 208 | * @return int 209 | */ 210 | public function zadd($key, ...$dictionary) 211 | { 212 | if (is_array(end($dictionary))) { 213 | foreach (array_pop($dictionary) as $member => $score) { 214 | $dictionary[] = $score; 215 | $dictionary[] = $member; 216 | } 217 | } 218 | 219 | $options = []; 220 | 221 | foreach (array_slice($dictionary, 0, 3) as $i => $value) { 222 | if (in_array($value, ['nx', 'xx', 'ch', 'incr', 'gt', 'lt', 'NX', 'XX', 'CH', 'INCR', 'GT', 'LT'], true)) { 223 | $options[] = $value; 224 | 225 | unset($dictionary[$i]); 226 | } 227 | } 228 | 229 | return $this->command('zadd', array_merge([$key], [$options], array_values($dictionary))); 230 | } 231 | 232 | /** 233 | * Return elements with score between $min and $max. 234 | * 235 | * @param string $key 236 | * @param mixed $min 237 | * @param mixed $max 238 | * @param array $options 239 | * @return array 240 | */ 241 | public function zrangebyscore($key, $min, $max, $options = []) 242 | { 243 | if (isset($options['limit']) && ! array_is_list($options['limit'])) { 244 | $options['limit'] = [ 245 | $options['limit']['offset'], 246 | $options['limit']['count'], 247 | ]; 248 | } 249 | 250 | return $this->command('zRangeByScore', [$key, $min, $max, $options]); 251 | } 252 | 253 | /** 254 | * Return elements with score between $min and $max. 255 | * 256 | * @param string $key 257 | * @param mixed $min 258 | * @param mixed $max 259 | * @param array $options 260 | * @return array 261 | */ 262 | public function zrevrangebyscore($key, $min, $max, $options = []) 263 | { 264 | if (isset($options['limit']) && ! array_is_list($options['limit'])) { 265 | $options['limit'] = [ 266 | $options['limit']['offset'], 267 | $options['limit']['count'], 268 | ]; 269 | } 270 | 271 | return $this->command('zRevRangeByScore', [$key, $min, $max, $options]); 272 | } 273 | 274 | /** 275 | * Find the intersection between sets and store in a new set. 276 | * 277 | * @param string $output 278 | * @param array $keys 279 | * @param array $options 280 | * @return int 281 | */ 282 | public function zinterstore($output, $keys, $options = []) 283 | { 284 | return $this->command('zinterstore', [$output, $keys, 285 | $options['weights'] ?? null, 286 | $options['aggregate'] ?? 'sum', 287 | ]); 288 | } 289 | 290 | /** 291 | * Find the union between sets and store in a new set. 292 | * 293 | * @param string $output 294 | * @param array $keys 295 | * @param array $options 296 | * @return int 297 | */ 298 | public function zunionstore($output, $keys, $options = []) 299 | { 300 | return $this->command('zunionstore', [$output, $keys, 301 | $options['weights'] ?? null, 302 | $options['aggregate'] ?? 'sum', 303 | ]); 304 | } 305 | 306 | /** 307 | * Scans all keys based on options. 308 | * 309 | * @param mixed $cursor 310 | * @param array $options 311 | * @return mixed 312 | */ 313 | public function scan($cursor, $options = []) 314 | { 315 | $result = $this->client->scan($cursor, 316 | $options['match'] ?? '*', 317 | $options['count'] ?? 10 318 | ); 319 | 320 | if ($result === false) { 321 | $result = []; 322 | } 323 | 324 | return $cursor === 0 && empty($result) ? false : [$cursor, $result]; 325 | } 326 | 327 | /** 328 | * Scans the given set for all values based on options. 329 | * 330 | * @param string $key 331 | * @param mixed $cursor 332 | * @param array $options 333 | * @return mixed 334 | */ 335 | public function zscan($key, $cursor, $options = []) 336 | { 337 | $result = $this->client->zscan($key, $cursor, 338 | $options['match'] ?? '*', 339 | $options['count'] ?? 10 340 | ); 341 | 342 | if ($result === false) { 343 | $result = []; 344 | } 345 | 346 | return $cursor === 0 && empty($result) ? false : [$cursor, $result]; 347 | } 348 | 349 | /** 350 | * Scans the given hash for all values based on options. 351 | * 352 | * @param string $key 353 | * @param mixed $cursor 354 | * @param array $options 355 | * @return mixed 356 | */ 357 | public function hscan($key, $cursor, $options = []) 358 | { 359 | $result = $this->client->hscan($key, $cursor, 360 | $options['match'] ?? '*', 361 | $options['count'] ?? 10 362 | ); 363 | 364 | if ($result === false) { 365 | $result = []; 366 | } 367 | 368 | return $cursor === 0 && empty($result) ? false : [$cursor, $result]; 369 | } 370 | 371 | /** 372 | * Scans the given set for all values based on options. 373 | * 374 | * @param string $key 375 | * @param mixed $cursor 376 | * @param array $options 377 | * @return mixed 378 | */ 379 | public function sscan($key, $cursor, $options = []) 380 | { 381 | $result = $this->client->sscan($key, $cursor, 382 | $options['match'] ?? '*', 383 | $options['count'] ?? 10 384 | ); 385 | 386 | if ($result === false) { 387 | $result = []; 388 | } 389 | 390 | return $cursor === 0 && empty($result) ? false : [$cursor, $result]; 391 | } 392 | 393 | /** 394 | * Execute commands in a pipeline. 395 | * 396 | * @param callable|null $callback 397 | * @return \Redis|array 398 | */ 399 | public function pipeline(?callable $callback = null) 400 | { 401 | $pipeline = $this->client()->pipeline(); 402 | 403 | return is_null($callback) 404 | ? $pipeline 405 | : tap($pipeline, $callback)->exec(); 406 | } 407 | 408 | /** 409 | * Execute commands in a transaction. 410 | * 411 | * @param callable|null $callback 412 | * @return \Redis|array 413 | */ 414 | public function transaction(?callable $callback = null) 415 | { 416 | $transaction = $this->client()->multi(); 417 | 418 | return is_null($callback) 419 | ? $transaction 420 | : tap($transaction, $callback)->exec(); 421 | } 422 | 423 | /** 424 | * Evaluate a LUA script serverside, from the SHA1 hash of the script instead of the script itself. 425 | * 426 | * @param string $script 427 | * @param int $numkeys 428 | * @param mixed ...$arguments 429 | * @return mixed 430 | */ 431 | public function evalsha($script, $numkeys, ...$arguments) 432 | { 433 | return $this->command('evalsha', [ 434 | $this->script('load', $script), $arguments, $numkeys, 435 | ]); 436 | } 437 | 438 | /** 439 | * Evaluate a script and return its result. 440 | * 441 | * @param string $script 442 | * @param int $numberOfKeys 443 | * @param mixed ...$arguments 444 | * @return mixed 445 | */ 446 | public function eval($script, $numberOfKeys, ...$arguments) 447 | { 448 | return $this->command('eval', [$script, $arguments, $numberOfKeys]); 449 | } 450 | 451 | /** 452 | * Subscribe to a set of given channels for messages. 453 | * 454 | * @param array|string $channels 455 | * @param \Closure $callback 456 | * @return void 457 | */ 458 | public function subscribe($channels, Closure $callback) 459 | { 460 | $this->client->subscribe((array) $channels, function ($redis, $channel, $message) use ($callback) { 461 | $callback($message, $channel); 462 | }); 463 | } 464 | 465 | /** 466 | * Subscribe to a set of given channels with wildcards. 467 | * 468 | * @param array|string $channels 469 | * @param \Closure $callback 470 | * @return void 471 | */ 472 | public function psubscribe($channels, Closure $callback) 473 | { 474 | $this->client->psubscribe((array) $channels, function ($redis, $pattern, $channel, $message) use ($callback) { 475 | $callback($message, $channel); 476 | }); 477 | } 478 | 479 | /** 480 | * Subscribe to a set of given channels for messages. 481 | * 482 | * @param array|string $channels 483 | * @param \Closure $callback 484 | * @param string $method 485 | * @return void 486 | */ 487 | public function createSubscription($channels, Closure $callback, $method = 'subscribe') 488 | { 489 | // 490 | } 491 | 492 | /** 493 | * Flush the selected Redis database. 494 | * 495 | * @return mixed 496 | */ 497 | public function flushdb() 498 | { 499 | $arguments = func_get_args(); 500 | 501 | if (strtoupper((string) ($arguments[0] ?? null)) === 'ASYNC') { 502 | return $this->command('flushdb', [true]); 503 | } 504 | 505 | return $this->command('flushdb'); 506 | } 507 | 508 | /** 509 | * Execute a raw command. 510 | * 511 | * @param array $parameters 512 | * @return mixed 513 | */ 514 | public function executeRaw(array $parameters) 515 | { 516 | return $this->command('rawCommand', $parameters); 517 | } 518 | 519 | /** 520 | * Run a command against the Redis database. 521 | * 522 | * @param string $method 523 | * @param array $parameters 524 | * @return mixed 525 | * 526 | * @throws \RedisException 527 | */ 528 | public function command($method, array $parameters = []) 529 | { 530 | try { 531 | return parent::command($method, $parameters); 532 | } catch (RedisException $e) { 533 | if (Str::contains($e->getMessage(), ['went away', 'socket', 'read error on connection', 'Connection lost'])) { 534 | $this->client = $this->connector ? call_user_func($this->connector) : $this->client; 535 | } 536 | 537 | throw $e; 538 | } 539 | } 540 | 541 | /** 542 | * Disconnects from the Redis instance. 543 | * 544 | * @return void 545 | */ 546 | public function disconnect() 547 | { 548 | $this->client->close(); 549 | } 550 | 551 | /** 552 | * Pass other method calls down to the underlying client. 553 | * 554 | * @param string $method 555 | * @param array $parameters 556 | * @return mixed 557 | */ 558 | public function __call($method, $parameters) 559 | { 560 | return parent::__call(strtolower($method), $parameters); 561 | } 562 | } 563 | -------------------------------------------------------------------------------- /Connections/PredisClusterConnection.php: -------------------------------------------------------------------------------- 1 | client as $node) { 22 | $node->executeCommand(tap(new $command)->setArguments(func_get_args())); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Connections/PredisConnection.php: -------------------------------------------------------------------------------- 1 | client = $client; 30 | } 31 | 32 | /** 33 | * Subscribe to a set of given channels for messages. 34 | * 35 | * @param array|string $channels 36 | * @param \Closure $callback 37 | * @param string $method 38 | * @return void 39 | */ 40 | public function createSubscription($channels, Closure $callback, $method = 'subscribe') 41 | { 42 | $loop = $this->pubSubLoop(); 43 | 44 | $loop->{$method}(...array_values((array) $channels)); 45 | 46 | foreach ($loop as $message) { 47 | if ($message->kind === 'message' || $message->kind === 'pmessage') { 48 | $callback($message->payload, $message->channel); 49 | } 50 | } 51 | 52 | unset($loop); 53 | } 54 | 55 | /** 56 | * Parse the command's parameters for event dispatching. 57 | * 58 | * @param array $parameters 59 | * @return array 60 | */ 61 | protected function parseParametersForEvent(array $parameters) 62 | { 63 | return (new Collection($parameters)) 64 | ->transform(function ($parameter) { 65 | return $parameter instanceof ArrayableArgument 66 | ? $parameter->toArray() 67 | : $parameter; 68 | })->all(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Connectors/PhpRedisConnector.php: -------------------------------------------------------------------------------- 1 | createClient(array_merge( 34 | $config, $options, $formattedOptions 35 | )); 36 | }; 37 | 38 | return new PhpRedisConnection($connector(), $connector, $config); 39 | } 40 | 41 | /** 42 | * Create a new clustered PhpRedis connection. 43 | * 44 | * @param array $config 45 | * @param array $clusterOptions 46 | * @param array $options 47 | * @return \Illuminate\Redis\Connections\PhpRedisClusterConnection 48 | */ 49 | public function connectToCluster(array $config, array $clusterOptions, array $options) 50 | { 51 | $options = array_merge($options, $clusterOptions, Arr::pull($config, 'options', [])); 52 | 53 | return new PhpRedisClusterConnection($this->createRedisClusterInstance( 54 | array_map($this->buildClusterConnectionString(...), $config), $options 55 | )); 56 | } 57 | 58 | /** 59 | * Build a single cluster seed string from an array. 60 | * 61 | * @param array $server 62 | * @return string 63 | */ 64 | protected function buildClusterConnectionString(array $server) 65 | { 66 | return $this->formatHost($server).':'.$server['port']; 67 | } 68 | 69 | /** 70 | * Create the Redis client instance. 71 | * 72 | * @param array $config 73 | * @return \Redis 74 | * 75 | * @throws \LogicException 76 | */ 77 | protected function createClient(array $config) 78 | { 79 | return tap(new Redis, function ($client) use ($config) { 80 | if ($client instanceof RedisFacade) { 81 | throw new LogicException( 82 | extension_loaded('redis') 83 | ? 'Please remove or rename the Redis facade alias in your "app" configuration file in order to avoid collision with the PHP Redis extension.' 84 | : 'Please make sure the PHP Redis extension is installed and enabled.' 85 | ); 86 | } 87 | 88 | $this->establishConnection($client, $config); 89 | 90 | if (array_key_exists('max_retries', $config)) { 91 | $client->setOption(Redis::OPT_MAX_RETRIES, $config['max_retries']); 92 | } 93 | 94 | if (array_key_exists('backoff_algorithm', $config)) { 95 | $client->setOption(Redis::OPT_BACKOFF_ALGORITHM, $config['backoff_algorithm']); 96 | } 97 | 98 | if (array_key_exists('backoff_base', $config)) { 99 | $client->setOption(Redis::OPT_BACKOFF_BASE, $config['backoff_base']); 100 | } 101 | 102 | if (array_key_exists('backoff_cap', $config)) { 103 | $client->setOption(Redis::OPT_BACKOFF_CAP, $config['backoff_cap']); 104 | } 105 | 106 | if (! empty($config['password'])) { 107 | if (isset($config['username']) && $config['username'] !== '' && is_string($config['password'])) { 108 | $client->auth([$config['username'], $config['password']]); 109 | } else { 110 | $client->auth($config['password']); 111 | } 112 | } 113 | 114 | if (isset($config['database'])) { 115 | $client->select((int) $config['database']); 116 | } 117 | 118 | if (! empty($config['prefix'])) { 119 | $client->setOption(Redis::OPT_PREFIX, $config['prefix']); 120 | } 121 | 122 | if (! empty($config['read_timeout'])) { 123 | $client->setOption(Redis::OPT_READ_TIMEOUT, $config['read_timeout']); 124 | } 125 | 126 | if (! empty($config['scan'])) { 127 | $client->setOption(Redis::OPT_SCAN, $config['scan']); 128 | } 129 | 130 | if (! empty($config['name'])) { 131 | $client->client('SETNAME', $config['name']); 132 | } 133 | 134 | if (array_key_exists('serializer', $config)) { 135 | $client->setOption(Redis::OPT_SERIALIZER, $config['serializer']); 136 | } 137 | 138 | if (array_key_exists('compression', $config)) { 139 | $client->setOption(Redis::OPT_COMPRESSION, $config['compression']); 140 | } 141 | 142 | if (array_key_exists('compression_level', $config)) { 143 | $client->setOption(Redis::OPT_COMPRESSION_LEVEL, $config['compression_level']); 144 | } 145 | }); 146 | } 147 | 148 | /** 149 | * Establish a connection with the Redis host. 150 | * 151 | * @param \Redis $client 152 | * @param array $config 153 | * @return void 154 | */ 155 | protected function establishConnection($client, array $config) 156 | { 157 | $persistent = $config['persistent'] ?? false; 158 | 159 | $parameters = [ 160 | $this->formatHost($config), 161 | $config['port'], 162 | Arr::get($config, 'timeout', 0.0), 163 | $persistent ? Arr::get($config, 'persistent_id', null) : null, 164 | Arr::get($config, 'retry_interval', 0), 165 | ]; 166 | 167 | if (version_compare(phpversion('redis'), '3.1.3', '>=')) { 168 | $parameters[] = Arr::get($config, 'read_timeout', 0.0); 169 | } 170 | 171 | if (version_compare(phpversion('redis'), '5.3.0', '>=') && ! is_null($context = Arr::get($config, 'context'))) { 172 | $parameters[] = $context; 173 | } 174 | 175 | $client->{$persistent ? 'pconnect' : 'connect'}(...$parameters); 176 | } 177 | 178 | /** 179 | * Create a new redis cluster instance. 180 | * 181 | * @param array $servers 182 | * @param array $options 183 | * @return \RedisCluster 184 | */ 185 | protected function createRedisClusterInstance(array $servers, array $options) 186 | { 187 | $parameters = [ 188 | null, 189 | array_values($servers), 190 | $options['timeout'] ?? 0, 191 | $options['read_timeout'] ?? 0, 192 | isset($options['persistent']) && $options['persistent'], 193 | ]; 194 | 195 | if (version_compare(phpversion('redis'), '4.3.0', '>=')) { 196 | $parameters[] = $options['password'] ?? null; 197 | } 198 | 199 | if (version_compare(phpversion('redis'), '5.3.2', '>=') && ! is_null($context = Arr::get($options, 'context'))) { 200 | $parameters[] = $context; 201 | } 202 | 203 | return tap(new RedisCluster(...$parameters), function ($client) use ($options) { 204 | if (! empty($options['prefix'])) { 205 | $client->setOption(Redis::OPT_PREFIX, $options['prefix']); 206 | } 207 | 208 | if (! empty($options['scan'])) { 209 | $client->setOption(Redis::OPT_SCAN, $options['scan']); 210 | } 211 | 212 | if (! empty($options['failover'])) { 213 | $client->setOption(RedisCluster::OPT_SLAVE_FAILOVER, $options['failover']); 214 | } 215 | 216 | if (array_key_exists('serializer', $options)) { 217 | $client->setOption(Redis::OPT_SERIALIZER, $options['serializer']); 218 | } 219 | 220 | if (array_key_exists('compression', $options)) { 221 | $client->setOption(Redis::OPT_COMPRESSION, $options['compression']); 222 | } 223 | 224 | if (array_key_exists('compression_level', $options)) { 225 | $client->setOption(Redis::OPT_COMPRESSION_LEVEL, $options['compression_level']); 226 | } 227 | }); 228 | } 229 | 230 | /** 231 | * Format the host using the scheme if available. 232 | * 233 | * @param array $options 234 | * @return string 235 | */ 236 | protected function formatHost(array $options) 237 | { 238 | if (isset($options['scheme'])) { 239 | return Str::start($options['host'], "{$options['scheme']}://"); 240 | } 241 | 242 | return $options['host']; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /Connectors/PredisConnector.php: -------------------------------------------------------------------------------- 1 | 10.0], $options, Arr::pull($config, 'options', []) 25 | ); 26 | 27 | if (isset($config['prefix'])) { 28 | $formattedOptions['prefix'] = $config['prefix']; 29 | } 30 | 31 | if (isset($config['host']) && str_starts_with($config['host'], 'tls://')) { 32 | $config['scheme'] = 'tls'; 33 | $config['host'] = Str::after($config['host'], 'tls://'); 34 | } 35 | 36 | return new PredisConnection(new Client($config, $formattedOptions)); 37 | } 38 | 39 | /** 40 | * Create a new clustered Predis connection. 41 | * 42 | * @param array $config 43 | * @param array $clusterOptions 44 | * @param array $options 45 | * @return \Illuminate\Redis\Connections\PredisClusterConnection 46 | */ 47 | public function connectToCluster(array $config, array $clusterOptions, array $options) 48 | { 49 | $clusterSpecificOptions = Arr::pull($config, 'options', []); 50 | 51 | if (isset($config['prefix'])) { 52 | $clusterSpecificOptions['prefix'] = $config['prefix']; 53 | } 54 | 55 | return new PredisClusterConnection(new Client(array_values($config), array_merge( 56 | $options, $clusterOptions, $clusterSpecificOptions 57 | ))); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Events/CommandExecuted.php: -------------------------------------------------------------------------------- 1 | time = $time; 53 | $this->command = $command; 54 | $this->parameters = $parameters; 55 | $this->connection = $connection; 56 | $this->connectionName = $connection->getName(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Limiters/ConcurrencyLimiter.php: -------------------------------------------------------------------------------- 1 | name = $name; 51 | $this->redis = $redis; 52 | $this->maxLocks = $maxLocks; 53 | $this->releaseAfter = $releaseAfter; 54 | } 55 | 56 | /** 57 | * Attempt to acquire the lock for the given number of seconds. 58 | * 59 | * @param int $timeout 60 | * @param callable|null $callback 61 | * @param int $sleep 62 | * @return mixed 63 | * 64 | * @throws \Illuminate\Contracts\Redis\LimiterTimeoutException 65 | * @throws \Throwable 66 | */ 67 | public function block($timeout, $callback = null, $sleep = 250) 68 | { 69 | $starting = time(); 70 | 71 | $id = Str::random(20); 72 | 73 | while (! $slot = $this->acquire($id)) { 74 | if (time() - $timeout >= $starting) { 75 | throw new LimiterTimeoutException; 76 | } 77 | 78 | Sleep::usleep($sleep * 1000); 79 | } 80 | 81 | if (is_callable($callback)) { 82 | try { 83 | return tap($callback(), function () use ($slot, $id) { 84 | $this->release($slot, $id); 85 | }); 86 | } catch (Throwable $exception) { 87 | $this->release($slot, $id); 88 | 89 | throw $exception; 90 | } 91 | } 92 | 93 | return true; 94 | } 95 | 96 | /** 97 | * Attempt to acquire the lock. 98 | * 99 | * @param string $id A unique identifier for this lock 100 | * @return mixed 101 | */ 102 | protected function acquire($id) 103 | { 104 | $slots = array_map(function ($i) { 105 | return $this->name.$i; 106 | }, range(1, $this->maxLocks)); 107 | 108 | return $this->redis->eval(...array_merge( 109 | [$this->lockScript(), count($slots)], 110 | array_merge($slots, [$this->name, $this->releaseAfter, $id]) 111 | )); 112 | } 113 | 114 | /** 115 | * Get the Lua script for acquiring a lock. 116 | * 117 | * KEYS - The keys that represent available slots 118 | * ARGV[1] - The limiter name 119 | * ARGV[2] - The number of seconds the slot should be reserved 120 | * ARGV[3] - The unique identifier for this lock 121 | * 122 | * @return string 123 | */ 124 | protected function lockScript() 125 | { 126 | return <<<'LUA' 127 | for index, value in pairs(redis.call('mget', unpack(KEYS))) do 128 | if not value then 129 | redis.call('set', KEYS[index], ARGV[3], "EX", ARGV[2]) 130 | return ARGV[1]..index 131 | end 132 | end 133 | LUA; 134 | } 135 | 136 | /** 137 | * Release the lock. 138 | * 139 | * @param string $key 140 | * @param string $id 141 | * @return void 142 | */ 143 | protected function release($key, $id) 144 | { 145 | $this->redis->eval($this->releaseScript(), 1, $key, $id); 146 | } 147 | 148 | /** 149 | * Get the Lua script to atomically release a lock. 150 | * 151 | * KEYS[1] - The name of the lock 152 | * ARGV[1] - The unique identifier for this lock 153 | * 154 | * @return string 155 | */ 156 | protected function releaseScript() 157 | { 158 | return <<<'LUA' 159 | if redis.call('get', KEYS[1]) == ARGV[1] 160 | then 161 | return redis.call('del', KEYS[1]) 162 | else 163 | return 0 164 | end 165 | LUA; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Limiters/ConcurrencyLimiterBuilder.php: -------------------------------------------------------------------------------- 1 | name = $name; 63 | $this->connection = $connection; 64 | } 65 | 66 | /** 67 | * Set the maximum number of locks that can be obtained per time window. 68 | * 69 | * @param int $maxLocks 70 | * @return $this 71 | */ 72 | public function limit($maxLocks) 73 | { 74 | $this->maxLocks = $maxLocks; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Set the number of seconds until the lock will be released. 81 | * 82 | * @param int $releaseAfter 83 | * @return $this 84 | */ 85 | public function releaseAfter($releaseAfter) 86 | { 87 | $this->releaseAfter = $this->secondsUntil($releaseAfter); 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Set the amount of time to block until a lock is available. 94 | * 95 | * @param int $timeout 96 | * @return $this 97 | */ 98 | public function block($timeout) 99 | { 100 | $this->timeout = $timeout; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * The number of milliseconds to wait between lock acquisition attempts. 107 | * 108 | * @param int $sleep 109 | * @return $this 110 | */ 111 | public function sleep($sleep) 112 | { 113 | $this->sleep = $sleep; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Execute the given callback if a lock is obtained, otherwise call the failure callback. 120 | * 121 | * @param callable $callback 122 | * @param callable|null $failure 123 | * @return mixed 124 | * 125 | * @throws \Illuminate\Contracts\Redis\LimiterTimeoutException 126 | */ 127 | public function then(callable $callback, ?callable $failure = null) 128 | { 129 | try { 130 | return (new ConcurrencyLimiter( 131 | $this->connection, $this->name, $this->maxLocks, $this->releaseAfter 132 | ))->block($this->timeout, $callback, $this->sleep); 133 | } catch (LimiterTimeoutException $e) { 134 | if ($failure) { 135 | return $failure($e); 136 | } 137 | 138 | throw $e; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Limiters/DurationLimiter.php: -------------------------------------------------------------------------------- 1 | name = $name; 63 | $this->decay = $decay; 64 | $this->redis = $redis; 65 | $this->maxLocks = $maxLocks; 66 | } 67 | 68 | /** 69 | * Attempt to acquire the lock for the given number of seconds. 70 | * 71 | * @param int $timeout 72 | * @param callable|null $callback 73 | * @param int $sleep 74 | * @return mixed 75 | * 76 | * @throws \Illuminate\Contracts\Redis\LimiterTimeoutException 77 | */ 78 | public function block($timeout, $callback = null, $sleep = 750) 79 | { 80 | $starting = time(); 81 | 82 | while (! $this->acquire()) { 83 | if (time() - $timeout >= $starting) { 84 | throw new LimiterTimeoutException; 85 | } 86 | 87 | Sleep::usleep($sleep * 1000); 88 | } 89 | 90 | if (is_callable($callback)) { 91 | return $callback(); 92 | } 93 | 94 | return true; 95 | } 96 | 97 | /** 98 | * Attempt to acquire the lock. 99 | * 100 | * @return bool 101 | */ 102 | public function acquire() 103 | { 104 | $results = $this->redis->eval( 105 | $this->luaScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks 106 | ); 107 | 108 | $this->decaysAt = $results[1]; 109 | 110 | $this->remaining = max(0, $results[2]); 111 | 112 | return (bool) $results[0]; 113 | } 114 | 115 | /** 116 | * Determine if the key has been "accessed" too many times. 117 | * 118 | * @return bool 119 | */ 120 | public function tooManyAttempts() 121 | { 122 | [$this->decaysAt, $this->remaining] = $this->redis->eval( 123 | $this->tooManyAttemptsLuaScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks 124 | ); 125 | 126 | return $this->remaining <= 0; 127 | } 128 | 129 | /** 130 | * Clear the limiter. 131 | * 132 | * @return void 133 | */ 134 | public function clear() 135 | { 136 | $this->redis->del($this->name); 137 | } 138 | 139 | /** 140 | * Get the Lua script for acquiring a lock. 141 | * 142 | * KEYS[1] - The limiter name 143 | * ARGV[1] - Current time in microseconds 144 | * ARGV[2] - Current time in seconds 145 | * ARGV[3] - Duration of the bucket 146 | * ARGV[4] - Allowed number of tasks 147 | * 148 | * @return string 149 | */ 150 | protected function luaScript() 151 | { 152 | return <<<'LUA' 153 | local function reset() 154 | redis.call('HMSET', KEYS[1], 'start', ARGV[2], 'end', ARGV[2] + ARGV[3], 'count', 1) 155 | return redis.call('EXPIRE', KEYS[1], ARGV[3] * 2) 156 | end 157 | 158 | if redis.call('EXISTS', KEYS[1]) == 0 then 159 | return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1} 160 | end 161 | 162 | if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then 163 | return { 164 | tonumber(redis.call('HINCRBY', KEYS[1], 'count', 1)) <= tonumber(ARGV[4]), 165 | redis.call('HGET', KEYS[1], 'end'), 166 | ARGV[4] - redis.call('HGET', KEYS[1], 'count') 167 | } 168 | end 169 | 170 | return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1} 171 | LUA; 172 | } 173 | 174 | /** 175 | * Get the Lua script to determine if the key has been "accessed" too many times. 176 | * 177 | * KEYS[1] - The limiter name 178 | * ARGV[1] - Current time in microseconds 179 | * ARGV[2] - Current time in seconds 180 | * ARGV[3] - Duration of the bucket 181 | * ARGV[4] - Allowed number of tasks 182 | * 183 | * @return string 184 | */ 185 | protected function tooManyAttemptsLuaScript() 186 | { 187 | return <<<'LUA' 188 | 189 | if redis.call('EXISTS', KEYS[1]) == 0 then 190 | return {0, ARGV[2] + ARGV[3]} 191 | end 192 | 193 | if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then 194 | return { 195 | redis.call('HGET', KEYS[1], 'end'), 196 | ARGV[4] - redis.call('HGET', KEYS[1], 'count') 197 | } 198 | end 199 | 200 | return {0, ARGV[2] + ARGV[3]} 201 | LUA; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Limiters/DurationLimiterBuilder.php: -------------------------------------------------------------------------------- 1 | name = $name; 63 | $this->connection = $connection; 64 | } 65 | 66 | /** 67 | * Set the maximum number of locks that can be obtained per time window. 68 | * 69 | * @param int $maxLocks 70 | * @return $this 71 | */ 72 | public function allow($maxLocks) 73 | { 74 | $this->maxLocks = $maxLocks; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Set the amount of time the lock window is maintained. 81 | * 82 | * @param \DateTimeInterface|\DateInterval|int $decay 83 | * @return $this 84 | */ 85 | public function every($decay) 86 | { 87 | $this->decay = $this->secondsUntil($decay); 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Set the amount of time to block until a lock is available. 94 | * 95 | * @param int $timeout 96 | * @return $this 97 | */ 98 | public function block($timeout) 99 | { 100 | $this->timeout = $timeout; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * The number of milliseconds to wait between lock acquisition attempts. 107 | * 108 | * @param int $sleep 109 | * @return $this 110 | */ 111 | public function sleep($sleep) 112 | { 113 | $this->sleep = $sleep; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Execute the given callback if a lock is obtained, otherwise call the failure callback. 120 | * 121 | * @param callable $callback 122 | * @param callable|null $failure 123 | * @return mixed 124 | * 125 | * @throws \Illuminate\Contracts\Redis\LimiterTimeoutException 126 | */ 127 | public function then(callable $callback, ?callable $failure = null) 128 | { 129 | try { 130 | return (new DurationLimiter( 131 | $this->connection, $this->name, $this->maxLocks, $this->decay 132 | ))->block($this->timeout, $callback, $this->sleep); 133 | } catch (LimiterTimeoutException $e) { 134 | if ($failure) { 135 | return $failure($e); 136 | } 137 | 138 | throw $e; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /RedisManager.php: -------------------------------------------------------------------------------- 1 | app = $app; 71 | $this->driver = $driver; 72 | $this->config = $config; 73 | } 74 | 75 | /** 76 | * Get a Redis connection by name. 77 | * 78 | * @param string|null $name 79 | * @return \Illuminate\Redis\Connections\Connection 80 | */ 81 | public function connection($name = null) 82 | { 83 | $name = $name ?: 'default'; 84 | 85 | if (isset($this->connections[$name])) { 86 | return $this->connections[$name]; 87 | } 88 | 89 | return $this->connections[$name] = $this->configure( 90 | $this->resolve($name), $name 91 | ); 92 | } 93 | 94 | /** 95 | * Resolve the given connection by name. 96 | * 97 | * @param string|null $name 98 | * @return \Illuminate\Redis\Connections\Connection 99 | * 100 | * @throws \InvalidArgumentException 101 | */ 102 | public function resolve($name = null) 103 | { 104 | $name = $name ?: 'default'; 105 | 106 | $options = $this->config['options'] ?? []; 107 | 108 | if (isset($this->config[$name])) { 109 | return $this->connector()->connect( 110 | $this->parseConnectionConfiguration($this->config[$name]), 111 | array_merge(Arr::except($options, 'parameters'), ['parameters' => Arr::get($options, 'parameters.'.$name, Arr::get($options, 'parameters', []))]) 112 | ); 113 | } 114 | 115 | if (isset($this->config['clusters'][$name])) { 116 | return $this->resolveCluster($name); 117 | } 118 | 119 | throw new InvalidArgumentException("Redis connection [{$name}] not configured."); 120 | } 121 | 122 | /** 123 | * Resolve the given cluster connection by name. 124 | * 125 | * @param string $name 126 | * @return \Illuminate\Redis\Connections\Connection 127 | */ 128 | protected function resolveCluster($name) 129 | { 130 | return $this->connector()->connectToCluster( 131 | array_map(function ($config) { 132 | return $this->parseConnectionConfiguration($config); 133 | }, $this->config['clusters'][$name]), 134 | $this->config['clusters']['options'] ?? [], 135 | $this->config['options'] ?? [] 136 | ); 137 | } 138 | 139 | /** 140 | * Configure the given connection to prepare it for commands. 141 | * 142 | * @param \Illuminate\Redis\Connections\Connection $connection 143 | * @param string $name 144 | * @return \Illuminate\Redis\Connections\Connection 145 | */ 146 | protected function configure(Connection $connection, $name) 147 | { 148 | $connection->setName($name); 149 | 150 | if ($this->events && $this->app->bound('events')) { 151 | $connection->setEventDispatcher($this->app->make('events')); 152 | } 153 | 154 | return $connection; 155 | } 156 | 157 | /** 158 | * Get the connector instance for the current driver. 159 | * 160 | * @return \Illuminate\Contracts\Redis\Connector|null 161 | */ 162 | protected function connector() 163 | { 164 | $customCreator = $this->customCreators[$this->driver] ?? null; 165 | 166 | if ($customCreator) { 167 | return $customCreator(); 168 | } 169 | 170 | return match ($this->driver) { 171 | 'predis' => new PredisConnector, 172 | 'phpredis' => new PhpRedisConnector, 173 | default => null, 174 | }; 175 | } 176 | 177 | /** 178 | * Parse the Redis connection configuration. 179 | * 180 | * @param mixed $config 181 | * @return array 182 | */ 183 | protected function parseConnectionConfiguration($config) 184 | { 185 | $parsed = (new ConfigurationUrlParser)->parseConfiguration($config); 186 | 187 | $driver = strtolower($parsed['driver'] ?? ''); 188 | 189 | if (in_array($driver, ['tcp', 'tls'])) { 190 | $parsed['scheme'] = $driver; 191 | } 192 | 193 | return array_filter($parsed, function ($key) { 194 | return $key !== 'driver'; 195 | }, ARRAY_FILTER_USE_KEY); 196 | } 197 | 198 | /** 199 | * Return all of the created connections. 200 | * 201 | * @return array 202 | */ 203 | public function connections() 204 | { 205 | return $this->connections; 206 | } 207 | 208 | /** 209 | * Enable the firing of Redis command events. 210 | * 211 | * @return void 212 | */ 213 | public function enableEvents() 214 | { 215 | $this->events = true; 216 | } 217 | 218 | /** 219 | * Disable the firing of Redis command events. 220 | * 221 | * @return void 222 | */ 223 | public function disableEvents() 224 | { 225 | $this->events = false; 226 | } 227 | 228 | /** 229 | * Set the default driver. 230 | * 231 | * @param string $driver 232 | * @return void 233 | */ 234 | public function setDriver($driver) 235 | { 236 | $this->driver = $driver; 237 | } 238 | 239 | /** 240 | * Disconnect the given connection and remove from local cache. 241 | * 242 | * @param string|null $name 243 | * @return void 244 | */ 245 | public function purge($name = null) 246 | { 247 | $name = $name ?: 'default'; 248 | 249 | unset($this->connections[$name]); 250 | } 251 | 252 | /** 253 | * Register a custom driver creator Closure. 254 | * 255 | * @param string $driver 256 | * @param \Closure $callback 257 | * 258 | * @param-closure-this $this $callback 259 | * 260 | * @return $this 261 | */ 262 | public function extend($driver, Closure $callback) 263 | { 264 | $this->customCreators[$driver] = $callback->bindTo($this, $this); 265 | 266 | return $this; 267 | } 268 | 269 | /** 270 | * Pass methods onto the default Redis connection. 271 | * 272 | * @param string $method 273 | * @param array $parameters 274 | * @return mixed 275 | */ 276 | public function __call($method, $parameters) 277 | { 278 | return $this->connection()->{$method}(...$parameters); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /RedisServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('redis', function ($app) { 19 | $config = $app->make('config')->get('database.redis', []); 20 | 21 | return new RedisManager($app, Arr::pull($config, 'client', 'phpredis'), $config); 22 | }); 23 | 24 | $this->app->bind('redis.connection', function ($app) { 25 | return $app['redis']->connection(); 26 | }); 27 | } 28 | 29 | /** 30 | * Get the services provided by the provider. 31 | * 32 | * @return array 33 | */ 34 | public function provides() 35 | { 36 | return ['redis', 'redis.connection']; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "illuminate/redis", 3 | "description": "The Illuminate Redis package.", 4 | "license": "MIT", 5 | "homepage": "https://laravel.com", 6 | "support": { 7 | "issues": "https://github.com/laravel/framework/issues", 8 | "source": "https://github.com/laravel/framework" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Taylor Otwell", 13 | "email": "taylor@laravel.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.3", 18 | "illuminate/collections": "^13.0", 19 | "illuminate/contracts": "^13.0", 20 | "illuminate/macroable": "^13.0", 21 | "illuminate/support": "^13.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Illuminate\\Redis\\": "" 26 | } 27 | }, 28 | "suggest": { 29 | "ext-redis": "Required to use the phpredis connector (^4.0|^5.0|^6.0).", 30 | "predis/predis": "Required to use the predis connector (^2.3|^3.0)." 31 | }, 32 | "extra": { 33 | "branch-alias": { 34 | "dev-master": "13.0.x-dev" 35 | } 36 | }, 37 | "config": { 38 | "sort-packages": true 39 | }, 40 | "minimum-stability": "dev" 41 | } 42 | --------------------------------------------------------------------------------