D7 theme API 概述

对于Drupal的主体来说,如果是比较简单的项目,直接在主题模板tpl的基础上覆写或是配合主题的预处理器基本上就可以实现需求。而对于高度定制化的需求,Drupal也可以很方便的实现需求,这里就不得不说到drupal在主题层面上提供了强大的API供大家使用。

主题渲染的流程

$flow
st=>start: 路由匹配回调执行 hook_menu callback
e=>end: 前端 html
op1=>operation: 可渲染数组 Renderable array
op2=>operation: 预处理加工 preprocessor
op3=>operation: 处理加工 processor
op4=>operation: 模板打印输出 tpl.php
st->op1->op2->op3->op4->e
$

页面html的生成与可渲染数组Renderable Array

对于D7来说,一个页面的整体完全是由一个大数组构建并按需将数组元素转化为html,构成页面的建筑材料以数组作为规范,也可以是一个AJAX响应,或者一个由hook_menu定义的路由回调的反馈结果,这也就意味着你的页面回调应该返回一个数组而不是字符串作为结果,数组的好处是结果可以很方便的增删改,而整串html拼接的字符操作起来灵活性很差。在这里我们统一将构成页面的数组称为可渲染数组(Renderable array).

一个D7页面响应回调的例子

//页面的两部分表格+分页,而实际的项目中,很可能还会有其他的元素,比如页头、页脚、侧边栏等加入构建完整页面所需要的数组中
//这个例子中,module_name_page函数很可能是一个menu路径的响应回调函数
//回调的结果$build就是一个可渲染数组

function module_name_page() {
  $build = array();
  $build['table'] = array(
    //键名含有'#'的表示的是主题的属性值,不带'#'的一般为该主题元素的子元素
    //这里需要注意的是#theme和#theme_wrappers这两个属性,这两个属性建对应的字符串值实际上是生成html的回调函数
    //'#theme'的值'table'负责表格的生成
    //'#theme_wrappers'在'#theme'执行完之后执行,可以在渲染后的html外添加自定义的html,例如在字段的html外添加字段组html的包裹元素如div等,注意'#theme_wrappers'的回调函数的执行结果必须包含元素的'#children'属性,该属性包含'#theme'回调函数生成的html和子元素
    '#theme' => 'table',
    '#header' => $header,
    '#rows' => $rows,
  );
  $build['pager'] = array(
    '#theme' => 'pager',
  );
  return $build;
}

注:关于 '#theme'和 '#theme_wrappers',官网API文档drupal_render( )有详细说明

前端对响应回调的处理

针对响应的可渲染数组,Drupal将这些结果交给主题的预处理器(preprocessor)和处理器(processor)进一步加工处理以方便在PHPTemplate的模板文件tpl中使用。

以一个node的页面为例

node页面的内容(renderable array)由函数node_view提供,回调结果提供给template_preprocess_node( )来预处理,预处理的作用是将大数组Renderable Array分解为一个个小数组,而这些小数组可以直接在模板文件node.tpl.php直接打印出来,打印完的结果,就是最终的html页面了。


function node_view($node, $view_mode = 'full', $langcode = NULL) {
  if (!isset($langcode)) {
    $langcode = $GLOBALS['language_content']->language;
  }

  // Populate $node->content with a render() array.
  node_build_content($node, $view_mode, $langcode);

  $build = $node->content;
  // We don't need duplicate rendering info in node->content.
  unset($node->content);

  $build += array(
    '#theme' => 'node',
    '#node' => $node,
    '#view_mode' => $view_mode,
    '#language' => $langcode,
  );

  // Add contextual links for this node, except when the node is already being
  // displayed on its own page. Modules may alter this behavior (for example,
  // to restrict contextual links to certain view modes) by implementing
  // hook_node_view_alter().
  if (!empty($node->nid) && !($view_mode == 'full' && node_is_page($node))) {
    $build['#contextual_links']['node'] = array('node', array($node->nid));
  }

  // Allow modules to modify the structured node.
  $type = 'node';
  drupal_alter(array('node_view', 'entity_view'), $build, $type);

  return $build;
}

