|
<?php |
|
|
|
namespace Nrk\Predis\Experiments; |
|
|
|
use Exception; |
|
use Generator; |
|
use InvalidArgumentException; |
|
use ReflectionFunction; |
|
use ReflectionMethod; |
|
use RuntimeException; |
|
use Traversable; |
|
use UnexpectedValueException; |
|
use Predis\Command\CommandInterface; |
|
|
|
/** |
|
* Future response class. |
|
*/ |
|
class FutureResponse |
|
{ |
|
protected $ready = false; |
|
protected $throw = true; |
|
protected $value = null; |
|
protected $binding = null; |
|
protected $validator = null; |
|
protected $readyListeners = []; |
|
protected $errorListeners = []; |
|
protected $transformers = []; |
|
protected $transformToArray = false; |
|
protected $transformToGenerator = false; |
|
|
|
/** |
|
* Returns whether the future response holds a finalized value. |
|
* |
|
* @return bool |
|
*/ |
|
public function isReady(): bool |
|
{ |
|
return $this->ready; |
|
} |
|
|
|
/** |
|
* Returns the finalized value of the future response. |
|
* |
|
* @throws RuntimeException when the future response has not been finalized yet |
|
* |
|
* @return mixed |
|
*/ |
|
public function value() |
|
{ |
|
if (!$this->isReady()) { |
|
throw new RuntimeException('Cannot retrieve a value from a non-finalized future response'); |
|
} |
|
|
|
return $this->value; |
|
} |
|
|
|
/** |
|
* Binds the future response value to an external variable. |
|
* |
|
* @param mixed $value |
|
* |
|
* @throws RuntimeException when the future response has been already finalized |
|
* |
|
* @return self |
|
*/ |
|
public function bind(&$value): self |
|
{ |
|
if ($this->isReady()) { |
|
throw new RuntimeException( |
|
'Cannot bind an external variable when the future response has been already finalized' |
|
); |
|
} |
|
|
|
$this->binding = &$value; |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Makes sure that the user-supplied callable is valid. |
|
* |
|
* The user-supplied callable must define at least two required parameters: |
|
* |
|
* - the first one of type Predis\Command\CommandInterface for the command. |
|
* - the second one of mixed type for the response. |
|
* |
|
* When the user-supplied callable defines just one required parameter it is |
|
* wrapped by another callable defining the signature as described above: in |
|
* this case the original callable receives just the response payload, this |
|
* is to make it easier to directly use PHP functions such as `is_array` for |
|
* validation or `iterator_to_array` for transformation, or any other single |
|
* parameter method that receives just one value. |
|
* |
|
* @param callable $callable User-supplied callable |
|
* |
|
* @return callable |
|
*/ |
|
protected static function ensureValidCallable(callable $callable): callable |
|
{ |
|
if (is_object($callable)) { |
|
$reflection = new ReflectionMethod($callable, '__invoke'); |
|
} elseif (is_array($callable)) { |
|
$reflection = new ReflectionMethod($callable[0], $callable[1]); |
|
} else { |
|
$reflection = new ReflectionFunction($callable); |
|
} |
|
|
|
$parameters = $reflection->getNumberOfRequiredParameters(); |
|
|
|
if ($parameters === 0) { |
|
throw new InvalidArgumentException('User-supplied callable must define at least one parameter'); |
|
} |
|
|
|
if ($parameters > 1 && $reflection->getParameters()[0]->getType()->getName() !== CommandInterface::class) { |
|
throw new InvalidArgumentException(sprintf( |
|
'User-supplied callable must define at least two parameters with the first one being a %s', |
|
CommandInterface::class |
|
)); |
|
} |
|
|
|
if ($parameters === 1) { |
|
$callable = function (CommandInterface $command, $response) use ($callable) { |
|
return $callable($response); |
|
}; |
|
} |
|
|
|
return $callable; |
|
} |
|
|
|
/** |
|
* Appends a callable invoked for response transformation. |
|
* |
|
* The user-supplied callable receives the input value as the only |
|
* argument and must always return the resulting value. |
|
* |
|
* Transformations are chained so the next transformer in the chain receives |
|
* the result of the previous transformation. |
|
* |
|
* @param callable $transformer Callable for value transformation |
|
* |
|
* @return self |
|
*/ |
|
public function transform(callable $transformer): self |
|
{ |
|
$this->transformers[] = static::ensureValidCallable($transformer); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Returns the response as an array. |
|
* |
|
* @return self |
|
*/ |
|
public function asArray(bool $value = true): self |
|
{ |
|
$this->transformToArray = $value; |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Transforms the response to an array. |
|
* |
|
* Non iterable values are returned as single-element arrays. |
|
* |
|
* @param mixed $response |
|
* |
|
* @return array |
|
*/ |
|
protected static function toArray($response): array |
|
{ |
|
if (is_array($response)) { |
|
return $response; |
|
} |
|
|
|
if ($response instanceof Traversable) { |
|
return iterator_to_array($response); |
|
} |
|
|
|
return (array) $response; |
|
} |
|
|
|
/** |
|
* Returns the response as a generator. |
|
* |
|
* @return self |
|
*/ |
|
public function asGenerator(bool $value = true): self |
|
{ |
|
$this->transformToGenerator = $value; |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Transforms the response to a generator. |
|
* |
|
* Non iterable values are returned as single-element generator. |
|
* |
|
* @param mixed $response |
|
* |
|
* @return Generator |
|
*/ |
|
protected static function toGenerator($response): Generator |
|
{ |
|
if ($response instanceof Generator) { |
|
return $response; |
|
} |
|
|
|
if (!is_iterable($response)) { |
|
$response = (array) $response; |
|
} |
|
|
|
foreach ($response as $key => $value) { |
|
yield $key => $value; |
|
} |
|
} |
|
|
|
/** |
|
* Attaches a callable acting as a validator for the response. |
|
* |
|
* The user-supplied callable is invoked when the future response is being |
|
* finalized, right before proceeding with transformations, to verify that |
|
* the raw response returned from Redis can be considered valid. |
|
* |
|
* The validator receives three arguments: |
|
* |
|
* - the command associated to the response returned from Redis; |
|
* - the response payload; |
|
* - an optional string by reference to customize the exception message; |
|
* |
|
* The validator must return a boolean to indicate whether the response can |
|
* be considered valid before proceeding. |
|
* |
|
* @param callable $listener Callable for validation |
|
* |
|
* @return self |
|
*/ |
|
public function validate(callable $validator): self |
|
{ |
|
$this->validator = static::ensureValidCallable($validator); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Appends a callable invoked when the future response has been finalized. |
|
* |
|
* The user-supplied callable receives two arguments: |
|
* |
|
* - the command associated to the response returned from Redis; |
|
* - the transformed response payload; |
|
* |
|
* Any value returned by the user-supplied callable is ignored. |
|
* |
|
* @param callable $listener Callable for notification |
|
* |
|
* @return self |
|
*/ |
|
public function ready(callable $listener): self |
|
{ |
|
$this->readyListeners[] = static::ensureValidCallable($listener); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Appends a callable invoked when the future response generates an error. |
|
* |
|
* The user-supplied callable receives three arguments: |
|
* |
|
* - the command associated to the response returned from Redis |
|
* - the transformed response |
|
* - the underlying exception instance |
|
* |
|
* Any value returned by the user-supplied callable is ignored. |
|
* |
|
* @param callable $listener Callable for notification |
|
* |
|
* @return self |
|
*/ |
|
public function error(callable $listener): self |
|
{ |
|
$this->errorListeners[] = static::ensureValidCallable($listener); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Appends internal trasformers to the user-supplied list of transformers. |
|
*/ |
|
protected function finalizeTransformers(): void |
|
{ |
|
if ($this->transformToArray) { |
|
$this->transformers[] = static::ensureValidCallable([$this, 'toArray']); |
|
} |
|
|
|
if ($this->transformToGenerator) { |
|
$this->transformers[] = static::ensureValidCallable([$this, 'toGenerator']); |
|
} |
|
} |
|
|
|
/** |
|
* Validates the response returned from Redis. |
|
* |
|
* @param CommandInterface $command Command associated to the response |
|
* @param mixed $response Response returned from Redis |
|
* |
|
* @return mixed |
|
*/ |
|
protected function validateResponse(CommandInterface $command, $response) |
|
{ |
|
if ($this->validator) { |
|
$message = null; |
|
|
|
if (false === call_user_func_array($this->validator, [$command, $response, &$message])) { |
|
throw new UnexpectedValueException( |
|
$message ?? "Failed validating response to {$command->getId()}" |
|
); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Transforms the response returned from Redis. |
|
* |
|
* @param CommandInterface $command Command associated to the response |
|
* @param mixed $response Response returned from Redis |
|
* |
|
* @return mixed |
|
*/ |
|
protected function transformResponse(CommandInterface $command, $response) |
|
{ |
|
$this->finalizeTransformers(); |
|
|
|
foreach ($this->transformers as $transformer) { |
|
$response = $transformer($command, $response); |
|
} |
|
|
|
return $response; |
|
} |
|
|
|
/** |
|
* Notify ready listeners when the future response has been finalized. |
|
* |
|
* @param CommandInterface $command Command associated to the response |
|
* @param mixed $response Response returned from Redis |
|
*/ |
|
protected function notifyReadyListeners(CommandInterface $command, $response): void |
|
{ |
|
foreach ($this->readyListeners as $listener) { |
|
$listener($command, $response); |
|
} |
|
} |
|
|
|
/** |
|
* Notify error listeners of errors during the future response finalization. |
|
* |
|
* @param CommandInterface $command Command associated to the response |
|
* @param mixed $response Response returned from Redis |
|
* @param Exception $exception Underlying exception |
|
*/ |
|
protected function notifyErrorListeners(CommandInterface $command, $response, Exception $exception): void |
|
{ |
|
foreach ($this->errorListeners as $listener) { |
|
$listener($command, $response, $exception); |
|
} |
|
} |
|
|
|
/** |
|
* Finalizes the future response with the value returned from Redis. |
|
* |
|
* @param CommandInterface $command Command associated to the response |
|
* @param mixed $response Response returned from Redis |
|
* |
|
* @throws RuntimeException when the future response has been already finalized |
|
*/ |
|
public function __invoke(CommandInterface $command, $response) |
|
{ |
|
if ($this->isReady()) { |
|
throw new RuntimeException('Cannot finalize a future response that has been already finalized'); |
|
} |
|
|
|
try { |
|
$this->validateResponse($command, $response); |
|
$this->value = $this->transformResponse($command, $response); |
|
} catch (Exception $exception) { |
|
$this->notifyErrorListeners($command, $response, $exception); |
|
|
|
// TODO: admittedly I'm still not sure how to handle exceptions from |
|
// validator and transformers callbacks, I have a flag here just to |
|
// quickly switch back and forth while experimenting with stuff. |
|
if ($this->throw) { |
|
throw $exception; |
|
} |
|
|
|
return; |
|
} |
|
|
|
$this->binding = $this->value; |
|
$this->ready = true; |
|
|
|
$this->notifyReadyListeners($command, $this->value); |
|
} |
|
} |