第七部 代码的变化

##Proudly Found Elsewhere(这一标题我想翻译成他山之石)

非我首创综合症“的反义词,代表了Drupal核心开发者的思维方式的转变——尝试找到最好的工具并集成到软件(Drupal)中去,而不是构造Drupal专属的,仅对Drupal用户有益的定制化技术。

在Drupal 8中你会看到很多这种哲学的体现。在众多的外部库中,我们引入了PHPUnit来进行单元测试,Guzzle来执行HTTP请求(Web Service),一系列的Symfony组件(可参考《Create your own framework on top of the Symfony2 Components》),Composer用于解决外部依赖和自动加载问题。

除此之外,这一变化也反映在核心基础代码之中,我们在Drupal 8中做出了《大量的架构变更》,以此来响应外部世界的进步:解耦,面向对象,对PHP的新特性例如命名空间和Traits的支持。

##Getting OOP-y with it(翻成啥,此处应有掌声?)

让我们来看看Drupal 8架构中的一些反映上述变化的代码:

Drupal 7: example.info

name = Example
description = An example module.
core = 7.x
files[] = example.test
dependencies[] = user

所有的Drupal模块都需要一个.info文件来完成系统中的注册动作。上面的例子是Drupal 7模块中的典型写法。文件格式是一种类似Ini的写法,但是其中还包含了一些Drupal风格的元素,例如array[]语法,这导致无法使用PHP的标准INI文件访问函数来访问info文件。files[]关键字在Drupal 7中用于触发自定义类的载入,也是一种Drupal特有的方式,模块开发者开发的面向对象代码必须添加一个files[]元素来定义类,看起来有点傻(views的info文件,这个吐槽深得我心)

###Drupal 8: example.info.yml

name: Example
description: An example module.
core: 8.x
dependencies:
  - user
# Note: New property required as of Drupal 8!
type: module

Drupal 8的info文件改为很多其他语言和框架都在使用的YAML格式。语法类似(“:”代替了 “=”,不同的数组语法),读写都很容易。类的自动加载,不再使用古怪的files[]关键字,而改头换面为用Composer支撑的 PSR-4标准。这种方式采用规定的类名称/文件夹的转换方式,形成一种更像自然语言的规则(modules/example/src/ExampleClass.php),Drupal不再需要手工注册即可自动载入面向对象的代码。

###Drupal 7: hook_menu()

example.module

<?php
/**
* Implements hook_menu().
*/
function example_menu() {
  $items['hello'] = array(
    'title' => 'Hello world',
    'page callback' => '_example_page',
    'access callback' => 'user_access',
    'access arguments' => 'access content',
    'type' => MENU_CALLBACK,
  );
  return $items;
}
/**
* Page callback: greets the user.
*/
function _example_page() {
  return array('#markup' => t('Hello world.'));
}
?>

这是Drupal 7经典的”Hello world”模块,定义了一个/hello的URL,当这个地址被访问时,会判断当前用户是否具有”access content”权限,如果鉴权通过,则会执行_example_page()的代码——显示渲染后的”Hello world”。hook_menu()是Drupal 7以及更早版本中被诟病已久的数组API(API => ArrayPI),这种方式的问题在于,很难进行输入(比如,忘掉return $items,然后抓耳挠腮不知错在何处),没有IDE能够对这种东西进行自动完成,对于关键字的新增和变更,文档也只能进行手工同步。hook_menu文档中也很明显的表现出一个问题:menu的负担太重了——注册URL以及相应的鉴权方法和执行代码,又提供了在界面上暴露连接的多种方式,甚至还能切换主题,以及更多的任务。

###Drupal 8: 路由和Controller

example.routing.yml

example.hello:
  path: '/hello'
  defaults:
    _content: '\Drupal\example\Controller\Hello::content'
  requirements:
    _permission: 'access content'

src/Controller/Hello.php

<?php
namespace Drupal\example\Controller;
use Drupal\Core\Controller\ControllerBase;

/**
* Greets the user.
*/
class Hello extends ControllerBase {
  public function content() {
    return array('#markup' => $this->t('Hello world.'));
  }
}
?>

