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
,around
或after
前缀。
例如,为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 调用原始方法null
,PHP将抛出一个致命错误,因为您的插件不接受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\PluginListInterface
whichMagento\Framework\Interception\PluginList\PluginList
负责定义何时调用与此优先级相关的 before、around 或 after 方法。
如果两个或多个插件具有相同的sortOrder
值或未指定它,则在节点 from和area中声明的组件加载顺序将定义合并顺序。检查文件中的组件加载顺序。sequence``module.xml
app/etc/config.php
Magento 在两个主要流程中的每个插件执行期间使用这些规则执行插件:
- 在执行被观察方法之前,从最低到最高开始
sortOrder
。- Magento 执行当前插件的
before
方法。 - 然后调用当前插件的
around
方法。- 插件方法的第一部分
around
被执行。 - 该
around
方法执行callable
.- 如果链中还有另一个插件,则所有后续插件都包装在一个独立的序列循环中,并开始执行另一个流程。
- 如果当前插件是链中的最后一个,则执行观察到的方法。
- 方法的第二部分
around
被执行。
- 插件方法的第一部分
- Magento 继续下一个插件。
- Magento 执行当前插件的
-
sortOrder
按照执行流程,在当前序列插件循环中 从最低到最高开始。- 当前插件的
after
方法被执行。 - Magento 继续下一个插件。
- 当前插件的
由于这些规则,被观察方法的执行流程不仅受到插件优先级的影响,还受到它们实现的方法的影响。
插件的around
方法会影响在它之后执行的所有插件的流程。
当before
和around
插件序列完成时,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
都是有效的。
禁用插件时,请确保使用相同的路径格式调用和禁用插件。