coroutine.php 3.7 KB

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