在Drupal 8的路由系统中,采用了同Symfony路由系统一致的YAML格式,用于实现url以及访问控制逻辑。原有的page callback方式现在采用了”Controller”类的方式(可参看model-view-controler模式),Controller遵循PSR-4标准存放于指定命名的文件夹中。这个类存在于example模块的命名空间中,这也防止了同其他模块的命名冲突。最后,这个类使用use和extends语句引入了ControllerBase类,因此这个controller能访问到所有的ControllerBase类的方法,例如$this->t()(t()函数的OO版本)。另外,ControllerBase是一个标准的PHP类,所有的方法和属性在IDE中都可以自动完成,这将大大减少猜测和出错的机会。

###Drupal 7: hook_block_X()

block.module

<?php
/**
* Implements hook_block_info().
*/
function example_block_info() {
  $blocks['example'] = array(
    'info' => t('Example block'),
  );
  return $blocks;
}
/**
* Implements hook_block_view().
*/
function example_block_view($delta = '') {
  $block = array();
  switch ($delta) {
    case 'example':
      $block['subject'] = t('Example block');
      $block['content'] = array(
        'hello' => array(
          '#markup' => t('Hello world'),
        ),
      );
      break;
  }
  return $block;
}
?>

这是Drupal中的典型的插接方式——包括block, 文本格式,图片效果等:某种_info()钩子,然后一个或多个其他钩子来进行其他动作(查看,应用和配置等)。这其中又包含大量的数组,这是因为这些API不具备自描述特性,除了肉眼观察大量模块的.api.php文件之外,别无他法——而这些文件也未必能够提供足够的信息要求你如何命名你的实现。有些是必选,有些不是,哪个是哪个,等等诸如此类的问题。

###Drupal 8: Blocks(以及很多其他东西)是一个插件

在Drupal 8中,这些古怪的API转换为新的插件系统,举例如下:

src/Plugin/Block/Example.php

<?php
namespace Drupal\example\Plugin\Block;
use Drupal\Core\Block\BlockBase;

/**
* Provides the Example block.
*
* @Block(
*   id = "example",
*   admin_label = @Translation("Example block")
* )
*/
class Example extends BlockBase {
  public function build() {
    return array('hello' => array(
      '#markup' => $this->t('Hello world.')
    ));
  }
}
?>

大多数内容跟Controller例子很像;一个插件就是一个基类(BlockBase)的派生,基类维护了很多基础内容。Block API声明了BlockPluginInterface接口,并进行了实现。

接口以IDE友好的方式提供和描述了各种API。学习Drupal 8新API的最好方式之一就是浏览接口。

类上方的注释被称为标注。用PHP注释来指定逻辑的元数据可能让人觉得奇怪,不过这种方式已经被众多的现代PHP库采用,并且也为PHP社区所接受。

###Drupal 7:Hooks

在Drupal 7以及更早的版本中,通过hook来实现扩展机制。作为API的作者,可以利用module_invoke_all(), module_implements(), drupal_alter()等方式来声明Hook,例如:

<?php
  // Compile a list of permissions from all modules for display on admin form.
  foreach (module_implements('permission') as $module) {
    $modules[$module] = $module_info[$module]['name'];
  }
?>

在一个需要响应这一事件的模块中,可以创建一个名为modulename_hookname()的函数,输出该hook指定的结果。例如:

<?php
/**
* Implements hook_permission().
*/
function menu_permission() {
  return array(
    'administer menu' => array(
      'title' => t('Administer menus and menu items'),
    ),
  );
}
?>

这在Drupal初期是一个很先进的扩展方式(Drupal在2001年出世,当时是PHP3的天下,没有提供面向对象或者类似的机制),这一方式也造成不少麻烦:

  • 利用”特定名称的函数”进行扩展的机制是一种很Drupal的方式,也是新晋开发者最难以理解的问题之一。

  • 至少有四种不同的函数可以触发hook:module_invoke()module_invoke_all()module_implements()drupal_alter()。这使得查找实现某一扩展的所有代码非常困难。

  • Hook缺乏一致性:有些”info”类的hook,提供一个数组(或者数组的数组的数组..),有些是“事件”类型的hook,在某些事情发生的时候触发。需要阅读每个hook的文档来确定各个hook需要的输入和输出。

###Drupal 8:事件

Hooks在Drupal 8的多数事件驱动行为中还是普遍存在的(info风格的hook被迁移到了YAML或者插件标注中),Drupal 8中迁移到Symfony(例如bootstrap/exit, 路由系统)的部分也随之转换为Symfony事件分发系统。在这个系统中,事件在某个逻辑触发的时候进行分发,模块可以订阅需要进行交互的事件。

