Skip to content

Commit f27f69e

Browse files
Create a page describing working with async code
Page described how to work with DeferredInterface introduced in magento/architecture#71
1 parent 20b844a commit f27f69e

File tree

1 file changed

+268
-0
lines changed

1 file changed

+268
-0
lines changed
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
# Asynchronous and deferred operations
2+
Asynchronous operations are not something native to PHP but it still is possible to execute some heavy
3+
operations simultaneously or delay them until they absolutely have to be finished.
4+
5+
To make writing asynchronous code easier Magento provides DeferredInterface to use
6+
as the result of asynchronous operations to allow client code to work with such
7+
operations as it would with usual values.
8+
9+
## DeferredInterface
10+
_Magento\Framework\Async\DeferredInterface_ is rather simple:
11+
```php
12+
interface DeferredInterface
13+
{
14+
/**
15+
* @return mixed Value.
16+
* @throws \Throwable
17+
*/
18+
public function get();
19+
20+
public function isDone(): bool;
21+
}
22+
```
23+
When client code needs the result _get()_ method will be called to retrieve the result
24+
and if it's possible to wait _isDone()_ can be used to see whether the wait is needed.
25+
26+
There are 2 types of asynchronous operations where _DeferredInterface_ can be used to describe the result:
27+
* Asynchronous operations in progress, calling _get()_ would wait for them to finish and return their result
28+
* Deferred operations, _get()_ would actually start an operation, wait for it to finish and return the result
29+
30+
Sometimes developers require more control over long asynchronous operations that's why
31+
there is an extended deferred variant - Magento\Framework\Async\CancelableDeferredInterface:
32+
```php
33+
interface CancelableDeferredInterface extends DeferredInterface
34+
{
35+
/**
36+
* @param bool $force Cancel operation even if it's already started.
37+
* @return void
38+
* @throws CancelingDeferredException When failed to cancel.
39+
*/
40+
public function cancel(bool $force = false): void;
41+
42+
/**
43+
* @return bool
44+
*/
45+
public function isCancelled(): bool;
46+
}
47+
```
48+
This interface is for operations that may take too long and can be canceled.
49+
50+
#### Client code
51+
Now let's see how client code of an asynchronous operations would look like.
52+
Assume _serviceA, serviceB and serviceC_ all execute asynchronous operations (like sending HTTP requests)
53+
```php
54+
public function aMethod() {
55+
//Started executing 1st operation
56+
$operationA = $serviceA->executeOp();
57+
//Executing 2nd operations at the same time
58+
$operationB = $serviceB->executeOp2();
59+
60+
//We need to wait for 1st operation to start operation #3
61+
$serviceC->executeOp3($operationA->get());
62+
63+
//We don't have to wait for operation #2, let client code wait for it if it needs the result
64+
//Operation number #3 is being executed simultaneously with operation #2
65+
return $operationB;
66+
}
67+
```
68+
And no callback in sight!
69+
70+
With deferred client code can start multiple operations at the same time, wait for operations required to finish
71+
and pass promise of a result to another method.
72+
73+
## Deferred proxy
74+
When writing a module or an extension you may not want to burden other developers
75+
with knowing that your method is performing an asynchronous operation and there is a way to hide it -
76+
_Magento\Framework\Async\ProxyDeferredFactory_, With it's help you can return values that seem like regular objects
77+
but are in fact deferred results.
78+
79+
Example:
80+
```php
81+
public function doARemoteCall(string $uniqueValue): CallResult
82+
{
83+
//Async HTTP request, get() will return a CallResult instance.
84+
//Call is in progress.
85+
$deferredResult = $this->client->call($uniqueValue);
86+
87+
//Returns CallResult instance that will call $deferredResult->get() when any of the object's methods is used.
88+
return $this->proxyDeferredFactory->createFor(CallResult::class, $deferredResult);
89+
}
90+
91+
public function doCallsAndProcess(): Result
92+
{
93+
//Both calls running simultaneously
94+
$call1 = $this->doARemoteCall('call1');
95+
$call2 = $this->doARemoteCall('call2');
96+
97+
//Only when CallResult::getStuff() is called the $deferredResult->get() is called.
98+
return new Result([
99+
'call1' => $call1->getStuff(),
100+
'call2' => $call2->getStuff()
101+
]);
102+
}
103+
```
104+
105+
## Using DeferredInterface for background operations
106+
As mentioned above the 1st type of asynchronous operations are operations executing in a background.
107+
DeferredInterface can be used here to give client code a promise of a not yet received
108+
result and wait for it by calling the _get()_ method.
109+
110+
Take a look at the example - creating shipments for multiple products:
111+
```php
112+
class DeferredShipment implements DeferredInterface
113+
{
114+
private $request;
115+
116+
private $done = false;
117+
118+
private $trackingNumber;
119+
120+
public function __construct(AsyncRequest $request)
121+
{
122+
$this->request = $request;
123+
}
124+
125+
public function isDone() : bool
126+
{
127+
return $this->done;
128+
}
129+
130+
public function get()
131+
{
132+
if (!$this->trackingNumber) {
133+
$this->request->wait();
134+
$this->trackingNumber = json_decode($this->request->getBody(), true)['tracking'];
135+
136+
$this->done = true;
137+
}
138+
139+
return $this->trackingNumber;
140+
}
141+
}
142+
143+
class Shipping
144+
{
145+
....
146+
147+
public function ship(array $products): array
148+
{
149+
$shipments = [];
150+
//Shipping simultaneously
151+
foreach ($products as $product) {
152+
$shipments[] = new DeferredShipment(
153+
$this->client->sendAsync(['id' => $product->getId()])
154+
);
155+
}
156+
157+
return $shipments;
158+
}
159+
}
160+
161+
class ShipController
162+
{
163+
....
164+
165+
public function execute(Request $request): Response
166+
{
167+
$shipments = $this->shipping->ship($this->producs->find($request->getParam('ids')));
168+
$trackingsNumbers = [];
169+
foreach ($shipments as $shipment) {
170+
$trackingsNumbers[] = $shipment->get();
171+
}
172+
173+
return new Response(['trackings' => $trackingNumbers]);
174+
}
175+
}
176+
```
177+
178+
Here multiple shipment requests are being sent at the same time with their results
179+
gathered later. If you don't want to write your own _DeferredInterface_ implementations processing
180+
results of asynchronous calls you can use _CallbackDeferred_ to provide callbacks that will be used
181+
when _get()_ is called.
182+
183+
## Using DeferredInterface for deferred operations
184+
2nd type of asynchronous operations are operations that are being postponed and executed only when a result is
185+
absolutely needed.
186+
187+
Let's see the next example:
188+
189+
Assume you are creating a repository for an entity, you have a method that returns a singular entity
190+
by ID, you want to make a performance optimization for cases when multiple entities are requested
191+
during the same request-response process so you wouldn't load each one separately.
192+
193+
```php
194+
class EntityRepository
195+
{
196+
private $requestedEntityIds = [];
197+
198+
private $identityMap = [];
199+
200+
...
201+
202+
/**
203+
* @return Entity[]
204+
*/
205+
public function findMultiple(array $ids): array
206+
{
207+
.....
208+
209+
//Adding found entities to the identity map be able to find them by ID.
210+
foreach ($found as $entity) {
211+
$this->identityMap[$entity->getId()] = $entity;
212+
}
213+
214+
....
215+
}
216+
217+
public function find(string $id): Entity
218+
{
219+
//Adding this ID to the list of previously requested IDs.
220+
$this->requestedEntityIds[] = $id;
221+
222+
//Returning deferred that will find all requested entities
223+
//and return the one with $id
224+
return $this->proxyDefferedFactory->createFor(
225+
Entity::class,
226+
new CallbackDeferred(
227+
function () use ($id) {
228+
if (empty($this->identityMap[$id])) {
229+
$this->findMultiple($this->requestedEntityIds);
230+
$this->requestedEntityIds = [];
231+
}
232+
233+
return $this->identityMap[$id];
234+
}
235+
)
236+
);
237+
}
238+
239+
....
240+
}
241+
242+
class EntitiesController
243+
{
244+
....
245+
246+
public function execute(): Response
247+
{
248+
//No actual DB query issued
249+
$criteria1Id = $this->entityService->getEntityIdWithCriteria1();
250+
$criteria2Id = $this->entityService->getEntityIdWithCriteria2();
251+
$criteria1Entity = $this->entityRepo->find($criteria1Id);
252+
$criteria2Entity = $this->entityRepo->find($criteria2Id);
253+
254+
//Querying the DB for both entities only when getStringValue() is called the 1st time.
255+
return new Response(
256+
[
257+
'criteria1' => $criteria1Entity->getStringValue(),
258+
'criteria2' => $criteria2Entity->getStringValue()
259+
]
260+
);
261+
}
262+
}
263+
```
264+
265+
## Examples in Magento
266+
Please see our asynchronous HTTP client _Magento\Framework\HTTP\AsyncClientInterface_
267+
and _Magento\Shipping\Model\Shipping_ with various _Magento\Shipping\Model\Carrier\AbstractCarrierOnline_
268+
implementations to see how DeferredInterface can be used to work with asynchronous code.

0 commit comments

Comments
 (0)