// 这里的view_mode、teaser、title、page等将可以在模板文件中被直接打印

function template_preprocess_node(&$variables) {
  $variables['view_mode'] = $variables['elements']['#view_mode'];
  // Provide a distinct $teaser boolean.
  $variables['teaser'] = $variables['view_mode'] == 'teaser';
  $variables['node'] = $variables['elements']['#node'];
  $node = $variables['node'];

  $variables['date']      = format_date($node->created);
  $uri = entity_uri('node', $node);
  $variables['node_url']  = url($uri['path'], $uri['options']);
  $variables['title']     = check_plain($node->title);
  $variables['page']      = $variables['view_mode'] == 'full' && node_is_page($node);

  // Flatten the node object's member fields.
  $variables = array_merge((array) $node, $variables);

  // Helpful $content variable for templates.
  $variables += array('content' => array());
  foreach (element_children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  } 
  ...
}

node.tpl.php模板文件

<div id="node-<?php print $node->nid; ?>" class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>

  <?php print $user_picture; ?>

  <?php print render($title_prefix); ?>
  <?php if (!$page): ?>
    <h2<?php print $title_attributes; ?>><a href="<?php print $node_url; ?>"><?php print $title; ?></a></h2>
  <?php endif; ?>
  <?php print render($title_suffix); ?>

  <?php if ($display_submitted): ?>
    <div class="submitted">
      <?php print $submitted; ?>
    </div>
  <?php endif; ?>

  <div class="content"<?php print $content_attributes; ?>>
    <?php
      // We hide the comments and links now so that we can render them later.
      hide($content['comments']);
      hide($content['links']);
      print render($content);
    ?>
  </div>

  <?php print render($content['links']); ?>

  <?php print render($content['comments']); ?>

</div>

Drupal Entity API

D8 Entity可以通过Entity Validation API对多种方式(Form、REST)保存的Entity进行验证。

Entity是含有预定义方法的类

方法类型 方法示例
一般方法 $entity->id()
特定entity的特定方法 $node->getTitle()

注: 通常这些方法定义在interfaces中,目前还没有比较完备的D8 Traits文档
Classes, traits, and interfaces

处理器/调用 Handlers

entity处理器实际上是预定义了对entity进行处理的方法的类。

存储处理器 Storage handler - 支持载入、保存和删除entity等操作,包括对entity多次修订版本、多语言翻译版本和配置字段。

另外,除了存储处理器,还有其他的处理器例如许可控制处理器AccessControlHandler、视图处理器Viewing,列表处理器Listings,表单处理器Forms。

Entity的两种类型

  • Configuration Entity (由D8的Configuration系统使用,可以在安装时配置默认选项,注意Configuration Entity以文件的形式存在,而非存储在数据库表中,例如D8很多的模块下都有config目录,而该目录下的yml文件,一般都是Configuration Entity的配置文件。)
  • Content Entity (包含可配置的基础字段,并且可以根据需要添加其他额外字段,支持多版本和多语言。)

Entity类型

规范

模块的Entity的命名规范
例如:Transport模块定义了名称为Car的entity


