Drupal 8 插件 API

原文链接:Plugin API in Drupal 8

插件是小型的可插拔功能模块。 拥有类似功能的插件属于同一种插件类型

Drupal包含很多插件和插件类型。例如,'Field widget' (字段小工具)就是一种插件类型,而具体的字段类型就是插件。管理员用户可以从字段类型插件列表中选择并设置字段所使用的类型。

D8的插件系统提供了一套指导原则和可重用的代码组件,使开发者能够公开他们的可插拔组件,如需要,还能够通过用户界面管理这些组件。

插件是由模块定义的:一个模块可以提供不同类型的插件,不同的模块可以提供各自特定类型的插件。

概述

插件系统由三种基本元素构成:

  1. 插件类型

    插件类型是一个控制中心类,它决定了插件如何被系统发现和实例化。这个类型会描述其下所属插件的核心目标,例如后端缓存,图片操作,区块等等。

  2. 插件发现模式

    插件发现是指在可用代码库中查找具有特定类型的、合适的插件实例的过程。(Plugin Discovery is the process of finding plugins within the available code base that qualify for use within this particular plugin type's use case.)

  3. 插件工厂

    插件工厂负责使用给定的用例来实例化具体插件。

此外,插件系统还包含一些特定场景下比较有用的组件:

  • 插件衍生

    插件衍生允许使用一个通用插件代替多个插件。这在用户输入的数据可能影响可用插件的情况下非常有用。例如,假设菜单是用一个插件来展示的,当管理员创建了新的菜单,那么新菜单也能够被该插件展示,而不需要去创建一个额外的插件来展示它。考虑到帮助文档,尤其是用例的帮助文档,既能被输出,也能被应用到别的地方(原文:allowing for help text specific to the use case to be rendered and utilized.),因此插件衍生也支持在用户界面中用多个插件来代替一个插件。插件衍生工具的主要目的是将部分已配置的插件作为优先级插件使用,以免与其他用户界面的插件产生混淆,从而减少管理者使用这些插件的负担。

  • 修饰器发现模式

    修饰器发现是另外一种发现模式,为了包装已存在的发现方法。核心目前提供cacheDecorator来缓存被选择了插件类型的(插件的)发现过程。如果需要的话,这种模式也可以扩展到其他情况下。(原文:A discovery decorator is another available discovery method meant to wrap an existing discovery method. Core currently supplies the cacheDecorator which will cache the discovery process that is chosen for a plugin type. This pattern could be expanded to other use cases if necessary.)
  • 插件映射

    插件映射允许映射一些东西(通常是一个字符串)到一个特定的插件实例。插件类型能够使用这种方法返回一个基于自定义名称的完全配置和初始化的插件,而不需要手动使用API去初始化和配置插件的实例。

本文将就这些概念给出深入的讨论,最佳实践和代码示例。

为何使用插件

原文链接: Why Plugins?

插件有点像PHP原生接口外加一点扩展:插件系统能够(通过神奇的命名空间)发现每一个接口的实现类,(默认情况下使用注解来)处理元数据并为那些插件类提供工厂。

插件实现相同的接口,却提供截然不同的行为——就像裁剪效果无法代替缩放效果(至少对于最终用户来说——插件系统使用相同的方式处理这两种扩展,这是非常必要的)。另外,如果你的接口期待实现类的行为一致而内部结构不同(就像database cache和memcache之于缩放和裁剪,不具有类似的不同之处),只需要在service.yml文件中定义,而不是使用插件系统。

基于注解的插件

原文链接:Annotations-based plugins

Drupal 8 中大部分插件使用注解来注册和描述元数据。也有一些插件类型由核心提供:

  • Entities (未来可能不作为插件)
  • Blocks (在 */lib/Drupal/*/Plugin/Block/* 查看更多示例)
  • 字段格式器,字段小工具>(在 */lib/Drupal/*/Plugin/field/* 查看更多示例)
  • 所有Views插件(在 */lib/Drupal/*/Plugin/views/* 查看更多示例)

你应该在除此之外的文档中去查看一些实际案例。

psr-4

插件使用的注解被注册在PHP文件中使用psr-4标准,Drupal核心也是使用这个标准。

要注册插件,基于你的Drupal module root放置文件:src/Plugin/$plugin_type/Example.php

例如:core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php

为了告诉系统这是一个插件,你需要为你的class添加如下注释:

<?php
/**
 * @Plugin(
 *
 * )
 */
?>

使用这种annotations class的一个具体的插件是UserNameUnique,在 core/modules/user/src/Plugin/Validation/Constraint/UserNameUnique.php文件中

这个注解仅包含id和label。

<?php
/**
 * Checks if a user name is unique on the site.
 *
 * @Plugin(
 *   id = "UserNameUnique",
 *   label = @Translation("User name unique", context = "Validation")
 * )
 */
class UserNameUnique extends Constraint {
...
}
?>

为何使用注解?

相对于其他的发现机制,注解的元数据存在于同一个文件,并作为该插件的实现类的一个组成部分。这使得插件更容易被找到,也更容易被自定义,仅需简单地复制一个现有的插件。

注解允许复杂的结构化数据,你也可以指明某些字符串是被翻译的。很多情况下插件的相关联的自定义注解类既能作为文档,也能够为元数据设置默认值。

另外,注解能额外提升Drupal的性能,它使Drupal在发现插件时使用很少的内存。原来的实例中每个类都有一个getinfo()方法,类似Drupal 7 的test classes。这意味着每一个类都必须被加载到内存来获取其信息,并且内存不会被释放除非请求结束,这会大大增加PHP对Peek Memory的需求。相反,Drupal解析注解的实现只是简单的对文件的文本进行分词,而不是作为PHP文件加载,所以使内存的使用率降到最低。

注解语法

简而言之,注解是一种天然的key-value数据结构,包括嵌套的。

注解的语法来自于Doctrine项目(具体参考http://docs.doctrine-project.org/en/2.0.x/reference/annotations-referenc...),尽管Drupal的语法稍有不同,比如每个值后面都换行,但这任然有用。

  • 必须以插件id开始,相当于Drupal 7 的机器代码,作为插件的唯一键。
  • 顶级的KEYS可以使用双引号
  • 下级的KEYS必须使用双引号
  • 只能使用双引号,不能使用单引号,否则抛出异常
  • 值可用的数据类型
    • String:必须使用引号(例如:"foo"
    • Numbers:不能使用引号(例如:21)——使用了引号会被转化成字符串
    • Boolean:不能使用引号(例如:TRUE 或 FALSE)——使用了引号会被转化成字符串
    • Lists:使用大括号,末尾不要用逗号
      base = {
        "node",
        "foo"
      }
    • Maps:使用大括号,用等号分隔键和值,末尾不要用逗号
      edit = {
        "editor" = "direct"
      }
  • 可以使用常量

自定义注解类

基础的插件注解能被用于发现(插件),但是如果你想在插件定义中添加默认值和文档,你可以使用自定义注解类。

我们来看一个实际案例,plaintext格式器,位于text/src/Plugin/field/formatter/TextPlainFormatter.php

它使用自己的注解类,FieldFormatter,继承自\Drupal\Component\Annotation\Plugin

<?php
/**
 * Plugin implementation of the 'text_plain' formatter.
 *
 * @FieldFormatter(
 *   id = "text_plain",
 *   label = @Translation("Plain text"),
 *   field_types = {
 *     "text",
 *     "text_long",
 *     "text_with_summary"
 *   },
 *   edit = {
 *     "editor" = "direct"
 *   }
 * )
 */
class TextPlainFormatter {
?>

在你自己的插件类型中使用注解

如果你自己写了一个插件类型,并且想要使用注解,只需要在你的plugin manager里面引用AnnotatedClassDiscovery——见下面代码。

AnnotatedClassDiscovery构造函数的第一个参数是这个插件类型被存放的子目录/子命名空间。所以下例中,插件会被在$module/src/Plugin/field/formatter文件夹中搜索到。

<?php
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
class
FormatterPluginManager extends PluginManagerBase {
 
/**
   * Constructs a FormatterPluginManager object.
   *
   * @param array $namespaces
   *   An array of paths keyed by their corresponding namespaces.
   */
 
public function __construct(array $namespaces) {
   
// This is the essential line you have to use in your manager.
   
$this->discovery = new AnnotatedClassDiscovery('Plugin/field/formatter', $namespaces);
   
// Every other line is a good practice.
   
$this->discovery = new ProcessDecorator($this->discovery, array($this, 'processDefinition'));
   
$this->discovery = new AlterDecorator($this->discovery, 'field_formatter_info');
   
$this->discovery = new CacheDecorator($this->discovery, 'field_formatter_types', 'field');
  }
}
?>

注入的命名空间来自于依赖注入容器。例如FieldBundle:

<?php
/**
 * @file
 * Contains Drupal\field\FieldBundle.
 */
namespace Drupal\field;
use
Symfony\Component\DependencyInjection\ContainerBuilder;
use
Symfony\Component\HttpKernel\Bundle\Bundle;
/**
 * Field dependency injection container.
 */
class FieldBundle extends Bundle {
 
/**
   * Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
   */
 
public function build(ContainerBuilder $container) {
   
// Register the plugin managers for our plugin types with the dependency injection container.
   
$container->register('plugin.manager.field.widget', 'Drupal\field\Plugin\Type\Widget\WidgetPluginManager')
      ->
addArgument('%container.namespaces%');
   
$container->register('plugin.manager.field.formatter', 'Drupal\field\Plugin\Type\Formatter\FormatterPluginManager')
      ->
addArgument('%container.namespaces%');
  }
}
?>

调用你自定义的插件管理器,你需要在调用构造函数的时候注入命名空间。

<?php
 $type
= new CustomPluginManager(\Drupal::getContainer()->getParameter('container.namespaces'));
?>

D8 插件发现模式

原文链接:D8 Plugin Discovery

插件发现是Drupal根据给定类型查找插件的过程。每种插件类型都必须设置一个发现方法(说明在plugin manager 文档中)。

插件的发现组件实现DiscoveryInterface接口,定义了每个发现类必须有的方法。

<?php
/**
 * @file
 * Contains \Drupal\Component\Plugin\Discovery\DiscoveryInterface.
 */
namespace Drupal\Component\Plugin\Discovery;
/**
 * Defines the plugin discovery interface.
 */
interface DiscoveryInterface {
 
/**
   * Gets a specific plugin definition.
   *
   * @param string $plugin_id
   *   A plugin id.
   *
   * @return array
   *   A plugin definition.
   */
 
public function getPluginDefinition($plugin_id);
 
/**
   * Gets the definition of all plugins for this type.
   *
   * @return array
   *   An array of configuration definitions.
   */
 
public function getPluginDefinitions();
}
?>

有四种不同的核心发现类型。

  1. StaticDiscovery
    StaticDiscovery允许发现类自身直接注册插件。一个protected变量$definitions保存所有的通过public方法setDefinition()注册的插件定义。所有通过这种方式定义的插件都能够在plugin manager documentation中被列出。
  2. HookDiscovery
    HookDiscovery类允许插件发现使用Drupal的hook_component_info()/hook_component_info_alert()模式。有了这个发现器,插件管理器可以调用信息钩子来获取可用插件列表。
  3. AnnotatedClassDiscovery
    AnnotatedClassDiscovery类使用包含插件定义的注解名称,比如,@Plugin@EntityType,在插件的文档部分发现插件,最小化插件发现阶段的内存使用率。AnnotatedClassDiscovery类的构造函数带有参数,$subdir,指明这个插件类型的sub-directory/sub-namespace。AnnotatedClassDiscovery类会扫描插件目录里面这些sub-directory目录里面的符合PSR-0规范的类来查找插件。
  4. YamlDiscovery
    YamlDiscovery允许插件在YAML文件中被定义。 Drupal核心使用该文件进行本地任务和本地操作。

发现装饰器(原文:Discovery Decorators)

发现装饰器是一个为了提供额外功能而封装了另一个发现机制的类(WiKi:Decorate pattern)。发现装饰器遵循与常规发现器类一样的接口,但是它的目的是与另一个发现器串联。发现装饰器的__construct方法需要一个DiscoveryInterface类型的参数和一些其他的必要参数。核心包含两个发现装饰器,我们先来看看CacheDecorater。

Drupal\Core\Plugin\Discovery\CacheDecorator

我们接下来讨论下这里存在的各种方法和它们的功能。

public function __construct(DiscoveryInterface $decorated,$cache_key = NULL);

如上所述,这个方法拥有一个兼容DiscoveryInterface接口的变量$decorated。这可以是任何一种前面讨论过的发现器类。除此之外还有一个变量$cache_key,当调用cache()->get()时会用到。

public function __getPluginDefinition($plugin_id);

如果你有看过任何其他发现器类的代码,应该会熟知这个方法。在CacheDecorator的情况下,我们会检查看看是否有这个已加载的缓存版本,如果我们不这样做,那么它将手动