下面的演示,是Drupal 8的配置API,位于core/lib/Drupal/Core/Config/Config.php。他定义了一系列的’CRUD’方法,例如save()、delete()等。每个方法在结束任务之后都会分发一个事件,让其他模块可以做出反应。例如Config::save():

<?php
  public function save() {
    // <snip>Validate the incoming information.</snip>
    // Save data to Drupal, then tell other modules this was just done so they can react.
    $this->storage->write($this->name, $this->data);
    // ConfigCrudEvent is a class that extends from Symfony's "Event" base class.
    $this->eventDispatcher->dispatch(ConfigEvents::SAVE, new ConfigCrudEvent($this));
  }
?>

最少有一个模块在配置变化时候需要做出反应:核心的语言模块。因为如果刚刚发生变更的配置中包含了缺省的站点语言,会需要清除PHP缓存文件来让配置生效.

要达到这个目的,语言模块要做三件事:

1.在language.services.yml文件(Symfony服务容器的配置文件,用于注册可重用代码)中注册一个订阅类:

language.config_subscriber:
  class: Drupal\language\EventSubscriber\ConfigSubscriber
  tags:
    - { name: event_subscriber }

2,在引用类中实现EventSubscriberInterface,并声明一个getSubscribedEvents方法,这个方法列出要处理的事件,并为每个事件提供一个或多个回调函数来进行处理,并提供权重,来决定调用顺序(Symfony的权重顺序同Drupal的相反):

<?php
class ConfigSubscriber implements EventSubscriberInterface {
  static function getSubscribedEvents() {
    $events[ConfigEvents::SAVE][] = array('onConfigSave', 0);
    return $events;
  }
}

3.定义回调方法,用于处理配置保存触发的事件:

<?php
  public function onConfigSave(ConfigCrudEvent $event) {
    $saved_config = $event->getConfig();
    if ($saved_config->getName() == 'system.site' && $event->isChanged('langcode')) {
      // Trigger a container rebuild on the next request by deleting compiled
      // from PHP storage.
      PhpStorageFactory::get('service_container')->deleteAll();
    }
  }
?>

上面的方式给开发者提供了一个明确的注册工具,让一个模块可以用多个类订阅不同的事件。这避免了过去我们经常要在hook中使用switch语句的情况,也避免了同一语句块中大量的不相干代码对开发人员造成的困扰,让我们可以用不同的类来处理不同的逻辑。这同时也意味着我们的事件机制是后加载的,而不是随时保存在PHP的内存中。

对事件进行调试,以及查找其实现代码也变得非常直观。从前需要在一大堆的过程化PHP函数中鉴别是否被hook调用,而现在只需要简单的查找注册事件即可,例如ConfigEvents::SAVE。

事件系统真正完成了到面向对象的转换。插件系统处理了info风格的hook,YAML代替了过去的注册系统,而事件系统则取代了事件风格的hook,并引入了强大的订阅机制,进一步提升了Drupal核心的扩展能力。

##还有很多

Drupal API的变更可以在Drupal 8 API页面看到,这里你能找到分门别类的Drupal 8 API介绍:

drupal8 api

还可以浏览https://drupal.org/list-changes来查看完整的Drupal 7和Drupal 8的API差异(载入时间貌似比较长)。每个API的变化都包含了变化前和变化后的代码示例,可以帮助你进行迁移,并指出这个变更所对应的issue,介绍变更的内容和原因。

drupal 8 change record

##打了好多字

转向现代的,面向对象的新Drupal,会多出很多啰嗦的问题。下面列出一些项目能够协助你跨越这些障碍:

  • Drupal Module Upgrader:如果要把Drupal 7模块升级到Drupal 8,一定要看看这个项目,他会告诉你哪里需要变更(指向相关的变更说明),或者自动的把你的代码转换到Drupal 8。你可以在视频中更多的了解该项目。

  • Console,对新手来说非常有用,自动生成.module/.info文件,PSR-4目录结构,YAML以及路由注册等内容。

  • 多数Drupal 8核心开发者极其信赖PhpStorm IDE,这个IDE的最新版本对Drupal开发提供了大量新功能。另外,如果你是Drupal的顶尖贡献者,可以免费获取(这不是广告,你可以加入 #drupal-contribute,看看会不会有一个小时没人提到PhpStorm。)

Avatar
崔秀龙

简单,是大师的责任;我们凡夫俗子,能做到清楚就很不容易了。

comments powered by Disqus
下一页
上一页

相关