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