原生的PHP并不支持异步操作,但是我们仍然有方法异步执行或者延迟完成复杂的任务。
为了让异步操作更方便,Magento 提供了 DeferredInterface 来完成异步操作,这让客户端代码像处理标准操作一样处理异步操作。
DeferredInterface
Magento\Framework\Async\DeferredInterface
非常简单
interface DeferredInterface
{
/**
* @return mixed Value.
* @throws \Throwable
*/
public function get();
public function isDone(): bool;
}
当客户端代码需要结果时,将调用get()方法来检索结果。 isDone()可以用来查看代码是否已经完成。
有2种类型的异步操作,可以使用DeferredInterface来描述结果。
- 对于正在进行的异步操作,调用get()将等待它们完成并返回其结果。
- 对于延迟操作,get()将实际启动操作,等待它完成,然后返回结果。
有时开发人员需要对长的异步操作进行更多的控制。这就是为什么有一个扩展的递延变体 Magento/Framework/Async/CancelableDeferredInterface
interface CancelableDeferredInterface extends DeferredInterface
{
/**
* @param bool $force Cancel operation even if it's already started.
* @return void
* @throws CancelingDeferredException When failed to cancel.
*/
public function cancel(bool $force = false): void;
/**
* @return bool
*/
public function isCancelled(): bool;
}
这个接口是为那些可能需要太长时间的操作准备的,可以取消。
客户端代码
假设服务A、服务B和服务C都执行异步操作,如HTTP请求,客户机代码将看起来就像:
public function aMethod() {
//Started executing 1st operation
$operationA = $serviceA->executeOp();
//Executing 2nd operations at the same time
$operationB = $serviceB->executeOp2();
//We need to wait for 1st operation to start operation #3
$serviceC->executeOp3($operationA->get());
//We don't have to wait for operation #2, let client code wait for it if it needs the result
//Operation number #3 is being executed simultaneously with operation #2
return $operationB;
}
而且没有看到回调!
有了延迟客户端,代码可以同时启动多个操作,等待需要的操作完成,并将结果的承诺传递给另一个方法。
ProxyDeferredFactory
当编写一个模块或扩展时,你可能不想让其他开发者知道你的方法正在执行异步操作。有一种方法可以隐藏它:使用自动生成的工厂YourClassName\ProxyDeferredFactory。在它的帮助下,你可以返回看起来像普通对象的值,但实际上是延迟的结果。
比如说:
public function __construct(CallResult\ProxyDeferredFactory $callResultFactory)
{
$this->proxyDeferredFactory = $callResultFactory;
}
....
public function doARemoteCall(string $uniqueValue): CallResult
{
//Async HTTP request, get() will return a CallResult instance.
//Call is in progress.
$deferredResult = $this->client->call($uniqueValue);
//Returns CallResult instance that will call $deferredResult->get() when any of the object's methods is used.
return $this->proxyDeferredFactory->create(['deferred' => $deferredResult]);
}
public function doCallsAndProcess(): Result
{
//Both calls running simultaneously
$call1 = $this->doARemoteCall('call1');
$call2 = $this->doARemoteCall('call2');
//Only when CallResult::getStuff() is called the $deferredResult->get() is called.
return new Result([
'call1' => $call1->getStuff(),
'call2' => $call2->getStuff()
]);
}
使用DeferredInterface进行后台操作
如上所述,第一类异步操作是在后台执行的操作。DeferredInterface 可以用来给客户端代码一个尚未收到的结果的承诺,并通过调用get()方法来等待它。
看看一个例子:为多个产品创建出货量:
class DeferredShipment implements DeferredInterface
{
private $request;
private $done = false;
private $trackingNumber;
public function __construct(AsyncRequest $request)
{
$this->request = $request;
}
public function isDone() : bool
{
return $this->done;
}
public function get()
{
if (!$this->trackingNumber) {
$this->request->wait();
$this->trackingNumber = json_decode($this->request->getBody(), true)['tracking'];
$this->done = true;
}
return $this->trackingNumber;
}
}
class Shipping
{
....
public function ship(array $products): array
{
$shipments = [];
//Shipping simultaneously
foreach ($products as $product) {
$shipments[] = new DeferredShipment(
$this->client->sendAsync(['id' => $product->getId()])
);
}
return $shipments;
}
}
class ShipController
{
....
public function execute(Request $request): Response
{
$shipments = $this->shipping->ship($this->products->find($request->getParam('ids')));
$trackingsNumbers = [];
foreach ($shipments as $shipment) {
$trackingsNumbers[] = $shipment->get();
}
return new Response(['trackings' => $trackingNumbers]);
}
}
在这里,多个装运请求被同时发送,其结果稍后收集。如果你不想写你自己的DeferredInterface实现,你可以使用CallbackDeferred来提供回调,当get()被调用时将会使用。
Using DeferredInterface for deferred operations
第二类异步操作是被推迟的操作,只有在绝对需要结果时才执行。
一个例子。
假设你正在为一个实体创建一个资源库,你有一个方法可以通过ID返回一个单一的实体。你想对在同一请求-响应过程中请求多个实体的情况进行性能优化,所以你不会单独加载它们
class EntityRepository
{
private $requestedEntityIds = [];
private $identityMap = [];
...
/**
* @return Entity[]
*/
public function findMultiple(array $ids): array
{
.....
//Adding found entities to the identity map be able to find them by ID.
foreach ($found as $entity) {
$this->identityMap[$entity->getId()] = $entity;
}
....
}
public function find(string $id): Entity
{
//Adding this ID to the list of previously requested IDs.
$this->requestedEntityIds[] = $id;
//Returning deferred that will find all requested entities
//and return the one with $id
return $this->proxyDeferredFactory->createFor(
Entity::class,
new CallbackDeferred(
function () use ($id) {
if (empty($this->identityMap[$id])) {
$this->findMultiple($this->requestedEntityIds);
$this->requestedEntityIds = [];
}
return $this->identityMap[$id];
}
)
);
}
....
}
class EntitiesController
{
....
public function execute(): Response
{
//No actual DB query issued
$criteria1Id = $this->entityService->getEntityIdWithCriteria1();
$criteria2Id = $this->entityService->getEntityIdWithCriteria2();
$criteria1Entity = $this->entityRepo->find($criteria1Id);
$criteria2Entity = $this->entityRepo->find($criteria2Id);
//Querying the DB for both entities only when getStringValue() is called the 1st time.
return new Response(
[
'criteria1' => $criteria1Entity->getStringValue(),
'criteria2' => $criteria2Entity->getStringValue()
]
);
}
}
Examples in Magento
请看我们的异步HTTP客户端 Magento\Framework\HTTP\AsyncClientInterface 和 Magento\Shipping\Model\Shipping 与各种Magento\Shipping\Model\Carrier\AbstractCarrierOnline 的实现,看看DeferredInterface如何用于异步代码的工作.