magento2 [必备技巧]之 Plugins的使用

Plugin 插件/拦截器
插件或拦截器是一个类,它通过拦截函数调用并在该函数调用之前、之后或周围运行代码来修改公共类函数的行为。这允许您替换扩展任何接口的原始公共方法的行为。

希望拦截和更改公共方法行为的扩展可以创建一个Plugin类。

这种拦截方法减少了更改同一类或方法的行为的扩展之间的冲突。您的Plugin类实现会改变类函数的行为,但不会改变类本身。Magento 根据配置的排序顺序依次调用这些拦截器,因此它们不会相互冲突。

限制

插件不能用于以下:

Final methods
Final classes
Non-public methods
Class methods (such as static methods)
__construct and __destruct
Virtual types
Objects that are instantiated before Magento\Framework\Interception is bootstrapped
Objects that implement Magento\Framework\ObjectManager\NoninterceptableInterface

声明一个插件

模块中的di.xml文件为类对象声明了一个插件:

 <config>
    <type name="{ObservedType}">
      <plugin name="{pluginName}" type="{PluginClassName}" sortOrder="1" disabled="false" />
    </type>
</config>

您必须指定这些元素:

  • type name. 插件观察的类或接口。
  • plugin name. 标识插件的任意插件名称。也用于合并插件的配置。
  • plugin type. 插件类的名称或其虚拟类型。指定此元素时使用以下命名约定:\Vendor\Module\Plugin\<ClassName>.

以下元素是可选的:

  • plugin sortOrder. 调用相同方法的插件使用此顺序运行它们。
  • plugin disabled. 要禁用插件,请将此元素设置为true. 默认值为false

定义插件

通过在公共方法之前、之后或周围应用代码,插件可以扩展或修改该方法的行为。

before、after 和 around 方法的第一个参数是一个对象,它提供对被观察方法类的所有公共方法的访问。

插件方法命名约定

Magento 的最佳实践是将要为其创建插件的类方法名称的第一个字母大写,然后再添加before,aroundafter前缀。

例如,为setName某个类的方法创建插件:

...
    public function setName($name)
    {
        ...
    }
...

在插件类中,setName方法可能具有以下名称之一:

  • beforeSetName
  • aroundSetName
  • afterSetName

如果要为其创建插件的类方法名称的第一个字母是underscore字符,则在插件类中不需要大写。

例如,为_construct某个类的方法创建插件:

...
    public function _construct()
    {
        ...
    }
...

_construct为插件类中的方法使用以下方法名称:

  • before_construct
  • around_construct
  • after_construct

之前的方法

Magento 在调用被观察方法之前运行所有之前的方法。这些方法必须与观察到的方法同名,前缀为“before”。

您可以使用 before 方法通过返回修改后的参数来更改观察方法的参数。如果有任何参数,该方法应返回这些参数的数组。如果该方法没有改变被观察方法的参数,它应该返回一个null值。

下面是一个 before 方法在将$name参数传递给被观察setName方法之前修改参数的示例。

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace My\Module\Plugin;

use Magento\Catalog\Model\Product;

class ProductAttributesUpdater
{
    public function beforeSetName(Product $subject, $name)
    {
        return ['(' . $name . ')'];
    }
}

后方法

Magento 在观察方法完成后运行所有方法。Magento 要求这些方法具有返回值,并且它们必须与观察到的方法具有相同的名称,并以 'after' 作为前缀。

您可以使用这些方法通过修改原始结果并在方法结束时返回它来更改观察方法的结果。

下面是一个 after 方法修改$result观察到的方法调用的返回值的示例。

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace My\Module\Plugin;

use Magento\Catalog\Model\Product;

class ProductAttributesUpdater
{
    public function afterGetName(Product $subject, $result)
    {
        return '|' . $result . '|';
    }
}

after 方法可以访问其观察方法的所有参数。当观察到的方法完成时,Magento 将结果和参数传递给下一个 after 方法。如果观察到的方法没有返回结果 ( @return void),那么它会将一个null值传递给下一个 after 方法。

下面是一个 after 方法的示例,它接受null来自被观察login方法的结果和参数Magento\Backend\Model\Auth

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace My\Module\Plugin;

use Magento\Backend\Model\Auth;
use Psr\Log\LoggerInterface;

class AuthLogger
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * @param Auth $authModel
     * @param null $result
     * @param string $username
     * @return void
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function afterLogin(Auth $authModel, $result, $username)
    {
        $this->logger->debug('User ' . $username . ' signed in.');
    }
}

After 方法不需要声明其观察方法的所有参数,除了该方法使用的参数以及来自观察方法的任何参数在那些使用的参数之前。

以下示例是一个带有 after 方法的类\Magento\Catalog\Model\Product\Action::updateWebsites($productIds, $websiteIds, $type)

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

