50. 插件系统(中)

从本质上讲,插件和钩子机制是系统各个部分传递信息和进行组织的一种机制,他们可以让各部分参与到某件事情中来,他们和事件派发器、服务定义一起形成了系统的神经系统,或称为系统总线或信息高速公路,因为插件、钩子、事件派发器、服务定义有一个共同特点:连接系统的各个部分,从而让核心和模块没有明确的边界,他们是一个整体,模块可以像核心一样参与系统建设,从而实现了drupal的高度模块化。

在上篇中介绍了插件的基本使用,在许多情况下已经够用了,本篇进一步介绍插件的一些高级使用,让我们从定义一个插件类型开始。

定义插件类型:

系统中所有的插件都会有一个插件类型,定义插件类型的地方是插件的源泉,那里收集系统各部分定义的本类型插件,然后去干某件事情,给其他部分参与这件事情的机会,核心之外模块也是可以定义插件类型的(开发者需要意识到在drupal中核心和模块是对等的,核心能干的事情模块也可以干,就连最最重要的容器都可以由模块替换掉),那么模块如何定义一个插件类型呢?按照下面的步骤:

首先定义一个本类型插件需要实现的公共接口,里面表述了本类型插件需要具备的共通行为,也就是公用方法,通常,根据情况该接口会继承系统已经定义好的一个或多个以下接口:

\Drupal\Component\Plugin\PluginInspectionInterface
\Drupal\Component\Plugin\ConfigurablePluginInterface
\Drupal\Component\Plugin\ContextAwarePluginInterface
\Drupal\Core\Plugin\PluginFormInterface
\Drupal\Core\Executable\ExecutableInterface

可选的定义一个插件基类,将相同功能代码放在里面,这样插件实现就不会重复定义了,比较方便,系统提供了一个公共基类:\Drupal\Core\Plugin\PluginBase

决定一种找出其他模块定义的本类型插件的机制,也就是插件的发现,在系统中大多使用释文发现,也可以用其他机制,比如配置模式文件采用搜索yaml文件的机制,不过你也可以采用多种混合使用,同时采用钩子机制

创建一个插件管理器服务,它通常实现了接口:Drupal\Component\Plugin\PluginManagerInterface,在里面实现具体的功能,包括插件发现机制、实例化插件的工厂方法、缓存、调用插件的方法等等,总的来说这个插件管理器就是提供给用户该类型插件的使用接口,大多数时候可以继承Drupal\Core\Plugin\DefaultPluginManager,这个基类默认使用释文发现机制,当然在你的管理器中可以进行扩充或更改,比如加入钩子发现机制等等,按照最佳实践约定,插件管理器的服务名使用“plugin.manager.”前缀加插件类型名,比如plugin.manager.element_info、plugin.manager.block等等,在运行时容器定义中搜索该前缀就能找到系统中所有的插件管理器了,默认标准安装中大约十多个。

可选的定义插件集plugin collection(见后)

系统默认提供了几种插件查找方式:释文、钩子、yaml文件、静态注册,如果需要,我们可以自定义自己的查找方式,也可以混用他们,这经常会用到装饰者模式去实现,大多数时候使用释文发现机制,以该方式有几个关键点:

定义一个名字空间后缀,这对应于模块中存放插件的子目录,释文发现机制会到系统开启的模块下去搜索这个目录,本类型所有的插件名字空间都要使用该后缀

定义一个释文类,它用于在释文发现机制中接受插件释文块中变量的注入,释文类是插件元数据的承载者,但它并不是插件定义,插件定义由释文对象的get方法得到,大多数时候是一个数组,但也可以是对象(比如实体插件),取决于释文类的实现,释文对象带来极大的灵活性,它通常会继承\Drupal\Component\Annotation\Plugin,至少包括了插件ID、插件类、模块名,注意在定义释文类时务必加入“@Annotation”到释文块中,以表明它是一个释文类,否则释文机制不会工作。

可选的在插件管理器中定义一个用于修改插件定义的钩子名,便于其他模块调整找到的插件定义

因为释文发现是很耗费计算资源的,且要遍历系统中许多的目录,所以务必设置缓存

插件派生(衍生)Plugin derivatives:

