You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

README.md 15 KiB

4 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. # Guzzle Promises
  2. [Promises/A+](https://promisesaplus.com/) implementation that handles promise
  3. chaining and resolution iteratively, allowing for "infinite" promise chaining
  4. while keeping the stack size constant. Read [this blog post](https://blog.domenic.me/youre-missing-the-point-of-promises/)
  5. for a general introduction to promises.
  6. - [Features](#features)
  7. - [Quick start](#quick-start)
  8. - [Synchronous wait](#synchronous-wait)
  9. - [Cancellation](#cancellation)
  10. - [API](#api)
  11. - [Promise](#promise)
  12. - [FulfilledPromise](#fulfilledpromise)
  13. - [RejectedPromise](#rejectedpromise)
  14. - [Promise interop](#promise-interop)
  15. - [Implementation notes](#implementation-notes)
  16. # Features
  17. - [Promises/A+](https://promisesaplus.com/) implementation.
  18. - Promise resolution and chaining is handled iteratively, allowing for
  19. "infinite" promise chaining.
  20. - Promises have a synchronous `wait` method.
  21. - Promises can be cancelled.
  22. - Works with any object that has a `then` function.
  23. - C# style async/await coroutine promises using
  24. `GuzzleHttp\Promise\coroutine()`.
  25. # Quick start
  26. A *promise* represents the eventual result of an asynchronous operation. The
  27. primary way of interacting with a promise is through its `then` method, which
  28. registers callbacks to receive either a promise's eventual value or the reason
  29. why the promise cannot be fulfilled.
  30. ## Callbacks
  31. Callbacks are registered with the `then` method by providing an optional
  32. `$onFulfilled` followed by an optional `$onRejected` function.
  33. ```php
  34. use GuzzleHttp\Promise\Promise;
  35. $promise = new Promise();
  36. $promise->then(
  37. // $onFulfilled
  38. function ($value) {
  39. echo 'The promise was fulfilled.';
  40. },
  41. // $onRejected
  42. function ($reason) {
  43. echo 'The promise was rejected.';
  44. }
  45. );
  46. ```
  47. *Resolving* a promise means that you either fulfill a promise with a *value* or
  48. reject a promise with a *reason*. Resolving a promises triggers callbacks
  49. registered with the promises's `then` method. These callbacks are triggered
  50. only once and in the order in which they were added.
  51. ## Resolving a promise
  52. Promises are fulfilled using the `resolve($value)` method. Resolving a promise
  53. with any value other than a `GuzzleHttp\Promise\RejectedPromise` will trigger
  54. all of the onFulfilled callbacks (resolving a promise with a rejected promise
  55. will reject the promise and trigger the `$onRejected` callbacks).
  56. ```php
  57. use GuzzleHttp\Promise\Promise;
  58. $promise = new Promise();
  59. $promise
  60. ->then(function ($value) {
  61. // Return a value and don't break the chain
  62. return "Hello, " . $value;
  63. })
  64. // This then is executed after the first then and receives the value
  65. // returned from the first then.
  66. ->then(function ($value) {
  67. echo $value;
  68. });
  69. // Resolving the promise triggers the $onFulfilled callbacks and outputs
  70. // "Hello, reader".
  71. $promise->resolve('reader.');
  72. ```
  73. ## Promise forwarding
  74. Promises can be chained one after the other. Each then in the chain is a new
  75. promise. The return value of a promise is what's forwarded to the next
  76. promise in the chain. Returning a promise in a `then` callback will cause the
  77. subsequent promises in the chain to only be fulfilled when the returned promise
  78. has been fulfilled. The next promise in the chain will be invoked with the
  79. resolved value of the promise.
  80. ```php
  81. use GuzzleHttp\Promise\Promise;
  82. $promise = new Promise();
  83. $nextPromise = new Promise();
  84. $promise
  85. ->then(function ($value) use ($nextPromise) {
  86. echo $value;
  87. return $nextPromise;
  88. })
  89. ->then(function ($value) {
  90. echo $value;
  91. });
  92. // Triggers the first callback and outputs "A"
  93. $promise->resolve('A');
  94. // Triggers the second callback and outputs "B"
  95. $nextPromise->resolve('B');
  96. ```
  97. ## Promise rejection
  98. When a promise is rejected, the `$onRejected` callbacks are invoked with the
  99. rejection reason.
  100. ```php
  101. use GuzzleHttp\Promise\Promise;
  102. $promise = new Promise();
  103. $promise->then(null, function ($reason) {
  104. echo $reason;
  105. });
  106. $promise->reject('Error!');
  107. // Outputs "Error!"
  108. ```
  109. ## Rejection forwarding
  110. If an exception is thrown in an `$onRejected` callback, subsequent
  111. `$onRejected` callbacks are invoked with the thrown exception as the reason.
  112. ```php
  113. use GuzzleHttp\Promise\Promise;
  114. $promise = new Promise();
  115. $promise->then(null, function ($reason) {
  116. throw new \Exception($reason);
  117. })->then(null, function ($reason) {
  118. assert($reason->getMessage() === 'Error!');
  119. });
  120. $promise->reject('Error!');
  121. ```
  122. You can also forward a rejection down the promise chain by returning a
  123. `GuzzleHttp\Promise\RejectedPromise` in either an `$onFulfilled` or
  124. `$onRejected` callback.
  125. ```php
  126. use GuzzleHttp\Promise\Promise;
  127. use GuzzleHttp\Promise\RejectedPromise;
  128. $promise = new Promise();
  129. $promise->then(null, function ($reason) {
  130. return new RejectedPromise($reason);
  131. })->then(null, function ($reason) {
  132. assert($reason === 'Error!');
  133. });
  134. $promise->reject('Error!');
  135. ```
  136. If an exception is not thrown in a `$onRejected` callback and the callback
  137. does not return a rejected promise, downstream `$onFulfilled` callbacks are
  138. invoked using the value returned from the `$onRejected` callback.
  139. ```php
  140. use GuzzleHttp\Promise\Promise;
  141. use GuzzleHttp\Promise\RejectedPromise;
  142. $promise = new Promise();
  143. $promise
  144. ->then(null, function ($reason) {
  145. return "It's ok";
  146. })
  147. ->then(function ($value) {
  148. assert($value === "It's ok");
  149. });
  150. $promise->reject('Error!');
  151. ```
  152. # Synchronous wait
  153. You can synchronously force promises to complete using a promise's `wait`
  154. method. When creating a promise, you can provide a wait function that is used
  155. to synchronously force a promise to complete. When a wait function is invoked
  156. it is expected to deliver a value to the promise or reject the promise. If the
  157. wait function does not deliver a value, then an exception is thrown. The wait
  158. function provided to a promise constructor is invoked when the `wait` function
  159. of the promise is called.
  160. ```php
  161. $promise = new Promise(function () use (&$promise) {
  162. $promise->resolve('foo');
  163. });
  164. // Calling wait will return the value of the promise.
  165. echo $promise->wait(); // outputs "foo"
  166. ```
  167. If an exception is encountered while invoking the wait function of a promise,
  168. the promise is rejected with the exception and the exception is thrown.
  169. ```php
  170. $promise = new Promise(function () use (&$promise) {
  171. throw new \Exception('foo');
  172. });
  173. $promise->wait(); // throws the exception.
  174. ```
  175. Calling `wait` on a promise that has been fulfilled will not trigger the wait
  176. function. It will simply return the previously resolved value.
  177. ```php
  178. $promise = new Promise(function () { die('this is not called!'); });
  179. $promise->resolve('foo');
  180. echo $promise->wait(); // outputs "foo"
  181. ```
  182. Calling `wait` on a promise that has been rejected will throw an exception. If
  183. the rejection reason is an instance of `\Exception` the reason is thrown.
  184. Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason
  185. can be obtained by calling the `getReason` method of the exception.
  186. ```php
  187. $promise = new Promise();
  188. $promise->reject('foo');
  189. $promise->wait();
  190. ```
  191. > PHP Fatal error: Uncaught exception 'GuzzleHttp\Promise\RejectionException' with message 'The promise was rejected with value: foo'
  192. ## Unwrapping a promise
  193. When synchronously waiting on a promise, you are joining the state of the
  194. promise into the current state of execution (i.e., return the value of the
  195. promise if it was fulfilled or throw an exception if it was rejected). This is
  196. called "unwrapping" the promise. Waiting on a promise will by default unwrap
  197. the promise state.
  198. You can force a promise to resolve and *not* unwrap the state of the promise
  199. by passing `false` to the first argument of the `wait` function:
  200. ```php
  201. $promise = new Promise();
  202. $promise->reject('foo');
  203. // This will not throw an exception. It simply ensures the promise has
  204. // been resolved.
  205. $promise->wait(false);
  206. ```
  207. When unwrapping a promise, the resolved value of the promise will be waited
  208. upon until the unwrapped value is not a promise. This means that if you resolve
  209. promise A with a promise B and unwrap promise A, the value returned by the
  210. wait function will be the value delivered to promise B.
  211. **Note**: when you do not unwrap the promise, no value is returned.
  212. # Cancellation
  213. You can cancel a promise that has not yet been fulfilled using the `cancel()`
  214. method of a promise. When creating a promise you can provide an optional
  215. cancel function that when invoked cancels the action of computing a resolution
  216. of the promise.
  217. # API
  218. ## Promise
  219. When creating a promise object, you can provide an optional `$waitFn` and
  220. `$cancelFn`. `$waitFn` is a function that is invoked with no arguments and is
  221. expected to resolve the promise. `$cancelFn` is a function with no arguments
  222. that is expected to cancel the computation of a promise. It is invoked when the
  223. `cancel()` method of a promise is called.
  224. ```php
  225. use GuzzleHttp\Promise\Promise;
  226. $promise = new Promise(
  227. function () use (&$promise) {
  228. $promise->resolve('waited');
  229. },
  230. function () {
  231. // do something that will cancel the promise computation (e.g., close
  232. // a socket, cancel a database query, etc...)
  233. }
  234. );
  235. assert('waited' === $promise->wait());
  236. ```
  237. A promise has the following methods:
  238. - `then(callable $onFulfilled, callable $onRejected) : PromiseInterface`
  239. Appends fulfillment and rejection handlers to the promise, and returns a new promise resolving to the return value of the called handler.
  240. - `otherwise(callable $onRejected) : PromiseInterface`
  241. Appends a rejection handler callback to the promise, and returns a new promise resolving to the return value of the callback if it is called, or to its original fulfillment value if the promise is instead fulfilled.
  242. - `wait($unwrap = true) : mixed`
  243. Synchronously waits on the promise to complete.
  244. `$unwrap` controls whether or not the value of the promise is returned for a
  245. fulfilled promise or if an exception is thrown if the promise is rejected.
  246. This is set to `true` by default.
  247. - `cancel()`
  248. Attempts to cancel the promise if possible. The promise being cancelled and
  249. the parent most ancestor that has not yet been resolved will also be
  250. cancelled. Any promises waiting on the cancelled promise to resolve will also
  251. be cancelled.
  252. - `getState() : string`
  253. Returns the state of the promise. One of `pending`, `fulfilled`, or
  254. `rejected`.
  255. - `resolve($value)`
  256. Fulfills the promise with the given `$value`.
  257. - `reject($reason)`
  258. Rejects the promise with the given `$reason`.
  259. ## FulfilledPromise
  260. A fulfilled promise can be created to represent a promise that has been
  261. fulfilled.
  262. ```php
  263. use GuzzleHttp\Promise\FulfilledPromise;
  264. $promise = new FulfilledPromise('value');
  265. // Fulfilled callbacks are immediately invoked.
  266. $promise->then(function ($value) {
  267. echo $value;
  268. });
  269. ```
  270. ## RejectedPromise
  271. A rejected promise can be created to represent a promise that has been
  272. rejected.
  273. ```php
  274. use GuzzleHttp\Promise\RejectedPromise;
  275. $promise = new RejectedPromise('Error');
  276. // Rejected callbacks are immediately invoked.
  277. $promise->then(null, function ($reason) {
  278. echo $reason;
  279. });
  280. ```
  281. # Promise interop
  282. This library works with foreign promises that have a `then` method. This means
  283. you can use Guzzle promises with [React promises](https://github.com/reactphp/promise)
  284. for example. When a foreign promise is returned inside of a then method
  285. callback, promise resolution will occur recursively.
  286. ```php
  287. // Create a React promise
  288. $deferred = new React\Promise\Deferred();
  289. $reactPromise = $deferred->promise();
  290. // Create a Guzzle promise that is fulfilled with a React promise.
  291. $guzzlePromise = new \GuzzleHttp\Promise\Promise();
  292. $guzzlePromise->then(function ($value) use ($reactPromise) {
  293. // Do something something with the value...
  294. // Return the React promise
  295. return $reactPromise;
  296. });
  297. ```
  298. Please note that wait and cancel chaining is no longer possible when forwarding
  299. a foreign promise. You will need to wrap a third-party promise with a Guzzle
  300. promise in order to utilize wait and cancel functions with foreign promises.
  301. ## Event Loop Integration
  302. In order to keep the stack size constant, Guzzle promises are resolved
  303. asynchronously using a task queue. When waiting on promises synchronously, the
  304. task queue will be automatically run to ensure that the blocking promise and
  305. any forwarded promises are resolved. When using promises asynchronously in an
  306. event loop, you will need to run the task queue on each tick of the loop. If
  307. you do not run the task queue, then promises will not be resolved.
  308. You can run the task queue using the `run()` method of the global task queue
  309. instance.
  310. ```php
  311. // Get the global task queue
  312. $queue = \GuzzleHttp\Promise\queue();
  313. $queue->run();
  314. ```
  315. For example, you could use Guzzle promises with React using a periodic timer:
  316. ```php
  317. $loop = React\EventLoop\Factory::create();
  318. $loop->addPeriodicTimer(0, [$queue, 'run']);
  319. ```
  320. *TODO*: Perhaps adding a `futureTick()` on each tick would be faster?
  321. # Implementation notes
  322. ## Promise resolution and chaining is handled iteratively
  323. By shuffling pending handlers from one owner to another, promises are
  324. resolved iteratively, allowing for "infinite" then chaining.
  325. ```php
  326. <?php
  327. require 'vendor/autoload.php';
  328. use GuzzleHttp\Promise\Promise;
  329. $parent = new Promise();
  330. $p = $parent;
  331. for ($i = 0; $i < 1000; $i++) {
  332. $p = $p->then(function ($v) {
  333. // The stack size remains constant (a good thing)
  334. echo xdebug_get_stack_depth() . ', ';
  335. return $v + 1;
  336. });
  337. }
  338. $parent->resolve(0);
  339. var_dump($p->wait()); // int(1000)
  340. ```
  341. When a promise is fulfilled or rejected with a non-promise value, the promise
  342. then takes ownership of the handlers of each child promise and delivers values
  343. down the chain without using recursion.
  344. When a promise is resolved with another promise, the original promise transfers
  345. all of its pending handlers to the new promise. When the new promise is
  346. eventually resolved, all of the pending handlers are delivered the forwarded
  347. value.
  348. ## A promise is the deferred.
  349. Some promise libraries implement promises using a deferred object to represent
  350. a computation and a promise object to represent the delivery of the result of
  351. the computation. This is a nice separation of computation and delivery because
  352. consumers of the promise cannot modify the value that will be eventually
  353. delivered.
  354. One side effect of being able to implement promise resolution and chaining
  355. iteratively is that you need to be able for one promise to reach into the state
  356. of another promise to shuffle around ownership of handlers. In order to achieve
  357. this without making the handlers of a promise publicly mutable, a promise is
  358. also the deferred value, allowing promises of the same parent class to reach
  359. into and modify the private properties of promises of the same type. While this
  360. does allow consumers of the value to modify the resolution or rejection of the
  361. deferred, it is a small price to pay for keeping the stack size constant.
  362. ```php
  363. $promise = new Promise();
  364. $promise->then(function ($value) { echo $value; });
  365. // The promise is the deferred value, so you can deliver a value to it.
  366. $promise->resolve('foo');
  367. // prints "foo"
  368. ```