|
| 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