11. Magento 2 拦截器插件

后端开发2019-10-18

Magento 2 开发内容目录

插件在技术上可以让你写出结构更合理的代码。拦截器(Interceptor)插件是 Magento 2 的一个小扩展,可以通过拦截一个公开的函数或方法来修改它的执行结果,拦截后可以在原函数之前(before)、之后(after)或包围(around)的方式添加新增代码。通过使用拦截器插件可以在对原类代码不做任何修改的情况下改变它的执行结果。

本文内容目录:

  • 插件的优点
  • 插件的限制
  • 创建新插件指南
    • 声明一个插件
    • 解释说明
    • 实现插件
    • 前置函数 Before
    • 后置函数 After
    • 包围函数 Around
    • 检测结果
  • 设置插件的优先级

或许可以认为事件监听器也能实现相同的功能,但它们之间也有一些不同之处。最主要的是,拦截器不仅使用依赖注入为类创建方法,并且可以为方法设置 sortOrder 属性,这样就可以检查插件链并确认执行顺序。这也是为什么插件可以实现不对类做任何修改并且也不会和其它插件有冲突。

插件的优点

做为一个模块开发者,你可以使用拦截器插件处理以下几方面:

  • 转发对象的任何函数调用或其它代码操作,对象必须是对象管理器(Object Manager)实例化的。
  • 修改任何对象方法的执行结果,对象必须是对象管理器(Object Manager)实例化的。
  • 修改任何对象方法的传入参数,对象必须是对象管理器(Object Manager)实例化的。
  • 使用同步或可预测的方式同时执行和其它模块一样的方法

插件的限制

拦截器插件不能操作的有:

  • 对象的实例化在 Magento\Framework\Interception 加载之前
  • Final 方法
  • Final 类
  • 任何包含有至少一个 final public 方法的类
  • 不是 public 的方法
  • 类方法(如静态方法)
  • __construct
  • 虚拟类型 Virtual type

创建插件指南

声明一个插件

声明插件是在 di.xml 文件中,路径 <MODULE_DIR>/etc/di.xml

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

解释说明

必须的属性:

  • type name: 要修改的类或接口名
  • plugin name: 插件标识,区分其它插件,也用于合并插件的配置
  • plugin type: 插件类名或它的虚拟类型名称。命名规则: \Vendor\Module\Plugin\<ModelName>Plugin

可选属性:

  • plugin sortOrder: 指定同一个方法绑定的所有插件执行顺序
  • plugin disabled: 可以快速启用或停用插件,默认值是 false。也可以在你的 di.xml 配置文件使用这个属性来停用核心或第三方的插件。

如下代码,编辑 app\code\Aqrun\HelloWorld\etc\di.xml 文件:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Aqrun\HelloWorld\Controller\Index\Example">
        <plugin name="aqrun_helloworld_plugin"
                type="Aqrun\HelloWorld\Plugin\ExamplePlugin"
                sortOrder="10" disabled="false"/>
    </type>
</config>

下面的代码定义 type 节点的 name 属性指定的类,目录 app/code/Aqrun/HelloWorld/Controller/Index/Example.php

<?php
namespace Aqrun\HelloWorld\Controller\Index;
 
class Example extends \Magento\Framework\App\Action\Action
{
    protected $title;
    public function execute()
    {
        $this->setTitle('欢迎');
        echo $this->getTitle();
        exit();
    }
 
    public function setTitle($title){
        echo __METHOD__ . '<br/>';
        return $this->title = $title;
    }
 
    public function getTitle()
    {
        echo __METHOD__ . '<br/>';
        return $this->title;
    }
}

根据 plugin 节点的 name 属性值 我们创建 ExamplePlugin.php 文件,路径:app/code/Aqrun/HelloWorld/Plugin/ExamplePlugin.php:

<?php
namespace Aqrun\HelloWorld\Plugin;
 
class ExamplePlugin
{
}

定义插件

插件通过使用 前置 before、后置 after、包围 around 等函数来扩展或修改公开方法的执行结果

首先要获取一个对象,并且对象为要监听的公开方法提供权限

插件的 3 个方法:

  • 前置 - beforeDispatch()
  • 包围 - aroundDispatch()
  • 后置 - afterDispatch()

前置函数(Before methods)

前置函数是在所有监听函数中最先执行的,函数名必须和被监听函数名一样只是加了 before 前缀。

