异步和延迟操作

更新

原生的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如何用于异步代码的工作.