1
1
### Why?
2
2
Service contracts in the future will often be executed in an asynchronous manner
3
- and it's time to introduce a standard Promise to Magento for asynchronous operations to employ
3
+ and it's time to introduce a standard Promise to Magento for asynchronous operations to employ.
4
+ Also operations like sending HTTP requests can be easily performed asynchronously since cUrl multi can be utilized
5
+ to send requests asynchronously.
4
6
### Requirements
5
- * Promises CANNOT be forwarded with _ then_ and _ otherwise_ ([ see explanation] ( #forwarding ) )
6
- * _ then_ accepts a function that will be executed when the promise is resolved, the callback will
7
- receive a single argument - result of the execution
8
- * _ otherwise_ accepts a function that will be executed if an error occurs during the asynchronous
9
- operation, it will receive a single argument - a _ Throwable_
10
- * Promises can be used in a synchronous way to prevent methods that use methods returning promises
11
- having to return a promise as well ([ see explanation] ( #callback-hell ) ); This will be done by promises having _ wait_ method
12
- * _ wait_ method does not throw an exception if the promise is rejected
13
- nor does it return the result of the resolved promise ([ see explanation] ( #wait-not-unwrapping-promises ) )
14
- * If an exception occurs during an asynchronous operation and no _ otherwise_ callback is
15
- provided then it will just be rethrown
7
+ * Avoid callbacks that cause noodle code and generally alien to PHP
8
+ * Introduce a way to work with asynchronous operations in a familiar way
9
+ * Employ solution within core code to serve as an example for our and 3rd party developers
16
10
### API
17
- ##### Promise to be used by client code
18
- When client code receives a promise from calling another object's method
19
- it shouldn't have access to _ resolve_ and _ reject_ methods, it should only be able to
20
- provide callbacks to process promised results and to wait for promised operation's execution.
11
+ ##### Deferred
12
+ A future that describes a values that will be available later.
13
+ If a library returns a promise or it's own implementation of a future it can be easily wrapped to support our interface.
21
14
22
15
This interface will be used as the return type of methods returning promises.
23
16
``` php
24
- interface PromiseInterface
17
+ interface DeferredInterface
25
18
{
26
19
/**
27
- * @throws PromiseProcessedException When callback was alredy provided.
20
+ * Wait for and return the value.
21
+ *
22
+ * @return mixed Value.
23
+ * @throws \Throwable When it was impossible to get the value.
28
24
*/
29
- public function then(callable $callback): void ;
30
-
25
+ public function get() ;
26
+
31
27
/**
32
- * @throws PromiseProcessedException When callback was alredy provided.
28
+ * Is the process of getting the value is done?
29
+ *
30
+ * @return bool
33
31
*/
34
- public function otherwise(callable $callback): void;
35
-
36
- public function wait(): void;
37
- }
38
- ```
39
- ##### Promise to be created
40
- This promise will be created by asynchronous code
41
- ``` php
42
- interface ResultPromiseInterface extends PromiseInterface
43
- {
44
- public function resolve($value): void;
45
-
46
- public function reject(\Throwable $exception): void;
32
+ public function isDone(): bool;
47
33
}
48
34
```
49
35
50
36
### Implementation
51
- A wrapper around [ Guzzle Promises] ( https://github.com/guzzle/promises ) will be created to implement the APIs above. Guzzle Promises fit
52
- most important criteria - they allow synchronous execution as well as asynchronous. It's a mature
53
- and well-known library and, while we would have to add guzzle/promises to our composer.json,
54
- the library is already required in Magento indirectly - we won't be actually adding a new dependency.
55
-
56
- There are other libraries like [ Reactphp Promises] ( https://github.com/reactphp/promise ) but they either do not provide synchronous way
57
- to interact with promises or are not as refined.
37
+ This interface will be used as a wrapper for libraries that return promises and deferred values.
58
38
59
39
### Explanations
60
- ##### Forwarding
61
- Consider this code
62
- ``` php
63
- $promise = $this->anotherObject->doStuff();
64
- $promise->then($doStuffCallback)
65
- ->otherwise($processErrorCallback)
66
- ->otherwise($processAnotherError)
67
- ->then($doOtherStuff)
68
- ->otherwise($processErrorCallback);
69
- ```
70
- Does 1st _ then_ return a forwarded promise? Or is it the same object?
71
- Then what promise is the second _ otherwise_ callback for?
72
- Code looking like this is confusing and it will be much cleaner if we don't use forwarding.
73
- ``` php
74
- $doStuffOperation = $this->someObject->doStuff();
75
- $result = null;
76
- $doStuffOperation->then(function ($response) use (& $result) { $result = $response; });
77
- $simultaneousOperation = $this->otherObject->processStuff();
78
- $doStuffOperation->wait();
79
- $updated = null;
80
- $saveOperation = $this->repo->save($result);
81
- $saveOperation->then(function ($result) use (& $updated) { $updated = $result; });
82
-
83
- //Waiting for all
84
- $saveOperation->wait();
85
- $simultaneousOperation->wait();
86
- return $updated;
87
- ```
88
- Here we clearly state that for _ save operation_ we need to _ do stuff operation_ to finish
89
- and _ simultaneous operation_ may run up until then end of our algorithm execution.
40
+ ##### Why not promises?
41
+ Promises mean callbacks. One callback is fair enough but multiple callbacks within the same method, callbacks for forwarded
42
+ promises create noodle-like hard to support code. Closures are a part of PHP but still are a foreign concept complicating
43
+ developer experience. Also it is an extra effort to ensure strict typing of return values and arguments with anonymous
44
+ functions.
90
45
91
-
92
- ##### Callback hell
93
- Consider this code responsible for placing orders
94
- ``` php
95
- class ServiceA
96
- {
97
- ....
98
-
99
- public function process(DTOInterface $dto): ProcessedInterface
100
- {
101
- ....
102
-
103
- $this->serviceB->processSmth($val)->then(function ($val) use ($processed) {
104
- $processed->setBValue($val);
105
- });
106
-
107
- return $processed;
108
- }
46
+ Other thing is that promises are meant to be forwarded which complicates things. It can be hard to understand what are
47
+ you writing a callback for - promised result? Another callback for promised result introduced earlier? OnFulfilled callback
48
+ for resolved value in a OnRejected callback to the initial promise?
109
49
110
- ....
111
- }
112
- ```
113
- We cannot be sure _ serviceB_ has finished doing it's stuff when we return _ $processed_ .
114
- So, if we cannot wait for the promise _ serviceB_ returned, the only thing we can do
115
- is to return a promise ourselves instead of _ ProcessedInterface_ . But then methods using
116
- _ ServiceA::process()_ would have to do the same - and PHP code is not supposed to be this way.
50
+ ##### Typing
51
+ Methods returning Deferred can still provide types for their actual returned values - they can extend the original interface
52
+ and add return type hint to the _ get()_ method.
117
53
118
- ##### Wait not unwrapping promises
119
- For _ wait_ method to also unwrap promises can result in confusion. It's better to have a single way of retreiving promised results and a single way of retreiving errors.
120
-
121
- Consider next situation:
122
- ``` php
123
- class ServiceA
124
- {
125
- public function doSmth(): void
126
- {
127
- $promise = $this->otherService->doSmthElse();
128
- $promise->otherwise(function ($exception) { $this->logger->critical($exception); });
129
- try {
130
- //wait would throw the exception processed in the otherwise callback once again
131
- $promise->wait();
132
- } catch (\Throwable $exception) {
133
- //we've already processed this
134
- }
135
- }
136
- }
137
- ```
138
- That is a simple situation but it illustrates how having multiple ways of receiving promised results may lead to duplicating code
54
+ ##### Advantage
55
+ Since deferred does not require any confusing callbacks and forwarding it's pretty easy to just treat it as a values
56
+ and only calling _ get()_ when you actually need it. Client code will look mostly like it's just a regular synchronous code.
139
57
140
- ### Using promises for service contracts
141
- #### Why use promises for service contracts?
58
+ ### Using Deferred for service contracts
59
+ #### Why use futures for service contracts?
142
60
Another way that was proposed to execute service contracts in an asynchronous manner was to use async web API, but there
143
61
are number of problems with that approach:
144
62
* Async web API allows execution of the same operation with different sets of arguments, but not different operations
@@ -151,17 +69,29 @@ are number of problems with that approach:
151
69
for each operation
152
70
153
71
So to allow execution of multiple service contracts from different domains it's best to send 1 request per operation
154
- and to let client code to chain, pass and properly receive promises of results of operations .
72
+ and to let client code to work with asynchronously received values almost as they would've with synchronous ones .
155
73
156
74
#### How will it look?
157
- There are to ways we can go about using promises for asynchronous execution of service contracts:
158
- * Service interfaces themselves returning promises for client code to use
159
-
75
+ There are to ways we can go about using Deferred for asynchronous execution of service contracts:
76
+ * Service interfaces themselves returning deferred values for client code to use
77
+
78
+ _ contract's deferred_ :
79
+ ``` php
80
+ interface DTODeferredInterface extends DeferredInterface
81
+ {
82
+ /**
83
+ * @inheritDoc
84
+ * @return DTOInterface
85
+ */
86
+ public function get(): DTOInterface;
87
+ }
88
+ ```
89
+
160
90
_ service contract_ :
161
91
``` php
162
92
interface SomeRepositoryInterface
163
93
{
164
- public function save(DTOInterface $data): PromiseInterface ;
94
+ public function save(DTOInterface $data): DTODeferredInterface ;
165
95
}
166
96
```
167
97
@@ -179,28 +109,28 @@ There are to ways we can go about using promises for asynchronous execution of s
179
109
....
180
110
181
111
//Both operations running asynchronously
182
- $promise = $this->someRepo->save($dto);
183
- $anotherPromise = $this->someService->doStuff();
184
- //Waiting for both results
185
- $promise->wait ();
186
- $anotherPromise->wait ();
112
+ $deferredDTO = $this->someRepo->save($dto);
113
+ $deferredStuff = $this->someService->doStuff();
114
+ //Started both processes at the same time, waiting for both to finish
115
+ $dto = $deferredDTO->get ();
116
+ $stuff = $deferredStuff->get ();
187
117
}
188
118
}
189
119
```
190
- * Using a runner that will accept interface name, method name and arguments that will return a promise
120
+ * Using a runner that will accept interface name, method name and arguments that will return a deferred
191
121
192
122
_ async runner_ :
193
123
``` php
194
124
interface AsynchronousRunnerInterface
195
125
{
196
- public function run(string $serviceName, string $serviceMethod, array $arguments): PromiseInterface ;
126
+ public function run(string $serviceName, string $serviceMethod, array $arguments): DeferredInterface ;
197
127
}
198
128
```
199
129
_ regular service_ :
200
130
``` php
201
131
interface SomeRepositoryInterface
202
132
{
203
- public function save(DTOInterface $dto): void ;
133
+ public function save(DTOInterface $dto): DTOInterface ;
204
134
}
205
135
```
206
136
_ client code_ :
@@ -222,18 +152,19 @@ There are to ways we can go about using promises for asynchronous execution of s
222
152
....
223
153
224
154
//Both operations running asynchronously
225
- $promise = $this->runner->run(SomeRepositoryInterface::class, 'save', [$dto]);
226
- $anotherPromise = $this->runner->run(SomeServiceInterface::class, 'doStuff', []);
227
- //Waiting for both results
228
- $promise->wait ();
229
- $anotherPromise->wait();
155
+ $deferredDTO = $this->runner->run(SomeRepositoryInterface::class, 'save', [$dto]);
156
+ $deferredStuff = $this->runner->run(SomeServiceInterface::class, 'doStuff', []);
157
+ //Started both processes at the same time, waiting for both to finish
158
+ $dto = $deferredDTO->get ();
159
+ $stuff = $deferredStuff->get()
230
160
}
231
161
}
232
162
```
233
163
234
- ### Using promises for existing code
164
+ ### Using deferred for existing code
235
165
We have a standard HTTP client - Magento\Framework\HTTP\ClientInterface, it can benefit from allowing async requests
236
- functionality for developers to use by employing promises.
166
+ functionality for developers to use by employing promises. Since it's an API, and a messy one at that, we should create
167
+ a new asynchronous client.
237
168
238
169
This client is being used in Magento\Shipping\Model\Carrier\AbstractCarrierOnline to create package shipments/
239
170
shipment returns in 3rd party systems, the process can be optimized by sending requests asynchronously to create
0 commit comments