释文发现机制中,插件的发现是通过扫描名字空间下子目录的方式进行的,不同类型的插件放置在不同的子目录里,有一种特殊情况:如果一种类型的插件还充当其他多个类型的插件(如字段类型插件,也充当数据类型化插件),那么会有多个子目录,此时插件类文件应该放在哪个子目录里面呢?是不是每个类型都复制一份呢?显然这是不优雅的,此时只需要将类放置在其中一种类型的子目录下,其他类型子目录下放置一个基本插件来充当指针的角色即可,此基本插件有基本插件定义,通过这个中间定义可以得到所有的插件定义(原理见下),这就是插件派生,但这只是派生的用途之一。

派生是指将一个插件定义转换为多个插件定义的机制,转换逻辑由“派生器”进行,需要进行派生的插件(上文提到的指针插件)必须在释文中通过deriver根键定义派生器类,例如:

系统菜单块插件:Drupal\system\Plugin\Block\SystemMenuBlock的释文如下:

/**
 * Provides a generic Menu block.
 *
 * @Block(
 *   id = "system_menu_block",
 *   admin_label = @Translation("Menu"),
 *   category = @Translation("Menus"),
 *   deriver = "Drupal\system\Plugin\Derivative\SystemMenuBlock"
 * )
 */

使用派生的插件还有很多,如:

Drupal\Core\Field\Plugin\DataType\FieldItem

Drupal\Core\Entity\Plugin\DataType\EntityAdapter

默认安装下系统提供了14个派生插件,通过释文发现机制从这些插件中得到的插件定义称为基本插件定义,插件id为基本插件id,然后根据基本插件定义里面提供的派生器得到多个派生出的插件定义,插件id称为派生插件id,这样就将一个插件定义转化为了多个插件定义,在插件管理器中每个插件定义的插件id以如下方式指定:

“基本插件ID:派生插件ID”

在插件的发现机制实现上采用装饰者模式,代码位于默认插件管理器中,见:

Drupal\Core\Plugin\DefaultPluginManager::getDiscovery()

在该方法中释文发现对象被派生装饰者代理,释文发现对象只能得到基本插件定义,由派生装饰者实例化派生器,执行派生器以将基本插件定义转化为派生出的多个插件定义。

派生转换的重点在于基本插件定义中指定的派生器,所有派生器必须要实现接口:

\Drupal\Component\Plugin\Derivative\DeriverInterface

如果派生器需要容器协助工作,那么可以实现以下接口:

\Drupal\Core\Plugin\Discovery\ContainerDeriverInterface

该接口继承了DeriverInterface接口,为了利用容器,它提供工厂方法create去实例化自己,实现这个接口的派生器,在装饰者代理中自动调用create工厂方法给派生器注入容器对象。

举一个具体例子,在类型化组件中得到类型化定义的代码如下:

$definition = \Drupal::typedDataManager()->getDefinitions();
print_r(array_keys($definition));

现在去掉派生机制,在Drupal\Core\Plugin\DefaultPluginManager::getDiscovery()方法后面加以下方法:
 

public function getyunke() {
        if (!$this->discovery) {
            $discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
            $this->discovery = $discovery;
        }
        return $this->discovery;
    }

然后在控制器中运行以下代码:

$definition = \Drupal::typedDataManager()->getyunke()->getDefinitions();
print_r(array_keys($definition));

只能得到以下类型:

Array
(
    [0] => filter_format
    [1] => entity
    [2] => entity_reference
    [3] => field_item
    [4] => any
    [5] => binary
    [6] => boolean
    [7] => datetime_iso8601
    [8] => duration_iso8601
    [9] => email
    [10] => float
    [11] => integer
    [12] => list
    [13] => language
    [14] => language_reference
    [15] => map
    [16] => string
    [17] => timespan
    [18] => timestamp
    [19] => uri
)

可以看到派生机制去掉后,大量插件消失了,只提供了基本插件定义。

在插件派生机制中,如何产生插件定义,完全由派生器定义,这带来极大的灵活性。派生出来的插件可以实现以下接口,从而方便使用:

Drupal\Component\Plugin\DerivativeInspectionInterface

如果你还不能理解派生,那么直白、本质的讲就是:

在传统释文发现机制中,我们得到的每个插件定义都对应于插件类型路径下的一个类文件,而派生扩展了这点,使得一个插件定义可以对应多个、任意位置的类文件,从而产生多个插件定义,新产生的插件以“基本插件ID:派生插件ID”的形式指定id,这种对应关系完全取决于派生器的实现,在内部可以让开发者随意指定类文件和插件定义数据,这是非常灵活的,派生器会被默认插件管理器中的发现装饰者自动调用。

