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.

532 lines
16 KiB

3 years ago
  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::of()`.
  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. $promise = new Promise();
  142. $promise
  143. ->then(null, function ($reason) {
  144. return "It's ok";
  145. })
  146. ->then(function ($value) {
  147. assert($value === "It's ok");
  148. });
  149. $promise->reject('Error!');
  150. ```
  151. # Synchronous wait
  152. You can synchronously force promises to complete using a promise's `wait`
  153. method. When creating a promise, you can provide a wait function that is used
  154. to synchronously force a promise to complete. When a wait function is invoked
  155. it is expected to deliver a value to the promise or reject the promise. If the
  156. wait function does not deliver a value, then an exception is thrown. The wait
  157. function provided to a promise constructor is invoked when the `wait` function
  158. of the promise is called.
  159. ```php
  160. $promise = new Promise(function () use (&$promise) {
  161. $promise->resolve('foo');
  162. });
  163. // Calling wait will return the value of the promise.
  164. echo $promise->wait(); // outputs "foo"
  165. ```
  166. If an exception is encountered while invoking the wait function of a promise,
  167. the promise is rejected with the exception and the exception is thrown.
  168. ```php
  169. $promise = new Promise(function () use (&$promise) {
  170. throw new Exception('foo');
  171. });
  172. $promise->wait(); // throws the exception.
  173. ```
  174. Calling `wait` on a promise that has been fulfilled will not trigger the wait
  175. function. It will simply return the previously resolved value.
  176. ```php
  177. $promise = new Promise(function () { die('this is not called!'); });
  178. $promise->resolve('foo');
  179. echo $promise->wait(); // outputs "foo"
  180. ```
  181. Calling `wait` on a promise that has been rejected will throw an exception. If
  182. the rejection reason is an instance of `\Exception` the reason is thrown.
  183. Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason
  184. can be obtained by calling the `getReason` method of the exception.
  185. ```php
  186. $promise = new Promise();
  187. $promise->reject('foo');
  188. $promise->wait();
  189. ```
  190. > PHP Fatal error: Uncaught exception 'GuzzleHttp\Promise\RejectionException' with message 'The promise was rejected with value: foo'
  191. ## Unwrapping a promise
  192. When synchronously waiting on a promise, you are joining the state of the
  193. promise into the current state of execution (i.e., return the value of the
  194. promise if it was fulfilled or throw an exception if it was rejected). This is
  195. called "unwrapping" the promise. Waiting on a promise will by default unwrap
  196. the promise state.
  197. You can force a promise to resolve and *not* unwrap the state of the promise
  198. by passing `false` to the first argument of the `wait` function:
  199. ```php
  200. $promise = new Promise();
  201. $promise->reject('foo');
  202. // This will not throw an exception. It simply ensures the promise has
  203. // been resolved.
  204. $promise->wait(false);
  205. ```
  206. When unwrapping a promise, the resolved value of the promise will be waited
  207. upon until the unwrapped value is not a promise. This means that if you resolve
  208. promise A with a promise B and unwrap promise A, the value returned by the
  209. wait function will be the value delivered to promise B.
  210. **Note**: when you do not unwrap the promise, no value is returned.
  211. # Cancellation
  212. You can cancel a promise that has not yet been fulfilled using the `cancel()`
  213. method of a promise. When creating a promise you can provide an optional
  214. cancel function that when invoked cancels the action of computing a resolution
  215. of the promise.
  216. # API
  217. ## Promise
  218. When creating a promise object, you can provide an optional `$waitFn` and
  219. `$cancelFn`. `$waitFn` is a function that is invoked with no arguments and is
  220. expected to resolve the promise. `$cancelFn` is a function with no arguments
  221. that is expected to cancel the computation of a promise. It is invoked when the
  222. `cancel()` method of a promise is called.
  223. ```php
  224. use GuzzleHttp\Promise\Promise;
  225. $promise = new Promise(
  226. function () use (&$promise) {
  227. $promise->resolve('waited');
  228. },
  229. function () {
  230. // do something that will cancel the promise computation (e.g., close
  231. // a socket, cancel a database query, etc...)
  232. }
  233. );
  234. assert('waited' === $promise->wait());
  235. ```
  236. A promise has the following methods:
  237. - `then(callable $onFulfilled, callable $onRejected) : PromiseInterface`
  238. Appends fulfillment and rejection handlers to the promise, and returns a new promise resolving to the return value of the called handler.
  239. - `otherwise(callable $onRejected) : PromiseInterface`
  240. 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.
  241. - `wait($unwrap = true) : mixed`
  242. Synchronously waits on the promise to complete.
  243. `$unwrap` controls whether or not the value of the promise is returned for a
  244. fulfilled promise or if an exception is thrown if the promise is rejected.
  245. This is set to `true` by default.
  246. - `cancel()`
  247. Attempts to cancel the promise if possible. The promise being cancelled and
  248. the parent most ancestor that has not yet been resolved will also be
  249. cancelled. Any promises waiting on the cancelled promise to resolve will also
  250. be cancelled.
  251. - `getState() : string`
  252. Returns the state of the promise. One of `pending`, `fulfilled`, or
  253. `rejected`.
  254. - `resolve($value)`
  255. Fulfills the promise with the given `$value`.
  256. - `reject($reason)`
  257. Rejects the promise with the given `$reason`.
  258. ## FulfilledPromise
  259. A fulfilled promise can be created to represent a promise that has been
  260. fulfilled.
  261. ```php
  262. use GuzzleHttp\Promise\FulfilledPromise;
  263. $promise = new FulfilledPromise('value');
  264. // Fulfilled callbacks are immediately invoked.
  265. $promise->then(function ($value) {
  266. echo $value;
  267. });
  268. ```
  269. ## RejectedPromise
  270. A rejected promise can be created to represent a promise that has been
  271. rejected.
  272. ```php
  273. use GuzzleHttp\Promise\RejectedPromise;
  274. $promise = new RejectedPromise('Error');
  275. // Rejected callbacks are immediately invoked.
  276. $promise->then(null, function ($reason) {
  277. echo $reason;
  278. });
  279. ```
  280. # Promise interop
  281. This library works with foreign promises that have a `then` method. This means
  282. you can use Guzzle promises with [React promises](https://github.com/reactphp/promise)
  283. for example. When a foreign promise is returned inside of a then method
  284. callback, promise resolution will occur recursively.
  285. ```php
  286. // Create a React promise
  287. $deferred = new React\Promise\Deferred();
  288. $reactPromise = $deferred->promise();
  289. // Create a Guzzle promise that is fulfilled with a React promise.
  290. $guzzlePromise = new GuzzleHttp\Promise\Promise();
  291. $guzzlePromise->then(function ($value) use ($reactPromise) {
  292. // Do something something with the value...
  293. // Return the React promise
  294. return $reactPromise;
  295. });
  296. ```
  297. Please note that wait and cancel chaining is no longer possible when forwarding
  298. a foreign promise. You will need to wrap a third-party promise with a Guzzle
  299. promise in order to utilize wait and cancel functions with foreign promises.
  300. ## Event Loop Integration
  301. In order to keep the stack size constant, Guzzle promises are resolved
  302. asynchronously using a task queue. When waiting on promises synchronously, the
  303. task queue will be automatically run to ensure that the blocking promise and
  304. any forwarded promises are resolved. When using promises asynchronously in an
  305. event loop, you will need to run the task queue on each tick of the loop. If
  306. you do not run the task queue, then promises will not be resolved.
  307. You can run the task queue using the `run()` method of the global task queue
  308. instance.
  309. ```php
  310. // Get the global task queue
  311. $queue = GuzzleHttp\Promise\Utils::queue();
  312. $queue->run();
  313. ```
  314. For example, you could use Guzzle promises with React using a periodic timer:
  315. ```php
  316. $loop = React\EventLoop\Factory::create();
  317. $loop->addPeriodicTimer(0, [$queue, 'run']);
  318. ```
  319. *TODO*: Perhaps adding a `futureTick()` on each tick would be faster?
  320. # Implementation notes
  321. ## Promise resolution and chaining is handled iteratively
  322. By shuffling pending handlers from one owner to another, promises are
  323. resolved iteratively, allowing for "infinite" then chaining.
  324. ```php
  325. <?php
  326. require 'vendor/autoload.php';
  327. use GuzzleHttp\Promise\Promise;
  328. $parent = new Promise();
  329. $p = $parent;
  330. for ($i = 0; $i < 1000; $i++) {
  331. $p = $p->then(function ($v) {
  332. // The stack size remains constant (a good thing)
  333. echo xdebug_get_stack_depth() . ', ';
  334. return $v + 1;
  335. });
  336. }
  337. $parent->resolve(0);
  338. var_dump($p->wait()); // int(1000)
  339. ```
  340. When a promise is fulfilled or rejected with a non-promise value, the promise
  341. then takes ownership of the handlers of each child promise and delivers values
  342. down the chain without using recursion.
  343. When a promise is resolved with another promise, the original promise transfers
  344. all of its pending handlers to the new promise. When the new promise is
  345. eventually resolved, all of the pending handlers are delivered the forwarded
  346. value.
  347. ## A promise is the deferred.
  348. Some promise libraries implement promises using a deferred object to represent
  349. a computation and a promise object to represent the delivery of the result of
  350. the computation. This is a nice separation of computation and delivery because
  351. consumers of the promise cannot modify the value that will be eventually
  352. delivered.
  353. One side effect of being able to implement promise resolution and chaining
  354. iteratively is that you need to be able for one promise to reach into the state
  355. of another promise to shuffle around ownership of handlers. In order to achieve
  356. this without making the handlers of a promise publicly mutable, a promise is
  357. also the deferred value, allowing promises of the same parent class to reach
  358. into and modify the private properties of promises of the same type. While this
  359. does allow consumers of the value to modify the resolution or rejection of the
  360. deferred, it is a small price to pay for keeping the stack size constant.
  361. ```php
  362. $promise = new Promise();
  363. $promise->then(function ($value) { echo $value; });
  364. // The promise is the deferred value, so you can deliver a value to it.
  365. $promise->resolve('foo');
  366. // prints "foo"
  367. ```
  368. ## Upgrading from Function API
  369. A static API was first introduced in 1.4.0, in order to mitigate problems with functions conflicting between global and local copies of the package. The function API will be removed in 2.0.0. A migration table has been provided here for your convenience:
  370. | Original Function | Replacement Method |
  371. |----------------|----------------|
  372. | `queue` | `Utils::queue` |
  373. | `task` | `Utils::task` |
  374. | `promise_for` | `Create::promiseFor` |
  375. | `rejection_for` | `Create::rejectionFor` |
  376. | `exception_for` | `Create::exceptionFor` |
  377. | `iter_for` | `Create::iterFor` |
  378. | `inspect` | `Utils::inspect` |
  379. | `inspect_all` | `Utils::inspectAll` |
  380. | `unwrap` | `Utils::unwrap` |
  381. | `all` | `Utils::all` |
  382. | `some` | `Utils::some` |
  383. | `any` | `Utils::any` |
  384. | `settle` | `Utils::settle` |
  385. | `each` | `Each::of` |
  386. | `each_limit` | `Each::ofLimit` |
  387. | `each_limit_all` | `Each::ofLimitAll` |
  388. | `!is_fulfilled` | `Is::pending` |
  389. | `is_fulfilled` | `Is::fulfilled` |
  390. | `is_rejected` | `Is::rejected` |
  391. | `is_settled` | `Is::settled` |
  392. | `coroutine` | `Coroutine::of` |