1<?php 2 3namespace Sabre\Event; 4 5use Generator; 6use Exception; 7 8/** 9 * Turn asynchronous promise-based code into something that looks synchronous 10 * again, through the use of generators. 11 * 12 * Example without coroutines: 13 * 14 * $promise = $httpClient->request('GET', '/foo'); 15 * $promise->then(function($value) { 16 * 17 * return $httpClient->request('DELETE','/foo'); 18 * 19 * })->then(function($value) { 20 * 21 * return $httpClient->request('PUT', '/foo'); 22 * 23 * })->error(function($reason) { 24 * 25 * echo "Failed because: $reason\n"; 26 * 27 * }); 28 * 29 * Example with coroutines: 30 * 31 * coroutine(function() { 32 * 33 * try { 34 * yield $httpClient->request('GET', '/foo'); 35 * yield $httpClient->request('DELETE', /foo'); 36 * yield $httpClient->request('PUT', '/foo'); 37 * } catch(\Exception $reason) { 38 * echo "Failed because: $reason\n"; 39 * } 40 * 41 * }); 42 * 43 * @copyright Copyright (C) 2013-2015 fruux GmbH. All rights reserved. 44 * @author Evert Pot (http://evertpot.com/) 45 * @license http://sabre.io/license/ Modified BSD License 46 */ 47function coroutine(callable $gen) { 48 49 $generator = $gen(); 50 if (!$generator instanceof Generator) { 51 throw new \InvalidArgumentException('You must pass a generator function'); 52 } 53 54 // This is the value we're returning. 55 $promise = new Promise(); 56 57 $lastYieldResult = null; 58 59 /** 60 * So tempted to use the mythical y-combinator here, but it's not needed in 61 * PHP. 62 */ 63 $advanceGenerator = function() use (&$advanceGenerator, $generator, $promise, &$lastYieldResult) { 64 65 while ($generator->valid()) { 66 67 $yieldedValue = $generator->current(); 68 if ($yieldedValue instanceof Promise) { 69 $yieldedValue->then( 70 function($value) use ($generator, &$advanceGenerator, &$lastYieldResult) { 71 $lastYieldResult = $value; 72 $generator->send($value); 73 $advanceGenerator(); 74 }, 75 function($reason) use ($generator, $advanceGenerator) { 76 if ($reason instanceof Exception) { 77 $generator->throw($reason); 78 } elseif (is_scalar($reason)) { 79 $generator->throw(new Exception($reason)); 80 } else { 81 $type = is_object($reason) ? get_class($reason) : gettype($reason); 82 $generator->throw(new Exception('Promise was rejected with reason of type: ' . $type)); 83 } 84 $advanceGenerator(); 85 } 86 )->error(function($reason) use ($promise) { 87 // This error handler would be called, if something in the 88 // generator throws an exception, and it's not caught 89 // locally. 90 $promise->reject($reason); 91 }); 92 // We need to break out of the loop, because $advanceGenerator 93 // will be called asynchronously when the promise has a result. 94 break; 95 } else { 96 // If the value was not a promise, we'll just let it pass through. 97 $lastYieldResult = $yieldedValue; 98 $generator->send($yieldedValue); 99 } 100 101 } 102 103 // If the generator is at the end, and we didn't run into an exception, 104 // we can fullfill the promise with the last thing that was yielded to 105 // us. 106 if (!$generator->valid() && $promise->state === Promise::PENDING) { 107 $promise->fulfill($lastYieldResult); 108 } 109 110 }; 111 112 try { 113 $advanceGenerator(); 114 } catch (Exception $e) { 115 $promise->reject($e); 116 } 117 118 return $promise; 119 120} 121