插件集Plugin collections:

在实例化某个插件类型的插件时,通常调用插件管理器的实例化方法:
$pluginManager->createInstance($pluginID, $configuration);
这是实例化一个插件的办法,当有大量插件需要实例化时,需要多次调用还难于管理,此时可采用与容器类似的办法,将这个工作委派给一个对象来完成是比较好的,只需要传递给这个对象实例化插件所需信息即可,该信息可以是一个数组,如下:
$pluginConfiguration[$pluginID]= $configuration;
数组的键值是实例化时传给插件的配置数组,键名是插件id,这里我们更进一步的想想,如果键名是插件id,那么一个插件类只能实例化出一个插件对象来,当需要将一个插件实例化出多个插件对象怎么办呢?我们可以改进这个数组:
$pluginConfiguration[$instanceID]= $configuration;
将键名替换为实例的id,而不是插件的id,将后者放到$configuration中即可,有了这个数组再加上实例化插件用的插件管理器,该对象就可以在内部帮我们完成插件的实例化工作了,这还带来了一个好处:这和容器是很像的,内部只保留了配置数据,只有在真正需要插件时才进行实例化,节省了性能开支,这称为延迟实例化,是一个很重要的好处,当有大批插件可能被需要,但我们并不知道在运行时具体是哪些时,就可以在运行时才实例化,此时该特性甚至可以说是必要的,想象一下如果每次请求时容器里面的服务全都被实例化,是件多么可怕的事情。

以上就是“插件集PluginCollection”这个概念的来因,帮我们完成实例化工作的这个对象称为插件集Plugin collections对象,她不但负责实例化插件,还负责管理,系统组件中提供了一个插件集对象的抽象基类:
\Drupal\Component\Plugin\LazyPluginCollection
在核心中提供了一个默认实现:
Drupal\Core\Plugin\DefaultLazyPluginCollection
默认实现的插件集对象的构造函数接收两个参数:
插件管理器:依据传入的信息来实例化插件,插件集中的插件应该是同一类型的插件
配置信息:也就是前文探讨的实例化信息数组,包含集里所有插件的配置信息,键名是插件的实例id,键值为配置信息,配置信息中必须有插件id,注意插件id和实例id是不一样的,插件id对应插件类,实例id对应插件对象
该默认对象的使用示例代码如下:
 

$configuration=[…] //实例化插件时传入插件类的配置数据
$configuration [‘id’]=$pluginID; //指定插件id,用来找到插件类,默认键名为id,子类可以覆写掉
$configurations[$instance_id]= $configuration; // 构造传递给插件集对象的配置信息
$pluginCollection=new DefaultLazyPluginCollection($manager, $configurations)
$plugin =$pluginCollection->get($instance_id); //得到所需插件对象

我们可以看到以上插件集类中都有“Lazy”字样,这就是指上文提到的延迟实例化,插件集对象中保存了所有插件的配置信息,当需要某个插件时才依据配置信息实例化它,而不是初始化插件集对象时就马上全部都实例化了

由于延迟实例化的好处,有时候即便只需要实例化一个插件,这样做也是好的,因此系统还实现了一个特殊的插件集对象:
Drupal\Core\Plugin\DefaultSingleLazyPluginCollection
她只管理一个插件,需要时才实例化,充分发挥性能优势,因为只有一个插件,所以默认实例id采用插件id

插件集在块系统等地方被用到,系统为需要用到插件集的对象定义了一个接口:
Drupal\Core\Plugin\ObjectWithPluginCollectionInterface
里面只有一个方法:
public function getPluginCollections();
注意Collection是复数形式,该方法意为得到多个插件集,多个是什么意思呢?单个插件集对象里面管理的是同一类型的插件,一个对象可能会需要不同插件类型,因此就需要多个插件集,即便是同一类型插件也可以使用插件集对象进行插件分类,因此是复数,该方法返回一个数组,键名为表示插件分类的属性名,键值为该分类下的插件集对象,通过该接口可以方便的从对象外部操作她所需的插件。

 

插件依赖:

有些类型的插件可以有依赖,当依赖满足(存在)时插件才会被启用,典型的如实体(实体是一种插件,实际上drupal世界中插件的依赖概念几乎就是为实体服务的),如何表示插件的依赖数据呢?