使用前置函数来修改被监听方法的参数,可以返回修改后的参数数组。如果有多个参数,则与原参数顺序保持一致。如果返回参数无效,则对监听的方法参数不做修改。

<?php
namespace Aqrun\HelloWorld\Plugin;
 
use Aqrun\HelloWorld\Controller\Index\Example as ExampleAction;
 
class ExamplePlugin
{
    public function beforeSetTitle(ExampleAction $subject, $title)
    {
        $title = $title . ' 来到 ';
        echo __METHOD__ . '</br>';
 
        return [$title];
    }
}

后置函数(After methods)

后置函数在被监听方法执行完成后再开始执行,函数名也和被监听方法名一样,另外再加 after 前缀。

后置函数用于修改被监听方法的执行结果,因此也需要有一个返回值。

<?php
namespace Aqrun\HelloWorld\Plugin;
 
use Aqrun\HelloWorld\Controller\Index\Example as ExampleAction;
 
class ExamplePlugin
{
    public function afterGetTitle(ExampleAction $subject, $result)
    {
        echo __METHOD__ . '</br>';
        return '<h1>' . $result . '新世界' . '</h1>';
    }
}

包围函数(Around Methods)

包围函数允许代码在被监听方法运行前和后执行,因此你可以覆写方法。名称和被监听方法一样,需要添加前缀 around

包围函数的参数中在原始函数参数之前,有一个回调函数(callable)参数,回调函数会在插件链的下一个函数中被调用,也就是下一个插件或被监听方法也可以执行。

注意:如果没有声明回调函数(callable),那即不会调用下一个插件也不会调用原始方法。

<?php
namespace Aqrun\HelloWorld\Plugin;
 
use Aqrun\HelloWorld\Controller\Index\Example as ExampleAction;
 
class ExamplePlugin
{
    public function aroundGetTitle(ExampleAction $subject, callable $proceed)
    {
        echo __METHOD__ . ' - proceed() 之前 <br/>';
        $result = $proceed();
        echo __METHOD__ . ' - proceed() 之后 <br/>';
        return $result;
    }
}

检查结果

ExamplePlugin.php 所有代码:

<?php
namespace Aqrun\HelloWorld\Plugin;
 
use Aqrun\HelloWorld\Controller\Index\Example as ExampleAction;
 
class ExamplePlugin
{
    public function beforeSetTitle(ExampleAction $subject, $title)
    {
        $title = $title . ' 来到 ';
        echo __METHOD__ . '</br>';
        return [$title];
    }
 
    public function afterGetTitle(ExampleAction $subject, $result)
    {
        echo __METHOD__ . '</br>';
        return '<h1>' . $result . '新世界' . '</h1>';
    }
 
    public function aroundGetTitle(ExampleAction $subject, callable $proceed)
    {
        echo __METHOD__ . ' - proceed() 之前 <br/>';
        $result = $proceed();
        echo __METHOD__ . ' - proceed() 之后 <br/>';
        return $result;
    }
}

然后清空缓存查看如下所示:

11-interceptor-result.png

你需要小心应对原始参数,被调用的插件方法的参数必须一致并完全匹配。在处理时注意原始方法的参数签名及默认值。

比如下面的代码 SomeType 类型的参数默认可以是 null

<?php
namespace Aqrun\HelloWorld\Model;
class MyUtility
{
    public function save(SomeType $obj = null)
    {
        // do something
    }
}

如果用如下插件代码调用这个方法:

<?php
namespace Aqrun\HelloWorld\Plugin;
 
class MyUtilityPlugin
{
    public function aroundSave(
        \Aqrun\HelloWorld\Model\MyUtility $subject,
        \callable $proceed,
        SomeType $obj
    ){
      //do something
    }
}

注意: null 相当于没有参数或遗漏的参数

如果调用函数参数为 null,PHP 会报错,因此 null 不能传到插件里。保持参数一致非常重要。如果不确定参数值,可以使用变长参数和参数解包的方式实现。

namespace Aqrun\HelloWorld\Plugin;
 
class MyUtilityPlugin
{
    public function aroundSave(
        \Aqrun\HelloWorld\Model\MyUtility $subject,
        \callable $proceed,
        ...$args
    ){
        // do something
        $proceed(...$args);
    }
}

设置插件优先级

对监听同一个方法的插件可以使用 sortOrder 属性进行排序以队列的方式逐个执行。

示例代码

查看 https://gitee.com/aqrun/Aqrun_HelloWorld