第七部 代码的变化
##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介绍:
还可以浏览https://drupal.org/list-changes来查看完整的Drupal 7和Drupal 8的API差异(载入时间貌似比较长)。每个API的变化都包含了变化前和变化后的代码示例,可以帮助你进行迁移,并指出这个变更所对应的issue,介绍变更的内容和原因。
##打了好多字
转向现代的,面向对象的新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。)