Edit in GitHubLog an issue

Asynchronous and deferred operations

Asynchronous operations are not native to PHP but it is still possible to execute heavy operations simultaneously, or delay them until they absolutely have to be finished.

To make writing asynchronous code easier, Adobe Commerce and Magento Open Source provide the DeferredInterface to use with asynchronous operations. This allows client code to work with asynchronous operations just as it would with standard operations.

DeferredInterface#

Magento\Framework\Async\DeferredInterface is quite simple:

Copied to your clipboard
1interface DeferredInterface
2{
3 /**
4 * @return mixed Value.
5 * @throws \Throwable
6 */
7 public function get();
8
9 public function isDone(): bool;
10}

When the client code needs the result, the get() method will be called to retrieve the result. isDone() can be used to see whether the code has completed.

There are 2 types of asynchronous operations where DeferredInterface can be used to describe the result:

  • With asynchronous operations in progress, calling get() would wait for them to finish and return their result.
  • With deferred operations, get() would actually start the operation, wait for it to finish, and then return the result.

Sometimes developers require more control over long asynchronous operations. That is why there is an extended deferred variant - Magento\Framework\Async\CancelableDeferredInterface:

Copied to your clipboard
1interface CancelableDeferredInterface extends DeferredInterface
2{
3 /**
4 * @param bool $force Cancel operation even if it's already started.
5 * @return void
6 * @throws CancelingDeferredException When failed to cancel.
7 */
8 public function cancel(bool $force = false): void;
9
10 /**
11 * @return bool
12 */
13 public function isCancelled(): bool;
14}

This interface is for operations that may take too long and can be canceled.

Client code#

Assuming that serviceA, serviceB and serviceC all execute asynchronous operations, such as HTTP requests, the client code would look like:

Copied to your clipboard
1public function aMethod() {
2 //Started executing 1st operation
3 $operationA = $serviceA->executeOp();
4
5 //Executing 2nd operations at the same time
6 $operationB = $serviceB->executeOp2();
7
8 //We need to wait for 1st operation to start operation #3
9 $serviceC->executeOp3($operationA->get());
10
11 //We don't have to wait for operation #2, let client code wait for it if it needs the result
12 //Operation number #3 is being executed simultaneously with operation #2
13 return $operationB;
14}

And not a callback in sight!

With the deferred client, the code can start multiple operations at the same time, wait for operations required to finish and pass the promise of a result to another method.

ProxyDeferredFactory#

When writing a module or an extension, you may not want to burden other developers with having to know that your method is performing an asynchronous operation. There is a way to hide it: employ the autogenerated factory YourClassName\ProxyDeferredFactory. With its help, you can return values that seem like regular objects but are in fact deferred results.

For example:

Copied to your clipboard
1public function __construct(CallResult\ProxyDeferredFactory $callResultFactory)
2{
3 $this->proxyDeferredFactory = $callResultFactory;
4}
5
6....
7
8public function doARemoteCall(string $uniqueValue): CallResult
9{
10 //Async HTTP request, get() will return a CallResult instance.
11 //Call is in progress.
12 $deferredResult = $this->client->call($uniqueValue);
13
14 //Returns CallResult instance that will call $deferredResult->get() when any of the object's methods is used.
15 return $this->proxyDeferredFactory->create(['deferred' => $deferredResult]);
16}
17
18public function doCallsAndProcess(): Result
19{
20 //Both calls running simultaneously
21 $call1 = $this->doARemoteCall('call1');
22 $call2 = $this->doARemoteCall('call2');
23
24 //Only when CallResult::getStuff() is called the $deferredResult->get() is called.
25 return new Result([
26 'call1' => $call1->getStuff(),
27 'call2' => $call2->getStuff()
28 ]);
29}

Using DeferredInterface for background operations#

As mentioned above, the first type of asynchronous operations are operations executing in a background. DeferredInterface can be used to give client code a promise of a not-yet-received result and wait for it by calling the get() method.

Take a look at an example: creating shipments for multiple products:

Copied to your clipboard
1class DeferredShipment implements DeferredInterface
2{
3 private $request;
4
5 private $done = false;
6
7 private $trackingNumber;
8
9 public function __construct(AsyncRequest $request)
10 {
11 $this->request = $request;
12 }
13
14 public function isDone() : bool
15 {
16 return $this->done;
17 }
18
19 public function get()
20 {
21 if (!$this->trackingNumber) {
22 $this->request->wait();
23 $this->trackingNumber = json_decode($this->request->getBody(), true)['tracking'];
24
25 $this->done = true;
26 }
27
28 return $this->trackingNumber;
29 }
30}
31
32class Shipping
33{
34 ....
35
36 public function ship(array $products): array
37 {
38 $shipments = [];
39 //Shipping simultaneously
40 foreach ($products as $product) {
41 $shipments[] = new DeferredShipment(
42 $this->client->sendAsync(['id' => $product->getId()])
43 );
44 }
45
46 return $shipments;
47 }
48}
49
50class ShipController
51{
52 ....
53
54 public function execute(Request $request): Response
55 {
56 $shipments = $this->shipping->ship($this->products->find($request->getParam('ids')));
57 $trackingsNumbers = [];
58 foreach ($shipments as $shipment) {
59 $trackingsNumbers[] = $shipment->get();
60 }
61
62 return new Response(['trackings' => $trackingNumbers]);
63 }
64}

Here, multiple shipment requests are being sent at the same time with their results gathered later. If you do not want to write your own DeferredInterface implementation, you can use CallbackDeferred to provide callbacks that will be used when get() is called.

Using DeferredInterface for deferred operations#

The second type of asynchronous operations are operations that are being postponed and executed only when a result is absolutely needed.

An example:

Assume you are creating a repository for an entity and you have a method that returns a singular entity by ID. You want to make a performance optimization for cases when multiple entities are requested during the same request-response process, so you would not load them separately.

Copied to your clipboard
1class EntityRepository
2{
3 private $requestedEntityIds = [];
4
5 private $identityMap = [];
6
7 ...
8
9 /**
10 * @return Entity[]
11 */
12 public function findMultiple(array $ids): array
13 {
14 .....
15
16 //Adding found entities to the identity map be able to find them by ID.
17 foreach ($found as $entity) {
18 $this->identityMap[$entity->getId()] = $entity;
19 }
20
21 ....
22 }
23
24 public function find(string $id): Entity
25 {
26 //Adding this ID to the list of previously requested IDs.
27 $this->requestedEntityIds[] = $id;
28
29 //Returning deferred that will find all requested entities
30 //and return the one with $id
31 return $this->proxyDeferredFactory->createFor(
32 Entity::class,
33 new CallbackDeferred(
34 function () use ($id) {
35 if (empty($this->identityMap[$id])) {
36 $this->findMultiple($this->requestedEntityIds);
37 $this->requestedEntityIds = [];
38 }
39
40 return $this->identityMap[$id];
41 }
42 )
43 );
44 }
45
46 ....
47}
48
49class EntitiesController
50{
51 ....
52
53 public function execute(): Response
54 {
55 //No actual DB query issued
56 $criteria1Id = $this->entityService->getEntityIdWithCriteria1();
57 $criteria2Id = $this->entityService->getEntityIdWithCriteria2();
58 $criteria1Entity = $this->entityRepo->find($criteria1Id);
59 $criteria2Entity = $this->entityRepo->find($criteria2Id);
60
61 //Querying the DB for both entities only when getStringValue() is called the 1st time.
62 return new Response(
63 [
64 'criteria1' => $criteria1Entity->getStringValue(),
65 'criteria2' => $criteria2Entity->getStringValue()
66 ]
67 );
68 }
69}

Examples#

See our asynchronous HTTP client Magento\Framework\HTTP\AsyncClientInterface and Magento\Shipping\Model\Shipping with various Magento\Shipping\Model\Carrier\AbstractCarrierOnline implementations to see how DeferredInterface can be used to work with asynchronous code.

Was this helpful?
  • Privacy
  • Terms of Use
  • Do not sell my personal information
  • AdChoices
Copyright © 2022 Adobe. All rights reserved.