/* Transport Car * Defines the Car entity class * * @ContentEntityType( * id = "transport_car", * label = @Translation("Transport Car"), * bundle_label = @Translation("Transport"), * handlers = { * "storage" = "Drupal\transport\CarStorage", * "storage_schema" = "Drupal\transport\CarStorageSchema", * "view_builder" = "Drupal\transport\CarViewBuilder", * "access" = "Drupal\transport\CarAccessControlHandler", * "views_data" = "Drupal\transport\CarViewsData", * "form" = { * "default" = "Drupal\transport\CarForm", * "delete" = "Drupal\transport\Form\CarDeleteForm", * }, * "route_provider" = { * "html" = "Drupal\transport\Entity\CarRouteProvider", * }, * "translation" = "Drupal\transport_car\CarTranslationHandler" * }, * base_table = "transport_car_data", * data_table = "transport_car_field_data", * translatable = TRUE, * list_cache_contexts = { "user.transport_car_grants:view" }, * entity_keys = { * "id" = "cid", * "bundle" = "transport type", * "label" = "title", * "langcode" = "langcode", * "uuid" = "uuid", * }, * field_ui_base_route = "entity.transport_car.edit_form", * common_reference_target = TRUE, * permission_granularity = "bundle", * links = { * "canonical" = "/transport/car/{transport_car}", * "delete-form" = "/transport/car/{transport_car}/delete", * "edit-form" = "/transport/car/{transport_car}/edit", * } * ) * * * transport/src/Entity/Car.php * Car Entity类定义文件 * */ namespace Drupal\trasnsport\Entity class Car extend ContentEntityBase { use EntityChangedTrait; public function carFunction1 () { //do something }; } # 位于modules/custom/transport/src/Entity/目录下

Drupal8 / Composer / Drush / Drupal Console 相关

修复features暴力卸载

修复由暴力卸载features模块生成的插件而导致的"The following module is missing from the file system...drupal...bootstrap...250..."错误。
drupal官网也有关于此问题的链接

  1. drush pmu module-name
  2. composer remove drupal/module-name
  3. drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='module-name';"
  4. drush cr
  5. 刷新 status report 页面,看错误是否还在。
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='b2bcms_catalogue';"
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='b2bcms_manufacturing_process';"
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='b2bcms_products';"
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='b2bcms_quick_details';"
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='b2bcms_standards';"
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='config_update';"
### 命令格式
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='module_name';"

2017年07月25日20:43:25更新

实际上仅仅从上述数据库记录中依旧不能完全删除暴力卸载的残留。

Features包导入的配置项以自定义模块的方式安装到drupal中,除了在上述数据库中记录外,还会在core.extension配置项中留下记录。core.extension目测是以数据库记录的形式保存在数据库中,而不是yml明文配置文件的形式存储,对应网站后台的操作就是admin/modules, 这里记录用户安装卸载的模块的情况,所以需要从数据库中删除记录,而我找了一圈也没有找到具体这些模块记录是存储在哪一个表中,而系统的很多配置项大多是以序列化之后的形式存储在key_value表中,直接在数据库中修改序列化之后的数据是不现实的,而且这里也没有找到core.extension配置项。

除了直接动数据库这样的危险操作,还可以直接使用drush内置的config选项来灵活的修改系统选项。

#查看有哪些config相关的命令可以用
drush | grep config
 core-config (conf,    Edit drushrc, site alias, and Drupal settings.php files.
 config)
 core-quick-drupal     Download, install, serve and login to Drupal with minimal configuration and dependencies.
 site-install (si)     Install Drupal along with modules/themes/configuration using the specified install profile.
Config commands: (config)
 config-delete (cdel)  Delete a configuration object.
 config-edit (cedit)   Open a config file in a text editor. Edits are imported into active configuration after closing editor.
 config-export (cex)   Export configuration to a directory.
 config-get (cget)     Display a config value, or a whole configuration object.
 config-import (cim)   Import config from a config directory.
 config-list (cli)     List config names by prefix.
 config-pull (cpull)   Export and transfer config from one environment to another.
 config-set (cset)     Set config value directly. Does not perform a config import.

 #查看模块配置信息
 drush config-get core.extension

 #修改模块配置信息,删除无效模块
 drush config-edit core.extension
 #这里drush会呼叫vi编辑器,删除干扰的无效模块记录就可以了
 #保存修改后的记录,终端会提示core.extension配置项已更新
 #询问我们是否导入新的配置项,选择Y导入即可
            core.extension  update
Import the listed configuration changes? (y/n): y
#drush config-import可能需要config_update模块支持,如果提示你安装,安装该模块就可以了。

Drupal console generate:module命令不存在的问题

  1. MAC MAMP环境
  2. Drupal基于Github Composer-Drupal环境安装
  3. Composer基于Brew安装
  4. Drupal Console基于composer安装

使用Composer安装Drupal Console的办法

# 添加需要模块
composer require drupal/console:~1.0 --prefer-dist --optimize-autoloader
# 安装模块依赖组件
composer update drupal/console --with-dependencies

这里面有个问题,composer是基于项目的包管理工具,安装的包都是针对当前项目的,所以这里安装的drupalconsole(以下简称drupal)并没有默认安装在系统的/bin,/usr/bin/,/usr/share/bin等系统默认可以查找二进制命令的目录下,所以当你尝试运行drupal的时候,很可能会shell会报错,找不到drupal命令。

即使是使用 composer global require drupal/console:@stable 全局安装并且export PATH="$HOME/.composer/vendor/bin:$PATH"添加路径支持,那么在drupal安装的根目录(对于composer-drupal安装方式来说,就是项目根目录下的web目录)下执行 drupal genernate:module 你还是会收到shell提示如下错误:

[Symfony\Component\Console\Exception\CommandNotFoundException]
Command "generate:module" is not defined.

原因是因为drupal[drupal-console]是PHP脚本文件,运行时会有很多依赖,而全局化安装,一些具体的特殊命令例如generate等,依赖没有安装到位,所以就造成了这样的问题。

解决办法

  1. 针对 composer 安装 drupalconsole的方式 (注意,这里不要使用composer全局安装drupalconsole,安装了也不能解决你的问题)。
    cat << EOF >> ~/.bash_profile
    # 利用composer项目文件结构的特点,添加alias,使用相对路径来执行 drupalconsole 命令
    alias drupal='../vendor/drupal/console/bin/drupal'
    EOF
    source ~/.bash_profile
    

    这里需要注意的是,drupalconsole命令需要在web目录内执行,否则由于路径的问题,这会提示你找不到命令。

  2. 如果你使用drupalconsole官网curl命令下载安装的方式,很可能就不会出现这种问题。这种办法我目前还没有验证过。

Git 配置全局代理

大多数时候国外的Git repository 都会被墙,而gitlab虽然没有被抢,但是上传和下载速度感人,很有必要配置代理项。

# 设置git代理
git config --global https.proxy http://127.0.0.1:1080
git config --global https.proxy https://127.0.0.1:1080
# 取消设置代理
git config --global --unset http.proxy
git config --global --unset https.proxy

以上的方式,其实我并不推荐,git其实可以按照项目来设置代理项
只需要在项目的.git/config配置文件中添加代理设置就可以了,没有必要完全设置为全局代理

cat << EOF >> .git/config
[http]
    proxy=http://127.0.0.1:1080
[https]
    proxy=https://127.0.0.1:1080
EOF

这样只会针对当前项目配置代理。

Git配置忽略OSX下的.DS_Store文件

#添加全局gitignore设置项
echo .DS_Store >> ~/.gitignore_global
#配置使设置生效
git config --global core.excludesfile ~/.gitignore_global
#如果你的commit中已经存在.DS_Store文件,使用如下命令从git记录中删除
cd your-project-root-directory
find . -name .DS_Store -print0 | xargs -0 git rm --ignore-unmatch
#如果你仅仅相对当前的项目忽略.DS_Store文件
cd your-project-root-directory
echo ".DS_Store" >> ./.gitignore
echo "**/.DS_Store" >> ./.gitignore

API提供的特性

  • 开发者只需要使用API函数提供表结构,或是对表进行增删改操作。该特性使得模块的安装和更新简单化。

注:hook_schema_alter()修改已存在的表。

  • API使得数据库的选择不再是个问题,数据库底层的驱动API都会自动帮开发者完成。
  • 数据库递增更新, 简单一贯的CRUD API,可以方便的和CCK,VIEWS集成

 

数据类型

下表列出了字段类型和存储空间,以及每种组合可选用的底层数据库数据类型。Drupal7核心默认支持MySQL, PostgreSQL和SQLite

type size MySQL type & size/range PostgreSQL type & size/range SQLite type
serial tiny tinyint, 1 B serial, 4 B integer
serial small smallint, 2 B serial, 4 B integer
serial medium mediumint, 3 B serial, 4 B integer
serial big bigint, 8 B bigserial, 8 B integer
serial normal int, 4 B serial, 4 B integer
int tiny tinyint, 1 B smallint, 2 B integer
int small smallint, 2 B smallint, 2 B integer
int medium mediumint, 3 B int, 4 B integer
int big bigint, 8 B bigint, 8 B integer
int normal int, 4 B int, 4 B integer
float tiny float, 4 B real, 6 digits float
float small float, 4 B real, 6 digits float
float medium float, 4 B real, 6 digits float
float big double, 8 B double precision, 15 digits float
float normal float, 4 B real, 6 digits float
numeric normal numeric, 65 digits numeric, 1000 digits numeric
varchar normal varchar, 255 B (D6) or 64 KB (D7 and later)1 varchar, 1 GB varchar
char normal char, 255 B character, 1 GB (UNSUPPORTED)
text tiny tinytext, 256 B text, unlimited text
text small tinytext, 256 B text, unlimited text
text medium mediumtext, 16 MB text, unlimited text
text big longtext, 4 GB text, unlimited text
text normal text, 16 KB text, unlimited text
blob2 big longblob, 4 GB bytea, 4 GB blob
blob2 normal blob, 16 KB bytea, 4 GB blob
datetime3 normal3 datetime, years 1001 CE to 9999 CE3 timestamp, years 4713 BCE to 5874897 CE3 (UNSUPPORTED)3

注:D7的数据库API中已移去对【时间日期】数据类型的支持。如果需要存储该数据类型,需调用函数mysql_type 或 pgsql_type。 

Schema结构

drupal 文档页链接

Schema是由一个或多个表中,对应表结构的键和索引构成的结构化数组定义的。具体说来,schema由每个模块目录中,modulename.install文件中implement函数hook_schema()定义。hook_schema()会返回j结构化数组,模块定义的每一个表都会包含"tablename" => array(表的定义)这样的映射信息。数组中的键中对应的值在表创建时会被预处理。

  • 'description': 描述表的用途。引用其他表,需要将引用的表名放在{ }中。

举个栗子,node_revisions表中描述字段包含以下信息"Stores per-revision title and body data for each {node}."

  • 'fields':  字段数组用来映射'fieldnam' => array(field definition), 【field definit】数组, 描述表中的数据列,每一个列也是一个数组,相关的规格参数定义如下。

     

    • 'description': 定义字段与用途。引用其他表,需要将引用的表名放在{ }中。
    • 'type': SQL字段类型,常见的字段类型为'varchar', 'char', 'int', 'serial', 'float', 'numeric', 'text', 'blob' or 'datetime'. (如上表所示)
    • 'mysql_type', 'pgsql_type', 'sqlite_type'等, 特定数据库的数据类型,比如'mysql_type' => 'TIME' is 'pgsql_type' => 'time without time zone'.
    • 'size': 数据存储空间的大小,含以下值'tiny', 'small', 'medium', 'normal', 'big'. 可参照上表。
    • 'not null':  如为真,则表的该列不为空,默认值为false,即允许空。
    • 'default': 该字段的默认值。这里需要注意值的类型,例如你指定一个整数字段的默认值为'0',则会报错,因为'0'是字符串,不是整型。注意,二进制对象和文本对象是无法设定默认值的。
    • 'length':  char', 'varchar'或 'text' 类型数据字段的最大长度,其他类型忽略。注意varchar类型,该键的值必须定义,不可为空。
    • 'unsigned': 布尔值,决定当数据类型为数值时,是否显示数值符号。默认值为FALSE,其他字段可忽略。
    • 'precision', 'scale':  精度与小数位数。两个值都是必需的,其他字段忽略。
    • 'serialize': 布尔值,决定该字段是否会被序列化。
    • 'binary':布尔值,决定MYSQL是否强制使用大小写敏感的字符集来存储'char', 'varchar' 或 'text' 等字符型字段信息。MYSQL数据库特有,其他数据库已默认开启大小写敏感。

注:除'type'之外的其他参数都为可选,当然数值列需指定'precision' 和'scale'和'varchar' 列需指定 'length'这两种情况除外。

  • 'primary key': 数组,用来标识一个或多个键列来指定主键。

     

    • 键(key)列的值要么是命名字段的字符串类型,要么是一个包含命名字段的字符串和整数前缀的数组,其中整数前缀定义了字符串中会有多少字节或是字符会作为前缀添加到key中。如果数据库引擎主键不支持前缀设定,则该设定会被忽略。
    • 所有在主键中列出的字段都必须含有'not null' => TRUE的设定。
  • 'unique keys': 唯一键
  • 'indexes':  索引
  • 'foreign keys': 外键
  • 'mysql_suffix': 仅对drupal6适
  • 注:在Drupal 7中使用 'mysql_engine', 'mysql_character_set'和'collation'
  •  例如: 'mysql_suffix' => " DEFAULT CHARACTER SET UTF8 ENGINE = INNODB AUTO_INCREMENT=3844 "

这是基础(写给自己看的)

删除数组中含有指定值的数组元素

$tmp='c';
$arr=array(1=>'a','2'=>'b',3=>'c');
array_diff_key($arr,array_flip(array_keys($arr,$tmp)));

这个在修改drupal renderable arrry控制arrrtibute的class数组是非常有用。

模块前提知识

drupal基于.info和.yml文件来识别模块

 

调试:

设置drupal输出错误日志,配置settings.php文件

error_reporting(E_ALL);
ini_set('display_errors', TRUE);
ini_set('display_startup_errors', TRUE);

 

tail -f /var/log/apache2/error.log //有些时候日志的目录根据服务器的设置可能有所不同

Drupal模块命名规范

  1. 必须以字幕开始
  2. 只包含小写字母和下划线
  3. 模块名必须唯一,不可以其他模块,主题,安装预设profile相冲突
  4. 模块名不可以是以下任何被保留的条目:src, libvendor, assets, css, files, images, js, misc, templates, includes, fixtures, Drupal.

注意,模块名称中一定不要使用大写字幕,那样的话drupal将无法识别模块的hook调用。

新建模块文件夹

在drupal8以前的版本,模块默认放置在sites/all/modules文件夹下。模块名称和放置模块的目录并不一定名称相同,但是模块的代码引用和模块文件名filename.module需要使用模块机器名。

drupal8将所有的目录放置在drupal/modules下。

为方便管理模块,建议将社区贡献模块放置到drupal/modules/contribute下。

将自定义开发模块放置到drupal/modules/customer下。

一个完整的模块包含以下文件

module
--module.info.yml
--module.routing.yml
--module.module
--/src/files.php

 

 

Drush使用project manager来管理core和module

所以一般drush的核心和模块的操作命令一般都会带上pm的前缀。

查看更新状态

drush pm-updatestatus
//命令缩写
dursh ups
  • 更新Core和module文件
drush pm-update
//可以使用缩写
drush up

仅更新Core

drush pm-updatecore
//可以使用缩写
drush upc

 

  • 打印所有已安装的模块列表 (可以配合Pipe Grep命令过滤出自己想要的结果)
drush pm-list

安装命令

drush en module_name

直接en就可以,drush会自动下载模块相关文件,如果想省事,可以加上 -y 参数,这样drush就不会询问了。

  • drush卸载删除模块
drush dis module_name
//剩下的事情就要交给linux rm命令了
cd site_directory/sites/all/modules/
rm -r module_directory

drush 更新图片缓存

drush image-flush

 

这种情况通常出现在同步本地开发服务器和生产服务器

服务器会出现错误提示: 不支持的内容encode方式

 drush vset cache 0
 drush vset preprocess_css 0
 drush vset preprocess_js 0
 drush vset page_cache_maximum_age 0
 drush vset views_skip_cache TRUE