use Psr\Log\LoggerInterface;
use Magento\Catalog\Model\Product\Action;

class WebsitesLogger
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function afterUpdateWebsites(Action $subject, $result, $productIds, $websiteIds)
    {
        $this->logger->log('Updated websites: ' . implode(', ',  $websiteIds));
    }
}

在示例中,afterUpdateWebsites函数使用变量$websiteIds,因此它将该变量声明为参数。它还声明$productIds,因为它出现在$websiteIds被观察方法的参数签名中。after 方法没有列出$type,因为它没有在方法内部使用它,也没有放在 before$websiteIds中。

如果一个参数在被观察的方法中是可选的,那么 after 方法也应该将它声明为可选的。

围绕方法

Magento 在他们观察到的方法之前和之后在周围的方法中运行代码。使用这些方法允许您覆盖观察到的方法。around 方法必须与观察到的方法具有相同的名称,并以“around”作为前缀。

避免在不需要时使用环绕方法插件,因为它们会增加堆栈跟踪并影响性能。围绕方法插件的唯一用例是当所有其他插件和原始方法的执行需要终止时。如果您需要用于替换或更改函数结果的参数,请使用 after 方法插件。

在原始方法的参数列表之前,周围的方法会收到一个callable允许调用链中下一个方法的方法。当您的代码执行时callable,Magento 会调用下一个插件或观察到的函数。

如果 around 方法没有调用callable,它将阻止执行链中所有插件的 next 和原始方法调用。

以下是在观察方法之前和之后添加行为的环绕方法示例:

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace My\Module\Plugin;

use Magento\Catalog\Model\Product;

class ProductAttributesUpdater
{
    public function aroundSave(Product $subject, callable $proceed)
    {
        $someValue = $this->doSmthBeforeProductIsSaved();
        $returnValue = null;

        if ($this->canCallProceedCallable($someValue)) {
            $returnValue = $proceed();
        }

        if ($returnValue) {
            $this->postProductToFacebook();
        }

        return $returnValue;
    }
}

当您包装接受参数的方法时,您的插件必须接受这些参数,并且您必须在调用proceed可调用对象时转发它们。您必须小心匹配方法原始签名的默认参数和类型提示。

例如,下面的代码定义了一个SomeType可以为空的类型参数:

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace My\Module\Model;

class MyUtility
{
    public function save(SomeType $obj = null)
    {
        //do something
    }
}

您应该使用插件包装此方法:

/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace My\Module\Plugin;

use My\Module\Model\MyUtility;

class MyUtilityUpdater
{
    public function aroundSave(MyUtility $subject, callable $proceed, SomeType $obj = null)
    {
        //do something
    }
}

请注意,如果您错过= null了 Magento 调用原始方法nullPHP将抛出一个致命错误,因为您的插件不接受null

您负责将参数从插件转发到proceed可调用对象。如果您不使用/修改参数,则可以使用可变参数和参数解包来实现此目的:

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace My\Module\Plugin;

use My\Module\Model\MyUtility;

class MyUtilityUpdater
{
    public function aroundSave(MyUtility $subject, callable $proceed, ...$args)
    {
        //do something
        $proceed(...$args);
    }
}

优先插件

当多个插件观察相同的方法时,在中声明的节点的sortOrder属性确定插件的优先级。plugin``di.xml

实现的Magento\Framework\Interception\PluginListInterfacewhichMagento\Framework\Interception\PluginList\PluginList负责定义何时调用与此优先级相关的 before、around 或 after 方法。

如果两个或多个插件具有相同的sortOrder值或未指定它,则在节点 from和area中声明的组件加载顺序将定义合并顺序。检查文件中的组件加载顺序。sequence``module.xmlapp/etc/config.php

Magento 在两个主要流程中的每个插件执行期间使用这些规则执行插件:

  • 在执行被观察方法之前,从最低到最高开始sortOrder
    • Magento 执行当前插件的before方法。
    • 然后调用当前插件的around方法。
      • 插件方法的第一部分around被执行。
      • around方法执行callable.
        • 如果链中还有另一个插件,则所有后续插件都包装在一个独立的序列循环中,并开始执行另一个流程。
        • 如果当前插件是链中的最后一个,则执行观察到的方法。
      • 方法的第二部分around被执行。
    • Magento 继续下一个插件。
  • sortOrder按照执行流程,在当前序列插件循环中 从最低到最高开始。
    • 当前插件的after方法被执行。
    • Magento 继续下一个插件。

由于这些规则,被观察方法的执行流程不仅受到插件优先级的影响,还受到它们实现的方法的影响。

插件的around方法会影响在它之后执行的所有插件的流程。

beforearound插件序列完成时,Magento 调用after序列循环中的第一个插件方法,而不是该after方法正在执行的当前插件的around方法。

