1
0
mirror of https://github.com/danog/amp.git synced 2024-12-15 02:47:30 +01:00
amp/lib/functions.php
Daniel Lowrey 1cb293b776 Remove Combinator/Resolver in favor of functions
This commit removes the Combinator and Resolver classes altogether.
The relevant functionality is now concentrated entirely in the
analogous functions:

- all()
- some()
- any()
- first()
- map()
- filter()
- resolve()

Additionally, all Promisor implementations now *require* a Reactor
upon instantiation. Previously this parameter could be null and the
static singleton reactor instance would be used automatically.
2014-11-24 23:14:07 -05:00

556 lines
16 KiB
PHP

<?php
namespace Amp;
/**
* Get a singleton event reactor instance
*
* Note that the $factory callable is only invoked if no global reactor has yet been initialized.
*
* @param callable $factory Optional factory callable for initializing a reactor
* @return \Amp\Reactor
*/
function reactor(callable $factory = null) {
static $reactor;
return ($reactor = $reactor ?: ReactorFactory::select($factory));
}
/**
* If any one of the Promises fails the resulting Promise will fail. Otherwise
* the resulting Promise succeeds with an array matching keys from the input array
* to their resolved values.
*
* @param \Amp\Reactor $reactor
* @param array[\Amp\Promise] $promises
* @return \Amp\Promise
*/
function all(Reactor $reactor, array $promises) {
if (empty($promises)) {
return new Success([]);
}
$results = [];
$remaining = count($promises);
$promisor = new Future($reactor);
$isResolved = false;
foreach ($promises as $key => $resolvable) {
if (!$resolvable instanceof Promise) {
$results[$key] = $resolvable;
$remaining--;
continue;
}
$resolvable->when(function($error, $result) use (&$remaining, &$results, $key, $promisor) {
// If the promisor already failed don't bother
if (empty($remaining)) {
return;
}
if ($error) {
$remaining = 0;
$promisor->fail($error);
return;
}
$results[$key] = $result;
if (--$remaining === 0) {
$promisor->succeed($results);
}
});
}
// We can return $promisor directly because the Future Promisor implementation
// also implements Promise for convenience
return $promisor;
}
/**
* Resolves with a two-item array delineating successful and failed Promise results.
*
* The resulting Promise will only fail if ALL of the Promise values fail or if the
* Promise array is empty.
*
* The resulting Promise is resolved with an indexed two-item array of the following form:
*
* [$arrayOfFailures, $arrayOfSuccesses]
*
* The individual keys in the resulting arrays are preserved from the initial Promise array
* passed to the function for evaluation.
*
* @param \Amp\Reactor $reactor
* @param array[\Amp\Promise] $promises
* @return \Amp\Promise
*/
function some(Reactor $reactor, array $promises) {
if (empty($promises)) {
return new Failure(new \LogicException(
'No promises or values provided for resolution'
));
}
$results = $errors = [];
$remaining = count($promises);
$promisor = new Future($reactor);
foreach ($promises as $key => $resolvable) {
if (!$resolvable instanceof Promise) {
$results[$key] = $resolvable;
$remaining--;
continue;
}
$resolvable->when(function($error, $result) use (&$remaining, &$results, &$errors, $key, $promisor) {
if ($error) {
$errors[$key] = $error;
} else {
$results[$key] = $result;
}
if (--$remaining > 0) {
return;
} elseif (empty($results)) {
$promisor->fail(new \RuntimeException(
'All promises failed'
));
} else {
$promisor->succeed([$errors, $results]);
}
});
}
// We can return $promisor directly because the Future Promisor implementation
// also implements Promise for convenience
return $promisor;
}
/**
* Resolves with a two-item array delineating successful and failed Promise results.
*
* This function is the same as some() with the notable exception that it will never fail even
* if all promises in the array resolve unsuccessfully.
*
* @param \Amp\Reactor $reactor
* @param array $promises
* @return \Amp\Promise
*/
function any(Reactor $reactor, array $promises) {
if (empty($promises)) {
return new Success([], []);
}
$results = [];
$errors = [];
$remaining = count($promises);
$promisor = new Future($reactor);
foreach ($promises as $key => $resolvable) {
if (!$resolvable instanceof Promise) {
$results[$key] = $resolvable;
$remaining--;
continue;
}
$resolvable->when(function($error, $result) use (&$remaining, &$results, &$errors, $key, $promisor) {
if ($error) {
$errors[$key] = $error;
} else {
$results[$key] = $result;
}
if (--$remaining === 0) {
$promisor->succeed([$errors, $results]);
}
});
}
// We can return $promisor directly because the Future Promisor implementation
// also implements Promise for convenience
return $promisor;
}
/**
* Resolves with the first successful Promise value. The resulting Promise will only fail if all
* Promise values in the group fail or if the initial Promise array is empty.
*
* @param \Amp\Reactor $reactor
* @param array[\Amp\Promise] $promises
* @return \Amp\Promise
*/
function first(Reactor $reactor, array $promises) {
if (empty($promises)) {
return new Failure(new \LogicException(
'No promises or values provided for resolution'
));
}
$remaining = count($promises);
$isComplete = false;
$promisor = new Future($reactor);
foreach ($promises as $resolvable) {
if (!$resolvable instanceof Promise) {
$promisor->succeed($resolvable);
break;
}
$promise->when(function($error, $result) use (&$remaining, &$isComplete, $promisor) {
if ($isComplete) {
// we don't care about Futures that resolve after the first
return;
} elseif ($error && --$remaining === 0) {
$promisor->fail(new \RuntimeException(
'All promises failed'
));
} elseif (empty($error)) {
$isComplete = true;
$promisor->succeed($result);
}
});
}
// We can return $promisor directly because the Future Promisor implementation
// also implements Promise for convenience
return $promisor;
}
/**
* Map promised future values using the specified functor
*
* @param \Amp\Reactor $reactor
* @param array $promises
* @param callable $functor
* @return \Amp\Promise
*/
function map(Reactor $reactor, array $promises, callable $functor) {
if (empty($promises)) {
return new Success([]);
}
$results = [];
$remaining = count($promises);
$promisor = new Future($reactor);
foreach ($promises as $key => $resolvable) {
$promise = ($resolvable instanceof Promise) ? $resolvable : new Success($resolvable);
$promise->when(function($error, $result) use (&$remaining, &$results, $key, $promisor, $functor) {
if (empty($remaining)) {
// If the future already failed we don't bother.
return;
}
if ($error) {
$remaining = 0;
$promisor->fail($error);
return;
}
try {
$results[$key] = $functor($result);
if (--$remaining === 0) {
$promisor->succeed($results);
}
} catch (\Exception $error) {
$remaining = 0;
$promisor->fail($error);
}
});
}
// We can return $promisor directly because the Future Promisor implementation
// also implements Promise for convenience
return $promisor;
}
/**
* Filter future values using the specified functor
*
* If the functor returns a truthy value the resolved promise result is retained, otherwise it is
* discarded. Array keys are retained for any results not filtered out by the functor.
*
* @param \Amp\Reactor $reactor
* @param array $promises
* @param callable $functor
* @return \Amp\Promise
*/
function filter(Reactor $reactor, array $promises, callable $functor) {
if (empty($promises)) {
return new Success([]);
}
$results = [];
$remaining = count($promises);
$promisor = new Future($reactor);
foreach ($promises as $key => $resolvable) {
$promise = ($resolvable instanceof Promise) ? $resolvable : new Success($resolvable);
$promise->when(function($error, $result) use (&$remaining, &$results, $key, $promisor, $functor) {
if (empty($remaining)) {
// If the future result already failed we don't bother.
return;
}
if ($error) {
$remaining = 0;
$promisor->fail($error);
return;
}
try {
if ($functor($result)) {
$results[$key] = $result;
}
if (--$remaining === 0) {
$promisor->succeed($results);
}
} catch (\Exception $error) {
$promisor->fail($error);
}
});
}
// We can return $promisor directly because the Future Promisor implementation
// also implements Promise for convenience
return $promisor;
}
/**
* A co-routine to resolve Generators
*
* Returns a promise that will resolve when the generator completes. The final value yielded by
* the generator is used to resolve the returned Promise on success.
*
* Example:
*
* function anotherGenerator() {
* yield 1;
* }
*
* $generator = function() {
* $a = (yield 2);
* $b = (yield new Success(21));
* $c = (yield anotherGenerator());
* yield $a * $b * $c;
* };
*
* $reactor = new Amp\NativeReactor;
* $result = resolve($reactor, $generator())->wait();
* var_dump($result); // int(42)
*
* @param \Amp\Reactor $reactor
* @param \Generator $gen
* @return \Amp\Promise
*/
function resolve(Reactor $reactor, \Generator $gen) {
$promisor = new Future($reactor);
__advanceGenerator($reactor, $gen, $promisor);
return $promisor;
}
function __advanceGenerator(Reactor $reactor, \Generator $gen, Promisor $promisor, $previous = null) {
try {
if ($gen->valid()) {
$key = $gen->key();
$current = $gen->current();
$promiseStruct = __promisifyGeneratorYield($reactor, $key, $current);
$reactor->immediately(function() use ($reactor, $gen, $promisor, $promiseStruct) {
list($promise, $noWait) = $promiseStruct;
if ($noWait) {
__sendToGenerator($reactor, $gen, $promisor, $error = null, $result = null);
} else {
$promise->when(function($error, $result) use ($reactor, $gen, $promisor) {
__sendToGenerator($reactor, $gen, $promisor, $error, $result);
});
}
});
} else {
$promisor->succeed($previous);
}
} catch (\Exception $error) {
$promisor->fail($error);
}
}
function __promisifyGeneratorYield(Reactor $reactor, $key, $current) {
$noWait = false;
if ($key === (string) $key) {
goto explicit_key;
} else {
goto implicit_key;
}
explicit_key: {
$key = strtolower($key);
if ($key[0] === YieldCommands::NOWAIT_PREFIX) {
$noWait = true;
$key = substr($key, 1);
}
switch ($key) {
case YieldCommands::ALL:
// fallthrough
case YieldCommands::ANY:
// fallthrough
case YieldCommands::SOME:
if (is_array($current)) {
goto combinator;
} else {
$promise = new Failure(new \DomainException(
sprintf('"%s" yield command expects array; %s yielded', $key, gettype($current))
));
goto return_struct;
}
case YieldCommands::WAIT:
goto wait;
case YieldCommands::IMMEDIATELY:
goto immediately;
case YieldCommands::ONCE:
// fallthrough
case YieldCommands::REPEAT:
goto schedule;
case YieldCommands::ON_READABLE:
$ioWatchMethod = 'onReadable';
goto stream_io_watcher;
case YieldCommands::ON_WRITABLE:
$ioWatchMethod = 'onWritable';
goto stream_io_watcher;
case YieldCommands::ENABLE:
// fallthrough
case YieldCommands::DISABLE:
// fallthrough
case YieldCommands::CANCEL:
goto watcher_control;
case YieldCommands::NOWAIT:
$noWait = true;
goto implicit_key;
default:
$promise = new Failure(new \DomainException(
sprintf('Unknown or invalid yield key: "%s"', $key)
));
goto return_struct;
}
}
implicit_key: {
if ($current instanceof Promise) {
$promise = $current;
} elseif ($current instanceof \Generator) {
$promise = resolve($reactor, $current);
} elseif (is_array($current)) {
$key = YieldCommands::ALL;
goto combinator;
} else {
$promise = new Success($current);
}
goto return_struct;
}
combinator: {
$promises = [];
foreach ($current as $index => $element) {
if ($element instanceof Promise) {
$promise = $element;
} elseif ($element instanceof \Generator) {
$promise = resolve($reactor, $element);
} else {
$promise = new Success($element);
}
$promises[$index] = $promise;
}
$combinatorFunction = __NAMESPACE__ . "\\{$key}";
$promise = $combinatorFunction($reactor, $promises);
goto return_struct;
}
immediately: {
if (is_callable($current)) {
$watcherId = $reactor->immediately($current);
$promise = new Success($watcherId);
} else {
$promise = new Failure(new \DomainException(
sprintf(
'"%s" yield command requires callable; %s provided',
$key,
gettype($current)
)
));
}
goto return_struct;
}
schedule: {
if (is_array($current) && isset($current[0], $current[1]) && is_callable($current[0])) {
list($func, $msDelay) = $current;
$watcherId = $reactor->{$key}($func, $msDelay);
$promise = new Success($watcherId);
} else {
$promise = new Failure(new \DomainException(
sprintf(
'"%s" yield command requires [callable $func, int $msDelay]; %s provided',
$key,
gettype($current)
)
));
}
goto return_struct;
}
stream_io_watcher: {
if (is_array($current) && isset($current[0], $current[1]) && is_callable($current[1])) {
list($stream, $func, $enableNow) = $current;
$watcherId = $reactor->{$ioWatchMethod}($stream, $func, $enableNow);
$promise = new Success($watcherId);
} else {
$promise = new Failure(new \DomainException(
sprintf(
'"%s" yield command requires [resource $stream, callable $func, bool $enableNow]; %s provided',
$key,
gettype($current)
)
));
}
goto return_struct;
}
wait: {
$promisor = new Future($reactor);
$reactor->once(function() use ($promisor) {
$promisor->succeed();
}, (int) $current);
$promise = $promisor;
goto return_struct;
}
watcher_control: {
$reactor->{$key}($current);
$promise = new Success;
goto return_struct;
}
return_struct: {
return [$promise, $noWait];
}
}
function __sendToGenerator(Reactor $reactor, \Generator $gen, Promisor $promisor, \Exception $error = null, $result = null) {
try {
if ($error) {
$gen->throw($error);
} else {
$gen->send($result);
}
__advanceGenerator($reactor, $gen, $promisor, $result);
} catch (\Exception $error) {
$promisor->fail($error);
}
}