在drupal中依赖的事物有这几种类型:'module', 'theme', 'config', 'content',在具备依赖属性的插件中保存着依赖数据,它是一个数组,按照依赖对象的类型保存数据,如下:

      array(
        'config' => array('user.role.anonymous', 'user.role.authenticated'),
        'content' => array('node:article:f0a189e6-55fb-47fb-8005-5bef81c44d6d'),
        'module' => array('node', 'user'),
        'theme' => array('seven'),
      );

第一级键名为所依赖物的类型,每种类型下保存着被依赖的对象名,可以有多个,对象名规则为:

模块类型:对象名为模块名

主题类型:对象名为主题名

配置类型:对象名为配置对象名,如果是配置实体该名来自实体的getConfigDependencyName()方法

内容类型:系统中所有内容类型的信息都是实体,对象名为内容实体类型id+“:”+实体bundle+“:”+实体的UUID,同样来自实体的getConfigDependencyName()方法

系统为这一类具备依赖属性的插件提供了一个接口:

Drupal\Component\Plugin\DependentPluginInterface

该接口只声明了一个方法:calculateDependencies(),可以通过它计算出该插件的依赖。

在核心中提供了一个辅助计算插件依赖的特征:

Drupal\Core\Plugin\PluginDependencyTrait

它提供了一个可见性为protected 的calculatePluginDependencies方法,注意不是前文的calculateDependencies(),该特征用到了实体依赖特征:Drupal\Core\Entity\DependencyTrait,以实现添加依赖

我们知道插件和插件的定义是不一样的,插件定义可以是数组方式也可以是插件定义对象方式,为此系统为具备依赖属性的插件的插件定义对象也提供了一个接口:

Drupal\Core\Plugin\Definition\DependentPluginDefinitionInterface

并为它实现了一个特征:

Drupal\Core\Plugin\Definition\DependentPluginDefinitionTrait

实现了该接口的插件定义对象可以使用该特征设置和获取定义对象提供的依赖数据(只是定义对象提供的依赖,插件本身一般还可以设置依赖),插件对象用calculateDependencies()方法可以根据该插件定义对象计算出依赖,如果插件定义是以数组方式提供的,那么可以在键名“config_dependencies”中设置依赖,相应的,插件释文也使用该键声明依赖。

本篇就到这里,插件系统的下篇将介绍插件上下文、插件映射等余下内容。

/**************完***************/

做个纪念,这是本系列第50篇主题,总字数已超过26万字,以下是云客的寄语:

各位drupal社区的小伙伴大家好:

这是云客源码分析第50期,总字数已经超过26万字,按照每周发布一篇的速度连续进行了一年,非常感谢大家的陪伴,其间也获得了一些小伙伴的捐助,如云客之前所承诺的:这些费用全部无偿用于drupal深圳社区的建设,中国社区建设需要更多人,关于这个我已写在一篇帖子里面:

《为什么选择drupal?为什么做贡献?怎么学?怎么贡献?》

http://blog.csdn.net/u011474028/article/details/74295701

这里针对入门问题给一些建议:

刚刚接触drupal的小伙伴可以参阅@龙马(群4362258,qq号:178425145)组织翻译的《Beginning Drupal 8》中文版,接下来:

如果你是开发者,那么请看完symfony框架的http-kernel组件后,可以开始按发布顺序阅读《云客源码分析》系列主题,它是按照系统执行顺序依次介绍的,全部内容和官网文档一样来自于源代码,许多地方比英文官网文档更加详尽,是第一手的自主中文资料,是官网文档的重要补充。

如果你是设计者,那么请看@晴空(群345293977,qq号:2304167266)录制的主题视频教程,该教程还同步发布文档,作者还是drupal教程网站爱码文档汇的创建人,该网站有越来越多的作者在进行教程编写

如果你只想找人做drupal开发,那么他们都可胜任:@希望之翼(q:16740227)@煮不在乎(q:294535375)@Dan`s boy(q:740576915)@*葉子*(q:113017582)等等,这些都是云客知道并认可的,还有很多不认识的高手,不一一列举,请关注群

如果你想了解drupal的各类信息,请关注@BiaoGeBusy(q:349255833)的公众号:Drupal每日推荐;社区网站http://drupalchina.cn/ ;文档教程网站:http://nowicode.com ;Drupal大学 http://drupal001.net 等等,还有很多很优秀的资源不一一列出了

 

 

 

本书共94小节:

评论 (写第一个评论)