1<?php 2 3/* 4 * This file is part of the Predis package. 5 * 6 * (c) Daniele Alessandri <suppakilla@gmail.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Predis\Cluster; 13 14use Predis\Command\CommandInterface; 15use Predis\Command\ScriptCommand; 16 17/** 18 * Common class implementing the logic needed to support clustering strategies. 19 * 20 * @author Daniele Alessandri <suppakilla@gmail.com> 21 */ 22abstract class ClusterStrategy implements StrategyInterface 23{ 24 protected $commands; 25 26 /** 27 * 28 */ 29 public function __construct() 30 { 31 $this->commands = $this->getDefaultCommands(); 32 } 33 34 /** 35 * Returns the default map of supported commands with their handlers. 36 * 37 * @return array 38 */ 39 protected function getDefaultCommands() 40 { 41 $getKeyFromFirstArgument = array($this, 'getKeyFromFirstArgument'); 42 $getKeyFromAllArguments = array($this, 'getKeyFromAllArguments'); 43 44 return array( 45 /* commands operating on the key space */ 46 'EXISTS' => $getKeyFromAllArguments, 47 'DEL' => $getKeyFromAllArguments, 48 'TYPE' => $getKeyFromFirstArgument, 49 'EXPIRE' => $getKeyFromFirstArgument, 50 'EXPIREAT' => $getKeyFromFirstArgument, 51 'PERSIST' => $getKeyFromFirstArgument, 52 'PEXPIRE' => $getKeyFromFirstArgument, 53 'PEXPIREAT' => $getKeyFromFirstArgument, 54 'TTL' => $getKeyFromFirstArgument, 55 'PTTL' => $getKeyFromFirstArgument, 56 'SORT' => array($this, 'getKeyFromSortCommand'), 57 'DUMP' => $getKeyFromFirstArgument, 58 'RESTORE' => $getKeyFromFirstArgument, 59 60 /* commands operating on string values */ 61 'APPEND' => $getKeyFromFirstArgument, 62 'DECR' => $getKeyFromFirstArgument, 63 'DECRBY' => $getKeyFromFirstArgument, 64 'GET' => $getKeyFromFirstArgument, 65 'GETBIT' => $getKeyFromFirstArgument, 66 'MGET' => $getKeyFromAllArguments, 67 'SET' => $getKeyFromFirstArgument, 68 'GETRANGE' => $getKeyFromFirstArgument, 69 'GETSET' => $getKeyFromFirstArgument, 70 'INCR' => $getKeyFromFirstArgument, 71 'INCRBY' => $getKeyFromFirstArgument, 72 'INCRBYFLOAT' => $getKeyFromFirstArgument, 73 'SETBIT' => $getKeyFromFirstArgument, 74 'SETEX' => $getKeyFromFirstArgument, 75 'MSET' => array($this, 'getKeyFromInterleavedArguments'), 76 'MSETNX' => array($this, 'getKeyFromInterleavedArguments'), 77 'SETNX' => $getKeyFromFirstArgument, 78 'SETRANGE' => $getKeyFromFirstArgument, 79 'STRLEN' => $getKeyFromFirstArgument, 80 'SUBSTR' => $getKeyFromFirstArgument, 81 'BITOP' => array($this, 'getKeyFromBitOp'), 82 'BITCOUNT' => $getKeyFromFirstArgument, 83 'BITFIELD' => $getKeyFromFirstArgument, 84 85 /* commands operating on lists */ 86 'LINSERT' => $getKeyFromFirstArgument, 87 'LINDEX' => $getKeyFromFirstArgument, 88 'LLEN' => $getKeyFromFirstArgument, 89 'LPOP' => $getKeyFromFirstArgument, 90 'RPOP' => $getKeyFromFirstArgument, 91 'RPOPLPUSH' => $getKeyFromAllArguments, 92 'BLPOP' => array($this, 'getKeyFromBlockingListCommands'), 93 'BRPOP' => array($this, 'getKeyFromBlockingListCommands'), 94 'BRPOPLPUSH' => array($this, 'getKeyFromBlockingListCommands'), 95 'LPUSH' => $getKeyFromFirstArgument, 96 'LPUSHX' => $getKeyFromFirstArgument, 97 'RPUSH' => $getKeyFromFirstArgument, 98 'RPUSHX' => $getKeyFromFirstArgument, 99 'LRANGE' => $getKeyFromFirstArgument, 100 'LREM' => $getKeyFromFirstArgument, 101 'LSET' => $getKeyFromFirstArgument, 102 'LTRIM' => $getKeyFromFirstArgument, 103 104 /* commands operating on sets */ 105 'SADD' => $getKeyFromFirstArgument, 106 'SCARD' => $getKeyFromFirstArgument, 107 'SDIFF' => $getKeyFromAllArguments, 108 'SDIFFSTORE' => $getKeyFromAllArguments, 109 'SINTER' => $getKeyFromAllArguments, 110 'SINTERSTORE' => $getKeyFromAllArguments, 111 'SUNION' => $getKeyFromAllArguments, 112 'SUNIONSTORE' => $getKeyFromAllArguments, 113 'SISMEMBER' => $getKeyFromFirstArgument, 114 'SMEMBERS' => $getKeyFromFirstArgument, 115 'SSCAN' => $getKeyFromFirstArgument, 116 'SPOP' => $getKeyFromFirstArgument, 117 'SRANDMEMBER' => $getKeyFromFirstArgument, 118 'SREM' => $getKeyFromFirstArgument, 119 120 /* commands operating on sorted sets */ 121 'ZADD' => $getKeyFromFirstArgument, 122 'ZCARD' => $getKeyFromFirstArgument, 123 'ZCOUNT' => $getKeyFromFirstArgument, 124 'ZINCRBY' => $getKeyFromFirstArgument, 125 'ZINTERSTORE' => array($this, 'getKeyFromZsetAggregationCommands'), 126 'ZRANGE' => $getKeyFromFirstArgument, 127 'ZRANGEBYSCORE' => $getKeyFromFirstArgument, 128 'ZRANK' => $getKeyFromFirstArgument, 129 'ZREM' => $getKeyFromFirstArgument, 130 'ZREMRANGEBYRANK' => $getKeyFromFirstArgument, 131 'ZREMRANGEBYSCORE' => $getKeyFromFirstArgument, 132 'ZREVRANGE' => $getKeyFromFirstArgument, 133 'ZREVRANGEBYSCORE' => $getKeyFromFirstArgument, 134 'ZREVRANK' => $getKeyFromFirstArgument, 135 'ZSCORE' => $getKeyFromFirstArgument, 136 'ZUNIONSTORE' => array($this, 'getKeyFromZsetAggregationCommands'), 137 'ZSCAN' => $getKeyFromFirstArgument, 138 'ZLEXCOUNT' => $getKeyFromFirstArgument, 139 'ZRANGEBYLEX' => $getKeyFromFirstArgument, 140 'ZREMRANGEBYLEX' => $getKeyFromFirstArgument, 141 'ZREVRANGEBYLEX' => $getKeyFromFirstArgument, 142 143 /* commands operating on hashes */ 144 'HDEL' => $getKeyFromFirstArgument, 145 'HEXISTS' => $getKeyFromFirstArgument, 146 'HGET' => $getKeyFromFirstArgument, 147 'HGETALL' => $getKeyFromFirstArgument, 148 'HMGET' => $getKeyFromFirstArgument, 149 'HMSET' => $getKeyFromFirstArgument, 150 'HINCRBY' => $getKeyFromFirstArgument, 151 'HINCRBYFLOAT' => $getKeyFromFirstArgument, 152 'HKEYS' => $getKeyFromFirstArgument, 153 'HLEN' => $getKeyFromFirstArgument, 154 'HSET' => $getKeyFromFirstArgument, 155 'HSETNX' => $getKeyFromFirstArgument, 156 'HVALS' => $getKeyFromFirstArgument, 157 'HSCAN' => $getKeyFromFirstArgument, 158 'HSTRLEN' => $getKeyFromFirstArgument, 159 160 /* commands operating on HyperLogLog */ 161 'PFADD' => $getKeyFromFirstArgument, 162 'PFCOUNT' => $getKeyFromAllArguments, 163 'PFMERGE' => $getKeyFromAllArguments, 164 165 /* scripting */ 166 'EVAL' => array($this, 'getKeyFromScriptingCommands'), 167 'EVALSHA' => array($this, 'getKeyFromScriptingCommands'), 168 169 /* commands performing geospatial operations */ 170 'GEOADD' => $getKeyFromFirstArgument, 171 'GEOHASH' => $getKeyFromFirstArgument, 172 'GEOPOS' => $getKeyFromFirstArgument, 173 'GEODIST' => $getKeyFromFirstArgument, 174 'GEORADIUS' => array($this, 'getKeyFromGeoradiusCommands'), 175 'GEORADIUSBYMEMBER' => array($this, 'getKeyFromGeoradiusCommands'), 176 ); 177 } 178 179 /** 180 * Returns the list of IDs for the supported commands. 181 * 182 * @return array 183 */ 184 public function getSupportedCommands() 185 { 186 return array_keys($this->commands); 187 } 188 189 /** 190 * Sets an handler for the specified command ID. 191 * 192 * The signature of the callback must have a single parameter of type 193 * Predis\Command\CommandInterface. 194 * 195 * When the callback argument is omitted or NULL, the previously associated 196 * handler for the specified command ID is removed. 197 * 198 * @param string $commandID Command ID. 199 * @param mixed $callback A valid callable object, or NULL to unset the handler. 200 * 201 * @throws \InvalidArgumentException 202 */ 203 public function setCommandHandler($commandID, $callback = null) 204 { 205 $commandID = strtoupper($commandID); 206 207 if (!isset($callback)) { 208 unset($this->commands[$commandID]); 209 210 return; 211 } 212 213 if (!is_callable($callback)) { 214 throw new \InvalidArgumentException( 215 'The argument must be a callable object or NULL.' 216 ); 217 } 218 219 $this->commands[$commandID] = $callback; 220 } 221 222 /** 223 * Extracts the key from the first argument of a command instance. 224 * 225 * @param CommandInterface $command Command instance. 226 * 227 * @return string 228 */ 229 protected function getKeyFromFirstArgument(CommandInterface $command) 230 { 231 return $command->getArgument(0); 232 } 233 234 /** 235 * Extracts the key from a command with multiple keys only when all keys in 236 * the arguments array produce the same hash. 237 * 238 * @param CommandInterface $command Command instance. 239 * 240 * @return string|null 241 */ 242 protected function getKeyFromAllArguments(CommandInterface $command) 243 { 244 $arguments = $command->getArguments(); 245 246 if ($this->checkSameSlotForKeys($arguments)) { 247 return $arguments[0]; 248 } 249 } 250 251 /** 252 * Extracts the key from a command with multiple keys only when all keys in 253 * the arguments array produce the same hash. 254 * 255 * @param CommandInterface $command Command instance. 256 * 257 * @return string|null 258 */ 259 protected function getKeyFromInterleavedArguments(CommandInterface $command) 260 { 261 $arguments = $command->getArguments(); 262 $keys = array(); 263 264 for ($i = 0; $i < count($arguments); $i += 2) { 265 $keys[] = $arguments[$i]; 266 } 267 268 if ($this->checkSameSlotForKeys($keys)) { 269 return $arguments[0]; 270 } 271 } 272 273 /** 274 * Extracts the key from SORT command. 275 * 276 * @param CommandInterface $command Command instance. 277 * 278 * @return string|null 279 */ 280 protected function getKeyFromSortCommand(CommandInterface $command) 281 { 282 $arguments = $command->getArguments(); 283 $firstKey = $arguments[0]; 284 285 if (1 === $argc = count($arguments)) { 286 return $firstKey; 287 } 288 289 $keys = array($firstKey); 290 291 for ($i = 1; $i < $argc; ++$i) { 292 if (strtoupper($arguments[$i]) === 'STORE') { 293 $keys[] = $arguments[++$i]; 294 } 295 } 296 297 if ($this->checkSameSlotForKeys($keys)) { 298 return $firstKey; 299 } 300 } 301 302 /** 303 * Extracts the key from BLPOP and BRPOP commands. 304 * 305 * @param CommandInterface $command Command instance. 306 * 307 * @return string|null 308 */ 309 protected function getKeyFromBlockingListCommands(CommandInterface $command) 310 { 311 $arguments = $command->getArguments(); 312 313 if ($this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) { 314 return $arguments[0]; 315 } 316 } 317 318 /** 319 * Extracts the key from BITOP command. 320 * 321 * @param CommandInterface $command Command instance. 322 * 323 * @return string|null 324 */ 325 protected function getKeyFromBitOp(CommandInterface $command) 326 { 327 $arguments = $command->getArguments(); 328 329 if ($this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) { 330 return $arguments[1]; 331 } 332 } 333 334 /** 335 * Extracts the key from GEORADIUS and GEORADIUSBYMEMBER commands. 336 * 337 * @param CommandInterface $command Command instance. 338 * 339 * @return string|null 340 */ 341 protected function getKeyFromGeoradiusCommands(CommandInterface $command) 342 { 343 $arguments = $command->getArguments(); 344 $argc = count($arguments); 345 $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; 346 347 if ($argc > $startIndex) { 348 $keys = array($arguments[0]); 349 350 for ($i = $startIndex; $i < $argc; ++$i) { 351 $argument = strtoupper($arguments[$i]); 352 if ($argument === 'STORE' || $argument === 'STOREDIST') { 353 $keys[] = $arguments[++$i]; 354 } 355 } 356 357 if ($this->checkSameSlotForKeys($keys)) { 358 return $arguments[0]; 359 } else { 360 return; 361 } 362 } 363 364 return $arguments[0]; 365 } 366 367 /** 368 * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands. 369 * 370 * @param CommandInterface $command Command instance. 371 * 372 * @return string|null 373 */ 374 protected function getKeyFromZsetAggregationCommands(CommandInterface $command) 375 { 376 $arguments = $command->getArguments(); 377 $keys = array_merge(array($arguments[0]), array_slice($arguments, 2, $arguments[1])); 378 379 if ($this->checkSameSlotForKeys($keys)) { 380 return $arguments[0]; 381 } 382 } 383 384 /** 385 * Extracts the key from EVAL and EVALSHA commands. 386 * 387 * @param CommandInterface $command Command instance. 388 * 389 * @return string|null 390 */ 391 protected function getKeyFromScriptingCommands(CommandInterface $command) 392 { 393 if ($command instanceof ScriptCommand) { 394 $keys = $command->getKeys(); 395 } else { 396 $keys = array_slice($args = $command->getArguments(), 2, $args[1]); 397 } 398 399 if ($keys && $this->checkSameSlotForKeys($keys)) { 400 return $keys[0]; 401 } 402 } 403 404 /** 405 * {@inheritdoc} 406 */ 407 public function getSlot(CommandInterface $command) 408 { 409 $slot = $command->getSlot(); 410 411 if (!isset($slot) && isset($this->commands[$cmdID = $command->getId()])) { 412 $key = call_user_func($this->commands[$cmdID], $command); 413 414 if (isset($key)) { 415 $slot = $this->getSlotByKey($key); 416 $command->setSlot($slot); 417 } 418 } 419 420 return $slot; 421 } 422 423 /** 424 * Checks if the specified array of keys will generate the same hash. 425 * 426 * @param array $keys Array of keys. 427 * 428 * @return bool 429 */ 430 protected function checkSameSlotForKeys(array $keys) 431 { 432 if (!$count = count($keys)) { 433 return false; 434 } 435 436 $currentSlot = $this->getSlotByKey($keys[0]); 437 438 for ($i = 1; $i < $count; ++$i) { 439 $nextSlot = $this->getSlotByKey($keys[$i]); 440 441 if ($currentSlot !== $nextSlot) { 442 return false; 443 } 444 445 $currentSlot = $nextSlot; 446 } 447 448 return true; 449 } 450 451 /** 452 * Returns only the hashable part of a key (delimited by "{...}"), or the 453 * whole key if a key tag is not found in the string. 454 * 455 * @param string $key A key. 456 * 457 * @return string 458 */ 459 protected function extractKeyTag($key) 460 { 461 if (false !== $start = strpos($key, '{')) { 462 if (false !== ($end = strpos($key, '}', $start)) && $end !== ++$start) { 463 $key = substr($key, $start, $end - $start); 464 } 465 } 466 467 return $key; 468 } 469} 470