例子

例如,di.xml您的模块文件为类附加了三个插件Action

<config>
    <type name="Magento\Framework\App\Action\Action">
        <plugin name="vendor_module_plugina" type="Vendor\Module\Plugin\PluginA" sortOrder="10" />
        <plugin name="vendor_module_pluginb" type="Vendor\Module\Plugin\PluginB" sortOrder="20" />
        <plugin name="vendor_module_pluginc" type="Vendor\Module\Plugin\PluginC" sortOrder="30" />
    </type>
</config>

执行将有不同的流程,具体取决于这些类实现的方法,如以下场景中所述。

方案 A

使用这些方法:

插件A 插件B 插件C
排序 10 20 30
之前调度() 之前调度() 之前调度()
大约
调度后() 调度后() 调度后()

执行将按以下顺序:

  • PluginA::beforeDispatch()

  • PluginB::beforeDispatch()

  • PluginC::beforeDispatch()

    • Action::dispatch()
  • PluginA::afterDispatch()

  • PluginB::afterDispatch()

  • PluginC::afterDispatch()

场景 B(有一个callable周围)

使用这些方法:

插件A 插件B 插件C
排序 10 20 30
之前调度() 之前调度() 之前调度()
大约 周围调度()
调度后() 调度后() 调度后()

PluginB::使用类型aroundDispatch()定义$next参数callable。例如:

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

use Magento\Framework\App\Action\Action;

class PluginB
{
    public function aroundDispatch(Action $subject, callable $next, ...$args)
    {
        // The first half of code goes here
        // ...

        $result = $next(...$args);

        // The second half of code goes here
        // ...

        return $result;
    }
}

执行将按以下顺序:

  • PluginA::beforeDispatch()

  • PluginB::beforeDispatch()

  • PluginB::aroundDispatch()(Magento 调用前半部分callable

    • PluginC::beforeDispatch()

      • Action::dispatch()
    • PluginC::afterDispatch()

  • PluginB::aroundDispatch()(Magento 叫下半场之后callable

  • PluginA::afterDispatch()

  • PluginB::afterDispatch()

场景 B(没有callable周围)

使用这些方法:

插件A 插件B 插件C
排序 10 20 30
之前调度() 之前调度() 之前调度()
大约 周围调度()
调度后() 调度后() 调度后()

PluginB::aroundDispatch()没有用类型定义$nextcallable参数。例如:

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

use Magento\Framework\App\Action\Action;

class PluginB
{
    public function aroundDispatch(Action $subject, $next, $result)
    {
        // My custom code
        return $result;
    }
}

执行将按以下顺序:

  • PluginA::beforeDispatch()

  • PluginB::beforeDispatch()

    • PluginB::aroundDispatch()
  • PluginA::afterDispatch()

  • PluginB::afterDispatch()

因为 agrument 的callable类型$next不存在,Action::dispatch()所以不会被调用,Plugin C也不会被触发。

方案 C

假设这些方法:

插件A 插件B 插件C
排序 10 20 30
之前调度() 之前调度() 之前调度()
大约 周围调度() 周围调度()
调度后() 调度后() 调度后()

执行将按以下顺序:

  • PluginA::beforeDispatch()

  • PluginA::aroundDispatch()(Magento 调用上半场直到callable

    • PluginB::beforeDispatch()

    • PluginC::beforeDispatch()

    • PluginC::aroundDispatch()(Magento 调用上半场直到callable

      • Action::dispatch()
    • PluginC::aroundDispatch()(Magento 叫下半场之后callable

    • PluginB::afterDispatch()

    • PluginC::afterDispatch()

  • PluginA::aroundDispatch()(Magento 叫下半场之后callable

  • PluginA::afterDispatch()

配置继承

作为具有插件的类的实现或继承的类和接口也将从父类继承插件。

当系统位于特定区域(例如前端或后端)时,Magento 使用在全局范围内定义的插件。di.xml您可以使用区域文件扩展或覆盖这些全局插件配置。

例如,开发人员可以通过在后端区域的特定di.xml文件中禁用它来禁用后端区域中的全局插件。

禁用插件

可以在di.xml文件中禁用插件。要禁用插件,请将disabled插件声明的参数设置为true.

<type name="Magento\Checkout\Block\Checkout\LayoutProcessor">
    <plugin name="ProcessPaymentConfiguration" disabled="true"/>
</type>

ProcessPaymentConfiguration中声明的插件的名称在哪里vendor/magento/module-payment/etc/frontend/di.xml

请注意,可以通过两种方式调用同一个类:带前导斜杠或不带斜杠。

\Magento\Checkout\Block\Checkout\LayoutProcessor

Magento\Checkout\Block\Checkout\LayoutProcessor

都是有效的。

禁用插件时,请确保使用相同的路径格式调用和禁用插件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容