Drupal10-开发秘籍-全-
Drupal10 开发秘籍(全)
原文:
zh.annas-archive.org/md5/3a570588a9c8af18d564c63b70a284e2
译者:飞龙
前言
Drupal 是一个流行的内容管理系统,用于为小型企业、电子商务、企业系统等构建网站。由超过 4,500 名贡献者创建,Drupal 10 为 Drupal 带来了许多新功能。无论您是 Drupal 的新手还是经验丰富的 Drupalista,Drupal 10 开发食谱都包含了深入了解 Drupal 10 所提供内容的配方。我们首先帮助您创建和维护您的 Drupal 站点。接下来,熟悉配置内容结构和编辑内容。您还可以了解本版的全部新更新。这包括创建自定义页面、访问和操作实体、使用 Drupal 运行和创建测试以及将外部数据迁移到 Drupal。随着您的进步,您将学习如何使用现成的模块自定义 Drupal 的功能、提供扩展以及编写自定义代码来扩展 Drupal。您还将学习如何设计一个具有强大内容管理和编辑工作流程的数字体验平台。
您可以通过编写自定义模块来适应 Drupal 的需求,创建自定义插件、实体类型和页面。您还可以使用现代前端开发构建工具为您的 Drupal 站点供电。您将能够创建和管理 Drupal 站点的代码库。您可以通过将 Drupal 作为您的 API 平台,并为您的消费者提供服务来利用 Drupal。
本书涵盖内容
第一章,使用 Drupal 启动运行,介绍了如何创建新的 Drupal 站点以及运行 Drupal 的系统要求,然后使用基于 Docker 的现代本地开发工具在本地运行 Drupal 站点。
第二章,内容构建体验,深入探讨了如何设置您的内容编辑体验并添加编辑审查工作流程。
第三章,通过视图显示内容,介绍了如何使用视图模块创建一个列出博客的页面以及一个显示最近五篇博客的伴随块,视图模块是一个可视化的查询构建器。
第四章,使用自定义代码扩展 Drupal,探讨了如何创建一个可以安装到您的 Drupal 站点的自定义模块。
第五章,创建自定义页面,演示了如何使用控制器和路由创建自定义页面。创建自定义页面可以使您将 Drupal 扩展到不仅仅是内容页面的范畴。
第六章,访问和操作实体,涵盖了在 Drupal 中处理实体时的创建、读取、更新和删除(CRUD)操作。我们将创建一系列路由来创建、读取、更新和删除文章节点。
第七章,使用表单 API 创建表单,介绍了 Form API 的使用,它用于在 Drupal 中创建表单而无需编写任何 HTML。
第八章,使用插件即插即用,介绍了实现块插件。我们将使用插件 API 提供自定义字段类型,以及字段的小部件和格式化程序。最后一个菜谱将向您展示如何创建和使用自定义插件类型。
第九章,创建自定义实体类型,解释了如何为自定义数据模型创建自定义实体类型。
第十章,主题和前端开发,介绍了如何创建主题,使用 Twig 模板系统,并利用 Drupal 的响应式设计功能。
第十一章,多语言和国际化,展示了 Drupal 10 的多语言和国际化功能,
第十二章,使用 Drupal 构建 API,介绍了如何在 Drupal 中使用 JSON:API 创建 RESTful API,展示了如何通过 HTTP 请求读取和操作数据。
第十三章,使用 Drupal 运行和编写测试,深入探讨了使用 PHPUnit 为您的自定义模块代码运行和编写自动化测试。
第十四章,将外部数据迁移到 Drupal,解释了如何从旧版本的 Drupal 迁移到 Drupal 10,并介绍了使用迁移模块从 CSV 文件和 HTTP API 迁移内容和数据。
您需要为此书准备的
为了使用 Drupal 10 并运行本书中的代码示例,以下软件将是必需的:
软件 | 操作系统要求 |
---|---|
在 Docker 中运行的本地 Web 服务器(DDEV、Lando、Docksal 或 Docker4Drupal)或类似替代方案 MAMP | Windows、macOS 或 Linux |
用于代码编辑的 PhpStorm 或 VS Code | Windows、macOS 或 Linux |
终端、iTerm 或类似命令行工具 | Windows、macOS 或 Linux |
NodeJS、npm 和 Laravel Mix | Windows、macOS 或 Linux |
注意,有几个免费的开源工具可以用来在本地运行 Drupal – DDEV、Lando、Docksal 和 Docker4Drupal 是前四个社区解决方案。建议您使用您最熟悉或已经建立的解决方案。本书的第一章涵盖了使用 DDEV 运行 Drupal。不幸的是,我们无法深入探讨所有可能的解决方案。
在使用本书时,保留您所使用的任何解决方案的文档是个好主意,因为示例可能是一般化的,尤其是在运行命令时。
本书面向的对象
本书面向那些一直在使用 Drupal 的人,例如网站构建者、后端和前端开发者,以及那些渴望了解当他们开始使用 Drupal 10 时将面临什么的人。
部分
在本书中,您会发现一些频繁出现的标题(准备就绪、如何做…、它是如何工作的…、还有更多… 和 另请参阅)。
为了清楚地说明如何完成食谱,我们使用以下这些部分。
准备工作
本节告诉您在食谱中可以期待什么,并描述了如何设置任何必需的软件或初步设置。
如何做…
本节包含遵循食谱所需的步骤。
它是如何工作的…
本节通常包含对上一节发生情况的详细解释。
更多内容…
本节包含有关食谱的附加信息,以便您对食谱有更深入的了解。
参见
本节提供了指向其他有用信息的链接,这些信息对食谱很有帮助。
下载彩色图像
我们还提供了一份包含本书中使用的截图和图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/N7EpQ
。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件github.com/PacktPublishing/Drupal-10-Development-Cookbook
。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包可供使用,请访问github.com/PacktPublishing/
。查看它们吧!
使用的约定
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“在新建的目录中创建一个名为Announcement.php
的文件,以便我们可以为我们的实体类型定义Announcement
类。”
代码块设置如下:
{% for tag in node.getTags %}
<div>Tag: {{ tag.label }}</div>
{% endfor %}
任何命令行输入或输出都如下所示:
mkdir -p src/Entity
新术语和重要词汇以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中显示如下:“您将在权限表上看到生成的权限。”
小贴士或重要提示
它看起来像这样。
联系我们
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。
盗版:如果你在互联网上以任何形式发现我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过版权@packtpub.com 与我们联系,并提供材料的链接。
如果你有兴趣成为作者:如果你在某个领域有专业知识,并且你感兴趣的是撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享你的想法
一旦你阅读了Drupal 10 开发食谱,我们很乐意听听你的想法!请点击此处直接进入此书的亚马逊评论页面并分享你的反馈。
你的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
下载这本书的免费 PDF 副本
感谢你购买这本书!
你喜欢在旅途中阅读,但无法携带你的印刷书籍到处走吗? 你的电子书购买是否与你的选择设备不兼容?
别担心,现在每本 Packt 书籍你都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何地方、任何设备上阅读。直接从你最喜欢的技术书籍中搜索、复制和粘贴代码到你的应用程序中。
优惠远不止于此,你还可以获得独家折扣、时事通讯和每天收件箱中的精彩免费内容
按照以下简单步骤获取优惠:
- 扫描下面的二维码或访问以下链接
https://packt.link/free-ebook/9781803234960
-
提交你的购买证明
-
就这些!我们将直接将你的免费 PDF 和其他优惠发送到你的电子邮件
第一章:快速启动 Drupal
在本章中,我们将介绍如何创建新的 Drupal 网站以及运行 Drupal 的系统要求。我们还将介绍使用基于 Docker 的现代本地开发工具运行 Drupal 网站。
然后,我们将介绍如何在网站运行时添加和管理模块和主题扩展,以及如何在 Git 版本控制中管理您的 Drupal 代码库,最后部署该 Drupal 网站。到本章结束时,您将了解如何创建 Drupal 网站,在您的机器上本地运行它,并向该 Drupal 网站添加模块和主题。您还将了解如何管理版本控制中的 Drupal 代码库并部署 Drupal 网站。本章将为本书其余章节的工作奠定基础,并让您自己尝试使用 Drupal 进行实验。
在本章中,我们将介绍以下主题:
-
为新网站创建新的 Drupal 代码库
-
在本地运行 Drupal 网站
-
使用 Drush 管理 Drupal
-
使用 Composer 添加和管理模块和主题
-
使用版本控制管理您的 Drupal 代码库
-
成功部署您的 Drupal 网站
技术要求
本章将帮助您开始在您的计算机上本地使用 Drupal。
您将需要以下内容:
-
PHP 8.1 或更高版本
-
在您的机器上安装了 Docker 和 DDEV
-
Composer,PHP 包管理工具
-
Docker,用于运行本地环境
-
一个编辑器,例如 Visual Studio Code 或 PhpStorm
重要提示
前往 Composer 文档学习如何在您的系统上全局安装 Composer。
Linux/Unix/macOS: getcomposer.org/doc/00-intro.md#installation-linux-unix-osx
Windows: getcomposer.org/doc/00-intro.md#installation-windows
创建 Drupal 网站
使用Composer项目模板创建新的 Drupal 网站,并运行快速启动命令以创建预览网站。本节中提供的说明基于www.drupal.org/download
上的推荐安装说明。
准备工作
快速启动命令使用 SQLite 作为 Drupal 的数据库。在大多数操作系统的 PHP 安装中,这通常是现成的,已经安装。如果 SQLite 不可用,安装脚本将报错。如果发生这种情况,没关系!在本地运行您的 Drupal 网站中,我们将使用带有数据库的本地开发环境运行 Drupal。
Drupal 还需要几个 PHP 扩展。这些通常在大多数 PHP 发行版中可用。所需 PHP 扩展的最新列表可以在网上找到:www.drupal.org/docs/system-requirements/php-requirements#extensions
。
如何操作...
-
打开终端,导航到您想要创建新 Drupal 网站的位置。
-
使用
create-project
命令在mysite
目录中创建一个新的 Drupal 站点:composer create-project drupal/recommended-project mysite
重要提示
如果您的 PHP 安装缺少 Drupal 所需的任何扩展,此命令将失败。Composer
命令输出将解释缺少哪些扩展。
-
切换到新创建的
mysite
目录,其中包含 Drupal 站点的代码库:cd mysite
-
运行 快速开始 命令,使用 Umami Maganize 演示安装一个示例 Drupal 站点:
php web/core/scripts/drupal quick-start demo_umami
-
一旦安装脚本完成,您的浏览器将打开您的 Drupal 站点。如果您的浏览器没有打开,终端将打印一个登录链接:
图 1.1 – 运行快速开始命令的输出
-
按 Ctrl + C 退出 快速开始 服务器。
-
接下来,在您的编辑器中打开
web/sites/default/settings.php
。我们将修改此文件以修改配置同步目录并添加本地设置覆盖。 -
将配置同步目录(Drupal 站点配置可以导出的位置)设置为
../config
。路径相对于 web 目录。
快速开始命令的安装程序将生成一个默认值。在文件底部,找到类似以下的一行:
$settings['config_sync_directory'] =
'sites/default/files/config_....';
用以下行替换它:
$settings['config_sync_directory'] = '../config';
这确保了您导出的 Drupal 配置不在 web
文档根文件夹中。
-
在文件底部,添加以下代码片段以允许创建
settings.local.php
文件来覆盖settings.php
文件在不同环境中的设置:/**
* Include local environment overrides.
*/
if (file_exists($app_root . '/' . $site_path .
'/settings.local.php')) {
include $app_root . '/' . $site_path .
'/settings.local.php';
}
这检查是否也有一个 settings.local.php
文件与您的 settings.php
文件一起存在,如果存在则包含它。通过使用本地设置文件,您可以在 Drupal 设置文件中添加合理的默认值,但具有每个环境的配置和数据库连接设置。
- 通过这样,你已经设置了一个 Drupal 代码库和一个示例开发站点,我们将在这个章节和本书的其余部分中使用它。
它是如何工作的…
Composer 允许开发者从现有包创建新项目。Drupal 社区提供了 drupal/recommended-project
包作为 Drupal 站点的脚手架。当运行 create-project
时,它会复制 drupal/recommended-project
包并为我们安装依赖项,使我们的包副本可用于我们自己的。
Drupal 有一个配置管理系统,允许导出和导入配置。这允许你在本地进行配置更改并将它们推送到生产环境,而无需在您的实时站点上进行更改。这就是 config_sync_directory
的用途;它将在 使用版本控制管理您的 Drupal 代码 和 成功部署您的 Drupal 站点 菜单中更详细地介绍。
快速开始命令是在 Drupal 8.6 中添加的,用于快速为新开发者创建 Drupal 的开发环境。它以编程方式安装 Drupal,启动 PHP 内置的 web 服务器,并使用 SQLite 进行数据库访问。
快速开始命令也是可重入的。如果您重新运行该命令,它将使用现有的 SQLite 数据库文件。这允许您快速运行开发环境。我们将在“在本地运行您的 Drupal 网站”菜谱中介绍如何运行完整的环境。
更多内容...
虽然 drupal/recommended-project
是创建新 Drupal 网站的主要方式,但还有其他选项。其他项目模板包括社区替代方案和各种发行版模板,以及与 Drupal 合作的各种机构已经提供了他们自己的(内部和公开的。)
社区 Composer 项目模板
在 drupal/recommended project
之前,Drupal 没有一个标准的 Composer 项目模板。社区推动了这一倡议,并仍然维护了一个更具观点的 Drupal 项目模板。该模板可以在 github.com/drupal-composer/drupal-project
找到。
该项目增加了以下增强功能:
-
对环境文件和环境变量的支持
-
对依赖项的修补支持
-
Drush(虽然是一个事实上的工具,但不是由推荐项目模板提供的)
Drupal 发行版模板
Drupal 有一个概念叫做安装配置文件,通常被称为发行版。它们被称为发行版,因为它们提供了一个具有观点的 Drupal 构建。有几个发行版项目模板可供选择:
-
Open Social 是一个基于 Drupal 的社区参与软件:
github.com/goalgorilla/social_template/
-
Commerce Kickstart,一个基于 Drupal 和 Commerce 的预配置电子商务商店:
github.com/centarro/commerce-kickstart-project
-
Contenta CMS,一个基于 Drupal 的 API 首先解耦 CMS:
github.com/contentacms/contenta_jsonapi_project
参见
- Composer 文档:
getcomposer.org/doc/
在本地运行您的 Drupal 网站
要与 Drupal 一起工作,您需要一个本地开发环境。本地开发环境应该能够帮助您模拟您的生产环境,例如访问特定的 PHP 版本、数据库版本、Redis 以及其他服务。
为了实现这一点,我们将使用 Docker。我们不会直接与 Docker 交互,而是通过一个名为 DDEV 的工具。DDEV 是 Docker 之上的一个抽象,它为在本地运行 Drupal 网站提供了合理的默认设置,同时提供了扩展和添加额外服务的灵活性。
DDEV 与 Laravel 的 Sail 类似,但支持来自 Drupal 社区的各种 PHP 项目。
准备工作
您需要安装 Docker:
-
macOS 和 Windows 需要使用 Docker Desktop,因为容器在这两种操作系统上都不是本地运行的:
www.docker.com/products/docker-desktop
-
Linux 本地运行 Docker;最好查看 DDEV 的精选安装步骤:
ddev.readthedocs.io/en/stable/users/docker_installation/#linux-installation-docker-and-docker-compose
接下来,您可以安装 DDEV:
-
macOS 和 Linux:通过 Homebrew 软件包管理器进行安装:
ddev.readthedocs.io/en/stable/#homebrew-macoslinux
-
Windows:Windows 需要 WSL2,并提供了详细的说明:
ddev.readthedocs.io/en/stable/#installation-or-upgrade-windows-wsl2
DDEV 安装指南详细介绍了如何在每种操作系统上安装 Docker 和 DDEV:ddev.readthedocs.io/en/stable/#installation
。
如何操作…
-
确保 Docker 正在运行。
-
打开终端并导航到您的 Drupal 代码库。
-
运行
config
命令开始设置:ddev config
-
第一个提示是选择项目名称。使用默认名称,该名称是从当前目录名称推断出来的,或者提供自定义名称。按 Enter 继续操作。
-
选择 Drupal 网站的文档根目录。DDEV 的默认值会自动检测。按 Enter 使用默认值
web
。 -
下一个提示是项目类型。DDEV 提供平台检测,默认为
drupal10
。按 Enter 选择它。 -
DDEV 设置现在已完成!我们可以通过使用 启动 命令来启动本地环境:
ddev start
-
使用
launch
命令访问 Drupal 网站,这将带我们到交互式网站安装程序:ddev launch
-
通过在第一个两个表单上按 保存并继续 完成安装 Drupal。
-
安装完成后,填写网站的配置表单并按 保存。
-
您的 Drupal 网站现在正在运行!
工作原理…
DDEV 允许您构建可定制的本地开发环境,无需成为 Docker 专家,但仍可利用 Docker 的特定功能。DDEV 位于 Docker Compose 工具之上,用于运行多容器 Docker 应用程序。它为您生成 Compose 文件并运行 docker-compose
命令。这使得它非常适合团队共享,因为它消除了 Docker 的复杂性。
您的 DDEV 网站配置位于 Drupal 目录的 .ddev
目录中的 config.yaml
文件中。在此配置文件中可以自定义 PHP 版本、数据库类型、版本等。
当运行 Composer 或其他命令时,您需要通过 SSH 登录到 Web 应用程序容器。这可以通过 ssh
命令实现:
ddev ssh
通过 SSH 登录到 Web 应用程序,您可以直接在本地环境中运行命令。
更多内容...
使用 DDEV 和其他类似 DDEV 的工具,可以更改多个选项。
更改网站的 PHP 版本
要修改项目中使用的 PHP 版本,编辑 .ddev/config.yaml
并更改 php_version
属性。Drupal 10 项目类型默认为 PHP 8.1,这是 Drupal 所需的最小 PHP 版本。但您可以选择使用更新的 PHP 版本,因为它们被 Drupal 发布并支持。
更改数据库版本或类型
DDEV 支持 MySQL 和 MariaDB 数据库,但不支持 Postgres(尽管它被 Drupal 支持)。默认情况下,DDEV 使用 MariaDB 10.3:
-
要更改您的 MariaDB 版本,编辑
.ddev/config.yaml
并将mariadb_version
属性更改为所需的版本 -
要使用 MySQL 而不是 MariaDB,编辑
.ddev/config.yaml
并将mariadb_version
属性替换为mysql_version: "8.0"
一旦数据库中有数据,您就不能更改数据库类型或降级数据库版本。您必须首先使用 ddev delete
删除项目的数据库。
查看在线文档以了解每种数据库类型支持哪些版本:ddev.readthedocs.io/en/stable/users/extend/database_types/
添加自定义服务
DDEV 允许您通过在 .ddev
目录中编写 Docker Compose 文件来添加自定义服务。DDEV 会聚合 .ddev
目录中所有遵循 docker-compose.*.yml
命名约定的 Docker Compose 文件,其中 *
是一个唯一的单词。
例如,要添加 Redis,我们会在 .ddev
目录中创建一个名为 docker-compose.redis.yaml
的文件。它将包含以下 Docker Compose 清单,该清单定义了 Redis 服务并将其链接到 Web 应用程序服务:
version: "3.6"
services:
redis:
container_name: ddev-${DDEV_SITENAME}-redis
image: redis:6
ports:
- 6379
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: $DDEV_APPROOT
web:
links:
- redis:redis
有关附加服务的文档可以在 ddev.readthedocs.io/en/stable/users/extend/additional-services/
找到。
使用 DDEV 运行 Composer
DDEV 允许您在 Web 应用程序容器内运行 Composer。如果您在主机机器上使用不同的 PHP 版本,或者项目在 Web 应用程序容器中需要缺少的扩展,这将非常有用。
要运行 Composer 命令,请使用 ddev composer
。例如,以下命令将向 Drupal 网站添加一个新模块:
ddev composer require drupal/token
您可以提供任何 Composer 命令和参数。
参见
-
出色的 DDEV:DDEV 的博客、教程、技巧和窍门的集合:
github.com/drud/awesome-ddev
-
DDEV 配置:一组用于自定义 DDEV 网站的贡献食谱和代码片段:
github.com/drud/ddev-contrib
-
官方本地开发 指南:
www.drupal.org/docs/official_docs/en/_local_development_guide.html
使用 Drush 命令行工具
Drush是一个与 Drupal 交互的命令行工具,用于执行安装 Drupal 或执行各种维护任务等操作。在本食谱中,我们将向我们的 Drupal 网站添加 Drush,并使用它来安装 Drupal 网站。
警告
此示例使用会导致现有已安装网站数据丢失的命令。
准备工作
当使用 DDEV 时,您必须使用ddev ssh
命令 SSH 到 Web 应用程序容器中,以与 Drush 交互。
如何操作...
-
打开终端并导航到您的 Drupal 代码库。
-
添加
require
命令:composer require drush/drush
-
执行
site:install
命令以安装 Drupal:php vendor/bin/drush site:install --account-pass=admin
-
安装完成后,使用
user:login
命令生成一个一次性登录链接以访问 Drupal 网站。如果您没有点击返回的链接,浏览器应该会自动打开:php vendor/bin/drush user:login
-
安装
pm:enable
命令。按Enter键确认以安装布局构建器及其依赖项:php vendor/bin/drush pm:enable layout_builder
-
使用
cache:rebuild
命令重建 Drupal 的缓存:php vendor/bin/drush cache:rebuild
它是如何工作的...
Drush自Drupal 4.7以来一直是 Drupal 社区的一部分,并已成为每个 Drupal 开发者的必备工具。该项目可以在命令行上引导 Drupal,并执行与网站的交互。
Drush 有大量的命令,可以通过运行list
命令找到:
php vendor/bin/drush list
模块也可以提供自己的 Drush 命令。随着您向 Drupal 网站添加模块,可能会出现新的命令。在使用自定义代码扩展 Drupal的食谱中,我们将介绍如何创建自己的 Drush 命令。
更多...
Drush 有大量的命令和方式,可以使您与 Drupal 网站的工作变得更加容易。
使用 DDEV 运行 Drush
DDEV 提供了一种方法,可以在不使用ssh
命令 SSH 到 Web 应用程序容器的情况下运行 Drush,就像它对 Composer 所做的那样。要使用 DDEV 运行 Drush 命令,请使用ddev drush
。例如,以下命令将生成一个登录您的 Drupal 网站的链接:
ddev drush user:login
您可以提供任何 Drush 命令和参数。
检查是否有挂起的更新
保持已安装的模块和主题更新很重要,尤其是如果存在安全发布。Drush 提供了pm:security
命令,该命令检查已安装模块或主题的任何挂起的更新:
php vendor/bin/drush pm:security
导出或导入 SQL 文件到您的 Drupal 数据库
Drush 提供了一系列 SQL 命令,可以直接与您的数据库交互。
sql:dump
命令允许你创建一个可以导入到另一个数据库的数据库备份。默认情况下,除非传递了--result-file
选项,否则将打印 SQL 语句。文件路径相对于 Drupal 的文档根(web
目录):
php vendor/bin/drush sql:dump --result-file=../db-dump.sql
sql:cli
允许你执行 SQL 语句,包括导入 SQL 备份文件。例如,你可以使用此命令将生产 Drupal 数据库的 SQL 备份导入到本地开发环境中:
php vendor/bin/drush sql:cli < db-dump.sql
参见
- Drush 主页:
www.drush.org/latest/
添加和管理模块和主题扩展
由于其可组合的设计和庞大的贡献模块和主题生态系统,Drupal 极其灵活。Composer 用于通过其require
和update
命令在你的 Drupal 站点上安装和升级扩展。在本食谱中,我们将添加流行的Pathauto模块(www.drupal.org/project/pathauto
)和Barrio主题(www.drupal.org/project/bootstrap_barrio
),一个 Bootstrap 5 主题。
如何做到这一点...
-
打开终端并导航到你的 Drupal 代码库。
-
首先,我们将使用
require
命令添加Pathauto
模块:composer require drupal/pathauto
当 Composer 开始解析Pathauto
包时,它将输出一些数据。你会注意到Pathauto
的模块依赖项也被下载了:Token
和Chaos Tools
。
模块将被安装到web/modules/contrib
目录。
-
接下来,我们将添加 Barrio 主题:
composer require drupal/bootstrap_barrio
这将下载 Barrio 主题并将其放置到web/themes/contrib
目录。
-
现在,我们将使用 Drush 的
pm:enable
命令安装Pathauto
模块:php vendor/bin/drush pm:enable pathauto
此 Drush 命令将提示我们安装Pathauto
以及所需的模块依赖项(Chaos Tools
和Token
),默认选项为yes
。按Enter键继续。
命令将返回以下内容:
[success] Successfully enabled: pathauto, ctools,
token
-
接下来,我们将使用 Drush 的
theme:enable
命令启用 Barrio 主题,并使用config:set
命令将其设置为默认主题:php vendor/bin/drush theme:enable bootstrap_barrio
这仅启用主题;它不会使其成为你的 Drupal 站点使用的默认主题。要将其设置为默认主题,我们需要使用config:set
命令修改站点的配置:
php vendor/bin/drush config:set system.theme default
bootstrap_barrio
这修改了system.theme
配置,使其使用 Barrio 作为default
主题设置。
-
假设我们需要更新
Pathauto
的新版本。我们可以使用update
命令来更新我们的包:composer update drupal/pathauto
这将更新Pathauto
模块到下一个版本。然而,它不会更新其依赖项——也就是说,Chaos Tools
或Token
。使用--with-dependencies
选项确保Pathauto
的直接依赖项也被更新。
-
最后,我们将介绍如何从 Drupal 站点中删除我们添加的模块。首先,在从代码库中删除之前,我们必须从 Drupal 中卸载该模块:
php vendor/bin/drush pm:uninstall pathauto ctools
token
注意 Chaos Tools
和 Token
是如何作为参数添加的?这些模块被添加到我们的代码库中,并作为 Pathauto
的依赖项安装。在移除 Pathauto
之前,我们还需要卸载这些模块。
-
接下来,我们可以使用
remove
命令从代码库中移除Pathauto
:composer remove drupal/pathauto
它是如何工作的……
默认情况下,Composer 只能添加 drupal/
命名空间中可用的包。Drupal 项目模板添加了 Composer 模板的 composer.json
,以便这些包可用:
"repositories": [
{
"type": "composer",
"url": "https://packages.drupal.org/8"
}
],
Drupal 采取的模式是从 Drupal.org
下载项目进入 contrib
目录,而自定义代码则进入自定义目录。
一旦模块和主题被添加到 Drupal 代码库中,它们仍然需要安装。代码的存在并不意味着它们会立即激活。这也意味着从您的代码库中移除模块必须是一个两步过程。如果在卸载之前移除了模块的代码,Drupal 将会抛出错误。
还有更多……
Drupal 是一个在 Composer 之前创建的项目,直到 Drupal 8 生命周期中途才成为 Composer 兼容。还有一些其他事项需要说明。
Composer Library Installer 和 Drupal 扩展
默认情况下,Composer 会将包安装到 vendor
目录。Composer Library Installer 是一个框架可以用来修改特定包类型安装路径的包。
composer/installers
包作为 Drupal 项目模板的一部分被添加,并支持以下包类型及其目标:
-
drupal-core
:用于 Drupal 核心代码库的包类型。它安装在web/core
目录。 -
drupal-module
:用于模块的包类型。它安装在web/modules/contrib
目录。 -
drupal-theme
:用于主题的包类型。它安装在web/themes/contrib
目录。 -
drupal-profile
:用于配置文件的包类型。它安装在web/modules/profiles
目录。 -
drupal-library
:一种特殊的包类型,用于帮助将前端库下载到web/libraries
目录。
这些映射可以在您的 Drupal 项目的 composer.json
文件中找到。
更新 Drupal 核心版本
更新 Drupal 核心代码库比更新 drupal/core-recommended
包要复杂一些。如前所述,Drupal 最近增加了对真正的 Composer 构建支持。还有一个 drupal/core-composer-scaffold
包,它会复制所需的文件。
Composer 允许我们使用通配符来更新依赖项。升级 Drupal 核心最简单的方法是使用通配符和 -with-dependencies
选项:
composer update 'drupal/core-*' –with-dependencies
单引号用于转义 *
字符。此命令将同时更新 drupal/core-recommended
和 drupal/core-composer-scaffold
,以及 Drupal 核心的所有依赖包。
参见
-
使用 Composer 下载和更新文件的官方文档:
www.drupal.org/docs/user_guide/en/install-composer.html
-
从 Drupal.org 下载和安装主题的官方文档:
www.drupal.org/docs/user_guide/en/extend-theme-install.html
使用版本控制管理 Drupal 代码
现在我们有了 Drupal 代码库,是时候将代码放入 Git 的版本控制中了。我们还将导出 Drupal 网站的配置到 YAML 文件中,这样我们就可以在版本控制中跟踪网站配置。
在版本控制中跟踪您的代码可以更容易地与其他开发者协作、跟踪更改以及与持续集成和部署工具集成。即使您是唯一的项目开发者,也强烈推荐这样做。
准备工作
此食谱要求您在您的机器上安装 Git。如果您还没有安装 Git,请参阅以下资源:
-
Git 官方下载页面:
git-scm.com/downloads
-
GitHub 的 安装 Git 指南:
github.com/git-guides/install-git
如何操作...
-
打开终端并导航到您的 Drupal 代码库。
-
使用
init
命令初始化一个新的 Git 仓库:git init
您将看到有关初始化空仓库的消息,类似于以下内容:
Initialized empty Git repository in /Users/
mglaman/Sites/mysite/.git/
-
在我们将文件添加到 Git 以进行版本控制跟踪之前,我们必须创建一个
.gitignore
文件来指定我们不希望跟踪的文件。在项目根目录中创建一个.gitignore
文件。 -
我们希望忽略由 Composer 管理的代码目录、用户管理的文件(如上传文件)和敏感目录。请将以下内容添加到您的
.gitignore
文件中:# Ignore directories generated by Composer
/drush/contrib/
/vendor/
/web/core/
/web/modules/contrib/
/web/themes/contrib/
/web/profiles/contrib/
/web/libraries/
# Ignore local settings overrides.
/web/sites/*/settings.local.php
# Ignore Drupal's file directory
/web/sites/*/files/
这排除了 Composer 可能安装依赖项的所有目录、用户管理的文件(如上传文件)和敏感目录。我们不排除 web/sites/default/settings.php
,但确保排除 settings.local.php
文件。
-
现在,我们可以使用
add
命令将我们的文件添加到 Git 的跟踪列表中:git add .
我们使用点(.
)作为 add
命令的参数来添加当前目录中的所有文件。
-
使用
status
命令验证文件是否已添加到 Git 的跟踪列表:git status
您应该看到多行文件以绿色显示。绿色表示文件已暂存以供跟踪:
图 1.2 – status
命令的输出,绿色文件表示 Git 中跟踪的项目
-
现在,是时候使用
commit
命令提交更改了:git commit -m "Initial commit"
commit
命令记录对仓库的更改。提交需要提交信息。-m
标志允许您提供信息。
-
由于我们的代码现在在 Git 中跟踪,我们希望导出 Drupal 的配置并跟踪它。我们可以使用 Drush 的
config:export
命令来完成此操作:php vendor/bin/drush config:export
对于第一次导入,所有配置都将被导出。之后,您将被提示导出配置,因为它将覆盖现有文件。
-
将导出的配置文件添加到 Git:
git add config
您可以使用git status
来验证文件是否已准备好提交。
-
提交配置文件:
git commit -m "Add configuration files"
-
您的 Drupal 站点现在由 Git 管理,并且可以推送到 GitHub 或 GitLab 仓库!
它是如何工作的…
Git是一个免费的开源版本控制系统。在本食谱中,我们为构成我们的 Drupal 站点的文件创建了一个新的 Git 仓库。当文件被添加到 Git 仓库时,它们会被跟踪以监控文件的变化。然后,更改会被提交并创建版本控制历史中的新版本。Git 仓库可以被添加到 GitHub、GitLab 或其他服务以托管项目代码。
版本控制的好处在于它使得与其他开发者协作变得容易,而不会创建冲突的代码更改。它还使得代码可移植。Drupal 站点代码不仅存在于您的机器上,还存在于远程仓库(GitHub、GitLab 或其他服务)中。
几乎所有提供为 Drupal 站点提供持续集成和部署的平台即服务(PaaS)托管提供商都需要代码存在于 Git 仓库中。
相关内容
-
Git 文档:
git-scm.com/docs
-
Drupal.org 关于 Git 的文档:
www.drupal.org/docs/develop/git
成功部署您的 Drupal 站点
到目前为止,我们已经创建了一个 Drupal 代码库,设置了本地开发环境,并将我们的代码库放入版本控制中。现在,是时候介绍如何将您的 Drupal 站点部署到服务器上了。
部署您的 Drupal 站点不仅涉及复制代码文件,因为您可能需要运行模式更新和配置导入。在本食谱中,我们将使用rsync,一个高效的文件传输工具,将我们的 Drupal 站点代码库复制到服务器,并使用 Drush 创建成功的 Drupal 部署。
准备工作
本食谱需要通过SSH访问虚拟机,该虚拟机已安装了Linux、Apache、MySQL 和 PHP(LAMP)堆栈。许多云提供商,如Digital Ocean和 AWS Lightsail,提供 LAMP 堆栈虚拟机的单点安装:
-
一台具有 1GB 内存和一个 CPU 的虚拟机就足够了,通常是最低的虚拟机级别。
-
您必须能够使用基于 SSH 密钥的身份验证或密码身份验证访问虚拟机。
-
本食谱使用
root
用户,这不是最佳的安全实践。服务器安全实践和管理超出了本书的范围。
本食谱使用 IP 地址167.71.255.26
来访问服务器。请将167.71.255.26
替换为您服务器的 IP 地址。
在撰写本文时,MySQL PHP 库不支持 MySQL 实现的新的caching_sha2_authentication
身份验证。您需要一个具有mysql_native_password
身份验证方法访问数据库的用户。
这里是创建一个对dbuser
用户可访问、密码为dbpass
的drupaldb
数据库的 SQL 命令摘要:
CREATE DATABASE drupal
CREATE USER 'dbuser'@'%' IDENTIFIED WITH
mysql_native_password BY 'dbpass';
GRANT ALL ON drupal.* TO 'dbuser'@'%';
如何操作...
-
打开终端并导航到您的 Drupal 代码库。
-
为
rsync
创建一个忽略文件,以排除名为.rsyncignore
的目录,以下为代码:.git/
.ddev/
.vscode/
.idea/
web/sites/*/files/
web/sites/*/settings.local.php
web/sites/*/settings.ddev.php
*.sql
这将用于减少传输到生产服务器的文件数量,包括开发者工具配置、本地开发文件以及任何 SQL 转储。
-
现在,我们将使用
rsync
将我们的 Drupal 代码库部署到远程服务器:rsync -rzcEl . root@167.71.255.26:/var/www/html --
exclude-from=".rsyncignore" --delete
rzCE
标志控制文件如何复制到远程服务器。r
表示递归复制,z
在传输过程中压缩文件,C
使用校验和检查文件是否已修改并应该复制,E
保留文件的执行权限,l
保留链接。E
和l
标志对于 Composer 的vendor/bin
可执行文件非常重要。
.
代表我们的当前工作目录作为源,其中root@167.71.255.26:/var/www/html
是我们的目标。将root@167.71.255.26
替换为您的虚拟机的用户和 IP 地址。
exclude-from
选项使用我们的.rsyncignore
文件来跳过上传的文件,而delete
则从目标中删除不再有效的旧文件。
-
我们还需要使用 Drush 的
sql:dump
命令创建一个数据库 SQL 转储,以便进行一次性导入:php vendor/bin/drush sql:dump --result-file=../db-
dump.sql
我们还可以将其复制到服务器,稍后使用安全文件复制(SFC)导入。我们将将其上传到用户的家目录,而不是 Drupal 目录:
scp db-dump.sql root@167.71.255.26:db-dump.sql
-
为了完成设置,通过 SSH 连接到您的虚拟机:
ssh root@167.71.255.26
-
在复制文件后,我们还需要确保可写目录对 Web 服务器是可访问的,例如写入 CSS 和 JS 聚合文件以及编译的Twig模板。这是在第一次文件传输后设置 Drupal 站点时的一次性操作:
mkdir -p /var/www/html/web/sites/default/files/
chown -R www-data:www-data /var/www/html
/web/sites/default/files/
这确保了web/sites/default/files
目录由 Web 服务器用户拥有,以便它可以写入文件。
-
接下来,我们需要更新 Apache 使用的文档根,默认为
/var/www/html
。新的文档根是/var/www/html/web
。我们将使用sed
命令来替换值:sed -i "s,/var/www/html,/var/www/html/web,g"
/etc/apache2/sites-enabled/000-default.conf
sed
命令代表流编辑器,使得在文件中查找和替换文本变得非常容易。
-
我们需要让 Apache 知道我们的配置更改。使用以下命令:
systemctl reload apache2
这将重新加载 Apache 服务配置,并使其知道新的文档根。
-
在我们设置站点之前,我们将在服务器上创建我们的
settings.local.php
文件,以便 Drupal 知道如何连接到数据库。我们将使用nano
命令行编辑器来创建和编辑该文件:nano /var/www/html/web/sites/default
/settings.local.php
-
将以下代码添加到您的
settings.local.php
文件中:<?php
$databases['default']['default'] = [
'driver' => 'mysql',
'database' => 'drupal',
'username' => 'dbuser',
'password' => 'dbpass',
'host' => 'localhost',
'port' => 3306,
];
请确保更改数据库凭据,使其与您创建的服务器上的凭据匹配。如果不存在,Drupal 还会尝试为您创建数据库。
- 使用CTRL + X保存文件,然后Y,最后点击Enter。
使用 Drush 验证数据库连接设置:
cd /var/www/html
php vendor/bin/drush status
数据库凭据应与您添加到settings.local.php
中的内容匹配。
-
接下来,我们需要填充我们的数据库。我们将使用
sql:cli
命令从本地开发环境导入初始数据库:php vendor/bin/drush sql:cli < ~/db-dump.sql
-
然后,我们可以通过使用 Drush 的
deploy
命令来运行任何模式更新和配置导入来执行部署步骤:php vendor/bin/drush deploy
-
您现在可以访问您的 Drupal 站点并查看它了!
工作原理...
此配方涉及各种组件:一个虚拟服务器来托管我们的代码和提供我们的 Drupal 站点,用于传输文件的rsync
工具,以及 Drush 来执行所需的部署步骤。
使用 rsync 而不是 FTP 或甚至 scp(另一种基于命令行的文件传输工具)的一个好处是 rsync 是增量工作的。如果一个文件没有被修改,rsync 将不会传输该文件。它还会确保在远程服务器上删除的文件也会被删除,这是其他文件传输工具所不具备的。
Drush 的deploy
命令确保您的 Drupal 站点的数据库和配置是最新的。这是一个以最佳实践方式操作各种过程的命令。该命令运行 Drupal 的所有更新钩子以确保它们被执行(由模块提供的模式更新和状态更改),并且配置是从导出的配置文件同步的。《deploy》命令应该始终运行,就像您运行 Symfony 或 Laravel 应用程序的迁移一样。
还有更多...
接下来,我们将更详细地介绍关于托管和部署您的 Drupal 站点的内容。
平台即服务托管提供商
您可以通过利用以下列出的 Drupal(按字母顺序排列)的 PaaS 托管提供商之一来完全避免此过程:
-
Acquia Cloud:
www.acquia.com/products/drupal-cloud/cloud-platform/drupal-hosting
-
Pantheon:
pantheon.io/product/drupal-hosting
-
Platform.sh:
platform.sh/marketplace/drupal/
自动化部署
使用持续集成,此部署过程可以在 GitHub Actions、GitLab CI 或其他持续集成提供商上通过提交您的存储库来自动化。您需要配置一组额外的私钥,这些私钥被添加到您的持续集成工具中,以便服务可以 SSH 到您的服务器。
参见
-
Drupal.org 上的托管合作伙伴:
www.drupal.org/hosting
-
Drush
deploy
命令的文档:www.drush.org/latest/deploycommand/
第二章:内容构建体验
如您所知,Drupal 是一个在编辑能力和内容建模方面表现卓越的内容管理系统。在本章中,我们将介绍如何设置您的编辑体验并添加编辑审查工作流程。
本章将深入探讨创建自定义类型并利用不同字段创建高级结构化内容。我们将逐步了解用于创建内容的表单的自定义方法,并学习如何自定义内容的显示。接下来,我们将学习如何使用 Layout Builder
模块构建自定义落地页面。我们还将学习如何添加和管理内容,并利用菜单链接到内容。在本章结束时,您将为您的 Drupal 网站创建一个自定义的创作体验。
那么,让我们看看本章将涵盖哪些主题:
-
配置 WYSIWYG 编辑器
-
使用内容审查创建编辑工作流程
-
创建具有自定义字段的自定义内容类型
-
自定义编辑内容的表单显示
-
自定义内容的显示输出
-
使用布局构建落地页面
-
创建菜单和链接内容
-
使用工作区创建内容预览区域
配置 WYSIWYG 编辑器
Drupal 集成了 Editor
模块,它提供了一个 API 以集成 WYSIWYG 编辑器,尽管 CKEditor(默认编辑器)的贡献模块可以提供与其他 WYSIWYG 编辑器的集成。
文本格式控制内容的格式化和内容作者的 WYSIWYG 编辑器配置。标准的 Drupal 安装配置文件提供了一个完全配置的文本格式,其中启用了 CKEditor。我们将逐步介绍重新创建此文本格式的步骤。
在这个菜谱中,我们将使用自定义 CKEditor WYSIWYG 配置创建一个新的文本格式。
准备工作
在开始之前,请确保已安装 CKEditor
模块。此模块随 Drupal 的标准安装自动安装。
如何操作...
让我们创建一个具有自定义 CKEditor WYSIWYG 配置的新文本格式:
-
从管理工具栏中的 配置 进入,然后在 内容 创作 标题下找到 文本格式和编辑器。
-
点击 添加文本格式 开始创建下一个文本格式。
-
为文本格式输入一个名称,例如编辑器格式。
-
选择哪些角色可以访问此格式 – 这允许您对用户在创作内容时可以使用的内容进行细粒度控制。
-
从 文本编辑器 选择列表中选择 CKEditor。然后,将加载 CKEditor 的配置表单。
-
您现在可以使用内联编辑器将按钮拖放到提供的工具栏中,以配置您的 CKEditor 工具栏:
图 2.1 – 文本格式编辑表单
- 选择任何 已启用过滤器 选项,如图 图 2**.2 所示,除了 显示任何 HTML 为纯文本。这会与使用 WYSIWYG 编辑器的目的相悖:
图 2.2 – 启用的过滤器复选框
- 一旦您满意,请点击 保存配置 以保存您的配置并创建文本过滤器。现在,当用户向富文本字段添加内容时,它将可供用户使用。
它是如何工作的…
Filter
模块提供控制如何向用户展示富文本字段的文本格式。Drupal 将根据字段定义的文本格式渲染保存在文本区域中的富文本。标题中包含“格式化”的文本字段将遵守文本格式设置;其他将以纯文本形式渲染。
重要提示
文本格式和编辑器屏幕会警告由于配置不当而存在的安全风险。这是因为您可能授予匿名用户访问允许完整 HTML 或允许图像来源来自远程 URL 的文本格式的权限。这可能会使您的网站容易受到 跨站脚本攻击(XSS) 的攻击。跨站脚本攻击是指攻击者可以将恶意客户端脚本注入到您的网站中。
Editor
模块提供了一个连接到 WYSIWYG 编辑器和文本格式的桥梁。它修改了文本格式表单和渲染,以允许集成 WYSIWYG 编辑器库。这使得每个文本格式都可以为其 WYSIWYG 编辑器配置。
默认情况下,Editor
模块本身不提供编辑器。CKEditor
模块与 Editor API 一起工作,以启用 WYSIWYG 编辑器的使用。
贡献的模块可以为其他 WYSIWYG 编辑器提供支持。例如,TinyMCE
模块 (www.drupal.org/project/tinymce
) 将 Drupal 与 TinyMCE
编辑器(https://www.tiny.cloud/tinymce)集成。
还有更多…
Drupal 以细粒度方式提供对富文本渲染的控制,并以可扩展的方式,我们将在后面进一步讨论。
过滤器模块
当将字符串数据添加到支持文本格式的字段时,数据将被保存并保留为原始输入时的状态。对于文本格式的启用过滤器将在内容查看时才应用。Drupal 以这种方式工作,即保存原始内容,仅在显示时进行过滤。
在 Filter
模块启用的情况下,您可以指定根据创建文本的用户角色如何渲染文本。了解使用 WYSIWYG 编辑器的文本格式所应用的过滤器非常重要。例如,如果您选择了 显示任何 HTML 为纯文本 选项,则当查看时,WYSIWYG 编辑器所做的格式化将被删除。
改进的链接
WYSIWYG 编辑的一个主要组成部分是能够将链接插入到其他内容或外部网站上。与 CKEditor 集成的默认链接按钮允许进行基本的链接嵌入。这意味着内容编辑必须在事先知道他们的内部内容 URL 才能链接到它们。解决此问题的方案是位于 https://www.drupal.org/project/linkit 的 Linkit
模块。
可以使用以下 Composer 和 Drush 命令安装 LinkIt
模块:
dd /path/to/drupal
composer require drupal/linkit
php vendor/bin/drush en linkit –yes
Linkit
模块提供了对默认链接功能的直接替换。它添加了自动完成搜索内部内容,并为显示字段添加了额外的选项。Linkit
通过创建不同的配置文件来实现,这些配置文件允许您控制可以引用的内容、可以管理的属性以及哪些用户和角色可以使用 Linkit
配置文件。
CKEditor 插件
CKEditor
模块提供了一个名为 CKEditorPlugin 的插件类型。插件是 Drupal 中的可交换功能的小块。插件和插件开发将在第 8 章,使用插件即插即用 中介绍。此类型提供了 CKEditor 和 Drupal 之间的集成。
图片和链接功能是在 CKEditor
模块内部定义的插件。额外的插件可以通过贡献项目或自定义开发提供。
参考类 \Drupal\ckeditor5\Annotation\CKEditor5Plugin
(git.drupalcode.org/project/drupal/-/blob/10.0.x/core/modules/ckeditor5/src/Annotation/CKEditor5Plugin.php
) 以获取插件定义,以及 \Drupal\ckeditor5\Plugin\CKEditor5Plugin\ImageUpload
类 (https://git.drupalcode.org/project/drupal/-/blob/10.0.x/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/ImageUpload.php) 作为工作示例。
参考以下内容
参考第 8 章,使用插件即插即用,以获取 CKEditor 5 文档(https://www.drupal.org/docs/core-modules-and-themes/core-modules/ckeditor-5-module)。
创建带有内容审查的编辑工作流程
许多组织在内容可以在网站上发布之前必须遵循编辑工作流程。Content Moderation
模块允许在 Drupal 中创建的内容在发布之前经过编辑过程。在这个菜谱中,我们将创建一个内容审查工作流程,将内容置于草稿状态,然后进行审查、批准和发布。内容将保持草稿状态,直到发布之前对网站访客隐藏。
准备工作
在这个菜谱中,我们将使用标准安装,它提供了文章内容类型。任何内容类型都足够使用。
如何做到这一点...
-
首先安装
Content Moderation
模块及其依赖模块Workflows
:php vendor/bin/drush en content_moderation –yes
-
访问 配置 然后是 工作流程。此页面列出了所有配置的内容审查工作流程。点击 添加工作流程 创建一个新的工作流程。
-
在 标签 字段中,给它一个标签为 审批工作流程 并选择 内容审查 作为 工作流程类型。
-
工作流有两个默认状态:草稿和已发布。我们需要添加审查和批准状态。为我们的每个新状态,点击添加新状态链接。填写状态标签并按保存。不选中已发布和默认修订复选框。那些应该只用于发布状态。
-
调整状态的顺序,使其为草稿、审查、批准、已发布。在表单底部按保存,以便保存我们的顺序。
-
接下来,我们需要创建一个将草稿移动到审查的转换。点击添加新转换。将转换标签设置为准备审查。选择草稿作为起始状态。然后,选择审查作为目标状态并按保存。
-
现在,我们将创建从“审查”到“批准”的转换。点击添加新转换。将转换标签设置为需要批准。选择审查作为起始状态。然后,选择批准作为目标状态并按保存。
-
我们必须编辑默认的发布转换。取消选中从复选框中的草稿,并选择批准。
-
最后,我们必须将此工作流分配给内容实体。在此工作流适用于下,查找内容类型。按选择,将打开一个对话框。选中文章,然后在对话框中按保存。
-
在表单底部按保存。我们的内容审查工作流现在已完成!
它是如何工作的...
没有使用Content Moderation
,可发布的内容实体只有两种状态:未发布或已发布。也没有权限来控制谁可以将未发布的内容发布或反之亦然。Content Moderation
解决了这个问题。
Workflows
模块提供了一个定义状态和转换的 API。模块如Content Moderation
负责提供工作流类型插件以实现有意义的函数。Content Moderation
模块与 Drupal 内容实体的修订功能集成。
当编辑使用Content Moderation
的内容实体时,将有一个审查状态字段。此字段包含基于当前用户权限,内容可以转换到的状态。
参考信息
-
Drupal.org上的
Content Moderation
模块文档:https://www.drupal.org/docs/8/core/modules/content-moderation/overview -
Drupal.org
上的 Workflows 模块文档:www.drupal.org/docs/8/core/modules/workflows/overview
创建带有自定义字段的自定义内容类型
Drupal 在内容管理领域表现出色,它允许不同类型的内容。在本教程中,我们将向您展示如何创建一个自定义内容类型。我们将创建一个包含一些基本字段并可用于强调公司提供服务的场景的“服务”类型。
您还将学习如何在本食谱中将字段添加到内容类型中,这通常与在 Drupal 网站上创建新的内容类型相辅相成。
如何操作…
-
前往结构然后内容类型。点击添加内容类型以开始创建新的内容类型。
-
输入
服务
作为名称,以及可选的描述。 -
选择显示设置,取消勾选显示作者和日期信息复选框。这将隐藏作者和提交时间从服务页面。
-
点击保存和管理字段按钮以保存新的内容类型并管理其字段。
-
默认情况下,新内容类型会自动添加一个正文字段。我们将保留此字段。
-
我们将添加一个字段,为服务提供输入营销标题的方式。点击添加字段。
-
从下拉菜单中选择文本(纯文本),并将标签设置为营销标题。
重要提示
文本(纯文本)选项是一个常规文本字段。文本(格式化)选项将允许您在字段中显示的文本上使用文本格式。
-
在下一个表单上点击保存字段设置。在随后的表单上,点击保存设置以完成字段的添加。
-
字段已添加,现在可以创建此类内容。
工作原理…
在 Drupal 中,内容实体可以有不同的包。包指的是该实体类型的不同类型。这个词“包”来源于它是一组字段,因为每个内容实体类型的包都可以有不同的字段。当处理节点时,它们与内容同义,节点的包被称为内容类型。
当创建内容类型时,会为其创建一个默认的正文字段。这是通过在node.module
文件中调用node_add_body_field()
函数来完成的。对于那些希望在用户界面之外程序定义包字段的人来说,这是一个很好的参考点。
只有启用Field UI
模块,才能管理或添加字段。Field UI
模块为实体,如节点、块和分类术语,公开了管理字段、管理表单显示和管理显示选项。
自定义编辑内容的表单显示
表单模式允许网站管理员在修改内容实体时自定义编辑表单。在节点的情况下,您可以重新排列字段的顺序并更改用于字段节点编辑表单的表单元素。还有一个名为Field Group
的模块。Field Group
模块允许您将字段分组到字段集中。
在本食谱中,我们将安装Field Group
模块并修改表单显示以创建文章
内容类型。
如何操作…
-
首先,我们必须使用 Composer 将
Field Group
模块添加到 Drupal 站点,然后使用 Drush 安装它:composer require drupal/field_group
php vendor/bin/drush en field_group –yes
-
要自定义表单的显示模式,请前往结构然后内容类型。
-
我们将修改文章内容类型的表单。点击并展开操作按钮,然后选择管理****表单显示。
-
点击添加字段组以开始添加新的字段组。
-
从添加新组中选择详细信息侧边栏,给这个一个标签为元数据,然后点击保存并继续。
-
在下一个表单上点击创建组,并使用默认值来完成创建组。
-
将新创建的元数据组(如图图 2**.3所示)从禁用部分拖动到启用。直接在禁用标签上方即可。
-
将标签字段拖动,使其位于元数据组下面——在其下方,并且稍微向右:
图 2.3 – 管理显示表单,将标签小部件移动到元数据字段组组件下面
-
点击页面底部的保存按钮以保存您的更改。
-
前往创建新文章;您将在侧边栏中找到元数据标签,其中包含标签字段:
图 2.4 – 文章编辑表单,标签元素在侧边栏中
如何工作...
当构建内容实体表单时,表单会知道要使用的显示模式。然后,它调用显示模式,使用指定的字段小部件为每个字段构建组件。
这允许您在不替换整个表单的情况下自定义表单的特定部分。开发者可以创建新的字段小部件或利用贡献模块中的小部件来增强表单的功能。
字段组
不会创建字段小部件,而是在表单显示内部创建一个新的结构。然后,它将字段小部件排列成组。这提供了更组织化的内容编辑体验。
更多内容...
在下一节中,我们将讨论更多关于管理内容实体表单的内容。
管理表单显示模式
通过访问结构下的显示模式,可以在表单模式中添加额外的表单显示模式。每个内容实体类型都有一个隐藏的默认表单模式,它始终存在。可以通过显示管理表单添加和配置额外的表单显示模式。
这些表单及其配置的字段小部件本身并不直接与 Drupal 集成。使用自定义代码,甚至贡献的项目,它们可以用于嵌入特殊用途。
例如,有用户注册表单模式。用户注册表单是使用此显示模式和配置的小部件构建的,而不是在编辑现有用户时通常可用的内容。
自定义内容显示输出
Drupal 提供了显示视图模式,允许您自定义附加到实体的字段和其他属性。在本教程中,我们将调整文章
内容类型的摘要显示模式。每个字段或属性都有用于显示标签、显示信息的格式以及格式的额外设置的控件。
利用视图显示可以让您完全控制内容在您的 Drupal 网站上如何被查看。
如何操作...
-
现在,是时候通过导航到结构然后内容类型来定制表单显示模式了。
-
我们将修改
文章
内容类型的显示。点击下拉按钮箭头并选择管理显示。 -
点击摘要视图模式选项来修改它。摘要视图模式用于节点列表,例如默认主页。
-
将标签字段拖到隐藏部分。在查看摘要 视图模式时,文章上的标签将不再显示。
-
点击
600
到300
的设置齿轮图标。 -
点击保存以保存您所做的所有更改。
-
查看主页并审查已生效的更改。
它是如何工作的...
实体的默认渲染系统使用视图显示。视图显示模式是配置实体。由于视图显示模式是配置实体,因此可以使用配置管理进行导出。
当内容实体被渲染时,视图显示会遍历显示中配置的每个字段格式化器。字段格式化器是从管理显示表单的格式属性中选择的选项,它标识了用于渲染字段值的代码。字段值从实体中检索出来,并传递给已使用提供给视图显示的配置实例化的字段格式化器插件。然后,将此渲染数据集合传递给 Drupal 的其余渲染管道。
更多...
在下一节中,我们将讨论更多关于管理内容实体形式的项目。
管理视图显示模式
可以通过访问结构下的显示模式来添加额外的表单显示模式。每个内容实体类型都有一个隐藏的默认视图模式,它始终存在。可以通过显示管理表单添加和配置额外的视图显示模式。
这些视图模式可以在使用视图显示内容、实体引用的渲染实体字段格式化器或使用自定义代码渲染实体时被利用。
使用布局构建落地页
布局构建器
模块允许内容创建者使用拖放界面来自定义页面上的内容显示方式。与在视图显示模式中使用字段格式化器不同,这不需要开发者,并且可以为单个内容项进行自定义。使用布局构建器
,内容创建者从系统中可用的不同布局中选择,并在其中放置块以构建页面内容。在本教程中,我们将介绍安装布局构建器
并设置文章
内容类型的布局。
准备工作
在本教程中,我们将使用标准安装,它提供了文章
内容类型。任何内容类型都适用。
如何操作…
-
首先,安装
布局构建器
模块及其依赖模块布局发现
:php vendor/bin/drush en layout_builder –yes
-
我们必须选择使用
布局构建器
来显示我们内容类型的显示模式。访问结构然后内容类型,使用文章的下拉按钮点击管理显示。 -
找到标记为布局选项的部分并勾选使用布局构建器复选框。
-
点击保存以启用布局构建器。
-
现在,管理显示表单应显示管理布局按钮。
-
点击
布局构建器
用户界面来自定义文章布局。 -
默认情况下,显示内容预览复选框是勾选的。取消勾选此复选框以关闭生成的示例预览内容。
-
点击添加部分以创建新部分并选择两列布局。
-
选择33%/67%作为列宽并点击添加部分,保留管理标签为空。
-
现在我们已经添加了两个列的部分,我们可以将字段移动到这些布局部分。将图片字段拖到新部分的左侧,将正文字段拖到新部分的右侧。
-
点击保存布局以保存更改。
-
不使用代码,我们现在已为文章创建了一个布局,将图片放置在文章内容的侧边栏中。
它是如何工作的…
布局构建器
模块为实体类型提供了一种替代渲染系统。使用布局构建器
是内容实体类型每个显示模式的可选过程。如果实体类型的显示模式不由布局构建器
管理,则回退到使用字段格式化器的常规渲染系统。
布局由布局插件提供,这些插件具有匹配的 Twig 模板。模块和主题可以定义新的模板供使用。布局构建器
利用块来显示内容。布局构建器
中可以嵌入的块类型基于系统可用的块。
布局构建器
还将内容实体上的每个字段暴露为块,允许您将每个字段放置在不同的部分。
如同自定义节点或其他实体模板,如果你在未更新布局对应的 Twig 模板的情况下修改布局插件或嵌套元素,你可能会看到渲染不正确的内容。在做出此类更改时,务必相应地审查 Twig 模板。
更多内容…
当布局构建器
首次加入 Drupal 时,它是一个令人兴奋的补充,并且具有许多更多功能和定制,远超本指南所涵盖的内容。
可访问性
布局构建器
用户界面经过了严格的可访问性测试。整个布局构建器
用户界面可以使用键盘或其他辅助设备进行导航。
为每块内容定制布局
在配置布局选项时,使用布局构建器
用户界面。
布局覆盖也存储在附加到内容实体的字段数据中,这使得它随着修订版进行跟踪!这意味着可以为布局更改的内容创建新的草稿,并且可以通过内容``审核
工作流程进行发布。
扩展布局构建器的附加模块
有许多模块可以扩展布局构建器
,以定制其体验并提供默认布局。例如,如果你使用 Bootstrap 前端框架,Bootstrap Layout Builder (www.drupal.org/project/bootstrap_layout_builder
)模块提供了一个用于构建使用 Bootstrap 样式的布局的用户界面。
扩展布局构建器
的模块列表可以在Drupal.org
上找到:www.drupal.org/docs/8/core/modules/layout-builder/additional-modules
。
相关内容
Drupal.org
上的布局构建器模块文档:www.drupal.org/docs/8/core/modules/layout-builder
创建菜单和链接内容
Drupal 允许你将正在编写的链接内容链接到网站上的指定菜单,通常是主菜单。然而,你可以创建自定义菜单以提供内容的链接。在本指南中,我们将向你展示如何创建自定义菜单并将内容链接到它。然后,我们将菜单作为一个块放置在页面的侧边栏中。
准备工作
本指南假设你已经安装了标准安装配置文件,并且有默认的节点内容类型可供使用。你应该有一些内容创建以创建链接。
如何操作…
-
访问结构并点击菜单。
-
点击添加菜单。
-
提供一个侧边栏菜单的标题,以及可选的摘要,然后点击保存。
-
保存菜单后,点击添加链接按钮。
-
输入链接标题,然后输入内容的标题。表单将提供可链接内容的自动完成建议。
-
点击保存以保存菜单链接。
-
保存菜单链接后,转到结构,然后块布局。
-
点击
侧边栏菜单
并点击 放置块。 -
在以下表单中,点击 保存块。
-
通过点击管理菜单中的 首页 来查看您的 Drupal 网站。
工作原理…
菜单和链接是 Drupal 核心的一部分。创建自定义菜单和菜单链接的能力是通过 Menu UI
模块提供的。此模块在标准安装中已启用,但在其他安装中可能未启用。
菜单链接表单的链接输入允许您开始键入内容标题,并轻松地将它们链接到现有内容。它将自动将标题转换为内部路径。链接输入还接受常规路径,例如 /node/1
或外部路径。您可以使用 <front>
链接到主页,使用 <nolink>
渲染非链接的锚点标签,以及使用 <button>
创建键盘可访问的纯文本链接。
更多内容…
链接可以通过内容编辑表单本身进行管理,这将在下一部分介绍。
从其表单管理内容菜单链接
您可以从添加或编辑表单将内容添加到菜单中。菜单设置部分允许您切换菜单链接的可用性。菜单链接标题将默认反映内容的标题。
父项目允许您决定它将出现在哪个菜单以及哪个项目下。默认情况下,内容类型仅允许主菜单。编辑内容类型可以允许使用多个菜单或仅选择自定义菜单。
这允许您在不访问菜单管理屏幕的情况下填充主菜单或辅助菜单。
使用 Workspaces 创建内容预览区域
Workspaces
模块提供了一种在 Drupal 网站上处理内容的新方法。它允许您拥有网站内容的实时版本和并行草稿版本。常规的内容工作流程涉及多个内容片段,这些内容可能在不同时间被草稿和发布。Workspaces
模块提供了一种创建和准备同时发布的草稿的方法。
例如,在大型体育赛事期间,文章是根据获胜的队伍准备的。一旦宣布获胜者,该版本网站的内容就可以发布。在这个菜谱中,我们将安装 Workspaces
模块并介绍如何使用网站版本。
重要提示
在撰写本文时,Workspaces
模块是一个 实验性
模块。标记为实验性的模块正在积极开发中,并不被认为是稳定的。实验性模块提供了一种更轻松地向 Drupal 核心添加新功能的方法。您可以在 Drupal.org
上了解更多关于实验性模块政策的信息:www.drupal.org/about/core/policies/core-change-policies/experimental/policy-and-list
。
准备工作
在这个菜谱中,我们将使用标准安装,它提供了 基本页面
内容类型。任何内容类型都适用。
如何操作…
-
首先安装
Workspaces
模块:php vendor/bin/drush en workspaces --yes
-
访问您的 Drupal 网站;您会在工具栏的右侧注意到Live标签;这是当前工作区的标识符。
-
点击Live以打开工作区菜单。
-
点击“Stage”工作区的名称,然后在弹出的模态窗口中点击确认,询问我们是否想要激活并切换到“Stage”工作区。
-
在使用“Stage”工作区的同时创建三到四个新基本页面,并确保在推广****选项组中勾选推广到首页。
-
当您访问 Drupal 网站的首页时,您应该会看到您在首页列表中创建的页面。
-
现在,在另一个浏览器或私密模式下打开您的 Drupal 网站,您将看到主页显示尚未创建任何首页内容。这表明内容仅发布在“Stage”工作区,而不是在实时网站上。
-
回到您的 Drupal 网站,点击工具栏中的Stage标签以打开工作区菜单。
-
点击发布内容以开始将您的 Stage 内容发布到 Live 网站。
-
将会弹出一个确认表单。点击将项目发布到 Live以完成此过程。
-
如果您在其他浏览器或私密模式下再次测试您的网站,您将看到主页现在列出了您所有的新页面!
它是如何工作的...
Workspace
模块使用内容实体现有的修订功能。修订随后会跟踪到工作区,直到它们发布到 Live 工作区。Workspace
模块还增加了安全措施。除非在 Live 工作区中,否则无法保存修改网站配置的表单;该模块显示警告并禁用Workspace
模块,只允许在一个工作区中编辑一个内容项。
工作区还与用户账户相关联。这允许为特定用户创建分段工作区。这允许内容创建者创建新的工作区,但不能查看或修改其他内容创建者的工作区。
还有更多...
Workspaces
模块提供了本食谱中未涵盖的其他用户界面,并且还有另一种使用工作区的方法,而不仅仅是内容。
工作区何时将成为一个稳定的模块?
正在努力使Workspaces
模块变得稳定。这些问题已在 Drupal 核心问题队列中标记为WI critical(即工作流倡议关键)。问题列表可在此处找到:www.drupal.org/project/issues/search/drupal?status%5B%5D=Open&issue_tags_op=%3D&issue_tags=WI+critical
。
在工作区中管理内容更改
当工作区菜单在工具栏中打开时,您可以点击管理工作区链接以查看工作区中的所有活动更改。这使得内容管理员更容易审查工作区中已修改的内容。它还允许删除这些更改以恢复到原始内容。
本概述有助于回顾可能发布到实时工作空间的所有更改。
创建子工作空间
工作空间也可能有一个父工作空间。这允许您维护一个集中的预演工作空间,但迫使内容创建者在预演下拥有他们的子工作空间。所有内容修改都可以合并到预演,而不是每个贡献者的工作空间发布到实时。
使用工作空间测试新的站点重新设计
Drupal 有一个确定活动主题的机制,默认情况下是默认主题。可以编写代码根据特定条件覆盖当前主题。工作空间主题
模块(www.drupal.org/project/workspace_theme
)正是如此。
它为工作空间添加了一个新字段,允许您指定在激活该工作空间时使用不同的主题。这允许您使用新主题预览站点的重新设计,而无需将其设置为生产站点的默认主题,或者仅仅依赖于测试服务器。
参见
Drupal.org
上的工作空间模块文档:www.drupal.org/docs/8/core/modules/workspace/
第三章:通过视图显示内容
Drupal 中的 视图 模块是一个可视化查询构建器,它允许您在不编写任何代码的情况下构建动态内容显示。我们将介绍如何创建一个列出博客的页面以及一个显示五个最新博客的伴随区块。然后,我们将转向创建公开筛选器,以便最终用户可以控制视图结果。您还将学习一些使用上下文筛选器和自定义实体引用小部件输出的高级主题。最后,我们将介绍如何使用 图表 模块通过视图输出数据图表。
在本章中,我们将介绍以下菜谱:
-
创建博客落地页面
-
创建一个包含最近博客的区块
-
向用户公开筛选和排序以控制列表
-
上下文筛选器用于按路径参数筛选
-
在具有关系的视图中添加相关数据
-
提供实体引用结果视图
-
使用视图显示图表
创建博客落地页面
视图模块只做一件事,而且做得很好——列出内容。视图模块背后的力量在于它给予最终用户大量可配置的权力,以各种形式显示内容。本菜谱将介绍如何创建内容列表并将其链接到主菜单。我们将使用 文章 内容类型来创建博客落地页面。
如何操作...
- 前往结构然后视图。这将带您到所有创建的视图的管理概览。点击添加视图以创建一个新视图:
图 3.1 – 视图列表概述
-
第一步是提供博客视图的名称,它将作为管理标题(默认情况下)和显示标题。
-
接下来,我们将修改视图设置部分。我们想要显示文章类型的内容,并保留标记为字段为空。这将强制视图只显示文章内容类型的内容。
-
选择创建页面选项。页面标题和路径字段将根据视图名称自动填充,可以根据需要修改。现在,保留显示和其他设置为其默认值:
图 3.2 – 视图创建表单概述
-
点击保存并编辑以继续修改您的新视图。
-
在中间列的页面设置部分下,我们将更改菜单项设置。点击无菜单以更改菜单选项。
-
选择常规菜单项。提供菜单链接标题和可选描述。将父项设置为<主导航>:
图 3.3 – 视图的菜单设置表单
-
在对话框表单底部点击应用。
-
点击保存以保存您的视图。
-
保存你的视图后,从管理菜单点击返回网站。现在你将在 Drupal 网站的主菜单中看到链接。
它是如何工作的...
创建视图的第一步是选择你将要显示的数据类型。这被称为基础表,可以是任何类型的实体或专门暴露给视图的数据。
内容和节点
节点在视图中被标记为内容,你将在 Drupal 中找到这种术语的互换。
在创建视图页面时,我们添加了一个可访问的菜单路径。它告诉 Drupal 调用视图来渲染页面,这将加载你创建的视图并渲染它。然后,视图模块将此路径注册为 Drupal 路由系统中的一个路由,由它自己提供的控制器来渲染视图显示。
有显示样式和行插件用于格式化要渲染的数据。我们的配方使用了未格式化的列表样式,将每一行包裹在一个简单的div
元素中。我们本可以将它改为表格来显示格式化的列表。行显示控制了每一行的输出方式。
还有更多...
视图模块是 Drupal 核心中最灵活和最常用的模块之一。在下一节中,我们将进一步探讨视图的一些组件。
视图和显示
在使用视图时,你会看到一些不同的术语。一个关键的概念是要理解什么是显示。一个视图可以包含多个显示。每个显示都是某种类型。视图模块提供了以下显示类型:
-
附件:这是一个附加到同一视图中的另一个显示的显示。这些放置在另一个视图显示的页眉或页脚部分。
-
块:这允许你将视图作为块放置在你的 Drupal 网站上。
-
嵌入:这个显示旨在通过编程嵌入。
-
实体引用:这允许视图为实体引用字段提供结果。
-
订阅源:这种显示返回一个基于 XML 的订阅源,可以附加到另一个显示以渲染一个订阅图标,让用户可以订阅内容。
-
页面:这允许你显示特定路径的视图。
每个显示都可以有自己的配置。然而,每个显示将共享相同的基础表(内容、文件等)。这允许你在同一个视图中以不同的方式呈现相同的数据。
格式样式插件 – 样式和行
在视图中,有两种类型的样式插件代表你的数据如何显示:样式和行:
-
样式插件代表视图的整体格式
-
行插件代表视图结果中每一行的格式
例如,响应式网格样式将使用 CSS 网格输出结果,并使其对不同屏幕尺寸做出响应。同时,表格样式创建了一个带有字段标签作为表头的表格输出。
行插件定义了如何渲染行。内容的默认类型将使用选定的视图模式渲染实体。如果你选择字段,你可以手动选择要包含在你的视图显示结果中的字段以及每个字段的字段格式化程序。
每个样式插件都有一个对应的 Twig 模板,用于主题化输出。请参考第十章中的使用 Twig 模板部分,主题化和前端开发,以了解更多关于 Twig 模板的信息。
使用嵌入式显示
可用的每种显示类型都有一种方法可以通过用户界面暴露自己,除了嵌入式。通常,贡献和自定义模块使用视图来渲染显示,而不是手动编写查询和渲染输出。Drupal 提供了一个特殊的显示类型来简化这个过程。
如果我们要在菜谱中创建的视图中添加一个嵌入式显示,我们可以在自定义代码中使用以下渲染数组来程序化地输出我们的视图:
$view_render = [
'#type' => 'view',
'#name' => 'blog,
'#display_id' => 'embed_1'
];
当渲染时,#type
键告诉 Drupal 这是一个视图元素。然后我们将其指向我们新的显示,embed_1
。嵌入式显示类型没有特殊功能;它是一个简单的显示插件。这种做法的好处是它不会为了性能而执行额外的操作。
当你想在自定义页面、块或甚至表单中使用视图时,使用嵌入式显示是有益的。例如,Drupal Commerce 使用这种模式为其购物车块和结账时的订单摘要。视图用于在自定义块和表单中显示订单信息。
创建一个包含最新博客的块
在上一道菜谱中,我们使用视图模块创建了一个页面,用于在 Drupal 网站上列出文章以构建博客。视图可以包含多个显示。每个显示继承默认值,例如其样式和行格式、过滤器、排序、分页等。每种显示类型可能具有独特的设置,例如在上一道菜谱中配置菜单链接的页面设置。在这个菜谱中,我们将添加一个块显示,这样我们就可以在网站的任何地方按标题列出最新的五篇文章。
如何做到这一点...
-
前往结构然后视图。这将带您到所有创建的视图的管理概览。
-
找到在上一道菜谱中创建的博客视图,并点击编辑。
-
在显示下,点击页面旁边的添加按钮,从下拉菜单中选择块。这将创建一个新的块显示,我们可以进行配置:
图 3.4 – 添加新显示的菜单
-
在格式部分,点击内容(位于显示旁边)以配置使用的行格式。
-
在打开的对话框中,我们需要确保我们只修改此显示的行格式。在对于下拉菜单中,选择此块(覆盖),以便更改仅适用于此显示。
-
从单选按钮中选择字段选项并点击应用(此显示)以设置行格式以使用单个字段而不是显示模式:
图 3.5 – 样式格式表单对话框
- 将出现一个新对话框,允许您配置此行格式的选项。使用默认设置并点击应用。
重要提示
更改为使用字段行格式将自动添加标题字段,该字段已配置为链接到内容片段。
-
我们只想让此块显示最近的五篇博客。在分页器部分,点击使用分页器旁边的字段以启动配置分页器的对话框。
-
就像行格式一样,从对于选择列表中选择此块(覆盖)。然后,选择显示指定数量的项目选项。点击应用(此显示):
图 3.6 – 分页器选择表单对话框
-
在下一个对话框中,允许您配置分页器设置,将要显示的项目更改为5。点击应用以完成更改分页器。
-
点击保存以将您的更改保存到视图并创建块显示。
-
前往结构和块布局以将块放置在您的 Drupal 网站上。点击放置块以选择侧边栏****第一个区域。
-
通过输入您的视图名称:博客来过滤列表。点击放置块以将您的视图块添加到块布局。
-
我们不希望此块显示在我们的博客主页上。在
/blog
到页面文本框中。将单选选项更改为隐藏对于****列出的页面。 -
这防止了块在
/blog
路径上显示,这是我们文章列表的视图。 -
最后,点击保存块以提交您的更改。
它是如何工作的...
在 Drupal 中,块是一种插件类型。这些块可以嵌入到网站布局中,并且可以根据各种可见性设置显示或隐藏。视图模块与块模块集成,允许将视图显示放置为块。这使 Drupal 网站构建者能够创建可用于整个站点的动态内容显示。
更多内容...
现在,我们将探讨视图与块交互的其他一些方式。
与布局构建器一起使用
在第二章,“内容构建体验”,在使用布局构建着陆页菜谱中,我们使用了布局构建器来放置包含内容字段值的块。您可以使用布局构建器放置任何类型的块,包括视图中的块显示。内容创建者可以使用视图提供的动态内容创建着陆页。
向用户展示筛选和排序功能以控制列表
视图模块支持在视图显示中公开筛选器和排序,以便用户可以与之交互以调整结果。这可以通过允许用户通过文本筛选或调整结果排序顺序来实现。在本菜谱中,我们将修改用于创建博客页面的视图。我们将添加一个公开搜索筛选器,并允许用户按最新或最旧排序文章。
如何操作...
-
前往结构然后视图。这将带您到所有已创建视图的管理概览。
-
找到在第一个菜谱中创建的博客视图,并点击编辑。
-
在表单的筛选条件部分点击添加。
-
在打开的对话框中,我们需要确保我们只修改此显示的行格式。在为下拉菜单中,选择此页面(覆盖)以确保更改仅适用于此页面显示,而不是在先前的菜谱中创建的块显示。
-
在内容类别中选择标题,并点击添加(此显示):
图 3.7 – 添加筛选表单对话框
-
选中向访客展示此筛选器,允许他们更改它复选框,使其成为一个公开筛选器。在公开筛选器配置表单中,将操作符从等于更改为包含。这将允许进行更灵活的搜索。点击应用(此显示)以添加新筛选器。
-
在排序条件部分,点击内容:作者时间(降序)。从为选择列表中选择此页面(覆盖)。
-
选中向访客展示此排序,允许他们更改它复选框,使其成为一个公开排序。
-
点击应用(此显示)以更新排序配置。
-
点击保存以保存对视图的更改。
-
现在,当你查看
/blog
时,你可以搜索文章并更改它们的排序顺序:
图 3.8 – 带有公开筛选器和排序的博客着陆页
工作原理...
当筛选或排序被公开时,视图模块会将一个表单附加到视图显示。这个表单由\Drupal\views\Form\ViewsExposedForm
类控制。它从 URL 中读取查询参数值,并将它们映射到已知的公开筛选器和排序。它将这些值应用到筛选器和排序处理程序,覆盖它们的默认值,以便调整查询。
还有更多...
现在,我们将探讨使用暴露过滤器和排序时可用的一些额外选项。
暴露与非暴露过滤器和排序
过滤器允许您缩小视图显示的数据范围。过滤器可以是暴露的或不暴露的;默认情况下,过滤器不是暴露的。例如,使用内容:发布状态选项设置为是(已发布)以确保视图始终包含已发布的内容。这是一个您会配置以向网站访客显示内容的项。然而,如果是用于管理显示,您可能希望暴露该过滤器。这样,内容编辑者可以轻松查看尚未发布或已取消发布的内容。
所有过滤和排序标准都可以标记为暴露。
过滤器标识符
暴露过滤器通过解析 URL 中的查询参数来工作。在我们的博客页面上,当您通过标题搜索并提交暴露表单时,URL 现在将有一个title
、sort_by
和sort_order
的查询参数。
暴露过滤器有一个可以更改 URL 组件的过滤器标识符选项。这可以在编辑过滤器或排序时进行修改。
作为块的暴露表单
如果您的视图使用暴露过滤器,您可以选择将暴露表单放置在块中。启用此选项后,您可以在页面的任何位置放置该块,甚至是非视图页面。
在块中使用暴露表单的一个例子是用于搜索结果视图。您可以添加一个用于控制搜索结果的暴露过滤器。在块中的暴露过滤器可以轻松地放置在您网站的页眉中。当提交暴露过滤器块时,它将引导用户到视图的显示。在这个菜谱中,它将允许用户搜索文章,而无需在/``blog
页面上。
要将暴露的过滤器作为块启用,首先,您必须展开视图编辑表单右侧的高级部分。从高级部分点击块中的暴露表单选项。在打开的选项模态中,选择是单选按钮,然后点击应用。您可以将块放置在块****布局表单中。
根据路径参数进行过滤的上下文过滤器
视图可以被配置为接受上下文过滤器,也称为参数。上下文过滤器允许您提供一个动态或固定的参数来修改视图的查询。将其视为查询中的根条件。默认情况下,期望从 URL 中提供值;如果没有提供,可以选择默认操作。
在这个菜谱中,我们将创建一个名为/user/{user_id}/content
的新页面路径。{user_id}
的值将是 Drupal 中可用的任何用户 ID。
如何做到这一点...
-
前往结构然后视图。这将带您查看所有已创建视图的管理概览。点击添加视图以创建一个新的视图。
-
将视图名称设置为我的内容。
-
接下来,我们将修改视图设置部分。我们想显示所有类型的内容,并保持标记为字段为空。这将允许显示所有内容类型。
-
检查
user/%user/content
。点击保存并编辑以转到下一屏幕:
图 3.9 – 带路径变量的页面设置
路径中的百分号
当在视图页面显示路径中使用百分号时,Views 模块将其理解为上下文过滤器将使用的值的占位符。例如,给定/user/1234/content
路径,%user
的值将是1234
。
-
切换页面右侧表单的高级部分。在上下文****过滤器部分点击添加。
-
从内容类别中选择作者,然后点击添加并配置上下文****过滤器按钮。
-
将当过滤器值不在 URL 中时的默认值更改为显示“访问拒绝”,以防止所有内容因错误的路由值而显示。点击应用以添加上下文过滤器并关闭对话框:
图 3.10 – 上下文过滤器设置
-
在页面设置下,默认访问权限是查看已发布内容权限。点击查看已发布内容以将权限更改为查看用户信息。点击应用以设置页面显示的权限。
-
接下来,我们将把页面添加到用户页面上的菜单标签。从菜单选项中点击无菜单。
-
选择菜单标签并提供一个菜单链接标题,例如我的内容。点击应用以更改菜单设置:
图 3.11 – 配置的视图概述
-
然后,点击保存以保存视图。
-
前往
/user/1/content
,你将看到第一个用户创建的内容。它也将作为标签显示在查看和编辑旁边。
它是如何工作的…
上下文过滤器类似于在 Drupal 的路由系统中使用路由参数,该系统建立在 Symfony 的路由组件之上。路由参数在视图的页面显示路径中以百分号作为占位符。视图将按照它们放置的顺序将每个占位符与上下文过滤器匹配。这允许你拥有多个上下文过滤器,因此只需确保它们按正确顺序排列。
在其他显示类型上使用上下文过滤器
当使用其他显示类型,如块时,你需要使用提供一个默认值****选项。
大多数可以用作常规过滤器的字段都可以用作上下文过滤器。这种做法的好处是它们可以接收动态值,而无需强迫它们作为暴露给最终用户的过滤器。
更多内容…
现在我们将探讨使用上下文过滤器时可用的一些额外选项。
使用上下文过滤器预览
您仍然可以从编辑表单预览视图。您只需将上下文过滤器值添加到由正斜杠(/
)连接的文本表单中即可。在这个配方中,您可以通过在预览表单中输入1
并更新预览来复制导航到/user/1/content
。
提供默认值
当过滤器值不可用时,有一个选项可以提供默认值。例如,可以提供一个固定值作为后备,或者视图可以尝试从当前 URL 获取内容 ID 或使用当前登录用户。一些选项始终可用,而一些则取决于视图中显示的数据。
当使用具有块显示的视图时,默认值选项允许您在没有路径可用以提供参数值的情况下利用上下文过滤器。这在使用布局构建器创建落地页时特别有用。
修改页面标题
使用上下文过滤器,您可以操作当前页面的标题。您可以在“当过滤器值在 URL 中或提供默认值时”部分中检查“覆盖标题”选项。
此文本框允许您输入将显示的新标题。替换模式部分包含可用于动态标题内容的占位符。
上下文过滤器参数的验证
上下文过滤器可以附加验证要求。如果没有指定额外的验证,视图模块将采用预期的参数并尝试使其正常工作。您可以添加验证来帮助限制此范围并过滤掉无效的路由参数。
您可以通过勾选“指定验证标准”从“当过滤器值在 URL 中或提供默认值时”部分来启用验证。默认设置为“基本验证”,这允许您指定视图在数据无效时应该如何反应。根据我们的配方,这将是如果找不到路由参数中的 ID 对应的用户。
验证器选项列表不会被您选择的上下文过滤器项过滤。因此,其中一些可能不适用。对于我们的配方,您可能希望选择“用户 ID”选项。此验证器将确保用户 ID 存在。
这为您提供了对视图如何操作以及在使用上下文过滤器时如何执行查询的细粒度控制。
多个参数和排除
您可能配置上下文过滤器以允许“AND”或“OR”操作,同时使用上下文过滤器值进行排除而不是包含。这些选项在添加或编辑上下文过滤器时位于“更多”部分。
“AND”或“OR”操作。如果上下文过滤器参数包含由加号(+
)连接的一系列值,则它充当“OR”操作。如果值由逗号(,
)连接,则它类似于“AND”操作。
当勾选了排除
选项时,值将被排除在结果之外,而不是基于该值限制结果。例如,使用本菜谱中提供的用户 ID,我们可以排除该用户创建的内容,以显示其他所有用户创建的内容。
在具有关系的视图中添加相关数据
如本章开头所述,视图是一个可视化查询构建器。当你首次创建视图时,会指定一个基础表,从中提取数据。视图模块会自动知道如何连接字段数据表,例如正文文本或自定义附加字段。
当使用实体引用字段时,你可以将值显示为标识符、引用实体的标签或整个渲染的实体。然而,如果你基于引用字段添加关系,你将能够显示该实体可用的任何字段。
在本菜谱中,我们将更新用于管理文件的文件视图,以显示上传文件的用户的用户名。
如何操作…
-
前往结构然后视图。这将带您到所有已创建视图的管理概览。
-
找到文件视图并点击编辑:
图 3.12 – 视图列表中的文件视图
-
点击高级以展开该部分,然后点击添加,它位于关系旁边。
-
搜索用户。选择上传用户的关系选项,然后点击应用(此显示)。
-
接下来,我们将看到一个关系配置表单。点击应用(此显示)以使用默认值。
-
通过在字段部分点击添加来添加一个新字段。
-
搜索名称并从用户类别中选择名称字段。然后,点击应用(所有显示):
图 3.13 – 用户类别中的名称字段
-
此视图使用聚合,当你首次添加字段时,会显示一个新的配置表单。点击应用并继续以使用聚合默认值。
-
我们将使用默认字段设置,这将提供名称标签,并以用户名和链接到用户个人资料的方式格式化。点击应用(所有显示)。
-
点击保存以完成编辑视图并提交您的更改。
-
当在
/admin/content/files
中查看文件列表时,现在将显示上传文件的用户的用户名。
它是如何工作的…
Drupal 以规范化的格式存储数据。简而言之,数据库规范化涉及将数据组织到特定相关的表中。每种实体类型都有自己的数据库表,所有字段也都有各自的数据库表。当你创建一个视图并指定将显示哪种数据时,你实际上是在指定数据库中的一个基础表,视图将查询这个表。视图会自动将属于实体及其关系的字段与那些表关联起来。
当一个实体有一个实体引用字段时,你可以向引用的实体类型表添加一个关系。这是一个显式定义,而字段是隐式的。当关系被显式定义时,所有引用的实体类型的字段都会进入作用域。然后可以在引用的实体类型上显示、过滤和排序字段。
更多内容...
使用视图中的关系可以创建一些强大的显示效果。现在我们将讨论关于关系的一些附加信息。
通过实体引用字段提供的关系
Views 模块使用一系列钩子来检索数据,然后使用这些数据来表示与数据库交互的方式。其中之一是hook_field_views_data
钩子,它处理字段存储配置实体并将其数据注册到 Views 中。Views 模块代表 Drupal 核心实现此钩子,以添加实体引用字段的正向和反向关系。
由于实体引用字段具有固定的模式信息,Views 可以通过理解字段表名、目标实体表名和目标实体标识列来动态生成这些关系。
通过自定义代码提供的关系
有时候,你需要使用自己的自定义代码在数据库中定义一个关系。将数据库表暴露给 Views 模块的一个例子是在hook_views_data
钩子中,用于向 Views 模块暴露其数据库信息。
例如,dblog_schema
钩子实现返回 watchdog 数据库表的uid
列。这是用户表的键外键,用于将日志与用户关联起来。该列随后通过以下定义暴露给视图:
$data['watchdog']['uid'] = [
'title' => t('UID'),
'help' => t('The user ID of the user on which the log
entry was written.'),
'field' => [
'id' => 'standard',
],
'filter' => [
'id' => 'numeric',
],
'argument' => [
'id' => 'numeric',
],
'relationship' => [
'title' => t('User'),
'help' => t('The user on which the log entry as
written.'),
'base' => 'users_field_data',
'base field' => 'uid',
'id' => 'standard',
],
];
这个数组告诉 Views,watchdog
表有一个名为uid
的列。它在显示、过滤和排序方面是数值型的。关系键是一个信息数组,指导 Views 如何使用它来在users
表上提供关系(LEFT JOIN
)。用户实体使用users
表,其主键为uid
。
提供实体引用结果视图
实体引用字段允许您引用其他实体。通常,这与内容一起使用,以引用分类术语或相关内容。默认情况下,实体引用将显示所有可引用的实体。然而,使用视图模块及其实体引用视图显示类型,您可以提供更受控的结果。
在这个菜谱中,我们将创建一个实体引用视图,该视图根据当前作者创建的内容过滤引用。然后,我们将向用户账户表单添加一个字段,允许用户选择他们最喜欢的贡献内容。
如何做到这一点...
-
前往结构然后视图。这将带您到所有已创建视图的管理概览。点击添加视图以创建新的视图。
-
将视图名称设置为我的内容引用视图并保持当前的视图设置配置。
-
不要选择创建页面或块。点击保存并编辑以继续编辑您的视图。
-
点击添加按钮以创建新的显示。选择实体引用选项以创建新的显示:
图 3.14 – 添加显示的下拉菜单
-
样式格式将自动设置为实体引用列表旁边的设置以修改样式格式设置。
-
对于搜索字段,勾选内容:标题选项,然后点击应用。这就是该字段将执行自动完成搜索的内容:
图 3.15 – 实体引用样式选项
-
然后,我们将使用上下文过滤器来限制结果仅限于当前登录用户。在高级部分点击上下文过滤器中的添加。
-
在内容类别中选择作者选项,然后点击添加和配置上下文过滤器。
-
将当过滤器值不可用设置更改为提供默认值。从类型选择列表中选择从登录用户获取用户 ID。点击应用以添加上下文过滤器:
图 3.16 – 带有默认值的用户上下文过滤器
-
点击保存以保存视图。
-
在管理工具栏中转到配置,然后点击账户设置以点击管理字段来配置用户账户上的字段。
-
添加一个新的引用字段,该字段引用内容。命名为突出贡献并允许其具有无限值。点击保存字段设置按钮。
-
将引用类型方法更改为使用视图:通过实体引用视图过滤并选择我们刚刚创建的视图。点击保存设置按钮。
-
现在,当用户编辑他们的账户时,他们只能引用他们创建的内容作为此引用字段的值。
它是如何工作的…
实体引用字段定义提供了选择插件。视图模块提供了一个实体引用选择插件。这允许实体引用将数据收集到视图中以接收可用结果。
视图的显示类型要求您选择在使用自动完成小部件时将使用哪些字段进行搜索。如果您不使用自动完成小部件,而是使用选择列表或复选框和单选按钮,则它将返回视图显示的全部结果。
使用视图显示图表
在本食谱中,我们将使用图表模块通过视图模块创建图表。图表模块将不同的绘图库与视图模块集成。本食谱中创建的视图将根据从统计模块生成的统计数据显示图表,以图表化 Drupal 站点上的内容访问。
利用图表模块创建一个渲染数据图表的视图。
准备工作
本食谱需要足够的数据来放入图表中。我们将使用来自统计模块的数据,该模块跟踪内容页面浏览量。为了生成内容和查看统计信息,我们将使用Devel(开发)模块。此模块提供了生成示例内容和填充统计信息的方法。
要使用Devel
生成内容,我们必须使用 Composer 添加它,并使用Drush安装它:
composer require drupal/devel
php vendor/bin/drush en devel_generate statistics --yes
现在,我们可以生成内容。登录到您的 Drupal 站点并访问/admin/config/development/generate/content
。勾选文章复选框以生成文章。确保勾选为每个节点(node_counter 表)添加统计信息复选框,以便生成统计信息。按生成以生成示例内容。
如何做…
-
首先,我们必须使用 Composer 将图表模块添加到 Drupal 站点,并使用
Drush
安装它及其 Google 图表子模块:composer require drupal/charts
php vendor/bin/drush en charts charts_google --yes
-
前往结构然后视图。这将带您到所有已创建视图的管理概览。点击添加视图以创建新的视图。
-
将视图名称设置为内容统计,并保留默认的视图****设置值。
-
打开创建页面复选框以创建页面。使用提供的默认值。在页面显示设置下,将显示格式更改为字段的图表:
图 3.17 – 图表视图的页面设置
-
点击保存并编辑以继续对视图进行工作。
-
在字段部分,点击添加。在内容统计类别下搜索总浏览量。点击添加和配置字段以继续。
-
打开创建标签复选框,并将标签设置为总浏览量。点击应用以完成字段的添加。
-
接下来,我们需要配置图表以使用我们的总浏览量字段作为图表的数据。在格式部分,点击图表旁边的设置。
-
从图表库下拉菜单中选择Google。
-
保持标签字段选项设置为内容:标题。在提供数据列中,勾选totalcount复选框:
图 3.18 – 图表样式设置
-
点击应用以设置图表设置。
-
点击保存以保存您的视图。
-
访问
/content-statistics
以查看使用统计图表。
它是如何工作的…
图表模块提供了一个 API,可以与各种图表库(如 Google Charts、Highcharts 等)集成。开发者可以使用自定义代码创建图表,但其中一个最伟大的功能是它与视图的集成。
图表模块允许您以各种图表类型显示值。它还允许您提供多种数据类型以创建高级图表。每个图表库都有各种可配置的设置。
参见
-
Drupal.org 上的图表模块项目页面:
www.drupal.org/project/charts
第四章:使用自定义代码扩展 Drupal
Drupal 最伟大的组成部分是通过模块的可扩展性。在本章中,我们将探讨如何创建一个可以在你的 Drupal 站点上安装的自定义模块。本章将解释 PSR-4 自动加载如何与扩展一起工作,以及如何利用类自动加载。你将能够为自定义页面创建控制器并指定额外的权限以检查用户是否拥有它们。你还将了解 Drupal 中的钩子和事件是什么,以及如何与之交互。本章还为后续章节奠定了基础。
本章将涵盖以下内容:
-
创建一个模块
-
为你的模块提供配置设置
-
定义权限并检查用户是否有访问权限
-
钩入 Drupal 以响应实体更改
-
创建一个事件订阅者以响应事件
-
创建自定义 Drush 命令
技术要求
你可以在 GitHub 上找到本章使用的完整代码:github.com/PacktPublishing/Drupal-10-Development-Cookbook/tree/main/chp04
创建一个模块
扩展 Drupal 的第一步是创建一个自定义模块。尽管这项任务听起来令人畏惧,但可以通过几个简单的步骤完成。模块可以提供其他模块提供的功能性和定制,或者它们可以用作包含配置和站点状态的方式。
在这个示例中,我们将通过定义其modulename.info.yml
文件来创建一个模块,这是一个包含 Drupal 用于发现扩展和安装模块的信息的文件。
如何操作…
-
在你的
web/modules
目录中,创建一个名为custom
的新目录,然后在其中创建一个名为mymodule
的子目录。这将是你模块的目录。使用命令行,你可以使用以下命令创建目录:mkdir -p web/modules/custom/mymodule
这将创建所需的目录。
-
在你的模块目录中创建一个名为
mymodule.info.yml
的文件。这将包含标识模块给 Drupal 的元数据。 -
在
mymodule.info.yml
文件中添加一行,使用name
键为模块提供名称:name: My Module
-
我们必须使用键
type
定义扩展的类型。Drupal 不会仅通过目录位置来假设扩展类型:type: module
-
description
键允许你提供有关模块的额外信息,这些信息将在模块列表页面上显示:description: This is an example module from the Drupal
Development Cookbook!
-
扩展需要提供一个
core_version_requirement
,以使用语义化版本控制约束来标识模块与哪些版本的 Drupal 核心兼容:core_version_requirement: '>=10'
-
保存
mymodule.info.yml
文件,其内容如下:name: My Module
type: module
description: This is an example module from the Drupal
Development Cookbook!
core_version_requirement: '>=10'
-
接下来,在你的
module
文件中创建一个名为mymodule.module
的文件。这是允许我们添加钩子定义的扩展文件。它是一个普通的 PHP 文件,但文件扩展名与其扩展类型匹配。 -
对于这个示例,我们将提供一个在每次有效载荷上渲染消息的钩子:
<?php
/**
* Implements hook_page_top().
*/
function mymodule_page_top() {
\Drupal::messenger()->addStatus('Hello world!');
}
这实现了 hook_page_top
,每当页面被渲染时都会被调用。它使用消息传递服务向页面添加状态消息。
-
使用 Drush 安装您的模块:
php vendor/bin/drush en mymodule --yes
-
访问您的 Drupal 网站。
Hello world!
消息将被添加到每个页面:
图 4.1 – 显示“Hello world!”的 Drupal 页面
它是如何工作的…
Drupal 利用 info.yml
文件来定义扩展。Drupal 有一个扩展发现系统,它会定位这些文件并解析它们以发现模块。扩展发现将扫描您的整个 Drupal 代码库,并将 Drupal 核心目录优先考虑。
在这个配方中,我们为 core_version_requirement
约束提供了 >=10
。这个约束允许您的模块与最低的 Drupal 10.0.0 版本兼容,同时也与 Drupal 11 或更高版本兼容,简化了在下一个 Drupal 核心主要版本发布时的维护工作。如果您知道您的代码与之前的次要版本不兼容,它可能需要更新到 >=10.1.0
或甚至特定的补丁值 >=10.2.1
。
一种与 Drupal 集成的方法是使用其钩子系统。在运行时,其他模块可能会调用其他模块可以实现的钩子,以执行操作或修改数据。我们的配方实现了 hook_page_top
钩子。这个钩子是页面渲染生命周期的一部分,允许您在页面的最顶部添加可渲染内容。
模块命名空间
Drupal 使用由 PHP 框架互操作性小组(PHP-FIG)开发的 PSR-4 标准。PSR-4 标准是针对基于包的 PHP 命名空间自动加载类的,并被大多数库和框架使用,包括 Laravel 和 Symfony。它定义了一个标准,以了解如何根据命名空间和类名自动包含类。Drupal 模块在 Drupal 根命名空间下有自己的命名空间。
使用配方中的模块,我们的 PHP 命名空间将是 Drupal\mymodule
,这代表 web/modules/mymodule/src
文件夹。
使用 PSR-4,文件只需要包含一个类、接口或特质。这些文件需要与包含的类、接口或特质的名称具有相同的文件名。这允许类加载器将命名空间解析为目录路径,并知道类的文件名。当文件在文件中使用时,它将被自动加载。
创建 composer.json 文件
如果您正在编写一个仅将在您的网站上使用的自定义模块,这并不是必需的。然而,如果您计划将您的代码贡献给 Drupal.org 并分发它,您应该提供一个 composer.json
文件。
注意
Drupal.org 上的项目不需要创建 composer.json
文件。如果没有,将根据其 info.yml
文件的内容自动为其生成一个。这就是为什么建议创建一个的原因:为了明确。
在你的模块目录中创建一个 composer.json
文件。它看起来与 mymodule.info.yml
文件类似,但格式为 JSON:
{
"name": "drupal/mymodule",
"type": "drupal-module",
"description": "This is an example module from the Dru
pal Development Cookbook!",
"require": {
"drupal/core": ">=10"
}
}
name
键应该以 drupal/
前缀开头,以标识它位于 Drupal 包命名空间中。类型以 drupal-
前缀开头,以与我们在 第一章 中介绍的 Up and Running with Drupal 的 Composer 安装程序包兼容。
core_version_requirement
被转换为 Composer 的依赖定义,并针对 drupal/core
包。
还有更多...
关于 Drupal 模块和模块 info.yml
文件,我们可以探索更多细节。
模块依赖项
模块可以定义依赖项以确保在安装你的模块之前安装那些模块。
下面是 Pathauto
模块的 info.yml
文件的一个示例:
name: 'Pathauto'
description: 'Provides a mechanism for modules to automati
cally generate aliases for the content they manage.'
type: module
dependencies:
- ctools:ctools
- drupal:path
- token:token
dependencies
键指定在安装 Pathauto
模块之前必须先安装来自 Drupal 核心的 ctools
路径和 token
模块。在 第一章 的 Up and Running with Drupal 中,当我们安装 Pathauto
模块时,它们会自动安装。
Drupal 一直支持包含附加子模块的模块,这在其他系统中是不常见的做法。随着 Drupal 采用了 Composer,它强制在 info.yml
文件中实施命名空间依赖。这标识了根包以及它包含要安装的特定模块。
如果你的模块有依赖项并且你打算贡献它,请记住创建一个 composer.json
文件并定义你的依赖项,这样它们就会与 Composer 一起下载。
更多信息...
-
参考关于 PSR-4: 自动加载 的 规范:
www.php-fig.org/psr/psr-4/
-
通过添加
core_version_requirement
和其添加的原因来更改记录:www.drupal.org/node/3070687
-
Drupal.org 的模块创建文档:
www.drupal.org/docs/creating-custom-modules
为你的模块提供配置设置
模块可以利用配置设置来允许最终用户修改它们的工作方式。这些配置项是 YAML 文件。模块还可以在安装时为其他模块提供默认配置。一旦模块被安装,它提供的默认配置就会被导入到 Drupal 中。模块还可以通过安装钩子或更新钩子程序化地修改现有配置。
在这个菜谱中,我们将提供一个配置,创建一个新的联系表单,然后通过更新钩子来操作它。
准备工作
这个菜谱需要一个自定义模块,就像第一个菜谱中创建的那样。在这个菜谱中,我们将把这个模块称为 mymodule
。在需要的地方使用你模块的适当名称。
如何做…
-
在你的模块目录中创建一个
config
文件夹。然后,在那个目录中创建一个install
目录。Drupal 在这个安装目录中查找 YAML 配置:mkdir -p config/install
-
在
install
目录中创建一个contact.form.contactus.yml
文件来存储联系表单的 YAML 定义,即Contact Us
。 -
将以下 YAML 内容添加到
contact.form.contactus.yml
文件中:langcode: en
status: true
dependencies: { }
id: contactus
label: 'Contact Us'
recipients:
- webmaster@example.com
reply: ''
weight: 0
这个 YAML 文件代表了一个联系表单的导出配置对象。id
是联系表单的机器名,label
是用户界面的显示名称。recipients
键是一个有效的电子邮件地址的 YAML 数组。reply
键是用于 自动回复
消息的文本字符串。最后,weight
定义了表单在管理列表中的顺序。
注意
通常,你不会手动编写像这样的配置 YAML。如果需要,你通常将从 Drupal 网站中单独导出它。
-
使用 Drush 安装模块:
php vendor/bin/drush en mymodule --yes
-
现在 联系我们 表单将位于 联系表单 概览页上,位于 结构 之下。
-
在模块的目录中创建一个
mymodule.post_update.php
文件。这个文件包含在架构更改后要运行的更新钩子。 -
我们将创建一个名为
mymodule_post_update_change_contactus_reply()
的函数,该函数将由更新系统执行以修改联系表单的配置:<?php
/**
* Update "Contact Us" form to have a reply message.
*/
function mymodule_post_update_change_contactus_reply()
{
$contact_form = \Drupal\contact\Entity\Contact
Form::load('contactus');
$contact_form->setReply(t('Thank you for contacting
us, we will reply shortly'));
$contact_form->save();
}
这个函数使用实体的类来加载联系表单实体对象。它加载了 联系我们 的联系表单,这是我们的模块提供的,并将回复属性设置为新的值。
-
使用 Drush 为 Drupal 网站运行更新:
php vendor/bin/drush updb
Drush 将列出要应用的所有更新,包括你刚刚编写的更新。更新函数的注释将被输出到要应用更新的列表中。在审查更改后,你可以在命令行中输入 yes
来告诉 Drush 继续执行。
- 审查 联系我们 表单设置并验证回复消息是否已设置。
它是如何工作的…
Drupal 的 moduler_installer
服务,通过 \Drupal\Core\Extension\ModuleInstaller
提供,确保在安装时处理模块的 config
文件夹中定义的配置项。当安装模块时,通过 \Drupal\Core\Config\ConfigInstaller
提供的 config.installer
服务被调用以处理模块的默认配置。
如果 config.installer
服务尝试从模块的 config/install
文件夹导入已存在的配置,将抛出异常。模块不能通过 YAML 文件提供重复的配置或修改现有的配置对象。
由于模块不能通过提供给 Drupal 的 YAML 文件调整配置对象,它们可以利用更新系统来修改配置。更新系统有两个更新过程:模式更新和后续更新。由于我们没有进行模式级别的更改,我们使用了后续更新过程。这允许我们对现有配置对象进行修改。
在第七章《使用表单 API 创建表单》中,我们将创建一个用于修改配置设置的表单。
更多内容...
现在我们将深入探讨在处理模块和配置时的一些重要注意事项。
配置子目录
配置管理系统将在模块的config
文件夹中检查三个目录,具体如下:
-
install
-
可选
-
schema
install
文件夹指定要导入的配置。如果配置对象存在,安装将失败。optional
文件夹包含在满足以下条件时将安装的配置:
-
配置尚不存在
-
这是一个配置实体
-
它的依赖关系可以满足
如果任何一个条件失败,配置将不会安装,但不会停止模块的安装过程。schema
文件夹提供配置对象定义。
在安装时修改现有配置
配置管理系统不允许模块在已存在的安装上提供配置。例如,如果模块尝试提供system.site
并定义站点名称,它将无法安装。这是因为当您首次安装 Drupal 时,system
模块提供了这个配置对象。
模块也可能有一个.install
文件,例如我们食谱模块的mymodule.install
。这个文件是模块可能实现 Drupal 提供的hook_install
钩子和模式更新钩子的地方。
hook_install()
在模块的安装过程中执行。以下代码将在模块安装时将站点标题更新为Drupal Development Cookbook
!
<?php
/**
* Implements hook_install().
*/
function mymodule_install() {
// Set the site name.
\Drupal::configFactory()
->getEditable('system.site')
->set('name', 'Drupal Development Cookbook!')
->save();
}
可配置对象默认是不可变的,这意味着当通过默认的config
服务加载时,它们不能被更改或保存。要修改配置对象,您需要使用配置工厂来接收一个可编辑的配置对象实例。这个对象可以有set
和save
方法,这些方法被执行以更新配置对象中的配置。
模式更新钩子
本食谱提到了模式更新钩子。这些钩子旨在用于更改任何数据库模式或实体字段定义。当更新系统运行时,首先运行模式钩子;然后执行后续更新。
架构更新钩子被定义为 hook_update_N
,其中 N
是一个数字架构版本值。当架构更新钩子被执行时,它们将按照它们的架构版本顺序运行。通常,基本架构版本基于 Drupal 核心的主版本或模块的版本。在自定义代码中,它可以是你想要的任何东西。
架构更新的命名约定自 Drupal 8 以来一直在讨论中,涉及对贡献项目的语义版本支持。以下问题中讨论了这些命名约定:
参见
-
在 Drupal.org 上更新 API 文档:
www.drupal.org/docs/drupal-apis/update-api
-
第七章,使用表单 API 创建表单
定义权限和检查用户是否有权访问
在 Drupal 中,角色和权限被用来定义用户强大的访问控制列表。模块使用权限来检查当前用户是否有权执行操作、查看特定项目或执行其他操作。然后模块定义了使用的权限,以便 Drupal 能够了解它们。开发者可以构建角色,这些角色由启用的权限组成。
在这个配方中,我们将在一个模块中定义新的权限,该模块用于检查用户是否可以将内容标记为推广到首页或粘性在列表顶部。此权限将用于实体字段访问钩子,如果用户缺少权限,则拒绝访问字段。
准备工作
创建一个新的模块,就像我们在第一个配方中所做的那样。在这个配方中,我们将把这个模块称为 mymodule
。在以下配方中,根据需要使用你的模块名称。
创建一个新的具有内容编辑角色的 Drupal 用户。Drupal 会绕过第一个用户的访问检查。次要用户将需要展示权限。
如何做到这一点...
-
权限存储在
permissions.yml
文件中。在你的模块基本目录中添加一个mymodule.permissions.yml
文件。 -
首先,我们需要定义用于识别此权限的内部字符串,例如
can
promote nodes
:can promote nodes:
-
每个权限都是一个包含数据的 YAML 数组。我们需要提供一个
title
键,它将在权限页面上显示:can promote nodes:
title: 'Can promote content'
-
权限有一个
description
键,用于在权限页面上提供权限的详细信息:can promote nodes:
title: 'Can promote content'
description: 'Determines if the user can change pro
motion fields on content.'
-
保存你的
mymodule.permissions.yml
文件,并编辑模块的mymodule.module
文件,以便我们可以编写钩子来使用权限。 -
在你的
mymodule.module
文件中,添加一个名为mymodule_entity_field_access
的函数来实现hook_entity_field_access
。这将用于在实体表单的每个字段上以细粒度控制访问:function mymodule_entity_field_access(
$operation,
\Drupal\Core\Field\FieldDefinitionInterface
$field_definition,
\Drupal\Core\Session\AccountInterface $account
) {
return \Drupal\Core\Access\AccessResult::neutral();
}
Drupal 使用访问结果值对象来处理访问结果。访问结果可能是中立的、禁止的或允许的。实现hook_entity_field_access
钩子的插件必须返回一个访问结果,并且不能返回null
。
-
在我们的钩子中,我们将检查正在检查的字段名称。如果字段名称是
promote
或sticky
,我们将检查用户是否有can promote nodes
权限,并返回该访问结果:function mymodule_entity_field_access(
$operation,
\Drupal\Core\Field\FieldDefinitionInterface
$field_definition,
\Drupal\Core\Session\AccountInterface $account
) {
$field_name = $field_definition->getName();
if ($field_name === 'promote' || $field_name ===
'sticky') {
$can_promote_nodes = $account->hasPermission('can
promote nodes');
return Drupal\Core\Access\AccessResult::allowedIf
($can_promote_nodes);
}
return \Drupal\Core\Access\AccessResult::neutral();
}
访问结果对象有一个allowedIf
方法,根据提供的参数返回适当的结果。在这种情况下,如果用户有权限,它将返回AccessResult::allowed()
,如果没有,则返回AccessResult::neutral()
。
注意
Drupal 的访问系统需要明确的允许。如果一个访问结果是中立的,系统将继续处理访问结果。如果一个访问结果被返回为禁止或允许,访问检查将停止,并使用该结果。如果最终结果是中立的,由于没有明确允许,访问将不被授予。
- 新权限不会自动授予角色。在另一个浏览器或访客标签页中,以具有内容编辑角色的用户身份登录,并创建一个内容项。由于我们没有字段访问权限,侧边栏的推广选项部分将缺失:
图 4.2 – 由于缺少权限,推广选项被隐藏
- 作为您的管理用户,请前往人员然后进入权限,在我的****模块部分添加您对内容编辑角色的权限:
图 4.3 – 向内容编辑角色添加权限
- 使用您的内容编辑用户,再次尝试创建一个内容项。侧边栏中的推广选项部分将出现:
图 4.4 – 授予权限后,推广选项部分出现
它是如何工作的…
权限和角色由User
模块提供。user.permissions
服务发现由安装的模块提供的permissions.yml
文件。默认情况下,服务是通过\Drupal\user\PermissionHandler
类定义的。
Drupal 不会保存所有可用权限的列表。当加载权限页面时,系统的权限被加载。角色包含一个权限数组。
当检查用户的权限访问时,Drupal 会检查所有用户角色,以查看它们是否支持该权限。
注意
您可以将未定义的权限传递给用户访问检查,而不会收到错误。除非用户是 UID 1,否则访问检查将简单地失败。在 Drupal 中,UID 1 是根用户,不受安全检查或权限的约束。在授予该账户访问权限或使用用户 1 进行测试时要小心。
更多内容...
在接下来的章节中,我们将介绍更多在您的模块中处理权限的方法。
权限的访问限制标志
如果启用,权限可以标记为具有安全风险;这可以通过设置访问限制标志来完成。当此标志设置为restrict access: TRUE
时,它将在权限的描述中添加一个警告。
这允许模块开发者提供更多关于权限可能赋予用户的控制量的背景信息:
图 4.5 – 带有访问限制标志的权限示例
我们从配方中定义的权限看起来会是这样:
can promote nodes:
title: 'Can promote content'
description: 'Determines if the user can change promotion
fields on content.'
Restrict access: TRUE
通过编程定义权限
权限可以通过模块以编程方式或静态方式在 YAML 文件中定义。模块需要在它的permissions.yml
文件中提供一个permission_callbacks
键,该键包含一个可调用的方法或函数数组,以动态定义权限。
例如,Filter
模块根据在 Drupal 中创建的不同文本过滤器提供细粒度的权限:
permission_callbacks:
-Drupal\filter\FilterPermissions::permissions
这告诉user_permissions
服务执行\Drupal\Filter\FilterPermissions
类的权限方法。该方法预期返回一个与permissions.yml
文件相同结构的数组。
参见...
- 第五章,创建 自定义页面
将 Drupal 钩入以响应实体更改
最常见的集成点之一是将 Drupal 钩入以响应实体的创建、读取、更新和删除操作。实体系统还有钩子,在实例化新实体并在保存之前修改它时提供默认值。
在这个配方中,我们将创建一个钩子,每当新内容发布时都会运行,并发送一封电子邮件到网站的电子邮件地址作为新内容的通知。
如何做到...
-
首先,在您的
module
文件中创建一个名为mymodule.module
的文件。这是模块扩展文件,用于存储钩子实现。 -
接下来,我们将实现一个钩子来监听新节点实体被插入。创建一个名为
mymodule_node_insert
的函数,它是hook_ENTITY_TYPE_insert
钩子的实现:<?php
function mymodule_node_insert(\Drupal\node\
NodeInterface $node) {
}
-
在我们的
insert
钩子中,我们将检查节点是否被保存为已发布。如果是,我们将发送电子邮件通知:<?php
function mymodule_node_insert(\Drupal\node\
NodeInterface $node) {
if ($node->isPublished()) {
$site_mail = \Drupal::config('system.site')->
get('mail');
/** @var \Drupal\Core\Mail\MailManager
$mail_service */
$mail_service = \Drupal::service(
'plugin.manager.mail');
$mail_service->mail(
module: 'mymodule',
key: 'node_published',
to: $site_mail,
langcode: 'en',
params: ['node' => $node],
);
}
}
首先,我们检查节点是否已发布。节点实体类型实现了EntityPublishedInterface
接口,该接口提供了isPublished
方法。如果节点已发布,我们从配置中获取网站的电子邮件地址。要发送电子邮件,我们需要获取邮件管理器服务。使用邮件管理器服务,我们调用mail
方法。module
和key
参数用于在模块中调用另一个钩子以生成电子邮件内容。to
参数是电子邮件应发送的位置。langcode
表示电子邮件应发送的语言。最后,params
参数为生成电子邮件内容的钩子提供上下文值。
-
我们希望添加一个钩子,监听节点更新时的情况,因为它们可能最初被保存为未发布。创建一个名为
mymodule_node_update
的函数,以便我们可以实现hook_ENTITY_TYPE_update
钩子:function mymodule_node_update(\Drupal\node\NodeInter
face $node) {
}
-
在我们的
update
钩子中,我们将检查节点未更改的版本是否已发布。我们不希望发送重复的电子邮件。只有当节点之前未发布然后变为发布时,我们才发送电子邮件:function mymodule_node_update(\Drupal\node\NodeInter
face $node) {
if ($node->isPublished()) {
/** @var \Drupal\node\NodeInterface $original */
$original = $node->original;
if (!$original->isPublished()) {
$site_mail = \Drupal::config('system.site')->
get('mail');
/** @var \Drupal\Core\Mail\MailManager
$mail_service */
$mail_service = \Drupal::service('plugin
.manager.mail');
$mail_service->mail(
module: 'mymodule',
key: 'node_published_update',
to: $site_mail,
langcode: 'en',
params: ['node' => $node],
);
}
}
}
如您所见,此钩子几乎与我们的insert
钩子相同,但我们检查原始节点对象的值。实体存储在具有从数据库中未更改的值的实体上设置原始属性。这允许我们比较先前值和新的修改值。在我们的钩子中,我们在发送电子邮件之前验证原始节点尚未发布。
注意
此钩子使用node_published_update
键,这样我们就可以使用不同的电子邮件文本。
-
现在,我们需要创建一个名为
mymodule_mail
的函数,该函数实现hook_mail
。这将允许我们定义电子邮件通知的内容:function mymodule_mail($key, array &$message, $params) {
}
key
和params
参数是我们传递给邮件管理器mail
方法的值,以及我们如何识别要生成的内容。message
属性是一个表示要发送的电子邮件的数组,例如收件人和内容。
-
在我们的
mail
钩子中,我们将根据电子邮件是针对新发布的节点还是变为发布的update
节点提供不同的主题,以及一条消息:function mymodule_mail($key, array &$message, $params)
{
/** @var \Drupal\node\NodeInterface $node */
$node = $params['node'];
if ($key === 'node_published_insert') {
$message['subject'] = 'Newly published node: ' .
$node->label();
}
elseif ($key === 'node_published_update') {
$message['subject'] = 'Existing node published: '
. $node->label();
}
else {
// Unknown key.
Return;
}
$message['body'][] = 'The following node has been
published:';
$message['body'][] = $node->label();
$message['body'][] = $node->toUrl()->setAbsolute()
->toString();
}
我们检查key
值,并根据我们定义的键设置适当的电子邮件主题。消息中的body
键期望一个文本数组,Drupal 将将其转换为新行。
- 现在,每当节点发布时,都会向网站的电子邮件地址发送电子邮件,通知网站管理员新发布的内容。
它是如何工作的…
Drupal 中的实体系统具有各种触发钩子,用于在实体加载、创建(新实体的实例化)、保存或删除时与实体交互。在本配方中,我们监听了两个保存后钩子。一旦实体已保存,实体存储的保存后过程将调用新实体的insert
钩子或现有实体的update
钩子。
在这个配方中,我们使用了针对特定实体类型的钩子。这使我们能够在代码中更加简洁,并在钩子中使用适当的实体接口进行类型提示。每个实体操作钩子也可以通用实现。如果我们使用 hook_entity_insert
或 hook_entity_update
,它们将针对任何实体类型触发,例如分类术语或块。当使用更通用的钩子实现时,您需要使用 \Drupal\Entity\EntityInterface
进行类型提示,并使用 getEntityTypeId
方法来检查实体的类型。
实体的创建、读取、更新和删除操作的可用钩子已在 Drupal.org 上文档化,包括每个钩子的详细信息以及示例:api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/group/entity_crud/10.0.x
.
还有更多...
这个配方涵盖了挂钩到后保存钩子。在下一节中,我们将探讨其他可用的钩子。
在保存实体之前更改值
insert
和 update
钩子在实体保存后触发。还有一个预保存钩子,允许你在保存实体之前对其进行操作。
此钩子通常用于填充空值或确保值与预期的状态相匹配。例如,在 使用内容审核创建编辑工作流程 的配方中,位于 第二章,内容构建体验,我们使用了 Content Moderation
模块。Content Moderation
模块使用 hook_entity_presave()
钩子来确保内容根据工作流程的状态被标记为已发布或未发布。
创建事件订阅者以响应事件
Drupal 有两种方式来集成系统的各个部分:使用钩子或事件。钩子一直是 Drupal 生命周期的一部分,而事件是在 Drupal 8 中引入的。与具有隐式注册的钩子系统不同,事件调度系统使用显式注册来注册事件。
事件调度器系统来自 Symfony 框架,允许组件轻松地相互交互。在 Drupal 中,以及集成的 Symfony 组件中,事件被调度,事件订阅者可以监听事件并对更改或其他过程做出反应。
在这个配方中,我们将订阅 RequestEvent
事件,该事件在请求首次处理时触发。如果用户未登录,我们将将其导航到登录页面。
如何做到这一点...
-
在您的模块中创建
src/EventSubscriber/RequestSubscriber.php
。 -
定义
RequestSubscriber
类,该类实现了EventSubscriberInterface
接口:<?php
namespace Drupal\mymodule\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriber
Interface;
class RequestSubscriber implements EventSubscriber
Interface {
}
-
为了满足接口要求,我们必须添加一个
getSubscribedEvents
方法。这告诉系统我们正在订阅哪些事件以及需要调用的方法:<?php
namespace Drupal\mymodule\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriber
Interface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
class RequestSubscriber implements EventSubscriber
Interface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
RequestEvent::class => ['doAnonymousRedirect',
28],
];
}
}
事件名称是从事件对象的 PHP 类名派生出来的。在 getSubscribedEvent
方法中,我们构建一个关联数组以返回。事件类名是键,当分发该事件时将调用的我们的类方法。
注意
优先级将在 如何工作... 部分中讨论。它提供在 dynamic_page_cache
模块启用时解决可能冲突的示例。
-
创建我们指定的
doAnonymousRedirect
方法,它将接收当前请求的RequestEvent
对象:<?php
namespace Drupal\mymodule\EventSubscriber;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriber
Interface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
class RequestSubscriber implements EventSubscriber
Interface {
/**
* Redirects all anonymous users to the login page.
*
* @param \Symfony\Component\HttpKernel\Event\
RequestEvent $event
* The event.
*/
public function doAnonymousRedirect(RequestEvent
$event) {
// Make sure we are not on the user login route.
if (\Drupal::routeMatch()->getRouteName() ==
'user.login') {
return;
}
// Check if the current user is logged in.
if (\Drupal::currentUser()->isAnonymous()) {
// If they are not logged in, create a redirect
response.
$url = Url::fromRoute('user.login')->toString();
$redirect = new RedirectResponse($url);
// Set the redirect response on the event,
canceling default response.
$event->setResponse($redirect);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
RequestEvent::class => ['doAnonymousRedirect',
28],
];
}
}
为了防止重定向循环,我们将使用 RouteMatch
服务来获取当前路由对象,并验证我们是否已经在 user.login
路由页面。
然后,我们检查用户是否是匿名用户,如果是,将事件的响应设置为重定向响应。
-
现在我们已经创建了我们的类,在你的模块目录中创建一个
mymodule.services.yml
文件。 -
我们必须将我们的类注册到服务容器中,以便 Drupal 识别它将作为事件订阅者:
services:
mymodule.request_subscriber:
class: Drupal\mymodule\EventSubscriber\
RequestSubscriber
tags:
- { name: event_subscriber }
event_subscriber
标签告诉容器调用 getSubscribedEvents
方法并注册其方法。
-
如果模块已经安装,请安装模块或重建 Drupal 的缓存。
-
以匿名用户身份导航到任何页面 - 你将被重定向到登录表单。
它是如何工作的...
在 Drupal 和 Symfony 中,组件事件可以传递给事件分发器。Drupal 中的 event_dispatcher
服务是 Symfony 提供的优化版本,但完全兼容,并提供与 Symfony 的向后兼容层。当容器构建时,所有标记为 event_subscriber
的服务都会被收集。然后,它们被注册到 event_dispatcher
服务中,键是 getSubscribedEvents
方法返回的事件。
注意
Symfony 4.3 改变了事件分发的机制。以前,事件仅通过名称标识,事件对象作为值对象。在 Symfony 4.3 中,事件名称被设置为可选的。这也与 PHP-FIG 的 PSR-14 事件分发器相一致。
当 event_dispatcher
服务被告知分发一个事件时,它将调用所有已订阅服务上注册的方法。Drupal 仍然主要使用命名事件而不是事件对象,因为许多事件利用了相同的事件对象类。
\Symfony\Component\HttpKernel\KernelEvents
类记录了可用于与请求生命周期交互的事件,以成为响应,甚至在响应发送后,就像我们使用 RequestEvent
一样。然后,还有诸如 ConfigEvents::SAVE
和 ConfigEvents::DELETE
之类的其他事件被触发,允许你对配置的保存或删除做出反应,但不能直接通过事件对象调整配置实体。
还有更多...
事件订阅者需要了解创建服务、注册它们以及甚至依赖注入。我们将在下一节中进一步讨论这一点。
使用依赖注入
Drupal 利用一个服务容器,允许您声明类和服务并定义它们的依赖关系。对于服务,依赖关系是一个必须传递给其构造函数的参数。依赖注入是一种软件设计概念,在其基本层面上,它提供了一种使用类而无需直接引用它的方法。在我们的示例中,我们多次使用 \Drupal
全局静态类检索服务。这很方便,但在服务中是一种不良做法。它可能会使测试变得更加困难。
要实现依赖注入,首先,我们将向我们的类添加一个构造函数,该构造函数接受使用的服务(current_route_match
和 current_user
)并将它们与受保护的属性匹配以存储它们:
/**
* The route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Account proxy.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $accountProxy;
/**
* Creates a new RequestSubscriber object.
*
* @param \Drupal\Core\Routing\RouteMatchInterface
$route_match
* The route match.
* @param \Drupal\Core\Session\AccountProxyInterface
$account_proxy
* The current user.
*/
public function __construct(RouteMatchInterface
$route_match, AccountProxyInterface $account_proxy) {
$this->routeMatch = $route_match;
$this->accountProxy = $account_proxy;
}
我们可以替换任何对 \Drupal::
的调用为 $this->
:
/**
* Redirects all anonymous users to the login page.
*
* @param \Symfony\Component\HttpKernel\Event\Request
Event $event
* The event.
*/
public function doAnonymousRedirect(RequestEvent $event)
{
// Make sure we are not on the user login route.
if ($this->routeMatch->getRouteName() == 'user.login') {
return;
}
// Check if the current user is logged in.
if ($this->accountProxy->isAnonymous()) {
// If they are not logged in, create a redirect
response.
$url = Url::fromRoute('user.login')->toString();
$redirect = new RedirectResponse($url);
// Set the redirect response on the event, canceling
default response.
$event->setResponse($redirect);
}
}
最后,我们将更新 mymodule.services.yml
文件以指定我们的构造函数参数,以便在容器运行我们的事件订阅者时注入:
services:
mymodule.request_subscriber:
class: Drupal\mymodule\EventSubscriber\
RequestSubscriber
arguments: ['@current_route_match', '@current_user']
tags:
- { name: event_subscriber }
依赖注入一开始感觉和看起来很神奇。然而,随着使用和实践,它将开始变得更有意义,并在使用 Drupal 进行开发时变得习以为常。
参见
- PSR-14 事件调度器由 PHP-FIG:
www.php-fig.org/psr/psr-14/
创建自定义 Drush 命令
在整本书中,我们已使用 Drush 从命令行对 Drupal 网站执行操作。模块可以提供自定义 Drush 命令。这允许开发者创建帮助管理他们 Drupal 网站的命令。Drush 需要模块提供一个 composer.json
文件,该文件指示它们在哪里加载一个将注册 Drush 命令的服务文件。
在本食谱中,我们将创建一个新的 Drush 命令,用于打印 Drupal 安装的位置。
如何做到这一点...
-
Drush 提供了一个命令来生成创建自定义命令所需的文件。首先,运行以下命令:
php vendor/bin/drush generate drush-command-file
-
您将被提示提供
Module
机器名称的值;请输入mymodule
作为我们模块的名称。 -
接下来,按 Enter 键跳过转换旧版 Drush 命令文件。
-
命令输出将显示已创建或更新的文件:
图 4.6 – 命令文件生成器的输出
composer.json
文件包含有关 drush.services.yml
的信息。drush.services.yml
文件是一个服务定义文件,其中包含类及其参数。它为创建的 MymoduleCommands
类提供了一个初始定义。MymoduleCommands
类是用示例命令生成的。
-
drush.services.yml
文件很重要。它是一个服务定义文件,将服务标记为drush.command
,以便 Drush 收集类:services:
mymodule.commands:
class: \Drupal\mymodule\Commands\MymoduleCommands
tags:
- { name: drush.command }
-
命令在命令文件中定义为类方法。打开
src/Commands/MymoduleCommands.php
文件以创建一个新的命令。 -
创建一个名为
helloWorld
的新方法,我们将使用它来提供hello-world
命令:public function helloWorld() {
}
-
接下来,将以下代码添加到命令中,以输出消息并显示 Drupal 站点的目录:
public function helloWorld() {
$this->io()->writeln('Hello world!');
$self_alias = \Drush\Drush::aliasManager()->get
Self();
$drupal_root = $self_alias->root();
$this->io()->writeln("Drupal is located at: {$dru
pal_root}");
}
此方法使用io()
方法来获取输入/输出辅助工具,以便将内容写回终端。然后代码从别名管理器获取当前站点别名。Drush 支持使用别名连接到远程 Drupal 站点,其中“self”别名是当前本地 Drupal 站点。然后代码调用root
方法来显示 Drupal 代码库的目录。
-
最后,我们需要在方法的文档块中提供注解。这是 Drush 解释命令名的方式:
/**
* @command mymodule:hello-world
*/
public function helloWorld() {
$this->io()->writeln('Hello world!');
$self_alias = \Drush\Drush::aliasManager()->get
Self();
$drupal_root = $self_alias->root();
$this->io()->writeln("Drupal is located at: {
$drupal_root}");
}
-
现在,我们可以执行命令。首先,我们必须清除缓存,以便 Drupal 和 Drush 可以注册新的命令。然后,我们可以执行它:
php vendor/bin/drush cache-rebuild
php vendor/bin/drush mymodule:hello-world
它是如何工作的...
当 Drush 启动时,它会引导 Drupal。在启动过程中,它获取 Drupal 服务容器,并添加并注册\Drush\Drupal\FindCommandsCompilerPass
编译器传递。这个编译器传递扫描 Drupal 收集的services.yml
文件,并找到带有drush.command
标签的服务。这就是为什么模块必须有一个drush.services.yml
文件。这定义了服务,并适当地标记它们以便 Drush 发现。
由于 Drush 围绕 Drupal 构建,命令文件也可能被注入其他服务作为依赖。以下是一个使用Pathauto
模块依赖注入的drush.services.yml
文件示例:
services:
pathauto.commands:
class: \Drupal\pathauto\Commands\PathautoCommands
arguments:
- '@config.factory'
- '@plugin.manager.alias_type'
- '@pathauto.alias_storage_helper'
tags:
- { name: drush.command }
参见
- Drush 命令编写文档:
www.drush.org/latest/commands/
第五章:创建自定义页面
在本章中,我们将使用控制器创建自定义页面。控制器是一个类,它包含一个在 Drupal 访问特定路径时构建页面的方法。创建自定义页面允许你扩展 Drupal 超出仅内容页面的范围。本章将涵盖创建自定义页面、从路径接收动态值以及提供 JSON 或文件下载响应的过程。
在本章中,我们将学习以下配方:
-
定义控制器以提供自定义页面
-
使用路由参数
-
创建动态重定向页面
-
创建 JSON 响应
-
提供文件下载服务
技术要求
本章要求在你的 Drupal 网站上安装一个现有的自定义模块。这个自定义模块将包含在整个配方中创建的控制器。第四章,使用自定义代码扩展 Drupal,介绍了如何创建自定义模块。你可以在 GitHub 上找到本章使用的完整代码:github.com/PacktPublishing/Drupal-10-Development-Cookbook/tree/main/chp05
定义控制器以提供自定义页面
每当对 Drupal 网站发出 HTTP 请求时,URL 的路径会被路由到控制器。控制器负责返回已定义为路由的路径的响应。通常,这些控制器返回渲染数组供 Drupal 的渲染系统转换为 HTML。
在这个配方中,我们将定义一个控制器,它返回一个渲染数组以在页面上显示消息。
如何操作…
-
首先,我们需要在模块目录中创建
src/Controller
目录。我们将把我们的控制器类放在这个目录中,这给我们的controller
类赋予了Controller
命名空间:mkdir -p src/Controller
-
在控制器目录中创建一个名为
HelloWorldController.php
的文件。这将包含我们的HelloWorldController
控制器类。 -
我们的
HelloWorldController
类将扩展由 Drupal 核心提供的ControllerBase
基类:<?php
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
class HelloWorldController extends ControllerBase {
}
遵循 PSR-4 自动加载约定,我们的类位于 \Drupal\mymodule\Controller
命名空间中,Drupal 能够将其确定为我们的模块中的 src/Controller
目录。根据 PSR-4,文件名和类名也必须相同。
\Drupal\Core\Controller\ControllerBase
类提供了一些可以利用的实用方法。
-
接下来,我们将创建一个方法,该方法返回一个渲染数组以显示文本字符串。将以下方法添加到我们的
HelloWorldController
类中:/**
* Returns markup for our custom page.
*
* @returns array
* The render array.
*/
public function page(): array {
return [
'#markup' => '<p>Hello world!</p>'
];
}
页面方法返回一个渲染数组,Drupal 渲染系统将解析它。#markup
键表示一个没有额外渲染或主题化的值,并允许使用基本的 HTML 元素。
-
在你的模块目录中创建
mymodule.routing.yml
文件。routing.yml
文件由模块提供,用于定义路由。 -
定义路由的第一步是为路由提供一个名称,该名称用作其标识符,用于 URL 生成和其他目的:
mymodule.hello_world
-
为路由提供一个路径:
mymodule.hello_world:
path: /hello-world
-
接下来,我们将路径与我们的控制器注册。这是通过使用
defaults
键来完成的,我们在其中提供控制器和页面标题:mymodule.hello_world:
path: /hello-world
defaults:
_controller: Drupal\mymodule\Controller\
HelloWorldController::page
_title: 'Hello world!'
_controller
键是用于的完整类名,其中包含要使用的方法。_title
键提供了要显示的页面标题。
-
最后,定义一个
requirements
键来指定访问要求:mymodule.hello_world:
path: /hello-world
defaults:
_controller: Drupal\mymodule\Controller\
HelloWorldController::page
_title: 'Hello world!'
requirements:
_permission: 'access content'
_permission
选项告诉路由系统验证当前用户是否有查看页面的特定权限。
-
Drupal 缓存其路由信息。我们必须重建 Drupal 的缓存,以便它能够了解模块的新路由:
php vendor/bin/drush cr
-
访问您的 Drupal 站点上的
/hello-world
并查看您的自定义页面:
图 5.1 – /hello-world 页面
它是如何工作的...
Drupal 的路由建立在 Symfony 的路由组件之上。每个路由都有一个控制器类中的方法,该方法返回一个响应。最常见的是返回一个渲染数组。本章的其他食谱将向您展示如何直接返回响应对象。路由被收集到 Drupal 的路由系统中。
当 HTTP 请求到达 Drupal 时,系统会尝试将路径与已知路由匹配。如果找到路由,则使用路由的定义来提供页面。如果找不到路由,则显示 404 页面。如果找到路由,Drupal 将根据requirements
键执行访问检查。如果requirements
键条件失败,则 Drupal 可能返回 403 页面或 404 页面。
控制器返回响应后,Drupal 会检查该值是否是渲染数组或来自 Symfony 的HttpFoundation
组件的响应。MainContentViewSubscriber
类的onViewRenderArray
方法检查控制器响应是否为数组。如果是,则解析渲染器并将渲染数组转换为 HTML 响应。否则,它允许处理返回的响应对象。
当编写控制器时,不需要ControllerBase
基类;但它确实提供了对常见服务的简化访问,而无需设置依赖注入。
还有更多...
在接下来的章节中,我们将介绍更多关于路由的内容。
路由要求
路由可以通过requirements
键定义不同的访问要求。可以添加多个验证器。然而,必须有一个提供真值的结果,否则路由将返回 403,访问被拒绝。如果路由没有定义要求验证器,这也适用。
路由要求验证器是通过实现\Drupal\Core\Routing\Access\AccessInterface
定义的。以下列表显示了 Drupal 核心中定义的一些常见要求验证器:
-
_access: 'TRUE'
: 这始终授予路由访问权限。在授予此权限而不是权限或角色检查时要小心,因为这意味着任何人都可以访问该路由。 -
_entity_access
: 这验证当前用户能否执行实体操作。例如,node.update
验证用户能否更新 URL 参数中的节点。 -
_permission
: 这检查当前用户是否有提供的权限。 -
_user_is_logged_in
: 这验证用户是否已登录。'TRUE'
的值要求用户必须登录,而'FALSE'
是指用户已注销。'FALSE'
的一个例子是用户登录页面。
提供动态路由
路由系统允许模块以编程方式定义路由。这可以通过提供一个 routing_callbacks
键来实现,该键定义了一个类和将返回一个 \Symfony\Component\Routing\Route
对象数组的类和方法。
在模块的 routing.yml
文件中,你将定义路由回调键和相关类:
route_callbacks:
- 'Drupal\mymodule\Routing\CustomRoutes::routes'
Drupal\mymodule\Routing\CustomRoutes
类将会有一个名为 routes 的方法,该方法返回一个路由对象数组:
<?php
namespace Drupal\mymodule\Routing;
use Symfony\Component\Routing\Route;
class CustomRoutes {
public function routes() {
$routes = [];
// Create mypage route programmatically
$routes['mymodule.hello_world'] = new Route(
// Path definition
'/hello-world',
// Route defaults
[
'_controller' =>
'\Drupal\mymodule\Controller\
HelloWorldController::page',
'_title' => 'Hello world',
],
// Route requirements
[
'_permission' => 'access content',
]
);
return $routes;
}
}
如果一个模块提供了一个与路由交互的类,最佳实践是将它放在模块命名空间的路由部分。这有助于你识别其目的。
预期调用的方法返回一个已启动的路由对象数组。Route
类接受以下参数:
-
path
: 这是路由的路径。 -
defaults
: 这是路由及其控制器的默认值数组。 -
requirements
: 这是一个验证器数组,确保路由可访问。 -
options
: 这是一个数组,可以传递并可选地使用它来保存有关路由的元数据。
修改现有路由
当路由系统正在重建时,会触发一个事件来修改发现的路由。这涉及到实现事件订阅者。Drupal 提供了一个基类以简化此实现。在这里,\Drupal\Core\Routing\RouteSubscriberBase
实现了所需的接口方法并订阅了 RoutingEvents::ALTER
事件。
为你的模块创建一个 src/Routing/RouteSubscriber.php
文件来保存路由 subscriber
类:
<?php
namespace Drupal\mymodule\Routing;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
class RouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
public function alterRoutes(RouteCollection $collection) {
// Change path of mymodule.hello_world to just 'hello'
if ($route = $collection->get('mymodule.hello_world')) {
$route->setPath('/hello');
}
}
}
上述代码扩展了 RouteSubscriberBase
并实现了 alterRoutes
方法。如果存在,我们从路由集合中获取路由对象,并将路径从 /hello-world
更改为仅 /hello
。
然后,我们必须在模块的 mymodule.services.yml
文件中注册事件订阅者:
services:
mymodule.route_subscriber:
class: Drupal\mymodule\Routing\RouteSubscriber
tags:
- { name: event_subscriber }
添加此更改后,清除 Drupal 缓存。这使 Drupal 了解我们的事件订阅者,以便它可以对 RoutingEvents::ALTER
事件的触发做出反应。
参见
使用路由参数
在上一个菜谱中,我们定义了一个响应 /hello-world
路径的路由和控制器。路由可以向路径添加变量,称为路由参数,以便控制器可以处理不同的 URL。
在此菜谱中,我们将创建一个路由,它接受一个用户 ID 作为路由参数来显示该用户的信息。
开始
此菜谱使用在 定义控制器 菜谱中创建的路由来提供自定义页面。
如何操作…
-
首先,从上一个部分中删除
src/Routing/RouteSubscriber.php
文件和mymodule.services.yml
,并清除 Drupal 缓存,以免干扰我们即将要做的事情。 -
接下来,编辑
routing.yml
以便我们可以将user
路由参数添加到路径中:path: /hello-world/{user}
路由参数用开括号({
)和闭括号(}
)包裹。
-
接下来,我们将更新要求键,指定
user
参数应为用户 ID 的整数,并且当前用户有权查看其他配置文件:requirements:
user: '\d+'
_entity_access: 'user.view'
限制条件下的 user
键代表相同的路由参数。路由系统允许你提供正则表达式来验证路由参数的值。这验证了用户参数为任何数字值。
我们随后使用 _entity_access
要求来验证当前用户是否有权对用户类型实体执行视图操作。
-
然后,我们在路由定义中添加一个新的选项键,以便我们可以识别用户路由参数的类型:
options:
parameters:
user:
type: 'entity:user'
这定义了用户路由参数为 entity:user
类型。Drupal 将使用此类型将 ID 值转换为用户对象,供我们的控制器使用。
-
打开
src/Controller/HelloWorldController.php
文件,以便更新页面方法以接收用户路由参数值。 -
将页面方法更新为接收用户路由参数作为其方法参数的用户对象:
/**
* Returns markup for our custom page.
*
* @param \Drupal\user\UserInterface $user
* The user parameter.
*
* @returns array
* The render array.
*/
public function page(UserInterface $user): array {
return [
'#markup' => '<p>Hello world!</p>'
];
}
路由系统根据方法参数名称将路由参数值传递给方法。我们的参数定义指定 Drupal 应将 URL 中的 ID 转换为用户对象。
-
让我们调整从
Hello world!
返回的文本,用用户对象的电子邮件地址替换“world”:return [
'#markup' => sprintf('<p>Hello %s!</p>',
$user->getEmail()),
];
我们使用 sprintf
返回一个包含用户电子邮件地址的格式化字符串。
-
Drupal 缓存其路由信息。我们必须重建 Drupal 的缓存,以便它能够意识到模块新路由的变化:
php vendor/bin/drush cr
-
当你访问
/hello-world
时,没有路由参数的旧路径,Drupal 将返回 404 错误表示页面未找到。 -
当你访问
/hello-world/1
时,页面将显示包含用户电子邮件地址的格式化文本:
图 5.2 – 包含用户电子邮件地址的格式化文本
工作原理…
如前一个菜谱中讨论的,路由系统比较传入 HTTP 请求的路径。然后,路径与已注册的路由进行匹配。当一个路由不包含路由参数时,匹配是简单的。为了根据从 URL 传入的路径完成路由匹配,路由有一个编译好的匹配模式。这个匹配模式将路由路径及其路由模式转换成一个可以与传入 URL 的路径进行匹配的正则表达式。
Drupal\Core\Routing\RouteCompiler
类扩展了来自 Symfony 路由组件的RouteCompiler
。当路由被编译时,路由的路径被转换成一个正则表达式,用于与从传入 URL 的路径进行比较。考虑到我们的路由路径/hello-world/{user}
和\d+
的要求,以下是用户路由参数匹配的正则表达式,用于匹配我们的路由:
#^/hello\-world/(?P<user>\d+)$#sDu
当这个正则表达式通过preg_match
评估时,匹配将返回一个匹配的关联数组。如果从传入 URL 的路径匹配,那么匹配数组将包含一个键为 user,其值为在路径中找到的值。
Drupal 的路由系统有一个参数转换器的概念,它将路由参数值从其原始值转换。使用路由定义的options.parameters.user
为entity:user
,\Drupal\Core\ParamConverter\EntityConverter
类将用户 ID 转换为已加载的用户对象。
在路由匹配和参数转换之后,它们与控制器方法定义的参数进行匹配。来自 Symfony 的HttpKernel
的ArgumentResolver
使用反射在调用控制器处理请求之前组装正确的参数顺序。
参见
-
Symfony 的 HttpKernel 组件和参数解析的文档:
symfony.com/doc/current/components/http_kernel.html#4-getting-the-controller-arguments
-
Symfony 路由组件和路由参数的文档:
symfony.com/doc/current/routing.html#route-parameters
创建一个动态重定向页面
Drupal 中路由的控制器可以返回由 Symfony 的HttpFoundation
组件提供的请求对象,而不是渲染数组。当返回响应对象时,渲染系统被绕过,响应对象被直接处理。
在这个菜谱中,我们将创建一个路由,将认证用户重定向到主页,将匿名用户重定向到用户登录表单。
如何做到这一点…
-
首先,我们需要在模块目录中创建
src/Controller
目录。我们将把我们的控制器类放在这个目录中,这给我们的控制器类赋予了Controller
命名空间:mkdir -p src/Controller
-
在
Controller
目录下创建一个名为RedirectController.php
的文件。这个文件将包含我们的RedirectController
控制器类。 -
我们的
RedirectController
类将扩展由 Drupal 核心提供的ControllerBase
基类:<?php
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
class RedirectController extends ControllerBase {
}
根据 PSR-4 自动加载约定,我们的类位于 \Drupal\mymodule\Controller
命名空间中,Drupal 可以将其确定为模块中的 src/Controller
目录。根据 PSR-4,文件名和类名也必须相同。
\Drupal\Core\Controller\ControllerBase
类提供了一些实用方法,这些方法可以被利用。
-
接下来,我们将创建一个方法来检查当前用户是否已登录。更新
RedirectController
类以包含以下方法:<?php
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\RedirectResponse;
class RedirectController extends ControllerBase {
/**
* Returns redirect to home or user login form.
*
* @return \Symfony\Component\HttpFoundation\
RedirectResponse
* The redirect response.
*/
public function page(): RedirectResponse {
if ($this->currentUser()->isAuthenticated()) {
// Redirect to the homepage.
}
else {
// Redirect to the user login form.
}
}
}
ControllerBase
类提供了一个 currentUser
方法,用于访问当前用户对象。我们可以调用 isAuthenticated
方法来检查用户是否已登录。
-
接下来,我们将更新代码以返回一个重定向到由路由名称生成的 URL 的
RedirectResponse
对象:public function page(): RedirectResponse {
if ($this->currentUser()->isAuthenticated()) {
$route_name = '<front>';
}
else {
$route_name = 'user.login';
}
$url = \Drupal\Core\Url::fromRoute($route_name);
return new RedirectResponse($url->toString());
}
我们在 $route_name
变量中设置路由名称。虽然 <front>
不是一个典型的路径,但它是一个可用于路由到已设置为主页的适当路径的路由名称。Drupal 核心定义了如 <front>
和 <none>
这样的特殊别名,以便于路由。有关更多信息,请参阅核心 Url
类。
我们随后使用 \Drupal\Core\Url
类的静态 fromRoute
方法构建一个新的 URL 对象。然后我们返回一个重定向到我们 URL 的 RedirectResponse
对象。我们必须使用 URL 对象上的 toString
方法来获取 URL 字符串。
-
在你的模块目录中创建
mymodule.routing.yml
文件。routing.yml
文件由模块提供,用于定义路由。 -
定义路由的第一步是提供一个路由名称,该名称用作其标识符,用于 URL 生成和其他目的:
mymodule.user_redirect
-
为路由指定一个路径:
mymodule.user_redirect:
path: /user-redirect
-
接下来,我们将路径注册到我们的控制器。这是通过
defaults
键完成的,我们提供了控制器:mymodule.user_redirect:
path: /user-redirect
defaults:
_controller: Drupal\mymodule\Controller\
RedirectController::page
_controller
键是带有要使用的类方法的完全限定类名。
-
最后,定义一个
requirements
键来指定访问要求。我们希望路由始终可访问,因为我们处理了认证用户和匿名用户:mymodule.user_redirect:
path: /user-redirect
defaults:
_controller: Drupal\mymodule\Controller\
RedirectController::page
requirements:
_access: 'TRUE'
_access
选项允许我们指定一个路由始终可访问。
-
Drupal 缓存其路由信息。我们必须重建 Drupal 的缓存,以便它能够了解模块的新路由:
php vendor/bin/drush cr
-
以匿名用户身份访问你的 Drupal 网站的
/user-redirect
,你将被重定向到用户登录表单。如果你已认证,你将被重定向到主页。
它是如何工作的...
RedirectResponse
对象来自 Symfony 的HttpFoundation
组件,表示 HTTP 重定向响应。当转换为从 Web 服务器发送的响应时,它将设置位置头为提供的 URL。它还传递一个有效的 3xx HTTP 状态代码。重定向响应默认为 HTTP 状态码 302 找到。302 重定向旨在是临时的,并且不能被浏览器缓存。
Drupal 可以从给定的路由名称生成 URL。正如我们在之前的菜谱中学到的,路由名称用于标识特定的路径、控制器和其他信息。当调用toString
时,URL 对象将调用 URL 生成器将路由名称转换为 URL。
参见
- HTTP/1.1 RFC 重定向 3xx 规范:
datatracker.ietf.org/doc/html/rfc2616#section-10.3
创建 JSON 响应
路由也可以返回JavaScript 对象表示法(JSON)响应。JSON 响应通常用于构建 API 路由,因为它是一种所有编程语言都支持的交换格式。这允许暴露数据供第三方消费者使用。
在这个菜谱中,我们将创建一个返回包含有关 Drupal 站点信息的 JSON 响应的路由。
如何实现...
-
首先,我们需要在模块目录中创建
src/Controller
目录。我们将把我们的控制器类放在这个目录中,这将为我们的控制器类提供Controller
命名空间:mkdir -p src/Controller
-
在
Controller
目录中创建一个名为SiteInfoController.php
的文件。这将包含我们的SiteInfoController
控制器类。 -
我们的
SiteInfoController
类将扩展由 Drupal 核心提供的ControllerBase
基类:<?php
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\JsonResponse;
class SiteInfoController extends ControllerBase {
}
遵循 PSR-4 自动加载约定,我们的类位于\Drupal\mymodule\Controller
命名空间中,Drupal 可以将其确定为我们的模块中的src/Controller
目录。根据 PSR-4,文件名和类名也必须相同。
\Drupal\Core\Controller\ControllerBase
类提供了一些可以利用的实用方法。
-
接下来,我们将创建一个返回
JsonResponse
对象的方法。将以下方法添加到我们的SiteInfoController
类中:/**
* Returns site info in a JSON response.
*
* @return \Symfony\Component\HttpFoundation\
JsonResponse
* The JSON response.
*/
public function page(): JsonResponse {
return new JsonResponse();
}
page
方法返回一个JsonResponse
对象。这确保了响应的Content-Type
头设置为application/json
。
-
让我们使用来自
system.site
配置对象的 数据向我们的响应对象添加内容:/**
* Returns site info in a JSON response.
*
* @return \Symfony\Component\HttpFoundation\
JsonResponse
* The JSON response.
*/
public function page(): JsonResponse {
$config = $this->config('system.site');
return new JsonResponse([
'name' => $config->get('name'),
'slogan' => $config->get('slogan'),
'email' => $config->get('mail')
]);
}
我们使用ControllerBase
提供的config
方法检索system.site
配置对象。然后,我们将一个关联数组的数据提供给JsonResponse
对象。当发送响应时,该数组将被转换为 JSON。
-
在您的模块目录中创建
mymodule.routing.yml
文件。该文件由模块提供,用于定义路由。 -
定义路由的第一步是提供一个路由名称,该名称用于 URL 生成和其他目的:
mymodule.site_info
-
为路由指定路径:
mymodule.site_info:
path: /site-info
-
接下来,我们将路径注册到我们的控制器中。这是通过
defaults
键完成的,我们在这里提供控制器和页面标题:mymodule.site_info:
path: /site-info
defaults:
_controller: Drupal\mymodule\Controller\
SiteInfoController::page
_controller
键是用于的完全限定类名以及要使用的方法。
-
最后,定义一个
requirements
键来指定访问要求:mymodule.site_info:
path: /site-info
defaults:
_controller: Drupal\mymodule\Controller\
SiteInfoController::page
requirements:
_access: 'TRUE'
_access
选项允许路由始终可访问。
-
Drupal 缓存其路由信息。我们必须重建 Drupal 的缓存,以便它能够了解模块的新路由:
php vendor/bin/drush cr
-
前往你的 Drupal 网站的
/site-info
,你应该会收到如下所示的 JSON 响应:{
"name": "Drupal Development Cookbook",
"slogan": "Practical recipes to harness the power of
Drupal 10.",
"email": "admin@example.com"
}
它是如何工作的…
JsonResponse
对象用于表示 JSON 响应。其构造函数接收一个数组或对象的数据,并将其传递给 PHP 的json_encode
函数。它将响应的Content-Type
头设置为application/json
。
还有更多…
JSON 响应控制器可以增强以利用 Drupal 的 harness-caching 功能。
使用 Drupal 缓存 JSON 响应
Drupal 还提供了\Drupal\Core\Cache\CacheableJsonResponse
。这扩展了JsonResponse
,允许 Drupal 的页面缓存缓存响应。然而,你可能希望将system.site
配置对象作为响应的缓存依赖项。这样,如果该配置对象发生变化,响应缓存就会失效。
代码将更新如下:
class SiteInfoController extends ControllerBase {
/**
* Returns site info in a JSON response.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The JSON response.
*/
public function page(): JsonResponse {
$config = $this->config('system.site');
$response = new \Drupal\Core\Cache\
CacheableJsonResponse([
'name' => $config->get('name'),
'slogan' => $config->get('slogan'),
'email' => $config->get('mail')
]);
$response->addCacheableDependency($config);
return $response;
}
}
主要区别是我们不直接返回响应对象。首先,我们将配置对象设置为缓存依赖项。
参见
- 第十二章,使用 Drupal构建 API
提供下载文件服务
可以使用路由来通过BinaryFileResponse
响应对象提供文件下载。使用BinaryFileResponse
来提供文件下载允许你保持原始文件的 URL 私有或发送动态内容作为文件下载。
在这个菜谱中,我们将创建一个提供 PDF 下载的路由。
开始
这个菜谱使用一个位于模块同一目录下的 PDF 文件。你可以使用任何其他可用的文件类型,例如文本文件。可以在万维网联盟网站上找到一个测试 PDF:www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf
。
如何做到这一点…
-
首先,我们需要在模块目录中创建
src/Controller
目录。我们将把我们的控制器类放在这个目录中,这给我们的控制器类赋予了Controller
命名空间:mkdir -p src/Controller
-
在
Controller
目录下创建一个名为DownloadController.php
的文件。这将包含我们的DownloadController
控制器类:<?php
namespace Drupal\mymodule\Controller;
class SiteInfoController {
}
遵循 PSR-4 自动加载约定,我们的类位于\Drupal\mymodule\Controller
命名空间中,Drupal 能够将其确定为模块中的src/Controller
目录。根据 PSR-4,文件名和类名也必须相同。
-
接下来,我们将创建一个返回
BinaryFileResponse
对象的方法。将以下方法添加到我们的DownloadController
类中:/**
* Downloads a file.
*
* @return \Symfony\Component\HttpFoundation\
BinaryFileResponse
* The file response.
*/
public function page(): BinaryFileResponse {
// File paths are relative to the document root
(web.)
$file_path = 'modules/custom/mymodule/dummy.pdf';
$response = new BinaryFileResponse($file_path);
$response->setContentDisposition('attachment',
basename($file_path));
return $response;
}
页面方法返回一个 BinaryFileResponse
对象。我们定义了一个包含我们的 PDF 文件的文件路径。文件路径可能是相对于 Drupal 的文档根目录(web)。我们的模块目录是相对于文档根目录的 modules/custom/mymodule
。我们将 Content-Disposition
头部设置为附件,并指定一个文件名,以便文件自动下载。我们使用 PHP 的 basename 函数从路径中获取文件名。
-
控制器几乎准备好了。我们还应该为响应提供
Content-Type
头部。使用file.mime_type.guesser
服务,我们可以获取正确的头部值:/**
* Downloads a file.
*
* @return \Symfony\Component\HttpFoundation\
BinaryFileResponse
* The file response.
*/
public function page(): BinaryFileResponse {
// File paths are relative to the document root
(web.)
$file_path = 'modules/custom/mymodule/dummy.pdf';
/** @var \Drupal\Core\File\MimeType\
MimeTypeGuesser $guesser */
$guesser = \Drupal::service
('file.mime_type.guesser');
$mime_type = $guesser->guessMimeType($file_path);
$response = new BinaryFileResponse($file_path);
$response->headers->set('Content-Type',
$mimetype);
$response->setContentDisposition('attachment',
basename($file_path));
return $response;
}
file.mime_type.guesser
服务用于根据文件的扩展名确定文件的适当 MIME 类型。在这种情况下,它将 return application/pdf
。
-
在您的模块目录中创建
mymodule.routing.yml
文件。routing.yml
文件由模块提供,用于定义路由。 -
定义路由的第一步是提供一个路由名称,该名称用作其标识符,用于 URL 生成和其他目的:
mymodule.pdf_download
-
为路由提供一个路径:
mymodule.pdf_download:
path: /pdf-download
-
接下来,我们将使用我们的控制器注册路径。这是通过默认键完成的,我们在其中提供控制器和页面标题:
mymodule.pdf_download:
path: /pdf-download
defaults:
_controller: Drupal\mymodule\Controller
\DownloadController::page
_controller
键是用于的完整类名,其中包含要使用的方法。
-
最后,定义一个 requirements 键来指定访问要求:
mymodule.pdf_download:
path: /pdf-download
defaults:
_controller: Drupal\mymodule\Controller
\DownloadController::page
requirements:
_user_is_logged_in: 'TRUE'
_user_is_logged_in
选项要求用户必须经过身份验证才能访问路由。
-
Drupal 缓存其路由信息。我们必须重建 Drupal 的缓存,以便它能够了解模块的新路由:
php vendor/bin/drush cr
-
前往您的 Drupal 网站上的
/pdf-download
,您将被提示下载dummy.pdf
文件。
它是如何工作的…
BinaryFileResponse
存储有关文件的信息,直到响应准备发送。当响应准备发送时,基于文件大小填充 Content-Length
头部。然后,当响应发送时,文件的内容通过 Web 服务器流式传输到访客。
第六章:访问和使用实体
在本章中,我们将介绍在 Drupal 中处理实体的创建、读取、更新和删除(CRUD)操作。我们将创建一系列路由来创建、读取、更新和删除文章节点。
在本章中,我们将涵盖以下内容:
-
创建和保存实体
-
查询和加载实体
-
检查实体访问
-
更新实体的字段值
-
执行实体验证
-
删除实体
技术要求
本章需要自定义模块,该模块包含一个routing.yml
文件,并在模块的src/Controller
目录中有一个名为ArticleController
的控制器。在下面的示例中,模块名称为mymodule
。请根据需要替换。您可以在 GitHub 上找到本章使用的完整代码:github.com/PacktPublishing/Drupal-10-Development-Cookbook/tree/main/chp06
我们正在使用由标准 Drupal 安装创建的文章内容类型。
本章中的示例包含用于与每个示例中创建的代码交互的 HTTP 请求。这些 HTTP 请求可以用任何 HTTP 客户端运行。如果您使用 VSCode,请尝试REST Client扩展(marketplace.visualstudio.com/items?itemName=humao.rest-client
),或者如果您有 PhpStorm,请使用内置的HTTP Client(www.jetbrains.com/help/idea/http-client-in-product-code-editor.html
)。如果由于某些原因您没有编辑器或无法使其工作,您可以使用 Postman(www.postman.com/
)。
创建和保存实体
在这个示例中,我们将定义一个路由来创建一个新的文章。该路由将用于发送 JSON 的 HTTP POST
请求,以指定文章的标题和正文文本。
如何操作…
-
在您的模块中
ArticleController
控制器中创建一个store
方法,该方法将接收传入的请求对象:<?php
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class ArticleController extends ControllerBase {
public function store(Request $request):
JsonResponse {
}
}
我们需要请求对象,以便我们可以检索请求有效载荷中提供的 JSON。
-
接下来,我们将使用 Drupal 的 JSON 序列化实用工具类将请求的内容从 JSON 转换为 PHP 数组:
public function store(Request $request):
JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
}
尽管我们可以直接使用json_decode
函数,但利用 Drupal 标准提供的实用工具类可以标准化代码的工作方式。
-
然后,我们获取实体类型管理器并检索
node
实体类型的实体存储:public function store(Request $request):
JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
$entity_type_manager = $this->entityTypeManager();
$node_storage = $entity_type_manager->
getStorage('node');
}
实体类型管理器是实体类型信息的存储库,用于获取实体类型的处理程序,例如存储处理程序。在获取实体存储处理程序时,您需要传递实体类型 ID。
-
从存储中调用
create
方法来创建一个新的节点实体对象:public function store(Request $request):
JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
$entity_type_manager = $this->entityTypeManager();
$node_storage = $entity_type_manager->
getStorage('node');
$article = $node_storage->create([
'type' => 'article',
]);
}
create
方法使用实体类型类实例化一个新的实体对象。对于节点实体类型,我们的 $article
变量将是 \Drupal\node\Node
类型。在实例化新实体时,你必须指定其包,我们通过将 type
键设置为 article
来做到这一点。
-
向
create
方法提供一个title
和body
的关联数组,从我们接收到的请求 JSON 中复制值:public function store(Request $request): JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
$entity_type_manager = $this->entityTypeManager();
$node_storage = $entity_type_manager->
getStorage('node');
$article = $node_storage->create([
'type' => 'article',
'title' => $json['title'],
'body' => $json['body'],
]);
}
-
在实体对象上调用
save
方法以保存实体:public function store(Request $request):
JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
$entity_type_manager = $this->entityTypeManager();
$node_storage = $entity_type_manager->
getStorage('node');
$article = $node_storage->create([
'type' => 'article',
'title' => $json['title'],
'body' => $json['body'],
]);
$article->save();
}
-
我们现在将返回一个带有位置头部的响应,该头部包含新创建的节点 URL 和 HTTP 状态码
201 Created
:public function store(Request $request):
JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
$entity_type_manager = $this->entityTypeManager();
$node_storage = $entity_type_manager->
getStorage('node');
$article = $node_storage->create([
'type' => 'article',
'title' => $json['title'],
'body' => $json['body'],
]);
$article->save();
$article_url = $article->toUrl()->setAbsolute()->
toString();
return new JsonResponse(
$article->toArray(),
201,
['Location' => $article_url],
);
}
201 Created
状态码用于表示成功的响应,并表明已创建一个项目。当返回 201 Created
响应时,建议返回一个带有创建项目 URL 的 Location
头部。实体类有一个 toUrl
方法来返回一个 URL 对象,然后调用其 toString
方法将其从对象转换为字符串。
-
我们必须在模块的
routing.yml
中创建路由,指向ArticleController
的store
方法,以处理对/articles
路径的 HTTPPOST
请求:mymodule.create_article:
path: /articles
defaults:
_controller: Drupal\mymodule\Controller\
ArticleController::store
methods: [POST]
requirements:
_access: 'TRUE'
路由可以通过 methods
键指定它们支持的 HTTP 方法。在这个菜谱中,我们只将 requirements
设置为 _access: 'TRUE'
以绕过访问检查。这应该设置为 _entity_create_access: 'node'
。
-
重建你的 Drupal 网站的缓存,使其了解新的路由:
php vendor/bin/drush cr
-
可以使用以下 HTTP 请求来创建一个新节点,该节点是你的 Drupal 网站上的文章:
POST http://localhost/articles
Content-Type: application/json
Accept: application/json
{
"title": "New article",
"body": "Test body"
}
它是如何工作的...
create
方法用于实例化一个新的实体对象,并返回该实体类型的类对象。对于这个菜谱,node
存储将返回具有 \Drupal\node\Node
类的实体。create
方法接受的值基于该实体类型的字段定义。这包括基本字段和通过用户界面创建的任何字段。
在创建实体类型时必须指定其实体类型的包。这用于确定可用的字段,因为每个包可以有不同的字段。对于节点,包字段名为 type
。这就是为什么我们为 type
键提供 article
值。
然后 save
方法将实体提交到数据库存储。
当实体在其首次保存时插入,实体存储触发调用以下钩子,允许你钩入新实体的插入。正在插入的实体作为参数传递:
-
hook_ENTITY_TYPE_presave
-
hook_entity_presave
-
hook_ENTITY_TYPE_insert
-
hook_entity_insert
查询和加载实体
在这个菜谱中,我们将使用实体查询来查找所有已发布的文章并将它们作为 JSON 返回。我们还将允许通过查询参数指定排序顺序。
如何做到这一点…
-
在你的模块中创建
ArticleController
控制器的index
方法,它将接收传入的Request
对象:<?php
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class ArticleController extends ControllerBase {
public function index(Request $request):
JsonResponse {
}
}
request
对象将用于检索通过 URL 传递的查询参数。
-
从请求中获取排序查询参数,默认为
DESC
:public function index(Request $request):
JsonResponse {
$sort = $request->query->get('sort', 'DESC');
}
-
然后,我们获取实体类型管理器并检索节点实体类型的实体存储:
public function index(Request $request):
JsonResponse {
$sort = $request->query->get('sort', 'DESC');
$entity_type_manager = $this->entityTypeManager();
$node_storage = $entity_type_manager->
getStorage('node');
}
实体类型管理器是实体类型信息的存储库,用于获取实体类型的处理程序,例如存储处理程序。在获取实体存储处理程序时,您传递实体类型 ID。
-
从存储处理程序中调用
getQuery
方法。这将返回一个查询对象,用于执行实体查询:public function index(Request $request):
JsonResponse {
$sort = $request->query->get('sort', 'DESC');
$entity_type_manager = $this->entityTypeManager();
$node_storage = $entity_type_manager->
getStorage('node');
$query = $node_storage->getQuery()
->accessCheck(TRUE);
}
Drupal 要求指定在查询内容实体时实体查询是否应执行实体访问检查。我们必须调用accessCheck
方法并将其设置为TRUE
或FALSE
。在这种情况下,我们希望实体访问检查应用于实体查询。
-
我们将在查询中添加条件以确保只返回已发布的文章:
public function index(Request $request):
JsonResponse {
$sort = $request->query->get('sort', 'DESC');
$entity_type_manager = $this->entityTypeManager();
$node_storage = $entity_type_manager->
getStorage('node');
$query = $node_storage->getQuery();
->accessCheck(TRUE);
$query->condition('type', 'article');
$query->condition('status', TRUE);
}
condition
方法传递字段名和值以创建条件。第一个条件确保我们只查询类型(捆绑)为article
的节点。第二个条件是确保status
字段为true
表示已发布。
-
然后,我们指定查询的
sort
顺序来自我们的 URL 查询参数:public function index(Request $request):
JsonResponse {
$sort = $request->query->get('sort', 'DESC');
$entity_type_manager = $this->entityTypeManager();
$node_storage = $entity_type_manager->
getStorage('node');
$query = $node_storage->getQuery();
->accessCheck(TRUE);
$query->condition('type', 'article');
$query->condition('status', TRUE);
$query->sort('created', $sort);
}
sort
方法传递用于排序查询的字段名和方向ASC
或DESC
。
-
调用
execute
方法将执行实体查询并返回可用的实体 ID:public function index(Request $request):
JsonResponse {
$sort = $request->query->get('sort', 'DESC');
$entity_type_manager = $this->entityTypeManager();
$node_storage = $entity_type_manager->
getStorage('node');
$query = $node_storage->getQuery()
->accessCheck(TRUE);
$query->condition('type', 'article');
$query->condition('status', TRUE);
$query->sort('created', $sort);
$node_ids = $query->execute();
}
-
在调用
execute
之后,我们得到一个节点 ID 数组,可以将这些 ID 传递给实体存储中的loadMultiple
方法来加载节点:public function index(Request $request):
JsonResponse {
$sort = $request->query->get('sort', 'DESC');
$entity_type_manager = $this->entityTypeManager();
$node_storage = $entity_type_manager->
getStorage('node');
$query = $node_storage->getQuery()
->accessCheck(TRUE);
$query->condition('type', 'article');
$query->condition('status', TRUE);
$query->sort('created', $sort);
$node_ids = $query->execute();
$nodes = $node_storage->loadMultiple($node_ids);
}
-
我们现在可以使用
array_map
将节点转换为数组值并返回文章的 JSON 响应:public function index(Request $request):
JsonResponse {
$sort = $request->query->get('sort', 'DESC');
$entity_type_manager = $this->entityTypeManager();
$node_storage = $entity_type_manager->
getStorage('node');
$query = $node_storage->getQuery()
->accessCheck(TRUE);
$query->condition('type', 'article');
$query->condition('status', TRUE);
$query->sort('created', $sort);
$node_ids = $query->execute();
$nodes = $node_storage->loadMultiple($node_ids);
$nodes = array_map(function (\Drupal\node\
NodeInterface $node) {
return $node->toArray();
}, $nodes);
return new JsonResponse($nodes);
}
array_map
函数允许您转换现有数组中项的值。我们将使用array_map
遍历返回的节点实体并调用toArray
方法以获取它们的值作为数组。
-
我们必须在
routing.yml
中创建路由,指向ArticleController
的index
方法,以便对/articles
路径的 HTTPGET
请求:mymodule.get_articles:
path: /articles
defaults:
_controller: Drupal\mymodule\Controller\
ArticleController::index
methods: [GET]
requirements:
_permission: 'access content'
如果路由明确定义它们支持的 HTTP 方法,则路由可以共享相同的路径。
-
重建您的 Drupal 站点的缓存,使其了解新的路由:
php vendor/bin/drush cr
-
可以使用如下所示的 HTTP 请求来检索您的 Drupal 站点上的文章:
GET http://localhost/articles
Accept: application/json
它是如何工作的…
实体查询是 Drupal 数据库 API 之上的一个抽象。Drupal 将实体和字段数据存储在规范化的数据库表中。实体查询构建适当的数据库查询以检查实体的基础表、数据表以及任何字段表。它协调所有所需的JOIN
语句。
当执行实体查询时,它返回匹配实体的 ID。然后,将这些 ID 传递给loadMultiple
方法以检索实体对象。
还有更多…
实体查询可以做更多的事情。
计数查询
实体查询也可以执行计数而不是返回实体 ID。这是通过在实体查询对象上调用count
方法来完成的:
$node_storage->getQuery()
->accessCheck(FALSE)
->condition('type', 'article')
->condition('status', FALSE)
->count()
->execute();
上述代码将返回未发布文章的数量。
检查实体访问
在此配方中,我们将演示如何检查当前用户是否有权查看用于执行实体访问检查的_entity_access
路由要求。此配方将使用自己的实体访问控制,以便响应是一个404 Not Found
响应而不是403
Forbidden
响应。
如何做到这一点…
-
在你的模块中创建一个
ArticleController
控制器中的get
方法,该方法有一个参数用于node
实体对象,该对象将由路由参数提供:<?php
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\node\NodeInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class ArticleController extends ControllerBase {
public function get(NodeInterface $node):
JsonResponse {
}
}
如果使用之前配方中的相同控制器,这将添加一个新的use
语句用于Drupal\node\NodeInterface
接口。
-
然后,我们获取实体类型管理器并检索
node
实体类型的访问控制处理程序:public function get(NodeInterface $node):
JsonResponse {
$entity_type_manager = $this->entityTypeManager();
$access_handler = $entity_type_manager->
getAccessControlHandler('node');
}
实体类型管理器是实体类型信息的存储库,用于获取实体类型的处理程序,例如访问控制处理程序。
-
从访问控制处理程序中调用
access
方法:public function get(NodeInterface $node):
JsonResponse {
$entity_type_manager = $this->entityTypeManager();
$access_handler = $entity_type_manager->
getAccessControlHandler('node');
$node_access = $access_handler->access($node,
'view');
}
access
的第一个参数是实体。第二个参数是操作,对于此配方是view
。默认情况下,access
方法返回一个布尔值。
-
如果结果不允许,我们希望返回一个
404 Not Found
响应:public function get(NodeInterface $node):
JsonResponse {
$entity_type_manager = $this->entityTypeManager();
$access_handler = $entity_type_manager->
getAccessControlHandler('node');
$node_access = $access_handler->access($node,
'view');
if (!$node_access) {
return new JsonResponse(NULL, 404);
}
}
我们将返回一个 JSON 响应,但带有NULL
数据和404
状态码。
-
如果结果是允许的,返回文章节点内容的 JSON 响应:
public function get(NodeInterface $node):
JsonResponse {
$entity_type_manager = $this->entityTypeManager();
$access_handler = $entity_type_manager->
getAccessControlHandler('node');
$node_access = $access_handler->access($node,
'view');
if (!$node_access) {
return new JsonResponse(NULL, 404);
}
return new JsonResponse(
$node->toArray(),
);
}
-
我们必须在模块的
routing.yml
中创建一个路由,指向ArticleController
的get
方法,以便对/articles/{node}
路径的 HTTPGET
请求:mymodule.get_article:
path: /articles/{node}
defaults:
_controller: Drupal\mymodule\Controller\
ArticleController::get
requirements:
_permission: 'access content'
由于我们的路由参数名为node
,并且我们的控制器方法接受节点实体的类,Drupal 将自动将我们的node
参数转换为实体对象。
-
重建你的 Drupal 站点的缓存,使其了解新的路由:
php vendor/bin/drush cr
-
然后,可以使用如下 HTTP 请求检索你 Drupal 站点上的文章:
GET http://localhost/articles/1
Accept: application/json
它是如何工作的…
此配方未使用_entity_access
路由要求来展示实体验证。_entity_access
路由要求在路由参数中位于实体上的操作上调用访问方法。
以下操作被 Drupal 的实体访问系统识别:
-
view
: 用户被允许查看实体。 -
view_label
: 一个较少使用的操作。它用于检查用户是否有权至少查看实体标签/标题。 -
update
: 用户被允许更新实体。 -
delete
: 用户被允许删除实体。
注意
创建实体的访问是一个对访问控制处理器的不同调用,因为它不能与实体对象进行比较。访问控制处理器上的 createAccess
方法用于检查用户是否有权创建新实体。
通过在实体本身上调用 access
方法也可以检查实体的 access
。此菜谱旨在展示访问控制处理器。菜谱可以通过执行以下操作来检查访问:
$node_access = $node->access('view');
默认情况下,访问检查使用当前用户。访问检查允许传递一个替代用户账户以执行访问检查。这在在命令行或后台作业中运行代码时可能很有用:
$node_access = $node->access('view', $other_user);
更新实体的字段值
在此菜谱中,我们将定义一个路由来更新文章节点的字段值。该路由将用于发送 JSON 的 HTTP PATCH
请求,以指定新的标题和正文文本。
如何操作...
-
在
ArticleController
控制器中创建一个update
方法,该方法接收传入的请求和节点对象:<?php
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\node\NodeInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class ArticleController extends ControllerBase {
public function update(Request $request,
NodeInterface $node): JsonResponse {
}
}
我们需要请求对象,以便我们可以检索此方法提供的 JSON 负载以及节点对象以进行更新。
-
接下来,我们将使用 Drupal 的 JSON 序列化实用工具类将请求的内容从 JSON 转换为 PHP 数组:
public function update(Request $request,
NodeInterface $node): JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
}
虽然我们可以直接使用 json_decode
函数,但利用 Drupal 标准提供的实用工具类可以标准化代码的工作方式。
-
首先,如果请求的 JSON 中提供了,我们将更新文章节点标题:
public function update(Request $request,
NodeInterface $node): JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
if (!empty($json['title'])) {
$node->setTitle($json['title']);
}
}
一些实体类提供了设置特定字段值的方法,例如 Node
类使用 setTitle
来修改节点的 title
。
-
然后,如果请求的 JSON 中提供了,我们将更新文章的主体字段:
public function update(Request $request,
NodeInterface $node): JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
if (!empty($json['title'])) {
$node->setTitle($json['title']);
}
if (!empty($json['body'])) {
$node->set('field_body', $json['body']);
}
}
set
方法允许我们设置字段的值。第一个参数是字段名,第二个参数是字段的值。
-
更新字段后,我们可以保存实体并将其作为 JSON 响应返回:
public function update(Request $request,
NodeInterface $node): JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
if (isset($json['title'])) {
$node->setTitle($json['title']);
}
if (isset($json['body'])) {
$node->set('body', $json['body']);
}
$node->save();
return new JsonResponse(
$node->toArray()
);
}
-
我们必须在
routing.yml
中创建路由,该路由指向ArticleController
的update
方法,用于对/articles/{node}
路径的 HTTPPATCH
请求:mymodule.update_article:
path: /articles/{node}
defaults:
_controller: Drupal\mymodule\Controller\
ArticleController::update
methods: [PATCH]
requirements:
_access: 'TRUE'
此路由应具有 _entity_access: 'node.update'
作为其要求。然而,我们已使用 _access: 'TRUE'
来绕过此菜谱的访问检查。
-
重建您的 Drupal 网站缓存,使其了解新路由:
php vendor/bin/drush cr
-
然后,可以使用以下 HTTP 请求来更新您 Drupal 网站上的文章:
PATCH http://localhost/articles/1
Content-Type: application/json
Accept: application/json
{
"title": "New updated title!",
"body": "Modified body text"
}
它是如何工作的...
当更新一个实体时,实体存储会在数据库中更新字段值以及实体的数据库记录。实体存储会将字段值写入包含字段值的适当数据库表。
当实体被更新时,实体存储会触发以下钩子,允许您挂钩到实体的更新。正在更新的实体作为参数传递:
-
hook_ENTITY_TYPE_presave
-
hook_entity_presave
-
hook_ENTITY_TYPE_update
-
hook_entity_update
实体也可以进行验证,以确保其更新的值是正确的。这将在下一个菜谱执行实体验证中介绍。
执行实体验证
在这个菜谱中,我们将介绍实体验证。Drupal 已经集成了Symfony Validator组件。实体在保存之前可以进行验证。我们将基于上一个菜谱,允许更新文章节点以添加对其值的验证。
如何做到这一点...
-
我们将向上一个菜谱中的
update
方法添加验证:public function update(Request $request,
NodeInterface $node): JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
if (isset($json['title'])) {
$node->setTitle($json['title']);
}
if (isset($json['body'])) {
$node->set('body', $json['body']);
}
$node->save();
return new JsonResponse(
$node->toArray()
);
}
-
我们将修改
update
方法,在保存节点之前对其进行validate
:public function update(Request $request,
NodeInterface $node): JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
if (isset($json['title'])) {
$node->setTitle($json['title']);
}
if (isset($json['body'])) {
$node->set('body', $json['body']);
}
$constraint_violations = $node->validate();
$node->save();
return new JsonResponse(
$node->toArray()
);
}
我们将调用validate
方法,该方法将对实体及其字段值上的所有约束执行验证。
-
validate
方法返回一个包含任何约束违规的对象,并且不会抛出异常。我们必须检查是否存在任何违规:public function update(Request $request,
NodeInterface $node): JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
if (isset($json['title'])) {
$node->setTitle($json['title']);
}
if (isset($json['body'])) {
$node->set('body', $json['body']);
}
$constraint_violations = $node->validate();
if (count($constraint_violations) > 0) {
}
$node->save();
return new JsonResponse(
$node->toArray()
);
}
返回的对象,一个\Drupal\Core\Entity\EntityConstraintViolationListInterface
的实例,实现了\Countable
接口。这允许我们使用count
函数来查看是否存在任何违规。
-
如果存在约束违规,我们将构建一个错误消息数组,并返回一个
400 Bad Request
响应:public function update(Request $request,
NodeInterface $node): JsonResponse {
$content = $request->getContent();
$json = \Drupal\Component\Serialization\
Json::decode($content);
if (isset($json['title'])) {
$node->setTitle($json['title']);
}
if (isset($json['body'])) {
$node->set('body', $json['body']);
}
$constraint_violations = $node->validate();
if (count($constraint_violations) > 0) {
$errors = [];
foreach ($constraint_violations as $violation) {
$errors[] = $violation->getPropertyPath()
. ': ' . $violation->getMessage();
}
return new JsonResponse($errors, 400);
}
$node->save();
return new JsonResponse(
$node->toArray()
);
}
EntityConstraintViolationListInterface
对象也是可迭代的,允许我们遍历所有违规。从每个违规中,我们可以使用getPropertyPath
来识别无效字段,并使用getMessage
来获取有关无效值的信息。
-
以下 HTTP 请求会在
title
字段为空值时触发约束违规:PATCH https://localhost/articles/1
Content-Type: application/json
Accept: application/json
{
"title": "",
"body": "Modified body text"
}
它是如何工作的...
Drupal 使用Symfony Validator组件(symfony.com/components/Validator
)来验证数据。Validator 组件有一个概念,即需要验证的约束,如果验证失败,则会报告违规。Drupal 的实体验证采用自外向内的方法:验证实体级别的约束,然后逐个验证每个字段,逐级向下验证每个字段的属性。
实体验证在实体保存时不会自动运行。它是一个显式操作,在以编程方式操作实体时必须调用。Drupal 仅在通过其表单修改实体时调用实体验证。
同时,实体无效化的调用者必须选择如何对约束违规做出反应,就像我们的菜谱那样防止实体被保存。无效实体始终可以通过编程方式保存。
还有更多...
在验证实体值时,有更多选项。让我们在接下来的几节中看看这些选项。
直接验证字段
字段项类有一个validate
方法,可以直接验证特定字段而不是验证整个实体。这可以通过使用get
方法获取字段项,然后在字段项上调用validate
方法来完成:
$node->get('body')->validate();
过滤约束违规
EntityConstraintViolationListInterface
类扩展了由 Symfony 提供的 Symfony\Component\Validator\ConstraintViolationListInterface
类。这为 Drupal 添加了特定的方法来过滤返回的违规。以下方法可用于过滤违规:
-
getEntityViolations
:一些违规可能位于实体级别,而不是类级别。约束可能应用于实体级别,而不是特定字段。例如,Workspaces 模块在实体级别添加了一个约束来检查工作区冲突。 -
filterByFields
:给定一系列字段名,违规将仅限于那些适用于这些字段。 -
filterByFieldAccess
:此过滤器基于用户可访问的字段过滤违规。Drupal 允许以无效状态保存实体,特别是如果工作流允许权限较低的用户修改实体的特定字段。如果使用此过滤器,请谨慎,因为实体必须具有用户可能没有访问权限的更新字段。
之前的方法总是返回一个新的对象实例,并且不会对原始违规列表产生副作用。
删除实体
在此菜谱中,我们将逐步讲解如何删除实体。删除实体允许您从数据库中删除实体,使其不再存在。
如何操作…
-
在您的模块中,在
ArticleController
控制器中创建一个delete
方法,该方法有一个参数用于节点实体对象,该对象将由路由参数提供:<?php
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\node\NodeInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class ArticleController extends ControllerBase {
public function delete(NodeInterface $node):
JsonResponse {
}
}
-
要删除实体,您将调用
delete
方法:public function delete(NodeInterface $node):
JsonResponse {
$node->delete();
}
delete
方法立即从数据库存储中删除实体。
-
然后,我们将返回一个空的 JSON 响应:
public function delete(NodeInterface $node):
JsonResponse {
$node->delete();
return new JsonResponse(null, 204);
}
当没有返回内容时,我们将使用 204 No Content
状态码。
-
我们必须在
routing.yml
中创建路由,该路由指向ArticleController
的delete
方法,以便对/articles/{node}
路径进行 HTTPDELETE
请求:mymodule.delete_article:
path: /articles/{node}
defaults:
_controller: Drupal\mymodule\Controller\
ArticleController::delete
methods: [DELETE]
requirements:
_access: 'TRUE'
此路由的要求应该是 _entity_access: 'node.delete'
。然而,我们使用了 _access: 'TRUE'
来绕过此菜谱的访问检查。
-
重建您的 Drupal 网站缓存,使其了解新的路由:
php vendor/bin/drush cr
-
可以使用如下 HTTP 请求检索您 Drupal 网站上的文章:
DELETE http://localhost/articles/1
Accept: application/json
-
对文章进行第二次 HTTP 请求将返回
404 Not Found
响应,因为它已被删除:GET http://localhost/articles/1
Accept: application/json
它是如何工作的…
实体类上的 delete
方法被委派给实体类型存储处理器的 delete
方法。当内容实体被删除时,存储会从数据库中清除所有字段值和实体的数据库记录。删除是永久的,无法撤销。
当实体被删除时,实体存储会触发以下钩子,允许您挂钩到实体的删除。正在删除的实体作为参数传递:
-
hook_ENTITY_TYPE_predelete
-
hook_entity_predelete
-
hook_ENTITY_TYPE_delete
-
hook_entity_delete
在钩子中抛出异常将回滚数据库事务并阻止实体被删除,但如果不适当处理,也可能导致 Drupal 崩溃。
第七章:使用 Form API 创建表单
本章将介绍 Form API 的使用,该 API 用于在 Drupal 中创建表单而无需编写任何 HTML!本章将指导您创建一个表单来管理具有验证的自定义配置项。您还将学习如何使用states
属性实现条件性表单字段,以控制元素是隐藏的、可见的、必需的还是其他。我们还将演示如何在 Drupal 表单中实现 AJAX 以提供动态表单元素。最后,您将学习如何修改 Drupal 中的其他表单以自定义它们的应用。
在本章中,我们将介绍以下菜谱:
-
创建自定义表单并保存配置更改
-
验证表单数据
-
指定条件性表单元素
-
在 Drupal 表单中使用 AJAX
-
自定义 Drupal 中的现有表单
技术要求
本章需要安装一个自定义模块。在以下菜谱中,模块名称为mymodule
。请适当替换。您可以在 GitHub 上找到本章中使用的完整代码:github.com/PacktPublishing/Drupal-10-Development-Cookbook/tree/main/chp07
创建自定义表单并保存配置更改
在这个菜谱中,我们将创建一个表单,允许将公司名称和电话号码保存到配置中。表单被定义为实现\Drupal\Core\Form\FormInterface
的类。\Drupal\Core\Form\FormBase
作为表单的标准基类。我们将扩展这个类来创建一个新的表单,用于保存自定义配置。
如何操作...
-
首先,我们需要在模块目录中创建
src/Form
目录。我们将把我们的表单类放在这个目录中,这将为我们的表单类提供Form
命名空间:mkdir -p src/Form
-
在
Form
目录下创建一个名为CompanyForm.php
的文件。这个文件将包含我们的CompanyForm
表单类。 -
我们的
CompanyForm
类将扩展由 Drupal 核心提供的FormBase
类:<?php
namespace Drupal\mymodule\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class CompanyForm extends FormBase {
public function getFormId() {}
public function buildForm(array $form,
FormStateInterface $form_state) {}
public function submitForm(array &$form,
FormStateInterface $form_state) {}
}
遵循 PSR-4 自动加载约定,我们的类位于\Drupal\mymodule\Form
命名空间中,Drupal 可以将其确定为我们的模块中的src/Form
目录。根据 PSR-4,文件名和类名也必须相同。
\Drupal\Core\Form\FormBase
类提供了处理表单的内部逻辑。它只需要我们实现getFormId
、buildForm
和submitForm
方法,这些方法将在以下步骤中解释和实现。
-
所有表单都必须有一个唯一的字符串来标识表单。让我们通过更新
getFormId
方法来给我们的表单分配 IDcompany_form
:public function getFormId() {
return 'company_form';
}
-
buildForm
方法返回表单结构作为表单元素的数组。我们提供了一个文本字段用于公司名称和电话:public function buildForm(array $form,
FormStateInterface $form_state) {
$form['company_name'] = [
'#type' => 'textfield',
'#title' => 'Company name',
];
$form['company_telephone'] = [
'#type' => 'tel',
'#title' => 'Company telephone',
];
return $form;
}
buildForm
方法传递 $form
数组参数,这是我们的表单结构被添加的地方。在 $form
数组中,company_name
和 company_telephone
都被调用为 #type
(用于指定元素是什么)和 #title
(用作标签)。
-
接下来,我们想要加载现有的配置并将它的值传递给元素:
public function buildForm(array $form,
FormStateInterface $form_state) {
$company_settings = $this->config
('company_settings');
$form['company_name'] = [
'#type' => 'textfield',
'#title' => 'Company name',
'#default_value' => $company_settings->
get('company_name'),
];
$form['company_telephone'] = [
'#type' => 'tel',
'#title' => 'Company telephone',
'#default_value' => $company_settings->
get('company_telephone'),
];
return $form;
}
我们使用 config
方法加载一个名为 company_settings
的配置对象,它将存储我们的值。#default_value
属性允许指定一个初始值,该值应用于元素。
-
然后,我们必须添加一个提交按钮:
public function buildForm(array $form,
FormStateInterface $form_state) {
$company_settings = $this->
config('mymodule.company_settings');
$form['company_name'] = [
'#type' => 'textfield',
'#title' => 'Company name',
'#default_value' => $company_settings->
get('company_name'),
];
$form['company_telephone'] = [
'#type' => 'tel',
'#title' => 'Company telephone',
'#default_value' => $company_settings->
get('company_telephone'),
];
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => 'Submit',
];
return $form;
}
我们提供了一个带有 submit
类型的提交按钮作为表单元素。最佳实践是将提交按钮和其他按钮放置在 actions
渲染元素中。
-
填写
submitForm
并将其保存到mymodule.company_data
对象中:public function submitForm(array &$form,
FormStateInterface $form_state) {
$config = $this->configFactory()->
getEditable('mymodule.company_settings');
$config->set('company_name', $form_state->
getValue('company_name'));
$config->set('company_telephone', $form_state->
getValue('company_telephone'));
$config->save();
$this->messenger()->addStatus('Updated company
information');
}
为了保存我们的配置,我们必须使用 getEditable
方法从配置工厂中获取它。这允许我们保存对配置对象的更改。
-
添加一个新的路由,使表单可在
/company-form
路径下访问:mymodule.company_form:
path: /company-form
defaults:
_form: Drupal\mymodule\Form\CompanyForm
_title: Company form
requirements:
_access: 'TRUE'
-
重建你的 Drupal 网站的缓存以使其意识到新的路由:
php vendor/bin/drush cr
-
现在,你可以访问
/company-form
并访问你的表单:
图 7.1 – 公司表单的输出
它是如何工作的...
此配方创建了一个可以通过路由访问的表单,使用 _form
属性代替 _controller
属性。_form
属性包含表单类名。当 Drupal 的路由系统构建时,添加了一个 _controller
条目,由 \Drupal\Core\Controller\HtmlFormController::getContentResult
处理。此添加是通过 \Drupal\Core\Routing\Enhancer\FormRouteEnhancer
增强器完成的。HtmlFormController
将 _form
中的类名传递给表单构建器,并返回完整的表单渲染数组。
表单构建器负责确定表单是正在渲染还是如果请求是 HTTP GET
或 HTTP POST
,它正在处理提交。
注意
表单 API 自 Drupal 4.7 版本以来就存在于 Drupal 中。实际上,渲染数组系统是从表单 API 中诞生的,并在 Drupal 7 中创建。
当表单构建器处理表单提交时,表单结构也将从 buildForm
中构建。这样做是为了防止提交额外的无效值。传入的输入数据被映射到表单中的元素,并推送到表单的 state
值,这些值可以通过 getValue
用于特定元素值或 getValues
用于所有值来检索。一旦值已设置在表单状态中,表单构建器将调用表单验证服务。表单验证将在下一个配方 验证表单数据 中处理。
如果表单没有错误,那么表单构建将调用表单提交服务并调用表单的 submitForm
方法。
还有更多...
许多组件构成了通过 Drupal 的表单 API 创建的表单。我们将深入探讨其中的一些。
表单状态
\Drupal\Core\Form\FormStateInterface
对象表示表单及其数据的当前状态。表单状态包含用户提交给表单的数据以及构建状态信息。它还可以在表单构建和提交之间存储任意信息。表单状态还处理表单提交后的重定向。在接下来的菜谱中,您将与表单状态进行更多交互。
表单缓存
Drupal 使用缓存表来存储表单。这个表包含由表单构建标识符识别的构建表,这使得 Drupal 能够在 AJAX 请求期间验证表单,并在需要时轻松构建它们。保持表单缓存在持久存储中非常重要;否则,可能会产生不良后果,例如丢失表单数据或使表单无效。我们将在 在 Drupal 中使用 AJAX 的表单 菜谱中介绍 AJAX 表单。
ConfigFormBase 类
Drupal 提供了一个基础表单类来简化修改配置对象的表单。这是 \Drupal\Core\Form\ConfigFormBase
类。这个类防止在检索配置对象时从配置工厂调用 getEditable
。它还在表单提交时添加了默认提交按钮和消息。
这里是我们使用 ConfigFormBase
的表单类的更新版本。getEditableConfigNames
方法包含我们的配置对象名称,并允许在加载时对其进行编辑:
<?php
namespace Drupal\mymodule\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
class CompanyForm extends ConfigFormBase {
public function getFormId() {
return 'company_form';
}
protected function getEditableConfigNames() {
return ['mymodule.company_settings'];
}
public function buildForm(array $form, FormStateInterface
$form_state) {
$company_settings = $this->
config('mymodule.company_settings');
$form['company_name'] = [
'#type' => 'textfield',
'#title' => 'Company name',
'#default_value' => $company_settings->
get('company_name'),
];
$form['company_telephone'] = [
'#type' => 'tel',
'#title' => 'Company telephone',
'#default_value' => $company_settings->
get('company_telephone'),
];
return parent::buildForm($form, $form_state);
}
public function submitForm(array &$form,
FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$this->config('mymodule.company_settings')
->set('company_name', $form_state->
getValue('company_name'))
->set('company_telephone', $form_state->
getValue('company_telephone'))
->save();
}
}
参见
-
表单和渲染元素文档:
www.drupal.org/docs/drupal-apis/form-api/form-render-elements
-
可用表单和渲染元素列表:
api.drupal.org/api/drupal/elements/10.0.x
验证表单数据
我们将在上一个菜谱中创建的表单基础上添加验证。我们将在公司表单的表单元素中添加验证信息。还有一个 validateForm
方法,可以用来程序化地识别错误并防止表单提交。
准备工作
这个菜谱将使用在 创建自定义表单并保存配置更改 菜谱中创建的表单类。
如何操作...
-
首先,我们将使表单元素成为
required
。这将防止在未提供值的情况下提交表单。更新buildForm
中的表单元素以匹配以下内容:$form['company_name'] = [
'#type' => 'textfield',
'#title' => 'Company name',
'#required' => TRUE,
'#default_value' => $company_settings->
get('company_name'),
];
$form['company_telephone'] = [
'#type' => 'tel',
'#title' => 'Company telephone',
'#required' => TRUE,
'#default_value' => $company_settings->
get('company_telephone'),
];
这是通过向表单元素添加 '#required' => TRUE
来实现的。当表单元素被标记为 required
时,Drupal 将自动验证该字段是否有非空值。它还指定了 HTML5 的 required
属性,添加了客户端验证。
-
其次,我们将向电话表单元素添加输入约束。尽管我们有 HTML5 的
tel
元素,但它并不验证输入字符:$form['company_telephone'] = [
'#type' => 'tel',
'#title' => 'Company telephone',
'#required' => TRUE,
'#pattern' => '^[0-9-+\s()]*$',
'#default_value' => $company_settings->
get('company_telephone'),
];
#pattern
属性允许指定渲染的input
元素的 HTML5 pattern
属性。此模式表达式允许输入数字、破折号和括号,但不能输入常规的字母字符。
-
接下来,我们将覆盖
validateForm
方法,这允许进行程序性验证:public function validateForm(array &$form, FormStateInterface $form_state) {
$company_name = $form_state->
getValue('company_name');
if (str_contains($company_name, 'foo')) {
$form_state->setErrorByName(
'company_name',
'Name cannot contain "foo"'
);
}
}
我们使用表单状态的getValue
方法来获取company_name
的提交值。使用str_contains
,我们检查公司名称是否包含单词foo
。如果是,我们使用setErrorByName
方法在company_name
输入上设置错误。
- 当表单提交时,公司名称不能包含单词
foo
:
图 7.2 – 包含验证错误的公司名称字段
它是如何工作的…
当表单提交时,表单构建器根据传入的用户输入和表单元素创建表单状态值作为映射。表单构建器调用表单验证服务。表单验证从内到外进行。表单验证服务遍历每个表单元素,然后为表单类调用validateForm
方法。
当元素被验证时,这是评估#required
属性的时候。如果表单状态缺少元素的值,则将其标记为有错误。对于#pattern
也是如此。后端仍然验证传入的输入以确保它与提供的模式匹配。
参见
- 表单和渲染元素验证的附加属性文档:
www.drupal.org/docs/drupal-apis/form-api/form-render-elements
指定条件表单元素
表单 API 提供了一种定义表单元素状态的方法。这些状态映射到可以控制元素是否必需、可见等 JavaScript 交互。在这个例子中,我们将演示一个在复选框被选中之前禁用提交按钮的表单。
如何做到这一点…
-
在您的模块的
src/Form
目录中创建一个名为ApprovalRequiredForm.php
的文件,以保存ApprovalRequiredForm
表单类。 -
我们将定义具有表单 ID
mymodule_approval_form
的ApprovalRequiredForm
类:<?php
namespace Drupal\mymodule\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class ApprovalRequiredForm extends FormBase {
public function getFormId() {
return 'mymodule_approval_form';
}
public function buildForm(array $form,
FormStateInterface $form_state) {
return $form;
}
public function submitForm(array &$form,
FormStateInterface $form_state) {
}
}
-
在
buildForm
方法中,我们将首先创建一个checkbox
元素,该元素对于提交表单和控制提交按钮的状态是必需的:public function buildForm(array $form,
FormStateInterface $form_state) {
$form['approval'] = [
'#type' => 'checkbox',
'#title' => 'I acknowledge',
'#required' => TRUE,
];
return $form;
}
我们添加一个复选框并将其标记为#required
,这样表单就有客户端验证和后端验证。
-
接下来,我们添加我们的提交按钮。这将包含基于复选框控制其状态的逻辑:
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => 'Submit',
'#states' => [
'disabled' => [
':input[name="approval"]' => ['checked' =>
FALSE],
],
],
];
元素的#states
属性允许您通过元素选择器指定状态及其触发条件。我们希望如果我们的复选框approval
未被选中,按钮处于disabled
状态。
-
添加一个新的路由,使表单可通过
/approval-form
路径访问:mymodule.approval_form:
path: /approval-form
defaults:
_form: Drupal\mymodule\Form\ApprovalRequiredForm
_title: Approval form
requirements:
_access: 'TRUE'
-
重建您的 Drupal 网站的缓存,使其了解新的路由:
php vendor/bin/drush cr
-
现在,您可以访问
/approval-form
并使用该表单。提交按钮将在复选框被选中之前被禁用:
图 7.3 – 在复选框被选中之前禁用提交按钮的审批表单
它是如何工作的…
表单 API 通过其元素状态功能将 PHP 代码与 JavaScript 代码桥接。当表单被处理时,#states
属性的值被 JSON 编码并添加到渲染的元素作为 data-drupal-states
属性。
这是表单提交按钮上的 data-drupal-states
的结果:
data-drupal-states="{"disabled":{":input
[name=\u0022approval\u0022]":{"checked"
:false}}}"
当一个表单有状态时,core/misc/states.js
JavaScript 文件被添加到页面中。此文件使用 [data-drupal-states]
CSS 选择器来查找所有具有状态数据的元素。然后,这些数据被解析为 JSON 并进行评估。如果提供给元素的状态没有为控制其状态的元素提供正确的 CSS 选择器,则这些状态将不会在元素上工作。
状态的文档提供了有关可用状态和条件的信息:api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Form%21FormHelper.php/function/FormHelper%3A%3AprocessStates/10.0.x
.
在 Drupal 表单中使用 AJAX
表单 API 有一种机制可以在不编写任何 JavaScript 的情况下执行 AJAX 请求。在这个例子中,我们将创建一个带有增加和减少按钮的计数器。
如何做到这一点…
-
在您的模块的
src/Form
目录中创建一个名为CounterForm.php
的文件,以保存CounterForm
表单类。 -
我们将定义一个名为
CounterForm
的类,其表单 ID 为mymodule_counter_form
:<?php
namespace Drupal\mymodule\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class CounterForm extends FormBase {
public function getFormId() {
return 'mymodule_counter_form';
}
public function buildForm(array $form,
FormStateInterface $form_state) {
return $form;
}
public function submitForm(array &$form,
FormStateInterface $form_state) {
}
}
-
在
buildForm
方法中,我们将首先创建一个用于显示我们的计数器值的元素。该元素将在每次 AJAX 请求后替换并更新为当前计数:public function buildForm(array $form,
FormStateInterface $form_state) {
$count = $form_state->get('count') ?: 0;
$form['count'] = [
'#markup' => "<p>Total count: $count",
];
return $form;
}
我们将使用表单状态存储来维护 count
值。get
方法允许从表单状态存储中检索值。我们使用 ?:
运算符确保在表单首次加载时默认值为零。否则,值将是 null
,这不是一个整数。count
的表单状态值将在我们的 AJAX 回调中更新。
-
为了让 Drupal 在每次 AJAX 请求后正确更新我们的元素,我们需要将其包裹在一个具有 HTML ID 的元素中,我们可以针对该 ID:
$form['count'] = [
'#markup' => "<p>Total count: $count",
'#prefix' => '<div id="counter">',
'#suffix' => '</div>',
];
我们使用 #prefix
和 #suffix
键用 HTML 标记包裹我们的表单元素。这为我们提供了一个包装元素,我们可以针对该元素进行 AJAX 更新。
-
接下来,我们将添加一个将触发 AJAX 调用来增加我们的计数器的按钮:
$form['increment'] = [
'#type' => 'submit',
'#value' => 'Increment',
'#ajax' => [
'callback' => [$this, 'ajaxRefresh'],
'wrapper' => 'counter',
],
];
#ajax
属性允许一个元素执行 AJAX 请求以与表单交互。#ajax
属性需要指定一个回调,该回调包含一个方法来调用,用于指定要返回的表单部分,我们将在下一节中实现它。wrapper
属性包含应更新的元素的目标 HTML ID。
-
现在,我们将实现我们为
#ajax
回调指定的类中的ajaxRefresh
方法:public function ajaxRefresh(array $form,
FormStateInterface $form_state) {
return $form['count'];
}
#ajax
属性的回调负责返回应替换的表单子集。
-
然后,我们必须更新
submitForm
方法,以便在按下增加按钮时增加我们的计数器:public function submitForm(array &$form,
FormStateInterface $form_state) {
$count = $form_state->get('count') ?: 0;
$count++;
$form_state->set('count', $count);
$form_state->setRebuild();
}
我们从表单状态存储中检索 count
值,然后在增加值后再次设置它。然后我们在表单状态上调用 setRebuild
。这指示 Drupal 重建表单,以便更新的 count
值显示出来。
-
添加一个新的路由,使表单可通过
/counter-form
路径访问:mymodule.counter_form:
path: /counter-form
defaults:
_form: Drupal\mymodule\Form\CounterForm
_title: Counter form
requirements:
_access: 'TRUE'
-
重建您的 Drupal 网站的缓存,使其了解新的路由:
php vendor/bin/drush cr
-
现在,您可以访问
/counter-form
并使用该表单:
图 7.4 – 计数表单
它是如何工作的...
表单元素上的 #ajax
属性由 \Drupal\Core\Render\Element\RenderElement::preRenderAjaxForm
处理。它在构建表单的每个元素时被调用。它附加了将触发 AJAX 调用的默认事件。对于按钮,使用的是 mousedown
JavaScript 事件。mousedown
事件用于按钮的辅助功能,因为在其他元素中按下 Enter 键可能会触发表单按钮的点击。输入文本字段处于 blur
事件。单选按钮、复选框、选择列表和日期字段处于 click
事件。
当在表单上触发 AJAX 按钮时,它会提交。调用表单的 submitForm
方法。然后,调用元素的 #ajax
回调以返回表单。然而,如果 submitForm
方法要求重建表单,就像我们在表单状态上的 setRebuild
所做的那样,则在 #ajax
回调之前调用 buildForm
方法。这允许返回的表单元素与当前表单状态值和存储相匹配。
还有更多...
在以下章节中,我们将探讨如何使用表单 API 的 AJAX 功能。
指定触发 AJAX 时的事件
可以更改触发 AJAX 时的 JavaScript 事件。这是通过在 #ajax
中指定 type
属性来实现的。例如,以下代码会在文本元素每次按键时触发 AJAX,而不是在文本元素失去焦点时:
$element['#ajax']['type'] = 'keyup';
在自定义模板中使用 AJAX
如果您在一个使用模板输出的页面上使用 AJAX,请注意您必须在模板中将表单渲染为 {{ form|without(IDs of named form elements using
AJAX) }}
。
参见
自定义 Drupal 中的现有表单
表单 API 不仅提供创建表单的方法。还有通过自定义模块中的钩子修改现有表单的方法。通过使用这种技术,可以添加新元素,更改默认值,甚至可以隐藏元素以简化用户体验。
表单的修改不会在自定义类中发生;这是一个在模块文件中定义的钩子。在这个菜谱中,我们将使用 hook_form_FORM_ID_alter()
钩子向站点配置表单添加电话字段。
如何操作…
-
确保您的模块有一个
.module
文件来包含钩子,例如mymodule.module
。 -
我们将为
system_site_information_settings
表单实现hook_form_FORM_ID_alter
:<?php
use Drupal\Core\Form\FormStateInterface;
function mymodule_form_system_site_information
_settings_alter(array &$form, FormStateInterface
$form_state) {
// Code to alter form or form state here
}
Drupal 将调用此钩子,并传递当前表单数组及其表单状态对象。表单数组是通过引用传递的,允许我们的钩子修改数组而不返回任何值。这就是为什么 $form
参数前面有 ampersand (&
) 的原因。在 PHP 中,所有对象都是通过引用传递的,这就是为什么在 $form_state
前面没有 ampersand。
可以通过检查表单类的 getFormId
方法找到表单 ID。
-
接下来,我们将我们的电话字段添加到表单中,以便它可以显示和保存:
<?php
use Drupal\Core\Form\FormStateInterface;
function mymodule_form_system_site_information_
settings_alter(array &$form, FormStateInterface
$form_state) {
$form['site_information']['site_phone'] = [
'#type' => 'tel',
'#title' => 'Site phone',
'#default_value' => \Drupal::config('system.site')
->get('phone'),
];
}
我们从 system.site
配置对象中检索当前电话值,以便如果已经设置,则可以对其进行修改。
-
我们需要向表单添加一个提交处理程序,以便保存我们新字段的配置:
<?php
use Drupal\Core\Form\FormStateInterface;
function mymodule_form_system_site_information
_settings_alter(array &$form, FormStateInterface
$form_state) {
$form['site_information']['site_phone'] = [
'#type' => 'tel',
'#title' => 'Site phone',
'#default_value' => \Drupal::config('system.site')
->get('phone'),
];
$form['#submit'][] = 'mymodule_system_site_
information_phone_submit';
}
function mymodule_system_site_information_phone_submit
(array &$form, FormStateInterface $form_state) {
$config = Drupal::configFactory()->
getEditable('system.site');
$config ->set('phone', $form_state->
getValue('site_phone'));
}
$form['#submit']
修改将我们的回调添加到表单的提交处理程序中。这允许我们的模块在表单提交后与之交互。
mymodule_system_site_information_phone_submit
回调传递了表单数组和表单状态。我们加载当前的配置工厂以接收可以编辑的配置。然后我们加载 system.site
配置对象,并根据表单状态中的值保存 phone
。
-
重建您的 Drupal 站点的缓存,使其了解新的钩子,以便在查看站点设置表单时调用:
php vendor/bin/drush cr
-
访问位于
/admin/config/system/site-information
的站点设置配置表单:
图 7.5 – 修改后的站点设置表单
它是如何工作的…
在这个菜谱中,我们针对特定的 hook_form_FORM_ID_alter()
修改钩子。还有一个通用的 hook_form_alter()
钩子,它在所有表单上调用,允许在渲染时修改每个表单。这允许模块在需要时通用地修改所有表单,或者在一个钩子中修改多个不同的表单 ID。它还允许更明确的钩子目标。
形式数组是通过引用传递的,允许在这个钩子中做出修改并改变原始数据。这使我们能够添加元素或修改现有项,例如标题和描述。
第八章:插件即插即用
插件为 Drupal 中的许多项目提供动力,例如块、字段类型和字段格式化器。插件和插件类型由模块提供。它们提供可交换的特定功能。
在本章中,我们将实现一个 块插件。我们将使用插件 API 来提供自定义字段类型以及字段的组件和格式化器。最后一个菜谱将向您展示如何创建和使用自定义插件类型。
Drupal 小版本中插件系统的即将到来的变化
PHP 8 提供了一个名为 PHP 属性 的功能 (www.php.net/manual/en/language.attributes.overview.php
)。随着 Drupal 10 对 PHP 8.1 的采用,考虑采用 PHP 属性来替代本章中使用的代码文档注解。
在 Drupal 10.1 中,可能支持使用 PHP 属性而不是注解。这是 Drupal 10 的第一个小版本。注解将在 Drupal 10 中得到支持,但可能会被弃用。关于弃用注解以使用 PHP 属性的讨论可以在以下问题中找到:www.drupal.org/project/drupal/issues/3252386
。
在本章中,我们将通过以下菜谱深入了解 Drupal 提供的插件 API:
-
使用插件创建块
-
创建自定义字段类型
-
创建自定义字段组件
-
创建自定义字段格式化器
-
创建自定义插件类型
技术要求
本章将需要一个自定义模块的安装。在以下菜谱中,模块名称为 mymodule
。请适当替换。您可以在 GitHub 上找到本章使用的完整代码:github.com/PacktPublishing/Drupal-10-Development-Cookbook/tree/main/chp08
使用插件创建块
在 Drupal 中,块是可以在主题提供的区域中放置的内容。块用于展示特定类型的内容,例如用户登录表单、一段文本,以及更多。
块是注解插件。注解插件使用文档块来提供插件详情。它们在模块的 Plugin
类命名空间中被发现。Plugin/Block
命名空间中的每个类都将由 Block
模块的插件管理器发现。
在这个菜谱中,我们将定义一个块,它将显示版权片段和当前年份,并将其放置在页脚区域。
如何做到这一点…
-
首先,我们需要在模块目录中创建
src/Plugin/Block
目录。这将转换\Drupal\mymodule\Plugin\Block
命名空间并允许块插件发现:mkdir -p src/Plugin/Block
-
在新创建的目录中创建一个名为
Copyright.php
的文件,以便我们可以为我们的块定义Copyright
类。 -
Copyright
类将扩展\Drupal\Core\Block\BlockBase
类:<?php
namespace Drupal\mymodule\Plugin\Block;
use Drupal\Core\Block\BlockBase;
class Copyright extends BlockBase {
}
我们将扩展 BlockBase
类,该类实现了 \Drupal\Core\Block\BlockPluginInterface
并为我们提供了接口几乎所有方法的实现。
-
接下来,我们将在类文档块中编写插件注释。我们将提供块的标识符、管理标签和类别作为注释标签:
<?php
namespace Drupal\mymodule\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* @Block(
* id = "copyright_block",
* admin_label = @Translation("Copyright"),
* category = @Translation("Custom"),
* )
*/
class Copyright extends BlockBase {
}
注释提供在代码注释中,并以 @
为前缀。@Block
中的 @
符号指定这是一个 Block
注释。Drupal 将解析此并基于提供的属性创建一个插件定义。id
是内部机器名,admin_label
在块列表页上显示,而 category
则出现在块选择列表中。
-
我们需要实现
build
方法以满足\Drupal\Core\Block\BlockPluginInterface
接口。这返回要显示的输出:<?php
namespace Drupal\mymodule\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* @Block(
* id = "copyright_block",
* admin_label = @Translation("Copyright"),
* category = @Translation("Custom")
* )
*/
class Copyright extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$date = new \DateTime();
return [
'#markup' => t('Copyright @year© My
Company', [
'@year' => $date->format('Y'),
]),
];
}
}
build
方法返回一个渲染数组,使用 Drupal 的 t
函数将 @year
替换为格式化为完整年份的 \DateTime
对象的输出。
-
重新构建你的 Drupal 网站缓存以重建块插件定义缓存,这将导致插件定义的重新发现:
php vendor/bin/drush cr
-
从管理菜单中的 结构 转到 块布局 页面。在页脚第四个区域中,点击 放置块。
-
查看块列表并将自定义块添加到你的区域中,例如页脚区域。找到 版权 块并点击对话框表单中的 放置块:
图 8.1 – 版权块放置对话框
-
取消选择 显示标题 复选框,以便只渲染我们的块内容。点击 保存块 并接受所有其他默认设置。
-
访问你的 Drupal 网站,并验证版权声明是否显示了当前年份:
图 8.2 – Drupal 网站页脚中的版权块
它是如何工作的...
插件系统由多个可实例化的类组成,这些类具有类似接口。使用注释和插件管理器,Drupal 使这些类可被发现。这允许与插件交互并执行其功能。
\Drupal\Core\Block\BlockManager
类指定了块插件必须位于 Plugin\Block
命名空间中。它还定义了需要实现的基接口,以及用于解析类文档块的 Annotation
类。
在检索插件定义时,插件管理器首先检查是否已经发现并缓存了定义。如果没有缓存的插件定义,插件管理器将扫描 Drupal 中注册的可用的命名空间中的\Drupal\{extension}\Plugin\Block
命名空间下的插件。然后,发现的类将与包含注释数据的类文档一起处理,并将其作为可用的插件定义缓存。
当在\Drupal\Core\Block\BlockBase
中调用label
方法时,会显示插件注释中定义的可读名称。当一个块在渲染的页面上显示时,会调用build
方法,并将其传递给主题层以输出。
还有更多...
在创建块插件时,可以使用更多深入的项目。我们将在接下来的章节中介绍这些内容。
修改块
块可以通过三种不同的方式修改:可以修改插件定义、构建数组或视图数组输出。
一个模块可以在其.module
文件中实现hook_block_alter
,从而修改所有发现块的注释定义。这将允许模块将默认的user_login_block
从用户登录更改为登录:
/**
* Implements hook_block_alter().
*/
function mymodule_block_alter(&$definitions) {
$definitions['user_login_block']['admin_label'] =
t('Login');
}
一个模块可以实现hook_block_build_alter
并修改块的构建信息。钩子通过构建数组和当前块的实例传递。模块开发者可以使用此功能添加缓存上下文或更改缓存元数据的缓存性:
/**
* Implements hook_block_build_alter().
*/
function mymodule_block_build_alter(
array &$build,
\Drupal\Core\Block\BlockPluginInterface $block
) {
// Add the 'url' cache the block per URL.
if ($block->id() == 'myblock') {
$build['#cache']['contexts'][] = 'url';
}
}
您可以通过更改此配方中创建的块以输出时间戳而不是年格式来测试缓存元数据的修改。启用缓存后,您将看到值在相同的 URL 上保持不变,但每个页面的值将不同。
最后,一个模块可以实现hook_block_view_alter
以修改要渲染的块的输出。模块可以添加要渲染的内容或删除内容。这可以用来删除contextual_links
项,允许在网站的首页上进行内联编辑:
/**
* Implements hook_block_view_alter().
*/
function mymodule_block_view_alter(
array &$build,
\Drupal\Core\Block\BlockPluginInterface $block
) {
// Remove the contextual links on all blocks that provide
them.
if (isset($build['#contextual_links'])) {
unset($build['#contextual_links']);
}
}
块设置表单
块可以提供设置表单。此配方为版权文本提供了文本我的公司。而不是在代码中设置,这可以通过块设置表单中的文本字段来定义。
让我们重新审视包含我们块类的Copyright.php
文件。我们将覆盖基类提供的方法。以下方法将添加到本配方中编写的类中。
一个块可以覆盖默认的defaultConfiguration
方法,该方法返回设置键及其默认值的数组。然后可以覆盖blockForm
方法以返回表示设置表单的Form
API 数组:
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'company_name' => '',
];
}
/**
* {@inheritdoc}
*/
public function blockForm($form,
\Drupal\Core\Form\FormStateInterface $form_state) {
$form['company_name'] = [
'#type' => 'textfield',
'#title' => t('Company name'),
'#default_value' => $this->configuration
['company_name'],
];
return $form;
}
然后,必须实现blockSubmit
方法,该方法更新块的配置。以下代码从表单的状态中检索company_name
值,该状态包含提交的值,并将其设置为configuration
属性中的company_name
键:
/**
* {@inheritdoc}
*/
public function blockSubmit($form, \Drupal
\Core\Form\FormStateInterface $form_state) {
$this->configuration['company_name'] =
$form_state->getValue('company_name');
}
最后,可以将build
方法更新为使用新的配置项:
/**
* {@inheritdoc}
*/
public function build()
{
$date = new \DateTime();
return [
'#markup' => t('Copyright @year© @company', [
'@year' => $date->format('Y'),
'@company' => $this->configuration['company_name'],
]),
];
}
您现在可以返回到版权
块。新的设置将在块实例的配置表单中可用。
定义块的访问权限
默认情况下,块为所有用户渲染。默认访问方法可以被覆盖。这允许块仅对认证用户或基于特定权限显示:
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
$route_name = $this->routeMatch->getRouteName();
if ($account->isAnonymous() && !in_array($route_name,
['user.login', 'user.logout'])) {
return AccessResult::allowed()
->addCacheContexts(['route.name',
'user.roles:anonymous']);
}
return AccessResult::forbidden();
}
上述代码来自user_login_block
。它允许在用户未登录且不在登录或注销页面时访问该块。访问权限基于当前路由名称和用户当前的角色为匿名者进行缓存。如果没有传递这些值,则返回的访问权限被禁止,并且块不会被构建。
其他模块可以实现hook_block_access
来覆盖块的访问权限:
/**
* Implements hook_block_access().
*/
function mymodule_block_access(
\Drupal\block\Entity\Block $block,
$operation,
\Drupal\Core\Session\AccountInterface $account
) {
// Example code that would prevent displaying the
Copyright' block in
// a region different from the footer.
if ($operation == 'view' && $block->getPluginId() ==
'copyright') {
return
\Drupal\Core\Access\AccessResult::forbiddenIf($block-
>getRegion() != 'footer');
}
// No opinion.
return \Drupal\Core\Access\AccessResult::neutral();
}
实现上述钩子的模块将拒绝访问我们的版权
块,如果它没有被放置在页脚区域。如果块操作不是view
,并且块不是我们的版权
块,则传递一个neutral
访问结果。neutral
结果允许系统处理其他访问结果。否则,AccessResult::forbiddenIf
将根据传递给它的布尔值返回neutral
或forbidden
。
参见
-
参考本章的创建自定义插件类型配方
-
基于注解的插件文档:
www.drupal.org/docs/drupal-apis/plugin-api/annotations-based-plugins
-
关于
Block
模块提供的钩子信息:api.drupal.org/api/drupal/core%21modules%21block%21block.api.php/10
创建自定义字段类型
字段类型是通过插件系统定义的。每种字段类型都有自己的类和定义。可以通过一个自定义类来定义新的字段类型,该类将提供模式和信息属性。
字段类型定义了通过字段API 在实体上存储和处理数据的方式。字段小部件提供在用户界面中编辑字段类型的方法。字段格式化程序提供向用户显示字段数据的方法。两者都是插件,将在后面的配方中介绍。
在本例中,我们将创建一个简单的字段类型,称为realname
,用于存储姓名和姓氏,并将其添加到评论
类型中。
准备工作
此配方向评论
类型添加一个字段,这需要安装评论
模块。评论
模块默认与标准 Drupal 安装一起安装。
如何做到这一点…
-
首先,我们需要在模块目录中创建
src/Plugin/Field/FieldType
目录。Field
模块在Plugin\Field\FieldType
命名空间中查找字段类型:mkdir -p src/Plugin/Field/FieldType
-
在新创建的目录中创建一个名为
RealName.php
的文件,以便我们可以定义RealName
类。这将为我们提供用于姓氏和名字的realname
字段类型。 -
RealName
类将扩展\Drupal\Core\Field\FieldItemBase
类:<?php
namespace Drupal\mymodule\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
class RealName extends FieldItemBase {
}
我们将扩展 FieldItemBase
类,它满足 FieldType
插件类型继承接口定义的方法,除了 schema
和 propertyDefinitions
方法。
-
接下来,我们将在类文档块中编写插件注释。我们将提供字段类型的标识符、标签、描述、类别、默认小部件和格式化器:
<?php
namespace Drupal\mymodule\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
/**
* Plugin implementation of the 'realname' field type.
*
* @FieldType(
* id = "realname",
* label = @Translation("Real name"),
* description = @Translation("This field stores a
first and last name."),
* category = @Translation("General"),
* default_widget = "string_textfield",
* default_formatter = "string"
* )
*/
class RealName extends FieldItemBase {
}
@FieldType
注释告诉 Drupal 这是一个 FieldType
插件。以下属性被定义:
-
id
: 这是插件的机器名称 -
label
: 这是字段的可读名称 -
description
: 这是字段的可读描述 -
category
: 这是字段在用户界面中显示的类别 -
default_widget
: 这是用于编辑的默认表单小部件 -
default_formatter
: 这是默认的格式化器,您可以使用它来显示字段
-
RealName
类需要实现\Drupal\Core\Field\FieldItemInterface
中定义的schema
方法。这个方法返回一个数据库 API 架构信息的数组。请将以下方法添加到您的类中:/**
* {@inheritdoc}
*/
public static function schema(FieldStorage
DefinitionInterface $field_definition) {
return [
'columns' => [
'first_name' => [
'description' => 'First name.',
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'default' => '',
],
'last_name' => [
'description' => 'Last name.',
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
'default' => '',
],
],
'indexes' => [
'first_name' => ['first_name'],
'last_name' => ['last_name'],
],
];
}
schema
方法定义了字段数据表中的数据库列。我们定义一个列来存储 first_name
和 last_name
的值。
-
我们还需要实现
propertyDefinitions
方法。这个方法返回在schema
方法中定义的值的定义。请将以下方法添加到您的类中:/**
* {@inheritdoc}
*/
public static function propertyDefinitions
(FieldStorageDefinitionInterface
$field_definition) {
$properties['first_name'] =
DataDefinition::create('string')
->setLabel(t('First name'));
$properties['last_name'] =
DataDefinition::create('string')
->setLabel(t('Last name'));
return $properties;
}
此方法返回一个数组,其键与在架构中提供的相同列名。它返回数据定义来表示字段类型中的属性。
-
我们将覆盖另一个方法,即
mainPropertyName
方法,以指定first_name
是主要属性:/**
* {@inheritdoc}
*/
public static function mainPropertyName() {
return 'first_name';
}
此方法允许在存在多个值时指定用于自动检索字段值的主要属性。
-
重建您的 Drupal 网站的缓存以构建字段类型插件定义缓存,这将导致插件定义的重新发现:
php vendor/bin/drush cr
-
字段现在将出现在字段类型管理屏幕上。要使用它,请转到 结构,然后转到 评论类型。现在您可以转到 管理字段 并点击 添加字段,为您的评论添加一个真实姓名条目:
图 8.3 – 在“添加新字段”列表中出现的字段
它是如何工作的…
字段类型的插件管理器是plugin.manager.field.field_type
服务。此插件管理器定义字段类型插件必须在Plugin\Field\FieldType
命名空间中,并实现\Drupal\Core\Field\FieldItemInterface
。
当向实体类型添加新字段时,定义从字段类型管理器检索以填充字段类型列表。当向实体类型添加字段时,该实体类型的数据库存储将根据字段在propertyDefinitions
中提供的属性以及schema
方法中的模式进行更新。
更多内容...
字段类型可以实现一个方法来定义值是否为空。我们将在下一节中介绍这一点。
定义字段是否为空
字段类型类有一个isEmpty
方法,用于确定字段是否没有值。
字段类型扩展了Drupal\Core\TypedData\Plugin\DataType\Map
,这是 Drupal 的\Drupal\Core\TypedDate\ComplexDataInterface
接口中关联数组的类表示,它提供了isEmpty
方法。
默认功能是,只要有一个属性有值,字段就不会被视为为空。例如,如果我们的真实姓名字段中第一个或最后一个名称有值,则不会被视为为空。
字段类型可以提供它们自己的实现以提供更健壮的验证。
创建自定义字段小部件
字段小部件为实体表单中的字段提供表单组件。这些与表单 API 集成,以定义字段如何被编辑以及数据在保存之前如何格式化。字段小部件通过表单显示界面进行选择和定制。
在本食谱中,我们将创建一个用于本章中创建自定义字段类型食谱中创建的字段的控件。该字段小部件将为输入第一个和最后一个名称项提供两个文本字段。
准备工作
本食谱提供了一个字段小部件,用于上一节中创建的字段类型,创建自定义 字段类型。
如何实现...
-
首先,我们需要在模块目录中创建
src/Plugin/Field/FieldWidget
目录。Field
模块在Plugin\Field\FieldWidget
命名空间中查找字段小部件:mkdir -p src/Plugin/Field/FieldWidget
-
在新创建的目录中创建一个
RealNameDefaultWidget.php
文件,以便我们可以定义RealNameDefaultWidget
类。这将提供一个自定义表单元素来编辑字段的第一个和最后一个名称值。 -
RealNameDefaultWidget
类将扩展\Drupal\Core\Field\WidgetBase
类:<?php
namespace Drupal\mymodule\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
class RealNameDefaultWidget extends WidgetBase {
}
-
我们将扩展
WidgetBase
类,它满足继承接口为FieldWidget
插件类型定义的方法,除了formElement
方法。 -
我们将在插件的注释中提供字段小部件的标识符、标签和支持的字段类型:
<?php
namespace Drupal\mymodule\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'realname_default'
widget.
*
* @FieldWidget(
* id = "realname_default",
* label = @Translation("Real name"),
* field_types = {
* "realname"
* }
* )
*/
class RealNameDefaultWidget extends WidgetBase {
}
@FieldWidget
告诉 Drupal 这是一个字段小部件插件。它定义id
来表示机器名,可读名称为label
,以及小部件交互的字段类型作为field_types
属性。
-
我们需要实现
formElement
方法,以满足在扩展\Drupal\Core\Field\WidgetBase
后的剩余接口方法。将以下方法添加到您的类中:/**
* {@inheritdoc}
*/
public function formElement(
FieldItemListInterface $items,
$delta,
array $element,
array &$form,
FormStateInterface $form_state
) {
$element['first_name'] = [
'#type' => 'textfield',
'#title' => t('First name'),
'#default_value' => '',
'#size' => 25,
'#required' => $element['#required'],
];
$element['last_name'] = [
'#type' => 'textfield',
'#title' => t('Last name'),
'#default_value' => '',
'#size' => 25,
'#required' => $element['#required'],
];
return $element;
}
formElement
方法返回表单 API 数组结构,该结构应添加到每个字段项的实体表单中。元素项的名称 - first_name
和last_name
- 映射到字段属性名称,以便正确保存。
-
接下来,我们需要修改我们的原始
RealName
字段类型插件类,以使用我们创建的默认小部件。修改src/Plugin/FieldType/RealName.php
文件,并更新default_widget
注解属性为realname_default
:/**
* Plugin implementation of the 'realname' field type.
*
* @FieldType(
* id = "realname",
* label = @Translation("Real name"),
* description = @Translation("This field stores a
first and last name."),
* category = @Translation("General"),
* default_widget = "realname_default",
* default_formatter = "string"
* )
*/
class RealName extends FieldItemBase {
-
重建您的 Drupal 站点的缓存以更新字段类型定义和新字段小部件插件发现:
php vendor/bin/drush cr
-
添加到评论类型的字段现在将使用字段小部件:
图 8.4 – 评论表单上的真实姓名小部件
它是如何工作的...
字段类型的插件管理器是plugin.manager.field.widget
服务。此插件管理器定义字段类型插件必须在Plugin\Field\FieldWidget
命名空间中,并实现\Drupal\Core\Field\WidgetInterface
。
实体表单显示系统使用插件管理器将字段定义作为选项加载到表单显示配置表单上。当使用表单显示配置构建实体表单时,表单构建过程将formElement
方法返回的元素添加到实体表单中。
还有更多...
字段小部件有额外的提供更多信息的方法;它们将在下一节中介绍。
字段小部件设置和摘要
\Drupal\Core\Field\WidgetInterface
接口定义了三个可覆盖的方法,以提供设置表单和当前设置的摘要:
-
defaultSettings
: 这返回一个设置键和默认值的数组 -
settingsForm
: 这返回一个用于设置表单的表单 API 数组 -
settingsSummary
: 这允许返回并显示在字段管理显示表单上的字符串数组
可以使用小部件设置来更改用户看到的表单。可以创建一个设置,允许字段元素仅通过一个文本字段输入第一个或最后一个名字。
创建自定义字段格式化器
字段格式化器定义了字段类型将被呈现的方式。这些格式化器返回要由主题层处理的渲染数组信息。字段格式化器在显示模式接口上进行配置。
在本配方中,我们将创建一个格式化器,用于在本章的 创建自定义字段类型 配方中创建的字段。字段格式化器将显示姓氏和名字的值,作为全名。
准备工作
此配方提供了一个字段格式化器,用于在先前的配方中创建的字段类型,创建自定义 字段类型。
如何做到这一点...
-
首先,我们需要在模块目录中创建
src/Plugin/Field/FieldFormatter
目录。Field
模块在Plugin\Field\FieldFormatter
命名空间中查找字段格式化器:mkdir -p src/Plugin/Field/FieldFormatter
-
在新创建的目录中创建一个
RealNameFormatter.php
文件,以便我们可以定义RealNameFormatter
类。这将提供一个自定义格式化器来显示字段的值。 -
RealNameFormatter
类将扩展\Drupal\Core\Field\FormatterBase
类:<?php
namespace Drupal\mymodule\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
class RealNameFormatter extends FormatterBase {
}
我们扩展了 FormatterBase
类,这满足了 FieldFormatter
插件类型继承接口定义的方法,除了 viewElements
方法。
-
我们将在插件的注解中提供字段小部件的标识符、标签和支持的字段类型:
<?php
namespace Drupal\mymodule\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Plugin implementation of the 'realname_one_line'
formatter.
*
* @FieldFormatter(
* id = "realname_one_line",
* label = @Translation("Real name (one line)"),
* field_types = {
* "realname"
* }
* )
*/
class RealNameFormatter extends FormatterBase {
}
@FieldFormatter
告诉 Drupal 这是一个字段格式化插件。它定义了 id
来表示机器名,可读名称为 label
,以及格式化器交互的字段类型作为 field_types
属性。
-
我们需要实现
viewElements
方法以满足\Drupal\Core\Field\FormatterInterface
接口。这用于渲染字段数据。将以下方法添加到您的类中:/**
* {@inheritdoc}
*/
public function viewElements(
FieldItemListInterface $items,
$langcode
) {
$element = [];
foreach ($items as $delta => $item) {
$element[$delta] = [
'#markup' => $this->t('@first @last', [
'@first' => $item->first_name,
'@last' => $item->last_name,
]),
];
}
return $element;
}
字段值作为 FieldItemListInterface
可迭代对象提供给 viewElements
方法,其中包含每个字段项。Drupal 中的字段可以包含单个值,也可以包含无限数量的值。我们遍历每个值,创建一个模板字符串,将姓氏和名字的值在一行中显示为全名。
-
接下来,我们需要修改我们原始的
RealName
字段类型插件类,以使用我们创建的默认格式化器。打开src/Plugin/FieldType/RealName.php
文件,并将default_formatter
注解属性更新为realname_one_line
:/**
* Plugin implementation of the 'realname' field type.
*
* @FieldType(
* id = "realname",
* label = @Translation("Real name"),
* description = @Translation("This field stores a
first and last name."),
* category = @Translation("General"),
* default_widget = "realname_default",
* default_formatter = "realname_one_line"
* )
*/
class RealName extends FieldItemBase {
-
重建您的 Drupal 网站的缓存以更新字段类型定义和新字段格式化插件发现:
php vendor/bin/drush cr
-
添加到 评论类型 的字段现在将使用字段格式化器:
图 8.5 – 实名格式化器的输出
它是如何工作的...
字段类型插件管理器是 plugin.manager.field.formatter
服务。此插件管理器定义字段类型插件必须在 Plugin\Field\FieldFormatter
命名空间中,并实现 \Drupal\Core\Field\FormatterInterface
。
实体视图显示系统使用插件管理器来加载字段定义作为视图显示配置表单上的选项。当使用视图显示配置构建实体时,该过程遍历实体上的每个字段并调用配置的格式化器的 viewElements
方法。最终结果用于渲染实体的显示。
还有更多...
字段格式化器有额外的提供更多信息的方法;它们将在下一节中介绍。
格式化器设置和摘要
\Drupal\Core\Field\FormatterInterface
接口定义了三个可以重写的方法,以提供设置表单和当前设置的摘要:
-
defaultSettings
:这返回一个设置键和默认值的数组 -
settingsForm
:这返回一个用于设置表单的 Form API 数组 -
settingsSummary
:这允许返回并显示在字段的 manage 显示表单上的字符串数组
设置可以用来改变格式化器显示信息的方式。例如,可以实现这些方法来提供设置以隐藏或显示姓名的首字母或最后一个字母。
创建自定义插件类型
插件系统提供了一种在 Drupal 中创建不需要实体系统数据存储功能的专业对象的方法。正如我们通过块和字段插件所看到的,每种插件类型都服务于特定的目的,并允许扩展性。
在本食谱中,我们将创建一个名为 GeoLocator
的新插件类型,该类型将返回给定 IP 地址的国家代码。我们将创建一个插件管理器、默认插件接口、插件注释定义和插件实现。内容分发网络(CDN)通常提供带有访问者国家代码的 HTTP 头。我们将提供针对 Cloudflare 和 AWS CloudFront 的插件。
如何做到这一点...
-
所有插件都需要一个充当插件管理器的服务。在您的模块的
src
目录中创建一个名为GeoLocatorManager.php
的文件。这将包含GeoLocatorManager
类。 -
通过扩展 Drupal 核心提供的
\Drupal\Core\Plugin\DefaultPluginManager
类来创建GeoLocatorManager
类:<?php
namespace Drupal\mymodule;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
class GeoLocatorManager extends DefaultPluginManager {
}
DefaultPluginManager
提供了插件管理器的基本功能,要求实现者只需重写其构造函数。
-
接下来,我们需要重写
DefaultPluginManager
类中的__construct
方法来定义有关我们的插件类型的信息。注意,它将引用以下步骤中创建的代码:<?php
namespace Drupal\mymodule;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
class GeoLocatorManager extends DefaultPluginManager {
public function __construct(
\Traversable $namespaces,
CacheBackendInterface $cache_backend,
ModuleHandlerInterface
$module_handler
) {
parent::__construct(
'Plugin/GeoLocator',
$namespaces,
$module_handler,
'Drupal\mymodule\Plugin\GeoLocator
\GeoLocatorInterface',
'Drupal\mymodule\Annotation\GeoLocator'
);
}
}
Plugin/GeoLocator
的父构造函数的第一个参数指定GeoLocator
插件必须位于一个模块中。第四个参数Drupal\mymodule\Plugin\GeoLocator\GeoLocatorInterface
标识了GeoLocator
插件必须实现的接口。第五个参数Drupal\mymodule\Annotation\GeoLocator
指定了注释类,以便插件可以通过@GeoLocator
注释进行注册。
-
在我们创建
GeoLocator
插件接口和注释之前,我们将创建服务定义以注册我们的插件管理器。创建一个mymodule.services.yml
文件并添加以下内容:services:
plugin.manager.geolocator:
class: Drupal\mymodule\GeoLocatorManager
parent: default_plugin_manager
虽然不是必需的,但命名插件管理器服务时使用plugin.manager.
然后是插件类型名称是一种模式。我们可以使用父定义来告诉服务容器在构建我们的类时使用与default_plugin_manager
定义相同的参数。
-
所有基于注释的插件都必须提供一个注释类。在
src/Annotation
中创建GeoLocator.php
以提供GeoLocator
注释类,正如我们在插件管理器中指定的那样:<?php
namespace Drupal\mymodule\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* @Annotation
*/
class GeoLocator extends Plugin
{
/**
* The human-readable name.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
}
每个属性都是可以在插件注释中定义的项目。对于我们的插件,注释定义将是@GeoLocator
,因为注释的类名是GeoLocator
。
-
接下来,我们将定义在插件管理器中定义的插件接口。插件发现过程验证
GeoLocator
插件实现了此接口。在我们的模块的src/Plugin/GeoLocator
目录中创建一个GeoLocatorInterface.php
文件以保存接口:<?php
namespace Drupal\mymodule\Plugin\GeoLocator;
use Symfony\Component\HttpFoundation\Request;
interface GeoLocatorInterface {
/**
* Get the plugin's label.
*
* @return string
* The geolocator label
*/
public function label();
/**
* Performs geolocation on an address.
*
* @param Request $request
* The request.
*
* @return string|NULL
* The geolocated country code, or NULL if not
found.
*/
public function geolocate(Request $request):
?string;
}
我们提供了一个接口,以确保在处理GeoLocator
插件时,我们可以保证有这些预期的方法。geolocate
方法接收一个请求对象并返回一个国家代码,如果找不到则返回null
。
-
现在我们已经设置了插件类型,我们将创建第一个插件以支持 Cloudflare 国家代码头。为
Cloudflare
插件类创建src/Plugin/GeoLocator/Cloudflare.php
文件:<?php
namespace Drupal\mymodule\Plugin\GeoLocator;
use Drupal\Core\Plugin\PluginBase;
use Symfony\Component\HttpFoundation\Request;
/**
* @GeoLocator(
* id = "cloudflare",
* label = "Cloudflare"
* )
*/
class Cloudflare extends PluginBase implements
GeoLocatorInterface {
public function label() {
return $this->pluginDefinition['label'];
}
public function geolocate(Request $request): ?string {
return $request->headers->get('CF-IPCountry');
}
}
Cloudflare 通过一个名为CF-IPCountry
的 HTTP 头提供访问者的国家代码。此插件返回该头的值,如果不存在则返回null
。
-
接下来,我们创建一个插件以支持 AWS CloudFront 的国家代码头。为
CloudFront
插件类创建src/Plugin/GeoLocator/CloudFront.php
文件:<?php
namespace Drupal\mymodule\Plugin\GeoLocator;
use Drupal\Core\Plugin\PluginBase;
use Symfony\Component\HttpFoundation\Request;
/**
* @GeoLocator(
* id = "cloudfront",
* label = "CloudFront"
* )
*/
class CloudFront extends PluginBase implements
GeoLocatorInterface {
public function label() {
return $this->pluginDefinition['label'];
}
public function geolocate(Request $request): ?string {
return $request->headers->get('CloudFront-Viewer-
Country');
}
}
AWS CloudFront 通过一个名为CloudFront-Viewer-Country
的 HTTP 头提供访问者的国家代码。此插件返回该头的值,如果不存在则返回null
。
-
最后,我们将创建一个演示插件,该插件从查询参数中读取国家代码。为
RequestQuery
插件类创建src/Plugin/GeoLocator/RequestQuery.php
文件:<?php
namespace Drupal\mymodule\Plugin\GeoLocator;
use Drupal\Core\Plugin\PluginBase;
use Symfony\Component\HttpFoundation\Request;
/**
* @GeoLocator(
* id = "request_query",
* label = "Request query"
* )
*/
class RequestQuery extends PluginBase implements
GeoLocatorInterface {
public function label() {
return $this->pluginDefinition['label'];
}
public function geolocate(Request $request): ?string {
return $request->query->get('countryCode');
}
}
与其他插件不同,此插件返回 URL 中countryCode
查询参数的值。
-
以下是一个示例,它将在每个页面上设置国家代码消息,如果插件可以检测到国家代码:
<?php
/**
* Implements hook_page_top().
*/
function mymodule_page_top() {
$request = \Drupal::request();
/** @var \Drupal\mymodule\GeoLocatorManager $manager
*/
$manager = \Drupal::service
('plugin.manager.geolocator');
foreach ($manager->getDefinitions() as $plugin_id =>
$definition) {
/** @var \Drupal\mymodule\Plugin\GeoLocator
\GeoLocatorInterface */
$instance = $manager->createInstance($plugin_id);
$country_code = $instance->geolocate($request);
if ($country_code) {
\Drupal::messenger()->addStatus("Country:
$country_code");
break;
}
}
}
我们从插件管理器获取定义,并为每个插件创建一个实例。然后我们检查插件是否返回结果。如果插件返回国家代码,国家代码将被添加为消息,然后循环停止。
它是如何工作的…
插件和插件类型是将具有特定功能操作的类分组的一种方式。插件管理器提供了一种发现这些类和实例化的方法。在这个食谱的最后一步中,我们使用了插件管理器来查找每个定义,创建插件的实例,然后调用geolocate
方法从请求对象中找到一个国家代码。
插件管理器使用发现方法来查找插件类。默认情况下,使用\Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery
发现方法。子目录用于查找插件,我们在插件管理器的__construct
方法中指定为Plugin/GeoLocator
。注释类发现随后遍历命名空间到其目录的映射。它发现所需目录中的 PHP 文件。然后检查这些类是否具有正确的@GeoLocator
注释并确保它们实现了GeoLocatorInterface
接口。发现的类随后被注册为插件定义。
还有更多…
创建自定义插件类型有许多附加项;我们将在以下章节中讨论其中的一些。
指定一个修改钩子
插件管理器能够定义一个修改钩子。以下代码行将被添加到GeoLocatorManager
类的构造函数中,以提供hook_geolocator_plugins_alter
修改钩子。这被传递给模块处理服务以进行调用:
public function __construct(
\Traversable $namespaces,
CacheBackendInterface $cache_backend,
ModuleHandlerInterface
$module_handler
) {
parent::__construct(
'Plugin/GeoLocator',
$namespaces,
$module_handler,
'Drupal\mymodule\Plugin\GeoLocator
\GeoLocatorInterface',
'Drupal\mymodule\Annotation\GeoLocator'
);
// Specify the alter hook.
$this->alterInfo('geolocator_info');
}
在.module
文件中实现hook_geolocator_plugins_alter
的模块具有修改所有发现的插件定义的能力。它们还具有删除定义的插件条目或修改为注释定义提供的任何信息的能力。
使用缓存后端
插件可以使用缓存后端来提高性能。这可以通过在插件管理器的构造函数中指定缓存后端通过setCacheBackend
方法来完成。以下代码行将允许GeoLocator
插件定义被缓存,并且仅在缓存重建时被发现:
$this->setCacheBackend($cache_backend,
'geolocator_plugins');
没有指定缓存后端,Drupal 将扫描文件系统以查找由模块提供的任何注释过的GeoLocator
插件。$cache_backend
变量传递给构造函数。第二个参数提供缓存键。缓存键将添加当前语言代码作为后缀。
有一个可选的第三个参数,它接受一个字符串数组,表示将导致插件定义被清除的缓存标签。这是一个高级功能,插件定义通常应通过管理器的clearCachedDefinitions
方法清除。缓存标签允许在相关缓存被清除时同时清除插件定义。
通过管理器访问插件
插件通过管理器服务加载。插件管理器有各种用于检索插件定义的方法,如下所示:
-
getDefinitions
: 此方法将返回一个插件定义数组。它首先尝试检索缓存的定义(如果有),然后在返回之前设置已发现的定义的缓存。 -
getDefinition
: 这个方法接受一个预期的插件 ID,并返回其定义。 -
createInstance
: 这个方法接受一个预期的插件 ID,并返回该插件的初始化类。 -
getInstance
: 这个方法接受一个充当插件定义的数组,并从定义中返回一个初始化的类。
第九章:创建自定义实体类型
在第六章,访问和操作实体中,我们探讨了操作单个实体。本章涵盖了为自定义数据模型创建自定义实体类型。Drupal 中的实体由不同的实体类型组成。实体类型定义为注解插件。每个实体是其实体类型类的实例。实体类型也可以根据其捆绑进行增强,以具有不同的类。您将在本章中学习如何实现自己的自定义实体。
在本章中,我们将介绍以下菜谱:
-
使用自定义类为实体捆绑
-
创建配置实体类型
-
创建内容实体类型
-
为内容实体类型创建捆绑
-
实现实体的访问控制
-
提供自定义存储处理程序
技术要求
本章将需要一个未安装的自定义模块。Drupal 不会自动注册新的实体类型或更新已安装模块的实体类型。菜谱中的更多内容部分将解释如何为已安装的模块安装或更新实体类型。
在以下菜谱中,模块名称为mymodule
。请根据您的代码适当替换mymodule
实例。您可以在 GitHub 上找到本章使用的完整代码:github.com/PacktPublishing/Drupal-10-Development-Cookbook/tree/main/chp09
。
使用自定义类为实体捆绑
每个实体都是使用其实体类型类实例化的。一个\Drupal\node\Entity\Node
和一个\Drupal\taxonomy\Entity\Term
,无论实体的捆绑如何。Drupal 允许在实例化特定捆绑的实体时更改实体信息,以提供替代类。实体捆绑通常用于不同的特定业务逻辑。此功能允许创建与捆绑字段相关的特定业务逻辑的类。
在这个菜谱中,我们将创建一个实体捆绑类,用于当有菜谱节点时使用。
入门
在这个菜谱中,我们将使用 Umami 演示安装提供的菜谱内容类型。
如何做到这一点…
-
首先,我们需要在模块目录中创建
src/Entity
目录。这将为\Drupal\mymodule\Entity
命名空间,我们将在这里创建我们的捆绑类:mkdir -p src/Entity
-
在新创建的目录中创建一个名为
Recipe.php
的文件,以便我们可以定义用于我们的菜谱节点的Recipe
类。 -
Recipe
类将扩展\Drupal\node\Entity\Node
。捆绑类必须扩展其实体类型的类:<?php
namespace Drupal\mymodule\Entity;
use Drupal\node\Entity\Node;
class Recipe extends Node {
}
如果捆绑类没有扩展实体类型的类,Drupal 在处理实体类型定义时将抛出BundleClassInheritanceException
异常。
-
默认情况下,
getTags
将返回那些引用的术语实体:class Recipe extends Node {
public function getTags(): array {
/** @var \Drupal\Core\Field\EntityReferenceField
ItemListInterface $field_tags */
$field_tags = $this->get('field_tags');
return $field_tags->referencedEntities();
}
}
实体引用字段实现 EntityReferenceFieldItemListInterface
,该接口定义了 referencedEntities
方法。该方法加载并返回所有引用的实体对象。
-
在定义了捆绑类之后,我们必须实现
hook_entity_bundle_info_alter
来注册我们的捆绑类以用于 Recipe 内容类型:<?php
/**
* Implements hook_entity_bundle_info_alter().
*/
function mymodule_entity_bundle_info_alter(&$bundles) {
$bundles ['node']['recipe']['label'] = t('Recipe');
$bundles['node']['recipe']['class'] = Drupal\
mymodule\Entity\Recipe::class;
}
$bundles
参数是一个按实体类型键控的实体类型捆绑数组。要注册实体捆绑,您将该捆绑的 class
键设置为捆绑类的类名。
-
然后,可以在 Twig 模板中用于 Recipe 内容的
getTags
方法:{% for tag in node.getTags %}
<div>Tag: {{ tag.label }}</div>
{% endfor %}
它是如何工作的…
当从数据库中加载实体记录时,值会被传递到其实体类。内容实体类型支持捆绑,因此允许为每个捆绑定义类,这些类扩展了实体类型类。
这在实体存储 \Drupal\Core\Entity\ContentEntityStorageBase
基类及其 getEntityClass
方法中处理。该方法返回在将数据库记录映射到实例化的实体对象时应使用的类。此逻辑包含在 \Drupal\Core\Entity\EntityStorageBase::mapFromStorageRecords
方法中:
foreach ($records as $record) {
$entity_class = $this->getEntityClass();
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = new $entity_class($record, $this->
entityTypeId);
$entities[$entity->id()] = $entity;
}
每当加载实体实体时,可以做出断言来检查实体对象的实例以验证其捆绑并访问该捆绑类的特定方法。这些方法也允许在 Twig 模板标记中使用。Twig 模板在 第十章,主题和 前端开发 中有所介绍。
参见
- Drupal 9.3.0 引入捆绑类功能的变更记录:
www.drupal.org/node/3191609
创建配置实体类型
在这个菜谱中,我们将创建一个名为 Announcement
的配置实体类型。这将提供一个 配置实体,允许您创建、编辑和删除可以在网站上显示的重要公告消息。
配置实体不与 Drupal 的字段 API 交互,并且没有用户界面来添加字段。它们在其类上定义属性,就像其他框架中的模型一样。然而,配置实体在数据库中没有专用表。它们以序列化数据的形式存储在 config
表中。配置实体的目的是用于配置。配置实体的例子包括视图显示、表单显示和联系表单。
如何实现...
-
首先,我们需要在模块目录中创建
src/Entity
目录。这将转换为\Drupal\mymodule\Entity
命名空间并允许进行实体类型发现:mkdir -p src/Entity
-
在新创建的目录中创建一个名为
Announcement.php
的文件,这样我们就可以为我们的实体类型定义Announcement
类。 -
Announcement
类将扩展\Drupal\Core\Config\Entity\ConfigEntityBase
类并定义我们的实体类型的属性:<?php
namespace Drupal\mymodule\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
class Announcement extends ConfigEntityBase {
public string $label = '';
public string $message = '';
}
我们扩展了ConfigEntityBase
类,该类实现了\Drupal\Core\Config\Entity\ConfigEntityInterface
接口,并满足所有必需的方法实现,这样我们只需要定义我们的属性。label
属性将包含公告的描述,而message
属性将包含公告的文本。
注意
通常,实体类型的属性是受保护的,并使用方法来设置和获取值。为了简化此配方中的代码,我们正在使用公共属性。
-
接下来,我们将为我们的实体类型编写插件注解,在类文档块中:
/**
* @ConfigEntityType(
* id = "announcement",
* label = "Announcement",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "message",
* },
* admin_permission = "administer announcement",
* )
*/
class Announcement extends ConfigEntityBase {
@ConfigEntityType
符号指定这是一个ConfigEntityType
注解。id
是实体类型 ID,用于检索实体类型的存储和其他处理程序。label
是实体类型的可读名称。entity_keys
中的值指导 Drupalid
和label
属性是什么。
当指定config_export
时,我们正在告诉配置管理系统在导出我们的实体类型时哪些属性是可导出的。admin_permission
指定了管理实体类型所需的权限名称。
-
接下来,我们将向我们的注解中添加
handlers
键:/**
* @ConfigEntityType(
* id = "announcement",
* label = "Announcement",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "message",
* },
* admin_permission = "administer announcement",
* handlers = {
* "list_builder" = "Drupal\mymodule
\AnnouncementListBuilder",
* "form" = {
* "default" = "Drupal\mymodule
\AnnouncementForm",
* "delete" = "Drupal\Core\Entity
\EntityDeleteForm"
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing
\AdminHtmlRouteProvider",
* },
* },
* )
*/
class Announcement extends ConfigEntityBase {
处理程序数组指定了提供 Drupal 与我们的实体类型交互功能的类。我们将创建的list_builder
类用于显示我们的实体类型的实体表。form
处理程序指定了在创建、编辑或删除实体时使用的表单类。route_provider
处理程序是一个将为我们实体类型生成路由的处理程序数组。
-
最后,对于我们的实体类型的注解,我们将提供
route_provider
用于构建我们的实体类型路由的链接模板:/**
* @ConfigEntityType(
* id = "announcement",
* label = "Announcement",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "message",
* },
* admin_permission = "administer announcement",
* handlers = {
* "list_builder" = "Drupal\mymodule
\AnnouncementListBuilder",
* "form" = {
* "default" = "Drupal\mymodule
\AnnouncementForm",
* "delete" = "Drupal\Core\Entity\
EntityDeleteForm"
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\
AdminHtmlRouteProvider",
* },
* },
* links = {
* "collection" = "/admin/config/system/
announcements",
* "add-form" = "/admin/config/system/
announcements/add",
* "delete-form" = "/admin/config/system/
announcements/manage/{announcement}/delete",
* "edit-form" = "/admin/config/system/
announcements/manage/{announcement}",
* },
* )
*/
class Announcement extends ConfigEntityBase {
links
数组定义了从html
路由提供者期望的键,例如collection
(列表)、add-form
、delete-form
和edit-form
。路由提供者将为给定的路径生成路由。
-
通过在
src
目录中创建一个AnnouncementListBuilder.php
文件,定义我们的list_builder
处理程序中的AnnouncementListBuilder
类:<?php
namespace Drupal\mymodule;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
class AnnouncementListBuilder extends
ConfigEntityListBuilder {
/**
* Builds the header row for the entity listing.
*
* @return array
* A render array structure of header strings.
*
* @see \Drupal\Core\Entity
\EntityListBuilder::render()
*/
public function buildHeader()
{
$header['label'] = $this->t('Label');
return $header + parent::buildHeader();
}
/**
* Builds a row for an entity in the entity listing.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for this row of the list.
*
* @return array
* A render array structure of fields for this
entity.
*
* @see \Drupal\Core\Entity\
EntityListBuilder::render()
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
return $row + parent::buildRow($entity);
}
}
我们的AnnouncementListBuilder
类扩展了\Drupal\Core\Config\Entity\ConfigEntityListBuilder
,它提供了构建我们的实体表所需的所有必需方法。我们重写了buildHeader
和buildRow
方法以确保显示实体标签。
-
现在,我们将创建用于创建和编辑我们的实体类型的实体表单。在
src
目录中创建一个AnnouncementForm.php
文件,定义AnnouncementForm
类作为默认表单处理程序:<?php
namespace Drupal\mymodule;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\mymodule\Entity\Announcement;
class AnnouncementForm extends EntityForm {
/**
* Gets the actual form array to be built.
*
* @see \Drupal\Core\Entity\EntityForm::processForm()
* @see \Drupal\Core\Entity\EntityForm::afterBuild()
*/
public function form(array $form, FormStateInterface
$form_state) {
$form = parent::form($form, $form_state);
/** @var \Drupal\mymodule\Entity\Announcement
$entity */
$entity = $this->entity;
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#required' => TRUE,
'#default_value' => $entity->label,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $entity->id(),
'#disabled' => !$entity->isNew(),
'#machine_name' => [
'exists' => [Announcement::class, 'load'],
],
];
$form['message'] = [
'#type' => 'textarea',
'#title' => $this->t('Message'),
'#required' => TRUE,
'#default_value' => $entity->message,
];
return $form;
}
/**
* Form submission handler for the 'save' action.
*
* Normally this method should be overridden to
provide specific messages to
* the user and redirect the form after the entity has
been saved.
*
* @param array $form
* An associative array containing the structure of
the form.
* @param \Drupal\Core\Form\FormStateInterface
$form_state
* The current state of the form.
*
* @return int
* Either SAVED_NEW or SAVED_UPDATED, depending on
the operation performed.
*/
public function save(array $form, FormStateInterface
$form_state) {
$result = parent::save($form, $form_state);
if ($result === SAVED_NEW) {
$this->messenger()->addMessage('The announcement
has been created.');
}
else {
$this->messenger()->addMessage('The announcement
has been updated.');
}
$form_state->setRedirectUrl($this->entity->
toUrl('collection'));
return $result;
}
}
我们重写了form
方法以添加我们的表单元素。我们为label
属性定义了一个文本字段,然后是id
的machine_name
元素。此元素将转写并将label
值转换为便于存储的值,作为实体的标识符。message
属性使用一个普通的文本区域元素。
我们还覆盖了 save
方法以提供关于已进行的更改的消息。我们还确保用户被重定向回实体的集合页面。
-
为了使我们的公告可以从管理页面访问,我们需要定义一个菜单链接。创建一个
mymodule.links.menu.yml
文件并添加以下代码:mymodule.announcements:
title: 'Announcements'
parent: system.admin_config_system
description: 'Manage announcements.'
route_name: entity.announcement.collection
这将在指定的 parent
下注册一个针对指定 route_name
的菜单链接。
-
接下来,我们将定义一个动作链接。在 Drupal 中,动作链接是页面上通常用于将用户带到表单的按钮。我们的动作链接将添加一个
mymodule.links.action.yml
文件并添加以下内容:announcement.add:
route_name: entity.announcement.add_form
title: 'Add announcement'
appears_on:
- entity.announcement.collection
route_name
与我们为 add-form
链接生成的路由名称匹配。appears_on
数组指定动作链接应在哪些路由上渲染。
-
现在我们已经提供了我们的实体类型及其处理程序,我们需要创建一个描述我们配置实体属性的架构文件。我们必须创建
config/schema
目录,架构文件将驻留在该目录:mkdir -p config/schema
-
创建一个名为
mymodule.schema.yml
的文件,用于包含我们配置实体属性的架构定义:mymodule.announcement.*:
type: config_entity
label: 'Announcement'
mapping:
id:
type: string
label: 'ID'
label:
type: label
label: 'Label'
message:
type: text
label: 'Text'
mymodule.announcement.*
键指示 Drupal,所有的 announcement
配置实体都适用于此架构。该键是通过取模块名称(提供实体类型和实体类型的 ID)构建的。
-
在安装模块之前,我们必须定义
admin_permission
中指定的权限。创建一个mymodule.permissions.yml
文件并添加以下内容:administer announcement:
title: 'Administer announcements'
在实体类型的注解中定义权限并不会自动将其注册为 Drupal 的权限。
- 安装模块。在 配置 页面上,你现在将看到 公告,允许你创建公告配置实体。
图 9.1 – 配置页面上的公告链接
它是如何工作的…
实体类型是 Drupal 中的插件。实体类型管理器提供实体类型的发现和访问其处理程序。ConfigEntityType
插件类型的类是 \Drupal\Core\Config\Entity\ConfigEntityType
。这个类设置了默认的 storage
处理程序为 \Drupal\Core\Config\Entity\ConfigEntityStorage
,以及 entity_keys
条目用于 uuid
和 langcode
属性,这些属性在 ConfigEntityBase
类中定义。
实体类型预期将定义通过 admin_permission
注解值的管理权限。实际模式是使用单词 administer
然后跟实体类型 ID。这就是为什么我们的权限名称是 administered announcement
而不是 administer announcements
。关于实体类型权限和访问的更多内容,请参阅 实现实体访问控制的 食谱。
实体系统与路由系统集成,以调用已注册的route_provider
处理器。我们的实体类型使用AdminHtmlRouteProvider
类,它扩展了DefaultHtmlRouteProvider
类,并确保每个路由都使用管理主题渲染。
更多内容...
在以下章节中,我们将介绍更多关于定义配置实体类型的信息。
为已安装的模块注册配置实体
当模块首次安装时,Drupal 也会安装实体类型。如果模块已经安装,我们需要使用实体定义更新管理器来完成此操作。这是通过在模块的.install
文件中编写更新钩子来实现的。
以下更新钩子会在已安装提供模块的 Drupal 网站上安装公告实体类型:
function mymodule_update_10001() {
/** @var \Drupal\Core\Entity\EntityDefinition
UpdateManager $edum */
$edum = \Drupal::service
('entity.definition_update_manager');
$definition = $edum->getEntityType('announcement');
$edum->installEntityType($definition);
}
实体定义更新管理器允许通过其getEntityType
方法检索尚未安装的实体类型定义。然后必须将此定义传递给installEntityType
以正确安装新的实体类型。
使用 Drush 生成配置实体类型
Drush提供了代码生成工具来自动化本章中采取的步骤。以下命令将开始代码生成过程:
php vendor/bin/drush generate entity:configuration
命令将提示输入一个模块名称,该模块应提供实体类型、实体类型标签及其 ID。生成的代码将与本食谱中描述的代码不同,因为代码生成已得到改进。例如,在撰写本文时,它生成routing.yml
过度使用route_provider
。
图 9.2 – Drush 代码生成的输出
架构定义可用的数据类型
Drupal 核心提供了自己的配置信息。在core/config/schema
目录下有一个core.data_types.schema.yml
文件。这些是核心提供的基本数据类型,可以在创建配置架构时使用。该文件包含数据类型的 YAML 定义以及代表它们的类:
boolean:
label: 'Boolean'
class: '\Drupal\Core\TypedData\Plugin\DataType
\BooleanData'
email:
label: 'Email'
class: '\Drupal\Core\TypedData\Plugin\DataType\Email'
string:
label: 'String'
class: '\Drupal\Core\TypedData\Plugin\DataType
\StringData'
当配置架构定义指定了一个类型为电子邮件的属性时,该值将由\Drupal\Core\TypedData\Plugin\DataType\Email
类处理。数据类型是一种插件形式,每个插件的注释指定了验证约束。这是围绕 Symfony Validator组件构建的。
参见
-
第四章, 使用自定义代码扩展 Drupal
-
第八章, 使用插件即插即用
-
第七章, 使用 Form API 创建表单
-
请参考配置架构/元数据
www.drupal.org/node/1905070
创建内容实体类型
内容实体通过 Field UI 模块提供基础字段定义和可配置字段。内容实体还支持修订和翻译。内容实体提供了表单和视图两种显示模式,用于控制字段的编辑和显示。当实体未指定捆绑包时,将自动有一个与实体同名的捆绑包实例。
在这个菜谱中,我们将创建一个自定义内容实体,不指定捆绑包。我们将创建一个 Message 实体,它可以作为通用消息的内容实体。
如何做到这一点…
-
首先,我们需要在模块目录中创建
src/Entity
目录。这将转换为\Drupal\mymodule\Entity
命名空间,并允许进行实体类型发现:mkdir -p src/Entity
-
在新创建的目录中创建一个名为
Message.php
的文件,以便我们可以为我们的实体类型定义Message
类。 -
Message
类将扩展\Drupal\Core\Entity\ContentEntityBase
类:<?php
namespace Drupal\mymodule\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
class Message extends ContentEntityBase {
}
我们扩展 ContentEntityBase
,它实现了 \Drupal\Core\Entity\ContentEntityInterface
并满足所有必需的方法。
-
内容实体不为其值定义类属性,而是依赖于字段定义。我们必须为我们的内容实体类型定义基础字段:
class Message extends ContentEntityBase {
public static function baseFieldDefinitions
(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions
($entity_type);
$fields['title'] = BaseFieldDefinition
::create('string')
->setLabel(t('Title'))
->setRequired(TRUE)
->setDisplayOptions('form', [
'type' => 'string_textfield',
])
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
])
->setDisplayConfigurable('view', TRUE);
$fields['content'] = BaseFieldDefinition
::create('text_long')
->setLabel(t('Content'))
->setRequired(TRUE)
->setDescription(t('Content of the message'))
->setDisplayOptions('form', [
'type' => 'text_textarea',
])
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'text_default',
])
->setDisplayConfigurable('view', TRUE);
return $fields;
}
}
baseFieldDefinitions
方法定义了内容实体拥有的字段。它必须返回一个字段定义数组,使用 BaseFieldDefinition
生成。
-
接下来,我们将为我们的内容实体类型在类文档块中编写插件注解:
/**
* Defines the message entity class.
*
* @ContentEntityType(
* id = "message",
* label = @Translation("Message"),
* base_table = "message",
* entity_keys = {
* "id" = "message_id",
* "label" = "title",
* "uuid" = "uuid"
* },
* admin_permission = "administer message",
* field_ui_base_route =
"entity.message.collection",
* )
*/
class Message extends ContentEntityBase {
@ContentEntityType
符号指定这是一个 ContentEntityType
注解。id
是实体类型 ID,用于检索实体类型的存储和其他处理器。label
是实体类型的人类可读名称。base_table
是用于实体类型的数据库表名称。entity_keys
中的值指示 Drupal id
、label
和 uuid
属性是什么。
admin_permission
指定管理实体类型所需的权限名称。field_ui_base_route
包含一个路由名称,Field UI 将使用它来提供字段管理界面。
-
接下来,我们将向我们的注解中添加
handlers
键:/**
* Defines the message entity class.
*
* @ContentEntityType(
* id = "message",
* label = @Translation("Message"),
* base_table = "message",
* entity_keys = {
* "id" = "message_id",
* "label" = "title",
* "uuid" = "uuid"
* },
* admin_permission = "administer message",
* field_ui_base_route =
"entity.message.collection",
* handlers = {
* "list_builder" = "Drupal\mymodule\
MessageListBuilder",
* "form" = {
* "default" = "Drupal\mymodule\MessageForm",
* "delete" = "Drupal\Core\Entity\
ContentEntityDeleteForm",
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\
Routing\DefaultHtmlRouteProvider",
* },
* },
* )
*/
class Message extends ContentEntityBase {
处理器数组指定了提供 Drupal 与我们的实体类型交互功能的类。我们将创建的 list_builder
类用于显示我们实体类型的实体表。form
处理器指定了在创建、编辑或删除实体时使用的表单类。route_provider
处理器是一个处理器数组,将为我们的实体类型生成路由。
-
对于我们的实体类型的注解,我们将提供
route_provider
将用于为我们的实体类型构建路由的链接模板:/**
* Defines the message entity class.
*
* @ContentEntityType(
* id = "message",
* label = @Translation("Message"),
* base_table = "message",
* entity_keys = {
* "id" = "message_id",
* "label" = "title",
* "uuid" = "uuid"
* },
* admin_permission = "administer message",
* field_ui_base_route =
"entity.message.collection",
* handlers = {
* "list_builder" = "Drupal\mymodule
\MessageListBuilder",
* "form" = {
* "default" = "Drupal\mymodule\MessageForm",
* "delete" = "Drupal\Core\Entity\
ContentEntityDeleteForm",
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing
\DefaultHtmlRouteProvider",
* },
* },
* links = {
* "canonical" = "/messages/{message}",
* "add-form" = "/messages/add",
* "edit-form" = "/messages/{message}/edit",
* "delete-form" = "/messages/{message}/delete",
* "collection" = "/admin/structure/messages"
* },
* )
*/
class Message extends ContentEntityBase {
links
数组定义了从 html
路由提供程序期望的键,例如 collection
(列表)、add-form
、delete-form
和 edit-form
。路由提供程序将为给定的路径生成路由。
-
通过在
src
目录中创建一个MessageListBuilder.php
文件来创建在list_builder
处理器中定义的MessageListBuilder
类:<?php
namespace Drupal\mymodule;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
class MessageListBuilder extends EntityListBuilder {
public function buildHeader() {
$header['title'] = $this->t('Title');
return $header + parent::buildHeader();
}
public function buildRow(EntityInterface $entity) {
$row['title'] = $entity->toLink();
return $row + parent::buildRow($entity);
}
}
我们的MessageListBuilder
类扩展了\Drupal\Core\Entity\EntityListBuilder
,它提供了构建我们的实体表所需的所有方法。我们重写了builderHeader
和buildRow
方法,以确保实体标签被显示并链接到查看实体。
-
现在,我们将创建用于创建和编辑我们的实体类型的实体表单。通过在
src
目录中创建一个MessageForm.php
文件来创建作为默认表单处理器的MessageForm
类:<?php
namespace Drupal\mymodule;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Form\FormStateInterface;
class MessageForm extends ContentEntityForm {
public function save(array $form, FormStateInterface
$form_state) {
$result = parent::save($form, $form_state);
if ($result === SAVED_NEW) {
$this->messenger()->addMessage('The message has
been created.');
}
else {
$this->messenger()->addMessage('The message has
been updated.');
}
$form_state->setRedirectUrl($this->entity->
toUrl('collection'));
return $result;
}
}
我们重写了save
方法以提供关于已进行的更改的消息。我们确保用户被重定向回实体的集合页面。所有其他字段值的处理都在基本表单类中完成。
-
要使我们的消息在
mymodule.links.menu.yml
文件中的管理区域可访问,并添加以下内容:mymodule.messages:
title: 'Messages'
parent: system.admin_structure
description: 'Manage messages.'
route_name: entity.message.collection
这将在指定的parent
下注册一个菜单链接,对应于指定的route_name
。
-
接下来,我们将定义一个操作链接。在 Drupal 中,操作链接是页面上的按钮,通常用于将用户带到表单。我们的操作链接将添加一个
mymodule.links.action.yml
文件,并添加以下内容:message.add:
route_name: entity.message.add_form
title: 'Add message'
appears_on:
- entity.message.collection
route_name
与我们的路由提供程序为add-form
链接生成的路由名称匹配。appears_on
数组指定了操作链接应该渲染在哪些路由上。
-
要从
mymodule.links.task.yml
显示字段管理标签并注册我们的路由实体集合路由作为根任务:entity.message.collection_tab:
route_name: entity.message.collection
base_route: system.admin_content
title: 'Messages'
-
在安装模块之前,我们必须定义
admin_permission
中指定的权限。创建一个mymodule.permissions.yml
文件并添加以下内容:administer message:
title: 'Administer messages'
在实体类型的注解中定义权限不会自动将其注册为 Drupal 的权限。
- 安装模块。在
结构
页面,你现在将看到消息
,允许你创建消息内容实体。
图 9.3 – 消息收集,带有字段管理标签
它是如何工作的…
ContentEntityType
插件类型的类是\Drupal\Core\Entity\ContentEntityType
。这个类设置storage
处理器为\Drupal\Core\Entity\Sql\SqlContentEntityStorage
,该处理器控制将实体存储在数据库中。它还指定了一个view_builder
处理器,用于渲染实体,为\Drupal\Core\Entity\EntityViewBuilder
类。
实体类型预期通过admin_permission
注解值定义管理权限。实际模式是使用单词administer
然后是实体类型 ID。这就是为什么我们的权限名称是administer announcement
而不是administer announcements
。关于实体类型权限和访问的更多内容,请参阅为实体实现访问控制菜谱。
当定义基本字段定义时,它们的查看和表单显示模式配置是在代码中定义的。setDisplayOptions
方法提供了用于字段的默认显示选项。通过调用 setDisplayConfigurable
并传入 TRUE
,可以允许使用 Field UI 界面来控制字段,以管理查看和表单显示模式。
更多内容...
我们将讨论如何为我们的内容实体添加额外的功能。
为已安装的模块注册内容实体
当一个模块首次安装时,Drupal 也会安装实体类型。如果模块已经安装,我们需要使用实体定义更新管理器来完成此操作。这是通过在模块的 .install
文件中编写更新钩子来实现的。
以下更新钩子会在已经安装提供模块的 Drupal 网站上安装消息实体类型:
function mymodule_update_10001() {
/** @var \Drupal\Core\Entity\
EntityDefinitionUpdateManager $edum */
$edum = \Drupal::service
('entity.definition_update_manager');
$definition = $edum->getEntityType('message');
$edum->installEntityType($definition);
}
实体定义更新管理器允许通过其 getEntityType
方法检索尚未安装的实体类型定义。然后必须将此定义传递给 installEntityType
以正确安装新的实体类型。
使用 Drush 生成配置实体类型
Drush 提供了代码生成工具来自动化本章中采取的步骤。以下命令将开始代码生成过程:
php vendor/bin/drush generate entity:content
命令将提示输入一个模块名称,该模块应提供实体类型、实体类型标签及其 ID。它允许配置实体类型以可翻译并支持修订,这些将在以下章节中讨论。生成的代码将与本菜谱中描述的不同,因为代码生成已得到改进。
查看、编辑和删除的标签页
当编辑内容时,你可能已经使用了 links.task.yml
,就像在这个菜谱中定义的启用 Field UI 一样。以下定义将添加与本章中定义的内容实体类型相同的标签页:
entity.message.canonical:
route_name: entity.message.canonical
base_route: entity.message.canonical
title: 'View'
entity.message.edit_form:
route_name: entity.message.edit_form
base_route: entity.message.canonical
title: Edit
entity.message.delete_form:
route_name: entity.message.delete_form
base_route: entity.message.canonical
title: Delete
weight: 10
允许内容实体类型可翻译
Drupal 的一个主要特性是它处理多语言内容的能力,我们将在 第十一章,多语言和国际化 中介绍。为了使内容实体可翻译,它必须提供一个 langcode
实体键并将其基本字段标记为可翻译。
Message
实体的更新 entity_keys
注解看起来如下:
* entity_keys = {
* "id" = "message_id",
* "label" = "title",
* "uuid" = "uuid",
* "langcode" = "langcode",
* },
ContentEntityBase
类将检查此 langcode
实体键是否存在,并自动生成一个语言字段。
默认情况下,字段不可翻译。要将字段标记为可翻译,必须在字段定义对象上调用 setTranslatable
方法。以下代码使 title
字段可翻译:
$fields['title'] = BaseFieldDefinition::create('string')
->setLabel(t('Title'))
->setRequired(TRUE)
->setTranslatable(TRUE)
允许内容实体类型支持修订
Drupal 有内容审查和工作空间
模块,用于提供使用修订版本的高级内容管理工作流程,正如我们在第三章中所述,通过视图显示内容。为了支持修订版本,内容实体必须在其实体类型注释中提供附加信息,并将基字段标记为支持修订版本。
支持修订版本的内容实体类型必须定义revision_table
和revision_metadata_keys
。这些与base_table
和entity_keys
类似:
* revision_table = "message_revision",
* show_revision_ui = TRUE,
* revision_metadata_keys = {
* "revision_user" = "revision_uid",
* "revision_created" = "revision_timestamp",
* "revision_log_message" = "revision_log"
* },
show_revision_ui
键控制是否在修改实体时在实体表单中显示修订日志。
内容实体类型必须实现\Drupal\Core\Entity\RevisionLogInterface
,并可以使用\Drupal\Core\Entity\RevisionLogEntityTrait
特质来自动创建支持修订版本所需的基本字段和方法:
class Message extends ContentEntityBase {
use RevisionLogEntityTrait;
public static function baseFieldDefinitions
(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
// Add the revision metadata fields.
$fields += static::revisionLogBaseFieldDefinitions
($entity_type);
最后,字段必须被标记为支持修订版本:
$fields['title'] = BaseFieldDefinition::create('string')
->setLabel(t('Title'))
->setRequired(TRUE)
->setRevisionable(TRUE)
为内容实体类型创建一个包
包允许您拥有内容实体不同变体的版本。所有包共享相同的基字段定义,但不配置字段。这允许每个包都有自己的自定义字段。显示模式也取决于特定的包。这允许每个包都有自己的表单模式和视图模式配置。
使用前面菜谱中的自定义实体,我们将添加一个配置实体作为包。这将允许您为多个自定义字段配置有不同的消息类型。
如何做到这一点...
-
在
src/Entity
目录中创建一个名为MessageType.php
的文件,以便我们可以为我们的配置实体类型定义MessageType
类,该类将为我们的Message
实体提供包: -
MessageType
类将扩展\Drupal\Core\Config\Entity\ConfigEntityBundleBase
类,并定义我们的实体类型的属性:<?php
namespace Drupal\mymodule\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
class MessageType extends ConfigEntityBundleBase {
public string $label = '';
}
我们扩展了ConfigEntityBundleBase
类。这是一个扩展ConfigEntityBase
并提供用于为内容实体类型提供包的配置实体类型的各种增强功能的类。label
属性将包含消息类型的标签。
-
接下来,我们将为我们的实体类型在类文档块中编写插件注释:
/**
* @ConfigEntityType(
* id = "message_type",
* label = "Message type",
* config_prefix = "message_type",
* bundle_of = "message",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* },
* admin_permission = "administer message_type",
*/
class MessageType extends ConfigEntityBundleBase {
注释定义就像其他配置实体一样,除了bundle_of
键。bundle_of
键定义了此配置实体类型提供的包的实体类型,并表明这是一个包配置实体类型。
-
接下来,我们将向我们的注释中添加
handlers
键:/**
* @ConfigEntityType(
* id = "message_type",
* label = @Translation("Message type"),
* config_prefix = "message_type",
* bundle_of = "message",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* },
* admin_permission = "administer message_type",
* handlers = {
* "list_builder" = "Drupal\mymodule\
MessageTypeListBuilder",
* "form" = {
* "default" = "Drupal\mymodule
\MessageTypeForm",
* "delete" = "Drupal\Core\Entity
\EntityDeleteForm"
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing
\AdminHtmlRouteProvider",
* },
* },
* )
*/
class MessageType extends ConfigEntityBundleBase {
我们定义实体类型的处理程序,就像我们在创建配置实体类型菜谱中所做的那样,但使用我们特定的form
和list_builder
处理程序。
-
最后,为了我们实体类型的注释,我们将提供
route_provider
将用于为实体类型构建路由的链接模板:/**
* @ConfigEntityType(
* id = "message_type",
* label = @Translation("Message type"),
* config_prefix = "message_type",
* bundle_of = "message",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* },
* admin_permission = "administer message_type",
* handlers = {
* "list_builder" = "Drupal\mymodule\
MessageTypeListBuilder",
* "form" = {
* "default" = "Drupal\mymodule\
MessageTypeForm",
* "delete" = "Drupal\Core\Entity\
EntityDeleteForm"
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing
\AdminHtmlRouteProvider",
* },
* },
* links = {
* "collection" = "/admin/structure/message-
types",
* "add-form" = "/admin/structure/message-
types/add",
* "delete-form" = "/admin/structure/message-
types/{message_type}/delete",
* "edit-form" = "/admin/structure/message-
types/{message_type}/edit",
* },
* )
*/
class MessageType extends ConfigEntityBundleBase {
这为我们的实体在/admin/structure/message-types
上定义了路由。
-
通过在
src
目录中创建MessageTypeListBuilder.php
文件,定义我们list_builder
处理器中的MessageTypeListBuilder
类:<?php
namespace Drupal\mymodule;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
class MessageTypeListBuilder extends
ConfigEntityListBuilder {
/**
* Builds the header row for the entity listing.
*
* @return array
* A render array structure of header strings.
*
* @see \Drupal\Core\Entity\EntityListBuilder
::render()
*/
public function buildHeader() {
$header['label'] = $this->t('Label');
return $header + parent::buildHeader();
}
/**
* Builds a row for an entity in the entity listing.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for this row of the list.
*
* @return array
* A render array structure of fields for this
entity.
*
* @see \Drupal\Core\Entity\EntityListBuilder
::render()
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
return $row + parent::buildRow($entity);
}
}
-
接下来,我们将创建用于创建和编辑实体类型的实体表单。在
src
目录中创建MessageTypeForm.php
文件,定义MessageTypeForm
类作为默认表单处理器:<?php
namespace Drupal\mymodule;
use Drupal\Core\Entity\BundleEntityFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\mymodule\Entity\MessageType;
class MessageTypeForm extends BundleEntityFormBase {
/**
* Gets the actual form array to be built.
*
* @see \Drupal\Core\Entity\EntityForm::processForm()
* @see \Drupal\Core\Entity\EntityForm::afterBuild()
*/
public function form(array $form, FormStateInterface
$form_state) {
$form = parent::form($form, $form_state);
/** @var \Drupal\mymodule\Entity\MessageType
$entity */
$entity = $this->entity;
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#required' => TRUE,
'#default_value' => $entity->label,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $entity->id(),
'#machine_name' => [
'exists' => [MessageType::class, 'load'],
],
];
return $this->protectBundleIdElement($form);
}
/**
* Form submission handler for the 'save' action.
*
* Normally this method should be overridden to provide specific messages to
* the user and redirect the form after the entity has
been saved.
*
* @param array $form
* An associative array containing the structure of
the form.
* @param \Drupal\Core\Form\FormStateInterface
$form_state
* The current state of the form.
*
* @return int
* Either SAVED_NEW or SAVED_UPDATED, depending on
the operation performed.
*/
public function save(array $form, FormStateInterface
$form_state) {
$result = parent::save($form, $form_state);
if ($result === SAVED_NEW) {
$this->messenger()->addMessage('The message type
has been created.');
}
else {
$this->messenger()->addMessage('The message type
has been updated.');
}
$form_state->setRedirectUrl($this->entity->
toUrl('collection'));
return $result;
}
}
我们扩展了 BundleEntityFormBase
表单类,这是一个用于作为捆绑使用的配置实体类型的特殊基类。它提供了 protectBundleIdElement
方法,该方法防止对实体 ID 进行更改。
-
为了使消息类型可以从管理页面访问,我们需要定义一个菜单链接。将以下内容添加到
mymodule.links.menu.yml
文件中:mymodule.message_types:
title: 'Message types'
parent: system.admin_structure
description: 'Manage message types.'
route_name: entity.message_type.collection
这将在指定的 parent
下注册一个指向指定 route_name
的菜单链接。
-
接下来,我们需要更新
mymodule.links.action.yml
文件:message.add:
route_name: entity.message.add_page
title: 'Add message'
appears_on:
- entity.message.collection
message_type.add:
route_name: entity.message_type.add_form
title: 'Add message type'
appears_on:
- entity.message_type.collection
现在,由于 Message
实体类型有了捆绑,我们必须使用 message.add
任务的路由来添加 message_type
,以便在创建消息时可以选择消息类型。我们在消息类型集合页面上添加 message_type.add
以显示 添加消息类型 按钮。
-
我们必须为消息类型编辑表单路由添加一个任务链接,以便在
mymodule.links.task.yml
文件中:entity.message_type.edit_form:
route_name: entity.message_type.edit_form
base_route: entity.message_type.edit_form
title: Edit
-
我们必须在
mymodule.permissions.yml
文件中定义在admin_permission
注解键中定义的权限。将以下内容添加到mymodule.permissions.yml
文件中:administer message_types:
title: 'Administer message types'
-
我们必须为我们的配置实体类型定义配置模式。在
config/schema
目录中创建mymodule.schema.yml
文件以包含模式定义:mymodule.message_type.*:
type: config_entity
label: 'Message type settings'
mapping:
id:
type: string
label: 'Machine-readable name'
label:
type: label
label: 'Label'
-
接下来,我们需要更新
Message
实体类型的注解,以使用我们捆绑的实体类型:/**
* Defines the message entity class.
*
* @ContentEntityType(
* id = "message",
* label = @Translation("Message"),
* base_table = "message",
* entity_keys = {
* "id" = "message_id",
* "label" = "title",
* "uuid" = "uuid",
* "bundle" = "type",
* },
* admin_permission = "administer message",
* bundle_entity_type = "message_type",
* field_ui_base_route =
"entity.message_type.edit_form",
* handlers = {
* "list_builder" = "Drupal\mymodule\
MessageListBuilder",
* "form" = {
* "default" = "Drupal\mymodule\MessageForm",
* "delete" = "Drupal\Core\Entity\
ContentEntityDeleteForm",
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\
DefaultHtmlRouteProvider",
* },
* },
* links = {
* "canonical" = "/messages/{message}",
* "add-page" = "/messages/add",
* "add-form" = "/messages/add/{message_type}",
* "edit-form" = "/messages/{message}/edit",
* "delete-form" = "/messages/{message}/delete",
* "collection" = "/admin/structure/messages"
* },
* )
*/
我们添加了 bundle_entity_type
键,并给它分配了我们的配置实体类型的标识符,message_type
。然后我们将 field_ui_base_route
移动。
链接也进行了修改以支持捆绑。添加了 add-page
链接,这是一个创建消息时选择捆绑的着陆页。add-form
链接已更新,包含 message_type
参数。
- 安装模块。在 结构 页面上,你现在将看到 消息类型 与 消息 并列,允许你管理消息类型及其字段配置。
图 9.4 – 消息类型表单和标签页概览
它是如何工作的…
内容实体类型通过捆绑来分割实体类型为特定类型。捆绑可以仅是一个内容实体上的字符串值,或者更常见的是,指向配置实体,如本食谱所示。当一个内容实体类型没有定义捆绑时,它被认为有一个与实体类型同名的单一捆绑。
field_ui_base_route
必须指向配置实体类型的编辑表单路由。
内部,Drupal 使用bundle_entity_type
和bundle_of
键自动获取有关相关实体类型的信息,例如构建字段 UI 表单。
当在 Drush 中使用代码生成命令时,生成内容实体类型提供了一个选项来为捆绑生成配置实体类型。
实体访问控制实现
所有实体类型都有一个access
处理程序,用于控制用户是否有权创建、读取、更新或删除实体。配置和内容实体类型默认使用\Drupal\Core\Entity\EntityAccessControlHandler
类作为它们的访问处理程序。然而,此处理程序仅支持检查实体类型的admin_permission
。
Entity API 模块是一个贡献项目,用于在 Drupal 核心的实体系统中构建增强功能,并改善创建和维护自定义实体类型的开发者体验。它提供了一个权限提供者,用于为实体类型生成创建、读取、更新和删除权限。为了补充权限提供者,它还包含一个支持生成的权限的访问处理程序。
在本配方中,我们将使用Entity API
模块提供的权限提供者和访问处理程序,用于在本章先前创建的Message
实体类型。
准备工作
此配方使用Entity API
模块提供的实体处理程序类,并将其安装以使用它提供的实体处理程序类:
composer require drupal/entity
php vendor/bin/drush en entity --yes
如何操作...
-
首先,我们需要更新
mymodule.info.yml
文件,将Entity API
模块标记为依赖项:name: My Module
type: module
description: This is an example module from the Drupal
Development Cookbook!
core_version_requirement: '>=10'
dependencies:
- entity:entity
dependencies
数组包含命名空间化的模块名称。Drupal 利用此模式来支持包含子模块的项目,并作为单一代码库。
-
我们必须更新
Message
实体类型的注释,以指定实体类型的权限应按捆绑粒度划分:* admin_permission = "administer message",
* permission_granularity = "bundle",
* bundle_entity_type = "message_type",
* field_ui_base_route =
"entity.message_type.edit_form",
permission_granularity
键告诉系统应该生成哪些权限以及是否只需检查实体类型还是还应考虑捆绑。这样,对于特定用户角色,可能会有Announcement
消息的权限,但没有Bulletin
消息的权限。
-
接下来,我们添加
permission_provider
处理程序,它将生成我们的权限。请注意,{...}
表示与上一节相同的代码,为了提高可读性在此处已删除:* handlers = {
* "list_builder" = "Drupal\mymodule\
MessageListBuilder",
* "form" = {...},
* "route_provider" = {...},
* "permission_provider" = "\Drupal\entity\
EntityPermissionProvider",
* },
-
然后,我们提供支持我们生成的权限的访问处理程序:
* handlers = {
* "list_builder" = "Drupal\mymodule\
MessageListBuilder",
* "form" = {...},
* "route_provider" = {...},
* "permission_provider" = "\Drupal\entity\
EntityPermissionProvider",
* "access" = "\Drupal\entity\
EntityAccessControlHandler",
* },
-
我们将添加一个额外的处理程序,一个
query_access
处理程序,以在查询实体时实现我们的访问控制:* handlers = {
* "list_builder" = "Drupal\mymodule\
MessageListBuilder",
* "form" = {
* "default" = "Drupal\mymodule\MessageForm",
* "delete" = "Drupal\Core\Entity\
ContentEntityDeleteForm",
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\
DefaultHtmlRouteProvider",
* },
* "permission_provider" = "\Drupal\entity\
EntityPermissionProvider",
* "access" = "\Drupal\entity\
EntityAccessControlHandler",
* "query_access" = "\Drupal\entity\QueryAccess\
QueryAccessHandler",
* },
query_access
处理程序确保从实体查询返回的实体对用户是可访问的。
-
权限提供者还生成了在实体类型上指定的
admin_permission
。您可以从mymodule.permissions.yml
中删除administer message
权限。 -
重建 Drupal 的缓存,或者如果尚未安装,则安装模块。您将在权限表单上看到生成的权限。
图 9.5 – 消息类型的权限表单
它是如何工作的…
permission_provider
处理程序。权限提供程序随后检查实体类型并创建可能需要的正确权限。例如,如果实体类型实现了\Drupal\Core\Entity\EntityPublishedInterface
,它将生成允许查看该实体类型未发布实体的权限。权限基于实体类型注解中的permission_granularity
值。默认值是entity_type
。当使用entity_type
时,有一组权限用于处理该实体类型。
相关联的access
处理程序具有与权限提供程序提供的创建、读取、更新和删除权限相匹配的逻辑。query_access
处理程序在根据当前用户的权限查询实体时添加访问检查。当使用EntityPublishedInterface
时,这确保未发布实体不会暴露给没有查看权限的用户。
这允许您在不编写自己的访问逻辑的情况下,为您的自定义实体类型实现强大的访问控制。
还有更多...
这个配方展示了如何提供基于权限的实体访问。下一节将展示如何控制对特定字段的访问。
控制实体字段的访问
核心实体访问控制处理程序中的checkFieldAccess
方法可以被覆盖,以在修改实体时控制对特定实体字段的访问。如果没有被子类覆盖,默认的\Drupal\Core\Entity\EntityAccessControlHandler
类将始终返回允许的访问结果。该方法接收以下参数:
-
查看和编辑操作
-
当前字段的定义
-
要检查的用户会话
-
字段项值可能的列表
实体类型可以实现自己的访问控制处理程序并覆盖此方法,以提供对其基本字段修改的细粒度控制。一个很好的例子是User
模块及其\Drupal\user\UserAccessControlHandler
。
用户实体有一个pass
字段,用于用户的当前密码。还有一个created
字段,用于记录用户何时被添加到网站。
对于pass
字段,如果操作是查看,则返回denied
,但如果传递给checkFieldAccess
的$operation
参数是edit
,则允许访问:
case 'pass':
// Allow editing the password, but not viewing it.
return ($operation == 'edit') ? AccessResult::
allowed() : AccessResult::forbidden();
created
字段使用相反的逻辑。当用户登录时,网站可以查看但不能编辑:
case 'created':
// Allow viewing the created date, but not editing it.
return ($operation == 'view') ? AccessResult
::allowed() : AccessResult::neutral();
提供自定义存储处理程序
存储处理器控制实体的加载、保存和删除。内容实体类型具有默认的存储处理器\Drupal\Core\Entity\Sql\SqlContentEntityStorage
。配置实体类型具有默认的存储处理器\Drupal\Core\Config\Entity\ConfigEntityStorage
。这些类可以扩展以实现替代方法,并设置为实体类型的storage
处理器。
在本章中,我们将为之前创建的Message
实体类型创建一个方法来加载特定类型的所有消息。
如何做到这一点...
-
在模块的
src
目录中创建一个MessageStorage
类。这个类将扩展SqlContentEntityStorage
类:<?php
namespace Drupal\mymodule;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
class MessageStorage extends SqlContentEntityStorage {
}
内容实体类型的默认存储是SqlContentEntityStorage
类,这就是我们扩展该类的原因。
-
创建一个
loadMultipleByType
方法;使用此方法,我们将提供一个简单的方式来加载特定捆绑的所有消息:public function loadMultipleByType(string $type): array {
$message_ids = $this->getQuery()
->accessCheck(TRUE)
->condition('type', $type)
->execute();
return $this->loadMultiple($message_ids);
}
该方法执行带有访问检查的实体查询。它对捆绑值的type
字段执行条件,然后将返回的任何实体 ID 传递给加载和返回。
-
更新
Message
实体类型的注释以指定定制的storage
处理器:* handlers = {
* "list_builder" = "Drupal\mymodule\
MessageListBuilder",
* "form" = {
* "default" = "Drupal\mymodule\MessageForm",
* "delete" = "Drupal\Core\Entity\
ContentEntityDeleteForm",
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\
DefaultHtmlRouteProvider",
* },
* "storage" = "\Drupal\mymodule\MessageStorage",
* },
-
您现在可以使用以下代码以编程方式与您的消息实体进行交互:
$messages = \Drupal::entityTypeManager()
->getStorage('message')
->loadMultipleByType('alert');
它是如何工作的...
扩展SqlContentEntityStorage
确保我们的实体类型存储与数据库中内容实体存储的要求相匹配,并允许添加用于加载实体的自定义方法。
当从实体类型管理器检索实体类型存储时,可以使用这些方法。
第十章:主题化和前端开发
主题化 是一个过程,通过这个过程我们可以使用 CSS、JavaScript、Twig 模板和 HTML 影响实体的输出(节点、用户、分类术语、媒体等)。Drupal 随带了一些开箱即用的主题,在安装后提供基本的视觉和感觉。Olivero 是 Drupal 10 的默认网站主题,Claro 提供了管理主题。
在 Drupal.org 上有大量选项可供选择,这些选项要么为您自己的主题提供一个起点(例如 Bootstrap、ZURB Foundation 或 Barrio),要么提供一个完整的即用型解决方案。您还可以从头开始创建自己的自定义主题。
在 Drupal 页面加载后,屏幕上显示的所有内容都经过了主题化和渲染管道。这意味着您可以自定义您看到的主题,并控制其标记和样式。了解 Drupal 主题化工作原理将使您成为一个更有效的开发者,本章将帮助您做到这一点。
在本章中,我们将介绍以下食谱:
-
创建一个自定义主题以美化您的 Drupal 网站
-
将 CSS 和 JavaScript 添加到您的主题中
-
在您的主题中使用 Twig 模板
-
交付响应式图片
-
使用 Laravel Mix 编译 CSS 和 JavaScript 预处理器和后处理器
-
使用 Laravel Mix 通过实时重新加载为主题化 Drupal 网站提供
技术要求
您可以在 GitHub 上找到本章使用的完整代码:https://github.com/PacktPublishing/Drupal-10-Development-Cookbook/tree/main/chp10
创建一个自定义主题以美化您的 Drupal 网站
Drupal 提供了一个主题生成器,用于根据 Starterkit 主题创建 Drupal 网站的定制主题。这提供了所有基本的 CSS 样式表、JavaScript 文件和 Twig 模板,以开始自定义 Drupal 网站的视觉和感觉。主题生成器包含在 Drupal 核心提供的开发者命令行工具中,位于 core/scripts/drupal
。
如何做到这一点…
-
生成主题的脚本需要在
web
目录中执行,因此请在您的终端中导航到那里:cd web
-
从
core/scripts/drupal
脚本运行generate-theme
命令:php core/scripts/drupal generate-theme mytheme --name
"My theme"
generate-theme
命令接受一个参数 – 主题的机器名称。您还可以传递 name
选项来给它一个可读的名称。
-
主题生成后,它将在
web/themes
目录中可用。命令将输出类似以下内容:Theme generated successfully to themes/mytheme
-
您现在可以访问
/admin/appearance
并安装您的新主题:
图 10.1 – 安装新自定义主题的外观管理屏幕
它是如何工作的…
主题生成器使用 复制并忘记 模型,也称为 分支并忘记,与 Starterkit 主题一起。这意味着您的主题是您创建主题时 Starterkit 主题的副本,如果 Starterkit 主题在 Drupal 的后续版本中发生变化,则您的主题不会损坏。
这与其他系统不同,这些系统需要建立在子主题之上,例如 WordPress,或者从头开始构建,例如Laravel和Symfony。这允许你从一个起点创建主题并开始工作,无需担心由于基础主题的变化而导致的向后不兼容性。
其他主题可以作为起始套件。它们是贡献的主题,可以使用--starterkit
选项指定。
下一个配方将介绍如何向你的 Drupal 网站添加新的 CSS 样式表和 JavaScript 文件。
更多内容
接下来,我们将深入了解主题和主题生成器的更多信息。
创建你的起始套件主题
非常常见,组织机构在主题上有一个共享的起始基础。你可以定义自己的起始套件来创建新的主题并利用现有的设置。这是通过在你的主题的info.yml
文件中添加starterkit
键来完成的:
startekit: true
当此值设置为true
时,主题生成命令允许你使用它来生成新的主题。这个标志是必需的,因为有些主题并不打算以通用功能为基础的起始套件。
主题截图
主题可以在主题文件夹中的screenshot.png
图像文件或info.yml
文件下的screenshot
键中指定的文件中提供截图。
主题、标志和 favicon
Drupal 将网站的 favicon 和标志设置作为主题设置进行控制。这些设置基于主题,而不是全局的。主题目录中放置的favicon.ico
也将是网站favicon
的默认值。主题可以通过在主题目录中提供logo.svg
来提供默认的标志。可以通过在info.yml
文件中使用logo
键并指定文件路径来指定替代的标志文件。
你可以通过导航到外观然后点击当前主题的设置来更改网站的标志和 favicon。取消选中 favicon 和标志设置的默认复选框允许你提供自定义文件:
图 10.2 – 标志和 favicon 主题设置概览
相关阅读
-
Starterkit 主题的文档:
www.drupal.org/docs/core-modules-and-themes/core-themes
-
关于主题的
info.yml
文件属性的文档:www.drupal.org/node/2349827
向你的主题添加 CSS 和 JavaScript
在 Drupal 中,CSS 样式表和 JavaScript 文件与库相关联,并且特定的库被添加到页面中。这允许 CSS 样式表和 JavaScript 文件仅在需要时附加。主题可以与必须始终附加以进行全局样式的库相关联。
在这个菜谱中,我们将更新主题的 libraries.yml
文件以注册由自定义主题提供的 CSS 样式表和 JavaScript 文件。
准备工作
此菜谱使用由主题生成器创建的主题,如 创建自定义主题以美化你的 Drupal 站点 菜谱中所述。
如何操作…
-
首先,在你的主题目录中创建
css
和js
目录,分别用于存放 CSS 样式表和 JavaScript 文件:mkdir -p css js
-
在
css
目录下,添加一个styles.css
文件,该文件将包含你的主题 CSS 声明。为了演示目的,将以下 CSS 声明添加到styles.css
文件中:body {
background: cornflowerblue;
}
-
在
js
目录下,添加一个scripts.js
文件,该文件将包含主题的 JavaScript 项。为了演示目的,将以下 JavaScript 代码添加到scripts.js
文件中:(function(Drupal) {
Drupal.behaviors.myTheme = {
attach() {
console.log('Hello world!')
}
}
})(Drupal);
此脚本将在脚本加载时在控制台打印一条消息。
-
修改
mytheme.libraries.yml
文件并添加一个global-styling
库定义到你的主题中:global-styling:
version: VERSION
css:
theme:
css/styles.css: {}
js:
js/scripts.js: {}
dependencies:
- core/drupal
库的名称是 global-styling
。库可以定义一个 version
键并使用 VERSION
作为默认值,它默认为 Drupal 核心的最新版本。css/styles.css
文件被添加到 theme
组的 css
键。js/script.js
被添加到 js
键。
dependencies
键指定了 JavaScript 运行必须存在的其他库。
-
编辑
mytheme.info.yml
以将global-styling
库添加到libraries
键:name: 'My theme'
type: theme
'base theme': stable9
libraries:
- mytheme/global-styling
- mytheme/base
- mytheme/messages
- core/normalize
-
重建你的 Drupal 站点的缓存以重建库定义缓存,触发库定义和主题信息的重新发现:
php vendor/bin/drush cr
-
查看你的 Drupal 站点以验证背景颜色是否已更改,以及
Hello world!
是否已打印到浏览器的控制台:
图 10.3 – 附加了全局库的自定义主题
工作原理…
Drupal 使用库将 CSS 样式表和 JavaScript 文件附加到页面。这允许库依赖于其他库,例如基础 CSS 样式表或 JavaScript 功能。在这个菜谱中,core/drupal
依赖项确保 core/misc/drupal.js
被附加到页面。core/misc/core.js
添加全局的 Drupal
对象,这使得你可以定义 behaviors
。每当页面加载或 AJAX 事件被触发时,Drupal.behaviors
中的每个条目都会调用其 attach
函数。
资产聚合
如果 Drupal 配置为聚合资源,你将需要在每次文件更改后重建 Drupal 的缓存,以便创建一个新的聚合。这可以通过访问 /admin/config/development/performance
并取消聚合的复选框来完成。
Drupal 的 CSS 架构借鉴了可伸缩和模块化 CSS 架构(SMACSS)系统的概念来组织 CSS 声明。CSS 样式通过级联层应用。Drupal 库有不同的组,CSS 样式表可以附加到这些组上,以控制样式表加载的顺序。CSS 组如下:
-
base
:用于针对 HTML 元素的样式 -
layout
:用于布局页面的样式 -
component
:用于应用于设计组件的样式 -
state
:用于应用于组件的状态的样式 -
theme
:用于自定义组件的样式
主题可以指定在主题被使用的每个页面上包含的库。
还有更多
我们将在下一节中更详细地探讨使用库的更多选项。
库文件选项
{ }
)使用。
以下示例,取自core.libraries.yml
,为库添加了es6-promise
polyfill:
es6-promise:
version: "4.2.8"
license:
name: MIT
url: https://raw.githubusercontent.com/stefanpenner
/es6-promise/v4.2.8/LICENSE
gpl-compatible: true
js:
assets/vendor/es6-promise/es6-promise.auto.min.js: {
weight: -20, minified: true }
该库提供了一个兼容层,允许您控制库在其它库中的优先级。minified
属性指定文件已经过压缩,不需要由 Drupal 进行处理。
preprocess
属性支持布尔值。如果一个文件有preprocess: false
,它将不会与其他文件同时聚合。
覆盖和扩展其他库
主题可以使用其info.yml
文件中的libraries-override
和libraries-extend
键来覆盖库。这允许主题轻松地自定义现有库,而无需在特定库附加到页面时添加条件移除或添加其资产的逻辑。
libraries-override
键可以用来替换整个库,替换库中的选定文件,从库中移除资产,或禁用整个库。以下代码将允许主题提供自定义的 jQuery UI 主题:
libraries-override:
core/jquery.ui:
css:
component:
assets/vendor/jquery.ui/themes/base/core.css: false
theme:
assets/vendor/jquery.ui/themes/base/theme.css:
css/jqueryui.css
override
声明模仿了原始配置。指定false
将移除资产;否则,提供的路径将替换该资产。
libraries-extend
键可以用来加载与现有库一起使用的额外库。以下代码将允许主题将 CSS 样式表与选定的 jQuery UI 声明覆盖相关联,而无需始终将其包含在主题的其他资产中:
libraries-extend:
core/jquery.ui:
- mytheme/jqueryui-theme
使用starterkit
生成的主题已经包含了libraries-extend
的示例。
使用 CDN 或外部资源作为库
库还可以与外部资源一起工作,例如通过 CDN 加载的资产。这是通过提供文件位置的 URL 以及选定的文件参数来完成的。
以下是一个从BootstrapCDN
提供的jsDeliver
中添加 Font Awesome 字体图标库的示例:
mytheme.fontawesome:
remote: http://fontawesome.io/
version: 5.15.4
license:
name: SIL OFL 1.1
url: https://github.com/FortAwesome/Font-
Awesome/blob/5.x/LICENSE.txt
gpl-compatible: true
css:
base:
https://cdn.jsdelivr.net/npm/@fortawesome
/fontawesome-free@5.15.4/css/fontawesome.min.css: {
type: external, minified: true }
remote
键描述了库使用外部资源。虽然此键的验证仅限于其存在,但最好使用外部资源的主要网站来定义它:
remote: http://fontawesome.io/
与所有库一样,需要version
信息。这应该与添加的外部资源的版本相匹配:
version: 5.15.4
如果库定义了remote
键,它还需要定义license
键。这定义了许可名称和许可的 URL,并检查它是否与 GPL 兼容。如果没有提供此键,将抛出\Drupal\Core\Asset\Extension\LibraryDefinitionMissingLicenseException
异常:
license:
name: SIL OFL 1.1
url: https://github.com/FortAwesome/Font-
Awesome/blob/5.x/LICENSE.txt
gpl-compatible: true
最后,特定的外部资源以正常方式添加。而不是提供相对文件路径,提供外部 URL。
通过钩子操作库
模块可以提供动态库定义和修改库。模块可以使用hook_library_info()
钩子动态提供库定义。这不是定义库的推荐方式,但提供用于边缘用例。
模块不能使用libraries-override
或libraries-extend
,需要依赖于hook_library_info_alter()
钩子。您可以在core/lib/Drupal/Core/Render/theme.api.php
或api.drupal.org/api/drupal/core!lib!Drupal!Core!Render!theme.api.php/function/hook_library_info_alter/10
中了解此钩子。
将 JavaScript 放在页眉
默认情况下,Drupal 确保 JavaScript 放在页面最后。这通过首先加载页面的关键部分来提高页面加载性能。现在将 JavaScript 放在页眉是一个可选的选项。
要在页眉中渲染库,您需要添加header: true
键值对:
js-library:
header: true
js:
js/myscripts.js: {}
这将在页面的页眉中加载一个自定义 JavaScript 库及其依赖项。
参见
-
参考 Drupal 的 CSS 架构:在
www.drupal.org/node/1887918#separate-concerns
中分离关注点 -
SMACSS 网站:
smacss.com/
在您的主题中使用 Twig 模板
Drupal 的主题层使用Twig 模板系统,这是来自 Symfony 项目的一个组件。Twig是一种模板语言,其语法类似于 Django 和 Jinja 模板。
在此配方中,我们将覆盖用于文本输入的默认 Twig 模板,为电子邮件表单元素提供自定义。我们将使用Twig 语法向输入元素添加一个新类并提供占位符属性。
准备工作
此配方使用由主题生成器创建的主题,正如在创建自定义主题以美化您的 Drupal 网站配方中所做的那样。
如何做到这一点...
-
首先,在
template/form
目录中,将input.html.twig
文件复制为input--email.html.twig
。模板将如下所示:{#
/**
* @file
* Theme override for an 'input' #type form element.
*
* Available variables:
* - attributes: A list of HTML attributes for the
input element.
* - children: Optional additional rendered elements.
*
* @see template_preprocess_input()
*/
#}
<input{{ attributes }} />{{ children }}
属性变量是\Drupal\Core\Template\Attribute
的一个实例,这是一个用于存储 HTML 元素属性的值对象。
-
Attribute
类有一个用于添加新的class
属性值的addClass
方法。我们将使用addClass
向输入元素添加一个新类:<input{{ attributes.addClass('input--email') }} />{{
children }}
-
在上一行之前,我们将使用三元运算符创建一个 Twig 变量,使用
setAttribute
方法提供一个默认占位符:{% set placeholder = attributes.placeholder ?
attributes.placeholder :
'email@example.com'
%}
<input{{ attributes.addClass('input--
email').setAttribute('placeholder', placeholder)
}} />{{ children }}
使用 set
操作符创建一个名为 placeholder
的新变量。问号 (?
) 操作符检查 attributes
对象中的占位符属性是否为空。如果不为空,则使用现有值。如果值为空,则提供一个默认值。
-
重建您的 Drupal 网站缓存以重建 Drupal 网站的 Twig 模板:
php vendor/bin/drush cr
-
假设您已使用标准 Drupal 安装,在未登录状态下访问
/contact/feedback
,并查看电子邮件字段的更改:
图 10.4 – 电子邮件输入的 HTML 元素源代码
它是如何工作的…
Drupal 的主题系统是围绕主题钩子和主题钩子建议构建的。当一个主题钩子有双下划线 (__
) 时,Drupal 的主题系统会理解这一点,并且可以分解主题钩子以找到更通用的模板。
电子邮件输入元素的元素定义定义了 input__email
主题钩子。如果没有通过 Twig 模板或 PHP 函数实现 input__email
钩子,它将降级到仅有的 input
主题钩子和模板。
注意
Drupal 主题钩子使用下划线 (_
) 定义,但在 Twig 模板文件中使用时使用连字符 (-
)。
给定的电子邮件元素定义提供了 input__email
作为其主题钩子,Drupal 理解如下:
-
查找由主题或定义
input__email
的模块提供的名为input--email.html.twig
的 Twig 模板。 -
如果找不到模板,查找名为
input.html.twig
的 Twig 模板或定义input
的模块中的主题钩子。
Drupal 利用主题钩子建议来提供基于不同条件输出变化的方式。它允许站点主题为某些实例提供更具体的模板。主题钩子建议可以通过 .module
或 .theme
文件中的 hook_theme_suggestions()
钩子提供。
处理器,如 Drupal 的主题层,将变量传递给 Twig。可以通过将变量名括在花括号中来打印变量或对象属性。Drupal 核心所有默认模板都在文件的文档块中提供信息,详细说明了可用的 Twig 变量。
Twig 语法简单,包含基本的逻辑和函数。addClass
方法将接受 attributes
变量并添加提供的类,同时保留现有内容。
当提供主题钩子建议或修改现有模板时,您需要重建 Drupal 的缓存。与 PHP 一样,Drupal 会缓存编译后的 Twig 模板,这样在模板被调用时就不需要重新编译。
更多内容
在接下来的章节中,我们将进一步讨论使用 Twig。
调试模板文件选择和钩子建议
可以启用调试以检查构成页面的各种模板文件及其主题钩子建议,并检查哪些是活动的。这可以通过编辑sites/default/services.yml
文件来完成。如果不存在services.yml
文件,可以将default.services.yml
复制以创建一个。
您需要在文件的twig.config
部分下将debug: false
更改为debug: true
。这将导致 Drupal 主题层打印出包含模板信息的源代码注释。当debug
开启时,Drupal 不会缓存编译后的 Twig 模板,而是即时渲染它们。
另有一个设置可以防止您在每次模板文件更改时无需启用debug
就重建 Drupal 的缓存。可以将twig.config.auto_reload
布尔值设置为true
。如果设置为true
,当源代码更改时,Twig 模板将被重新编译。
参见
-
请参阅Twig 文档
-
请参阅hook_theme_suggestions的 API 文档
交付响应式图像
HTML5
的<picture>
标签和源集。利用断点模块,将映射到断点,以表示在每个断点使用的图像样式。响应式图像样式是通过将图像格式映射到特定断点和修饰符的配置。首先,您需要定义一个响应式图像样式,然后可以将其应用于图像字段。
在本教程中,我们将创建一个名为文章图像的响应式图像样式集,并将其应用于文章内容类型的图像字段。
如何做到这一点…
-
首先,您需要安装
responsive_image
模块:php vendor/bin/drush en responsive_image -y
-
从管理工具栏中,转到配置,然后在媒体部分下转到响应式图像样式。点击添加响应式图像样式以创建新的样式集。
-
从断点组选择列表中选择Olivero以使用 Olivero 主题定义的断点。
-
每个断点将按字段集分组。展开字段集,选择选择单个图像样式,然后选择合适的图像样式选项:
图 10.5 – 响应式图像样式表单
-
此外,在浏览器不支持源集或发生错误的情况下,选择备用图像样式选项。
-
点击保存以保存配置,并添加新的样式集。
-
前往结构和内容类型,从文章内容类型的下拉菜单中选择管理显示。
-
将图片字段的格式化器更改为响应式图片。
-
点击字段格式化器的设置齿轮图标,选择您的新响应式****图片样式集。从响应式图片****样式下拉菜单中选择文章图片:
图 10.6 – 响应式图片样式配置表
- 点击更新以保存字段格式化器设置,然后点击保存以保存字段显示设置。
它是如何工作的…
使用响应式图片格式化器的优点是其性能。浏览器将只下载适当source
标签中srcset
定义的资源。这不仅允许您提供更合适的图片大小,而且在小型设备上也会携带更小的负载。
响应式图片样式提供三个组件:
-
一个响应式图片元素
-
响应式图片样式配置实体
-
响应式图片字段格式化器
配置实体由字段格式化器消费并通过响应式图片元素显示。
响应式图片样式实体包含一个断点到图片样式映射的数组。可用的断点由选定的断点组定义。断点组可以随时更改;然而,之前的映射将会丢失。
响应式图片元素在每个断点处打印一个图片元素,定义一个新的源元素。断点的媒体查询值作为元素的媒体属性提供。
相关内容
- 请参考Mozilla 开发者网络(MDN)上的图片元素
developer.mozilla.org/en-US/docs/Web/HTML/Element/picture
使用 Laravel Mix 编译 CSS 和 JavaScript 预处理器和后处理器
Laravel Mix是一个简化 Webpack 配置的项目,Webpack 是一个用于前端开发的打包器。Laravel Mix 最初是为了与 Laravel 项目一起使用而构建的,但也可以用于任何需要打包 JavaScript 或 CSS 预处理器和后处理的项目。
在本食谱中,我们将向主题添加 Laravel Mix 以转换我们的 ES6 JavaScript 和 PostCSS,并根据所需的浏览器支持后处理我们的 CSS。
准备工作
本章的将 CSS 和 JavaScript 添加到您的主题食谱介绍了如何为您的主题创建资产库。本节将使用该食谱中的信息。
如何操作…
-
首先,在您的主题中创建一个
src
目录来存储将由 Webpack 处理的 CSS 和 JavaScript 源文件:mkdir src
-
接下来,在
src
目录中创建一个styles.css
文件,内容如下::root {
--text-color: rgb(43, 43, 43);
}
body {
color: var(--text-color);
}
此 CSS 文件使用 CSS 变量。虽然现在普遍支持,但PostCSS
将确保在后处理 CSS 样式表中保持向后兼容性。
-
然后,在
src
目录中创建一个scripts.js
文件,内容如下:(function(Drupal) {
Drupal.behaviors.myTheme = {
attach() {
console.log('Hello world!')
}
}
})(Drupal);
-
现在,我们将准备安装 Laravel Mix。在您的主题目录中,使用
npm init
命令初始化项目:npm init -y
这创建了一个package.json
文件,它存储了使用 npm 的项目依赖信息。
-
使用
npm install
来安装 Laravel Mix:npm install laravel-mix --save-dev
-
我们还需要安装
postcss-present-env
以确保我们的 CSS 经过后处理以兼容所有浏览器:npm install postcss-preset-env --save-dev
Laravel Mix 捆绑了@babel/preset-env
,这确保了 JavaScript 的浏览器兼容性。
-
现在我们有了 Laravel Mix,我们必须进行配置。在您的主题目录中创建一个
webpack.mix.js
文件,并使用以下配置:let mix = require('laravel-mix');
mix.js('src/scripts.js', 'dist/');
mix.css('src/styles.css', 'dist/', [
require('postcss-preset-env')
]);
mix
上的js
方法设置 Webpack 的 JavaScript 捆绑和转译。第一个参数是源文件,第二个参数是目标。mix
上的css
方法设置 PostCSS。第一个参数是源文件,第二个参数是目标。第三个参数将额外的插件传递给 PostCSS。
-
要编译我们的 CSS 和 JavaScript 文件,运行
npx mix
。这将创建dist/styles.css
和dist/scripts.js
:npx mix
-
现在,我们需要创建我们的主题库,它将注册来自
dist
目录的 CSS 和 JavaScript 文件:theme-styling:
version: VERSION
css:
theme:
dist/styles.css: {}
js:
dist/scripts.js: {}
dependencies:
- core/drupal
-
编辑
mytheme.info.yml
以将theme-styling
库添加到libraries
键:name: 'My theme'
type: theme
'base theme': stable9
starterkit: true
libraries:
- mytheme/theme-styling
- mytheme/base
- mytheme/messages
- core/normalize
-
重建您的 Drupal 网站的缓存以重建库定义缓存,触发库定义和主题信息的重新发现:
php vendor/bin/drush cr
-
您的 CSS 和 JavaScript 文件现在已注册,将允许您编写最新的 CSS 和 JavaScript,同时保持浏览器兼容性。
它是如何工作的…
Laravel Mix 是一个简化 Webpack 配置的包。当运行npx mix
时,这将运行 Laravel Mix 的脚本。Laravel Mix 读取webpack.mix.js
,构建 Webpack 配置,并代表您执行 Webpack。
参见
- Laravel Mix 文档:
laravel-mix.com/docs
使用 Laravel Mix 为您的 Drupal 网站添加实时重新加载的主题
在之前的配方使用 Laravel Mix 编译 CSS 和 JavaScript 预和后处理器中,我们为主题设置了 Laravel Mix。Laravel Mix 支持Browsersync
。Browsersync
是一个监控您的源文件更改并自动将更改注入浏览器的工具,因此您不需要手动刷新页面。
在这个配方中,我们将利用BrowserSync
功能来加速构建您的 Drupal 主题的开发。
入门
确保通过访问/admin/config/development/performance
禁用了 CSS 和 JavaScript 文件的聚合。
或者,您可以将以下配置覆盖添加到您的settings.php
中,以强制禁用聚合:
$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;
这也是默认通过在sites/default
中的example.settings.local.php
来设置的。
如何做到这一点…
-
要使用 Laravel Mix 配置 Browsersync,我们在
mix
上调用browserSync
方法:let mix = require('laravel-mix');
mix.js('src/scripts.js', 'dist/');
mix.css('src/styles.css', 'dist/', [
require('postcss-preset-env')
]);
mix.browserSync();
-
在配置 Browsersync 之前,我们需要 Laravel Mix 下载 Browsersync 的额外 Webpack 依赖项。Laravel Mix 在运行和首次配置时会自动执行此操作:
npx mix
您将看到类似以下的消息:必须安装额外的依赖项。这只需 片刻时间 即可。
-
我们需要指定我们的 Drupal 网站的 URL 以供
Browsersync
代理:mix.browserSync({
proxy: 'mysite.ddev.site:80'
});
localhost:3000
并将从提供的 URL 的 Drupal 网站代理内容。将 mysite.ddev.site
替换为您的 Drupal 网站的域名。
-
接下来,我们需要指定 Browsersync 应该监视哪些文件以通过提供
files
选项将更新注入浏览器:mix.browserSync({
proxy: 'mysite.ddev.site:80',
files: [
'dist/styles.css',
'dist/scripts.js',
]
});
这告诉 Browsersync 对我们编译的资产进行响应,这些资产是添加到页面上的。
-
最后,我们需要以开发模式运行 Laravel Mix 来监视变化。这将打开一个使用 Browsersync 的浏览器,并在文件更改时更新:
npx mix watch
-
现在您的浏览器已经打开,修改
src/styles.css
或src/scripts.js
,并监视您的 Drupal 网站的主题更新,而无需刷新页面。
它是如何工作的…
配置并收集所有必要的 Webpack 插件可能会很麻烦。Laravel Mix 提供了一种方法,可以轻松地在任何项目中配置 Webpack。Laravel Mix 支持 Vue、React 和 Preact 的 JavaScript 打包。它还内置了对 Sass 和 Less 的支持,以及 PostCSS。所有这些选项都可以在 Laravel Mix 的 API 文档中找到,网址为 laravel-mix.com/docs/6.0/api
。
更多内容
Browsersync 可以用于监视不仅仅是 CSS 和 JavaScript 文件的变化。
监视 Twig 模板的变化
Browsersync 也可以配置为监视 Twig 文件的变化,以便在 Twig 模板被修改时更新浏览器。要监视 Twig 模板的变化,请将 templates/**/*
添加到 files
选项中:
mix.browserSync({
proxy: 'mysite.ddev.site:80',
files: [
'dist/styles.css',
'dist/scripts.js',
'templates/**/*'
]
});
每次您修改主题的 Twig 模板时,Browsersync 都会为您刷新页面。
为了获得最佳效果,您需要确保您有一个包含以下内容的 sites/default/services.yml
文件来启用 Twig 调试:
twig.config:
debug: true
auto_reload: null
cache: true
这将确保每次修改 Twig 模板时都会重新构建它们。这在本食谱的 在主题中使用 Twig 模板 和 调试模板文件选择和 钩子建议 下的 更多内容 部分中有所说明。
您还需要绕过 Drupal 的 render
缓存,这可能会导致输出过时。请确保 settings.php
中有以下行,这些行也可以在 example.settings.local.php
中找到:
$settings['container_yamls'][] = DRUPAL_ROOT . '/sites/
development.services.yml';
$settings['cache']['bins']['render'] = 'cache.backend.null';
$settings['cache']['bins']['page'] = 'cache.backend.null';
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.
backend.null';
这将强制 Drupal 使用一个不缓存任何数据的缓存后端,您的 Drupal 网站可能会运行得更慢,但它允许您在不重建 Drupal 缓存的情况下构建您的主题。
参见
-
Browsersync 文档:
browsersync.io/docs/options/
-
Laravel Mix 文档:
laravel-mix.com/docs
第十一章:多语言和国际化
Drupal 最伟大的优势之一一直是其提供多语言和国际化的能力。您不仅可以让内容编辑员能够以多种语言添加网站内容,还可以翻译管理界面。
本章将介绍 Drupal 10 的多语言和国际化功能,这些功能自 Drupal 6 以来在每次发布中都得到了极大的增强。Drupal 的早期版本需要许多额外的模块来提供国际化工作,但现在大多数功能都由 Drupal 核心提供。
Drupal 核心提供了以下多语言模块:
-
语言:这为您提供了检测和支持多种语言的能力
-
界面翻译:这会将已安装的语言翻译成通过用户界面呈现的字符串
-
配置翻译:这允许您翻译配置实体,例如日期格式和视图
-
内容翻译:这带来了提供不同语言内容并在用户当前语言中显示的强大功能
每个模块在为您的 Drupal 网站创建多语言体验方面都发挥着特定的作用。在底层,Drupal 支持所有实体和缓存上下文的语言代码。这些模块公开了接口以实现和提供国际化体验。
在本章中,我们将介绍以下食谱,以使您的网站实现多语言和国际化的功能:
-
确定当前语言的选择方式
-
翻译管理界面
-
翻译配置
-
翻译内容
-
创建多语言视图
确定当前语言的选择方式
默认情况下,Drupal 能够确定它应该显示的内容和用户界面语言,而无需在管理屏幕上添加更多设置。这是确保在所有时间,向每个用户展示内容时都使用适当的语言所必需的步骤。
以下食谱将向您展示如何设置 Drupal 决定在页面上向用户展示内容时使用哪种语言的参数。您有几种方法可以检测要使用哪种语言。
准备工作
首先,登录到您的 Drupal 网站,并转到管理界面中的 扩展 部分。启用 语言、内容翻译 和 界面翻译 模块。
如何操作...
-
接下来,导航到管理界面中的 管理 | 配置 | 区域和语言 | 语言 部分。
-
点击 检测和选择 选项卡。在此屏幕上有两个部分:
- 顶部部分,界面文本语言检测,允许您指定如何选择界面文本的当前语言:
图 11.1 – 管理屏幕中的语言检测选项
-
正如你所见,有许多可用的语言检测方法:账户管理页面、URL、会话、用户、浏览器和选择的语言。
-
第二个部分,内容语言检测,允许你指定在向用户显示内容(浏览网站)时如何选择当前语言:
图 11.2 – 自定义内容语言检测以区别于界面文本语言检测设置
因此,有两个选项可供选择——内容语言和界面。
它是如何工作的...
Drupal 能够以多种方式检测和设置当前语言。它是如何知道如何做到这一点的呢?
在幕后,Drupal 通过其LanguageNegotiator
类在生成响应以服务用户时评估这些语言检测设置。它根据设置的顺序和配置选择检测到的语言。
LanguageNegotiator
类按顺序评估语言检测设置,这些设置是通过它们特定的语言协商插件实现来评估的,这些插件位于 Drupal 的modules/language/src/Plugin/LanguageNegotiation
目录中。
更多内容...
让我们来看看检测和选择标签页的两个部分。
界面文本语言检测
如所述,Drupal 能够为内容和界面本身提供翻译和语言检测功能,如图图 11.1所示。除此之外,Drupal 还能够控制这两种情况下的规则,使用户和编辑在网站的不同区域看到的语言具有最大灵活性。
让我们来看看图 11.1中显示的检测方法:
- 账户管理页面:此选项允许有权访问 Drupal 管理区域的用户设置管理界面的首选语言。当你启用此选项时,用户表单上会出现一个新字段,允许你选择要使用哪种语言。这对于可能希望将 Drupal 管理界面设置为一种语言,同时在另一种语言中审查/编辑内容的用户来说很有用。在这个例子中,用户将管理语言设置为西班牙语:
图 11.3 – 在用户账户中选择 Drupal 管理语言为西班牙语
-
/es/admin/config/languages
,其中es
是语言代码,并且将使用西班牙语来翻译用户界面的文本。在这种情况下,URL 中的语言代码始终设置为西班牙语。 -
(
/foo/bar?language=es
)或会话参数:
图 11.4 – 设置将触发翻译语言使用的会话参数
-
用户:此选项将根据站点语言下 Drupal 用户账户的语言偏好设置当前语言。如果用户编辑他们的账户,他们将看到此选项,并能够将其设置为列出的任何语言。
-
浏览器:此选项将根据用户的浏览器偏好设置(Chrome、Firefox 或 Safari)设置当前语言。
-
选择语言:此选项允许管理员设置整个站点的默认语言。这通常用作最后的回退设置,如果前面的选项都没有启用或配置。
内容语言检测
这些选项将确定在向用户显示内容时如何设置当前语言。只有当你启用其标题下方刚刚提到的自定义内容语言检测以区别于界面文本语言检测设置复选框时,它们才会出现,如图图 11**.2所示。
当启用时,你可以设置不同的标准来检测查看内容时的语言。如果不启用,则继承界面文本设置。本节中大多数可用的选项与我们之前提到的相同,只是有两个例外:
-
URL 中的
language_content_entity
请求参数 -
界面:启用此选项将使用上一节中界面文本检测配置中检测到的任何语言。
重要提示
注意,在两个部分中,选项都列在一个可拖动的表格中 - 你可以通过上下移动它们来设置语言检测的优先级。然而,在大多数情况下,默认设置就足够好了,适用于大多数场景。
翻译管理界面
接口翻译模块提供了一个翻译 Drupal 用户界面中找到的字符串的方法。通过利用Language
模块,接口翻译会自动从 Drupal 翻译服务器下载。默认情况下,接口语言是通过语言代码作为路径前缀来加载的。在默认的Language
配置下,路径将以默认语言为前缀。
接口翻译基于代码中提供的字符串,这些字符串通过内部翻译函数进行传递。
在这个菜谱中,我们将启用西班牙语,导入语言文件,并审查翻译的界面字符串,以提供缺失或自定义翻译。
准备工作
Drupal 为翻译文件提供自动安装过程。为了使其工作,你的 Web 服务器必须能够与localize.drupal.org/
进行通信。如果你的 Web 服务器无法从翻译服务器自动下载文件,你可以参考手动安装说明,这些说明将在更多…部分中介绍。
如何操作...
-
前往 扩展 并安装 接口翻译 模块。如果尚未安装,它将提示您启用 语言、文件 和 字段 模块。
-
模块安装后,点击 配置。在 区域和语言 部分下转到 语言 页面。
-
在语言概览表中点击 添加语言。
图 11.5 – 管理界面中的语言概览部分
-
添加语言 页面提供了一个选择列表,列出了所有可翻译到接口的语言。选择 西班牙语,然后点击 添加语言。
-
将运行一个批处理过程;安装翻译语言文件,并导入它们。
-
接口翻译 列指定了具有匹配翻译的活跃可翻译接口字符串的百分比。点击链接可以查看 用户界面 翻译 表单:
图 11.6 – 包含新添加的语言西班牙语的语言概览屏幕
-
过滤器可翻译字符串 表单允许您搜索翻译后的字符串或未翻译的字符串。从 搜索范围 下拉列表中选择 仅未翻译字符串,然后点击 过滤。
-
使用屏幕右侧的文本框,可以为 仅未翻译字符串 添加自定义翻译。输入条目的翻译。
图 11.7 – 在管理界面中翻译西班牙语的原始字符串
-
点击 保存翻译 以保存修改。
-
前往
/es/node/add
,您会注意到基本
页面内容类型的描述现在与您的翻译相匹配。
它是如何工作的...
接口翻译模块提供了 \Drupal\locale\LocaleTranslation
,该类实现了 \Drupal\Core\StringTranslation\Translator\TranslatorInterface
接口。这个类在 string_translation
服务下注册为可用的查找方法。
当调用 t
函数或 \Drupal\Core\StringTranslation\StringTranslationTrait::t
方法时,会调用 string_translation
服务以提供翻译后的字符串。string_translation
服务将遍历所有可用的翻译器,并在可能的情况下返回一个翻译后的字符串。
重要提示
开发者需要注意,这是确保模块字符串通过翻译函数传递的一个关键原因。这允许您识别需要翻译的字符串。
接口翻译提供的翻译器将尝试将提供的字符串与当前语言的已知翻译进行匹配。如果已保存翻译,则将返回该翻译。
还有更多...
在接下来的章节中,我们将探讨安装其他语言、检查翻译状态以及更多内容。
手动安装语言文件
可以通过从 Drupal.org 翻译服务器下载并通过语言界面上传来手动安装翻译文件。您还可以使用 导入
界面上传自定义 .po
文件。
Drupal 核心和大多数贡献项目在 Drupal 翻译网站上都有 .po
文件,可在 localize.drupal.org
找到。在网站上,点击所有可用语言的 Drupal 核心模块的 .po
文件。此外,点击一种语言将提供跨项目的更多翻译。
图 11.8 – Drupal.org 上可用的语言文件
您可以通过访问 .po
文件和相应的语言来导入 .po
文件。您可以像对待自定义创建的翻译一样处理上传的文件。如果您提供的是由 Drupal.org 未提供的自定义翻译文件,则建议这样做。如果您正在手动更新 Drupal.org 的翻译,请确保勾选覆盖现有非自定义翻译的复选框。最后一个选项允许您在 .po
文件提供的情况下替换自定义翻译。如果您已翻译了可能现在由官方翻译文件提供的缺失字符串,这可能很有用。
检查翻译状态
随着您添加新的模块,可用的翻译将增加。界面翻译
模块提供了一个可从 报告
页面访问的翻译状态报告。这将检查项目的默认翻译服务器,并检查是否有 .po
文件可用或是否已更改。对于自定义模块,您可以提供自定义翻译服务器,这在 *为自定义 * 模块 提供翻译 部分中有所介绍。
如果有更新可用,您将收到通知。然后您可以自动导入翻译文件更新或下载并手动导入它们。
导出翻译
在 .po
文件中。您可以导出在当前 Drupal 网站上发现的所有未翻译的源文本。这将提供一个基础 .po
文件,供翻译人员工作。
此外,您还可以下载特定语言。特定语言的下载可以包括非自定义翻译、自定义翻译和缺失翻译。下载自定义翻译可以帮助为 Drupal 社区的多语言和国际工作做出贡献!
界面翻译权限
接口翻译模块提供了一个名为翻译界面文本的单个权限。此权限允许用户与模块的所有功能进行交互。它带有安全警告标志,因为它允许具有此权限的用户自定义呈现给用户的所有输出文本。
然而,它确实允许您为翻译者提供一个角色,并限制他们仅对翻译界面进行访问。
为自定义模块提供翻译
模块可以在它们的目录中提供自定义翻译或指向一个远程文件。这些定义被添加到模块的info.yml
文件中。首先,如果您需要指定与项目的机器名称不同的接口翻译项目
键。
然后,您需要通过接口翻译服务器模式
键指定一个服务器模式。这可以是一个指向 Drupal 根目录的相对路径,例如modules/custom/mymodule/translation.po
,或者一个远程文件 URL,如example.com/files/translations/mymodule/translation.po
。
发行版(或其他模块)可以实现hook_locale_translation_projects_alter
来代表模块提供此信息或更改默认值。
服务器模式接受以下不同的标记:
-
%core
用于课程的版本(例如,10.x) -
%project
用于项目的名称 -
%version
用于当前版本字符串 -
%language
用于语言代码
关于接口翻译键和变量的更多信息,可以在接口翻译模块的基本文件夹中的local.api.php
文档文件中找到。
参见
-
请参阅
localize.drupal.org/translate/drupal8
的 Drupal 翻译服务器 -
您可以使用本地化服务器
www.drupal.org/node/302194
进行贡献 -
请参阅
api.drupal.org/api/drupal/core%21modules%21locale%21locale.api.php/8
的locale.api.php
文档 -
请参阅
www.drupal.org/node/1814954
的 PO 和 POT 文件
翻译配置
配置翻译
模块提供了一个接口,用于通过接口翻译和语言作为依赖项来翻译配置。此模块允许您翻译配置实体。能够翻译配置实体增加了国际化的一层。
接口翻译允许您翻译在您的 Drupal 站点代码库中提供的字符串。配置翻译允许您翻译您创建的可导入和导出的配置项,例如您的站点标题或日期格式。
在这个菜谱中,我们将翻译日期格式配置实体。我们将为丹麦语提供本地化日期格式,以提供更国际化的体验。
准备工作
您的 Drupal 网站需要启用两种语言才能使用配置翻译。从语言界面安装丹麦语言。
如何操作...
-
前往扩展并安装配置翻译模块。如果尚未安装,它将提示您安装界面翻译、语言、文件和字段模块。
-
模块安装后,转到配置。然后,转到区域和语言部分下的配置翻译页面。
-
点击配置实体选项表中的日期格式选项列表:
图 11.9 – 选择要翻译的配置实体类型
-
我们将翻译默认的长日期格式以表示丹麦格式。点击翻译默认长日期格式行。
-
点击添加以创建丹麦翻译:
图 11.10 – 为不同语言添加不同的日期格式
对于 l j. F, Y – H.i
。这将显示星期几、月份中的日期、月份、完整年份以及时间的 24 小时制表示。
-
点击保存翻译。
-
现在,当用户使用丹麦作为他们的语言浏览您的 Drupal 网站时,日期格式将根据他们的体验进行本地化。
它是如何工作的...
配置翻译
模块需要界面翻译;然而,它的工作方式并不相同。该模块修改了所有扩展\Drupal\Core\Config\Entity\ConfigEntityInterface
接口的实体类型。它在config_translation_list
键下添加了一个新的处理器。这用于构建可用配置实体及其捆绑包的列表。
该模块修改了 Drupal 的配置架构,并更新了默认配置元素定义,以使用\Drupal\config_translation\Form
下的指定类。这允许\Drupal\config_translation\Form\ConfigTranslationFormBase
及其子类正确保存通过配置翻译屏幕可以修改的翻译配置数据。
当配置被保存时,它被识别为集合的一部分。该集合被识别为language.LANGCODE
,所有翻译配置实体都通过此标识符保存和加载。以下是如何在数据库中存储配置项的示例:
图 11.11 – 包含特定语言配置文件的配置导出
当使用 es
语言代码浏览站点时,将加载适当的 block.block.bartik_account_menu
配置实体。如果您使用的是默认站点或没有语言代码,将使用带有空集合的配置实体。
还有更多...
配置实体及其可翻译的能力是 Drupal 8 多语言功能的重要组成部分。我们将在下一个配方中详细探讨它们。
修改配置翻译信息定义
模块可以调用 hook_config_translation_info_alter
钩子来修改发现的配置映射器。例如,Node
模块就是这样做的,以修改 node_type
配置实体:
/**
* Implements hook_config_translation_info_alter().
*/
function node_config_translation_info_alter(&$info) {
$info['node_type']['class'] = 'Drupal\node\
ConfigTranslation\NodeTypeMapper';
}
这将更新node_type
定义以使用\Drupal\node\ConfigTranslation\NodeTypeMapper
自定义映射器类。此类添加了节点类型的标题作为可配置的翻译项。
翻译视图
视图是配置实体。当启用配置翻译
模块时,可以翻译视图。这将允许您翻译显示的标题、暴露的表单标签和其他项目。有关更多信息,请参阅本章中的创建多语言视图配方。
翻译内容
内容翻译模块提供了一种翻译内容实体(如节点和块)的方法。每个内容实体都需要启用翻译功能,这允许您细粒度地决定哪些属性和字段需要翻译。
内容翻译是现有实体的副本,但带有适当的语言代码标记。当访客使用语言代码时,Drupal 会尝试使用该语言代码加载内容实体。如果不存在翻译,Drupal 将渲染默认未翻译的实体。
准备工作
您的 Drupal 站点需要启用两种语言才能使用内容翻译。从语言界面安装西班牙语。
如何操作...
-
转到扩展,并安装内容翻译模块。如果尚未安装,它将提示您安装语言模块。
-
在模块安装后,转到配置。在区域和语言部分下转到内容语言和翻译页面。
-
在要公开的内容设置旁边的复选框中勾选当前内容类型。
-
为基本页面启用内容翻译,并保留提供的默认设置,这些设置使每个字段都启用了翻译。点击保存配置:
图 11.12 – 选择特定内容类型可翻译的属性和字段
-
首先,创建一个新的基本页面节点。我们将在站点的默认语言中创建它。
-
在查看新节点时,点击翻译选项卡。从西班牙语语言行中,点击添加以创建节点的翻译版本:
图 11.13 – 将节点内容翻译成其他语言
- 内容将预填充为默认语言的内容。用翻译文本替换标题和正文:
图 11.14 – 为“关于我们”页面添加西班牙语翻译
- 点击保存并保持已发布(此翻译)以保存新的翻译。
它是如何工作的...
内容
翻译模块通过利用语言代码标志来工作。所有内容实体和字段定义都有一个语言代码键。内容实体有一个语言代码列,指定内容实体是哪种语言。字段定义也有一个语言代码列,用于识别内容实体的翻译。内容实体可以提供处理翻译的处理器定义;否则,内容翻译
模块将提供自己的。
每个实体和字段记录都保存了适当的语言代码。当加载实体时,会考虑当前的语言代码以确保加载正确的实体。
更多内容...
内容翻译不仅仅是提供不同语言的内容。Drupal 还提供了额外的功能,使管理和显示翻译内容更加灵活和健壮。
标记翻译为过时
内容翻译模块提供了一个机制,可以将已翻译的实体标记为可能过时。标记其他翻译为过时的标志提供了一种记录需要更新翻译的实体的方法:
图 11.15 – 如果正在编辑的内容发生变化,您可以标记其他翻译为过时
此标志不会更改任何数据,而是提供了一种审核工具。这使得它
翻译员可以轻松识别已更改并需要更新的内容。
内容实体的翻译选项卡将突出显示所有尚未翻译的翻译内容
标记为过时。随着它们的变化,编辑者可以取消选中标志。
翻译内容链接
大多数情况下,Drupal 菜单包含指向节点的链接。菜单链接默认不翻译,必须在内容翻译下启用自定义菜单链接选项。您需要从菜单管理界面手动翻译节点链接。
从节点创建和编辑表单启用菜单链接与翻译不兼容。如果您从翻译编辑菜单设置,它将编辑未翻译的菜单链接。
定义实体翻译处理器
内容翻译模块需要实体定义来提供有关翻译处理器的信息。如果此信息缺失,它将提供自己的默认值。
内容实体定义可以提供translation
处理器。如果没有提供,它们将默认为\Drupal\content_translation\ContentTranslationHandler
。节点提供此定义并使用它将内容翻译信息放入垂直标签中。
content_translation_metadata
键定义了如何与翻译元数据信息交互,例如标记其他实体为过时。content_translation_deletion
键提供了一个表单类来处理实体翻译删除。
创建多语言视图
视图作为配置实体,可以进行翻译。然而,多语言视图的力量并不仅仅在于配置翻译。视图允许您构建对当前语言代码做出反应的过滤器。这确保了翻译成用户语言的内容被显示。
在本食谱中,我们将创建一个多语言视图,显示最近的文章块。如果没有内容,我们将显示翻译后的“无结果”消息。
准备工作
您的 Drupal 站点需要启用两种语言才能使用内容翻译。从语言界面安装西班牙语。为文章启用内容翻译。您还需要一些翻译内容。
如何操作…
-
从结构进入视图,然后点击添加****新视图。
-
提供视图名称,例如“最近文章”,并将内容类型更改为“文章”。标记您想要创建一个块,然后点击保存和编辑。
-
添加新的过滤条件。搜索翻译语言并为内容添加过滤。将过滤器设置为检查页面选定的界面文本语言。这将仅显示内容已被翻译或基语言是当前语言:
图 11.16 – 将视图更改为以选定页面的语言返回结果
-
将“无结果行为”添加到“当前无”最近文章。
-
保存视图。
-
点击翻译选项卡。点击西班牙语行的添加以翻译该语言的视图。
-
展开主显示设置,然后修改最近文章显示选项的字段集。修改显示标题选项以提供翻译后的标题:
图 11.17 - 翻译视图显示标题属性
- 展开无结果行为以修改屏幕右侧的文本,使用屏幕左侧的文本框作为原始文本的来源:
图 11.18 – 翻译无结果行为文本
-
点击保存翻译。
-
将该块放置在您的 Drupal 网站上。通过
/es
访问网站并注意翻译后的视图
块:
图 11.19 – 翻译成西班牙语的首页
它是如何工作的…
视图提供了基于此元素的翻译语言过滤器。视图插件系统提供了一个收集和显示所有可用语言的机制。这些将被作为令牌内部保存,并在查询执行时用实际的语言代码替换。如果语言代码不再可用,您将看到所选内容的语言页面,并且当查看时视图将回退到当前语言。
重要提示
当您编辑由 Drupal 核心或贡献模块提供的视图时,会遇到翻译语言过滤器选项。虽然这不是用户界面中的选项,但将语言过滤器定义为***LANGUAGE_language_content***
是一种默认做法,这将强制视图成为多语言。
过滤器告诉视图根据实体的语言代码及其字段进行查询。
视图是配置实体。配置翻译模块允许您翻译视图。您可以从配置区域的主要配置翻译屏幕或通过编辑单个视图来翻译视图。
大多数翻译项都将位于主显示设置选项卡下,除非在特定显示中覆盖。每种显示类型也将有其自己的特定设置。
更多内容…
在 Drupal 中翻译界面元素相当深入。这很好地扩展到了视图,您可以在视图中翻译暴露的过滤器、显示格式和源自视图显示的菜单项。
翻译暴露表单项和过滤器
每个视图都可以从暴露表单部分翻译暴露的表单。这不会翻译表单上的标签,而是表单元素。您可以翻译提交按钮文本、重置按钮标签、排序标签,以及如何翻译升序或降序。
您可以从过滤器部分翻译暴露的过滤器标签。每个暴露的过滤器都会显示为一个可折叠的表单组,允许您配置管理标签和前端标签:
图 11.20 – 在视图中翻译暴露的过滤器标签
默认情况下,可用的翻译需要通过全局界面翻译上下文导入。
翻译显示和行格式项
一些显示格式有可翻译的项。这些可以在每个显示模式的章节中翻译。例如,以下项可以使用其显示格式进行翻译:
-
表格
格式允许您翻译表格摘要 -
RSS 源
格式允许您翻译源描述 -
页面
格式允许您翻译页面的标题 -
Block
格式允许您翻译块的标题
翻译页面显示菜单项
可以通过内容翻译模块翻译自定义菜单链接。视图使用页面显示;然而,它们不会创建自定义菜单链接实体。Views
模块将所有具有页面显示的视图直接注册到路由系统中,就像在模块的routing.yml
文件中定义一样:
图 11.21 – 翻译视图提供的菜单标签
例如,列出所有用户的People视图可以翻译成具有更新的标签页名称和链接描述。
第十二章:使用 Drupal 构建 API
Drupal 10 自带了一些很棒的功能,可以帮助您使用核心序列化和 JSON:API 模块构建 RESTful API。这些功能使您能够构建无头和/或解耦的解决方案,同时仍然可以与 Drupal 交互并查询数据。
在本章中,我们将探讨以下内容:
-
使用 JSON:API 从 Drupal 获取数据 (
jsonapi.org/
) -
使用 POST 和 JSON:API 创建数据
-
使用 PATCH 和 JSON:API 更新数据
-
使用 DELETE 和 JSON:API 删除数据
-
使用视图提供自定义数据源
-
使用 OAuth 方法
序列化模块提供了一种将数据序列化到或从 JSON 和 XML 等格式反序列化的方法。RESTful Web Services 模块随后通过 Web API 公开实体和其他资源类型。在 RESTful 资源端点执行的运算使用与在非 API 环境中相同的创建、编辑、删除和查看权限。JSON:API 模块在 API 路由上以 JSON 表示形式公开您的数据实体(节点、分类法、媒体和用户)。我们还将介绍如何处理 API 的自定义认证。
技术要求
本章中的所有 API 都通过 HTTP 操作。无论请求是否成功,都会返回 HTTP 响应代码。如果您不熟悉 HTTP 响应代码或只是需要复习它们,可以在此处查看:developer.mozilla.org/en-US/docs/Web/HTTP/Status
。当您使用 Drupal 中的 API 时,您将看到大量的 HTTP 响应代码,因此复习它们是个好主意。您可以在 GitHub 上找到本章中使用的完整代码:github.com/PacktPublishing/Drupal-10-Development-Cookbook/tree/main/chp12
使用 JSON:API 从 Drupal 获取数据
只需几步点击,我们就可以打开并公开 Drupal 中的数据,以便外部服务消费。这将使我们能够请求 Drupal 中的数据,并以 JSON 格式返回,使其易于消费。
准备工作
首先,我们需要启用一些核心模块。前往管理 | 扩展并启用以下模块:
-
HTTP 基本认证
-
JSON:API
-
RESTful Web Services
-
序列化:
图 12.1 – 在 Drupal 中启用必要的模块
JSON:API 模块使用序列化模块来处理响应的规范化以及从请求中数据的反规范化。端点支持特定的格式(JSON、XML 等),并且认证提供者支持在请求头中传递认证信息。
重要提示
注意,在撰写本文时,多语言支持仍在 JSON:API 下进行最终确定。您可以通过此处跟踪此问题的状态:www.drupal.org/project/drupal/issues/2794431
。
如何做到这一点……
您将在 Drupal 管理区域的 配置 部分中看到一个名为 JSON:API 的新区域:
图 12.2 – JSON:API 为 Drupal 管理区域添加了一个新的配置部分
JSON:API 默认提供的设置不多,除了两个安全设置之外。一个允许只有读取端点,而另一个允许在端点上执行 创建、读取、更新、删除 (CRUD) 操作:
目前请保持默认设置。创建一个新的 基本页面
节点并保存。
然后,转到此 URL(将 localhost
替换为您的本地站点域名):
https://localhost/jsonapi/node/page
Drupal 将以 JSON:API 的形式响应您网站中所有 基本页面
节点的信息:
如果您想通过 ID 获取特定的节点,您可以在请求 URL 中使用 filter
关键字作为查询参数。
filter
的格式为 filter[attribute]
。在此之后,我们可以通过在浏览器中请求此 URL 来获取 ID 为 1 的节点:
https://localhost/jsonapi/node/page?filter[
drupal_internal__nid]=1
您将得到与第一个示例相同的响应,但现在,响应中的 data
数组将只包含匹配 ID 1 的节点。这适用于您想要按任何属性筛选的任何属性。如果您知道实体的 UUID,您也可以直接通过将其用作参数来请求它:
https://localhost/jsonapi/node/page/{ENTITY UUID}
通过将 UUID 作为 URL 参数传递,您将获得特定 基本页面
节点类型的节点信息。使用标题中的单词 Test
创建更多 基本页面
节点。假设您想按标题中包含单词 Test
的节点进行筛选;您可以像这样请求:
https://localhost/jsonapi/node/page?filter[title][operator]
=CONTAINS&filter[title][value]=Test&fields[node--page]
=title
Drupal 只会返回标题中包含单词 “Test” 的节点。在这里,我们还添加了 &fields[node—page]=title
到请求中,以便更容易看到响应:
如果您想使用 cURL
而不是其他方式,可以这样做:
curl \
--header 'Accept: application/vnd.api+json' \
--url "https://localhost/jsonapi/node/page?filter
[title][operator]=CONTAINS&filter[title][value]=
Test&fields[node--page]=title" \
--globoff
您可以从任何 HTTP API、任何语言或命令行工具(如 cURL
)请求此内容,只要您的参数和选项正确。例如,请参阅使用 fetch
(developer.mozilla.org/en-US/docs/Web/API/Fetch_API
) 的适当文档,以将其与解耦的 JavaScript 应用程序集成。
curl: (60) SSL 证书问题
如果您在 cURL 中遇到关于 SSL 证书问题的错误,那么很可能是您的 SSL 证书无法验证。这在本地自签名证书中很常见。为了在开发中绕过此错误,您可以在之前提到的 cURL 请求中传递--insecure
标志。在生产中,请确保您有一个有效的正常工作的 SSL 证书 – 不要在实际应用中使用此标志。
JSON:API 还可以通过使用include
来请求相关数据,将其包含在响应中。如果我们向查询参数添加include
以及 JSON 项目关系部分下的任何关系名称,我们将在 JSON:API 响应中收到它们。
如果您想获取节点作者信息,例如,您可以像这样请求它:
https://localhost/jsonapi/node/page?filter[title][operator]=CONTAINS&filter[title][value]=Test&fields[node—page]=title&include=uid
我们将在响应中收到以下信息:
图 12.6 – 带有作者数据的示例 JSON:API 响应
注意,要查看用户数据,请求用户需要“查看用户信息”权限。否则,您将收到一个访问拒绝错误。
列在关系下的任何项目都可以请求 – 例如,作者数据、附加到返回实体的分类、附加到返回实体的媒体或其他类型的关联数据。
分页、过滤和排序请求
分页是通过附加一个页面查询参数来完成的。要限制请求为 10 个节点,我们必须使用append ?page[limit]=10
。要访问下一组结果,我们还必须传递page[offset]=10
。
以下是一个返回第一页和第二页结果的示例:
https://localhost/jsonapi/node/article?page[limit]=10
https://localhost/jsonapi/node/article?page[offset]=10&page
[limit]=10
每个请求都包含一个链接属性;当使用分页结果时,这还将包含下一页和上一页的链接。
过滤是通过附加一个过滤查询参数来完成的。以下是一个请求所有已提升到首页的节点的示例:
https://localhost/jsonapi/node/article?filter[promoted]
[path]=promote&filter[promoted][value]=1&filter[promoted]
[operator]==
每个过滤器都由一个名称定义 – 在前面的示例中,它是提升。然后,过滤器获取path
,即要过滤的字段。值和操作符决定了如何过滤。
排序是最简单的操作。在这里,添加了一个sort
查询参数。字段名称值是要排序的字段;要按降序排序,必须在字段名称前添加一个减号。以下示例分别展示了如何按nid
升序和降序排序:
https://localhost/jsonapi/node/article?sort=nid
https://localhost/jsonapi/node/article?sort=-nid
它是如何工作的…
Drupal 中的序列化模块提供了将数据结构从 JSON 和 XML 等格式进行归一化和反归一化的必要工具。当对 Drupal 发出请求时,负责的路由获取资源并将其归一化为一个数组;然后,一个注册的编码器(在这种情况下,JsonEncoder)以请求的格式(JSON、XML 等)返回数据:
图 12.7 – 高级查看 Drupal 中序列化工作方式
这是 Drupal 中数据归一化和序列化的基础。当您启用JSON:API
模块时,会在/jsonapi/
下的资源注册新路由。这些动态生成的路由会自动编码数据并以 JSON 格式返回。每次请求实体时,序列化和编码器负责构建响应数据。
还有更多...
安装 JSON:API 额外模块
JSON:API 额外模块提供了一个用户界面进行额外定制。JSON:API 额外模块应像所有其他模块一样添加到您的 Drupal 安装中 – 即,使用 Composer:
composer require drupal/jsonapi_extras:³.0
一旦该模块在 Drupal 中安装,您将能够启用或禁用端点、更改资源名称、更改资源路径、禁用字段、提供别名字段名称以及增强 JSON:API 路由的字段输出。
更改 JSON:API 路径前缀
使用“额外模块”,可以将 API 路径前缀从jsonapi
更改为api
或其他任何前缀。
从管理工具栏,导航到配置。在网络服务部分下,点击JSON:API 覆盖来自定义 JSON:API 实现。设置选项卡允许您修改 API 路径前缀:
图 12.8 – 使用 JSON:API 额外功能更改 JSON 路径前缀
更改路径前缀后,请在 Drupal 中清除缓存以查看结果和新路径。
禁用和增强返回的实体字段
JSON:API 额外模块允许您覆盖 JSON:API 模块自动暴露的端点。这允许您禁用返回的字段。它还允许您使用增强器简化字段属性的架构。
从管理工具栏,转到配置。在网络服务部分下,点击JSON:API 覆盖来自定义 JSON:API 实现。
要禁用端点,请点击任何端点的覆盖。勾选禁用复选框以关闭该特定端点:
图 12.9 – 使用 JSON:API 额外功能禁用资源,以便端点不可访问
要禁用、别名或使用增强器,请点击POST
/PATCH
请求:
图 12.10 – 您可以在 JSON:API 端点中更改字段别名并禁用字段
在这里,我们已经禁用了修订字段在 JSON:API 输出中显示。我们可以对创建和更改的字段应用增强器:
图 12.11 – 使用增强器格式化 JSON:API 返回的数据
在这个例子中,创建和更改的字段将不再返回 Unix 时间戳,而是返回RFC ISO 8601格式的时间戳。
回到并查看我们的 JSON:API 路由,我们可以看到修订字段已被删除,创建和更改的字段已格式化:
图 12.12 – 由 JSON:API Extras 模块定制的 JSON:API 响应
参见
JSON 和 XML 并不是 Drupal 作为响应返回数据的唯一格式。还有一些贡献的模块提供了其他数据格式,例如 PDF、CSV 和 XLS(Excel)序列化。如果您对它们的工作原理感到好奇,可以查看这些模块的源代码:
-
Excel 序列化序列化:
www.drupal.org/project/xls_serialization
使用 POST 通过 JSON:API 创建数据
返回数据的 API 很棒,但为了创建更功能化的解耦应用或集成外部服务,我们还需要能够将数据发送到 Drupal。阅读以下章节后,您将能够从远程源在 Drupal 10 应用程序中创建实体。
准备工作
我们将使用在 Drupal 10 标准配置文件中预安装的Article
内容类型。如果您没有Article
内容类型,请创建一个并添加一个基本字段,例如Body
。
如何做到这一点…
首先,我们需要告诉 Drupal 允许对 JSON:API 进行 CRUD 操作。返回到 Drupal 管理中配置部分的 JSON:API 设置页面,并启用接受所有 JSON:API 创建、读取、更新和 删除操作。:
图 12.13 – 启用 JSON:API 以允许更多操作
如果您不启用此功能,您将无法执行除从 JSON:API 端点读取数据之外的操作。
接下来,创建一个只有创建Article
节点所需权限的用户账户。这是我们将在创建新节点请求中使用该账户。出于安全原因,您不应在 API 中使用管理员账户。
在 Drupal 中创建 API 的每个请求中,我们都需要在请求被接受之前进行身份验证。
打开终端(命令行)。与上一节不同,我们无法在浏览器中直接执行这些请求。为此,我们将使用curl
,它在许多操作系统上都是预安装的:
curl \
--user chapter12:chapter12 \
--header 'Accept: application/vnd.api+json' \
--header 'Content-type: application/vnd.api+json' \
--request POST https://localhost/jsonapi/node/article \
--data-binary '{
"data": {
"type": "node--article",
"attributes": {
"title": "New article created by chapter12",
"body": {
"value": "This node was created using JSON:API!",
"format": "plain_text"
}
}
}
}'
再次提醒,记得将localhost
替换为你的开发站点的正确主机名。我们创建了一个名为chapter12
的用户,密码为chapter12
,并将其设置为内容编辑
角色(包含在标准安装配置文件中),该角色具有创建和编辑节点的基本权限。此用户通过--user
参数传递。
响应将返回成功或失败的授权响应。如果成功,你将看到一个大的 JSON 响应,反映新创建的节点,你将在 Drupal 中看到新节点:
图 12.14 – 通过 POST 请求创建的节点
我们还可以看到,它被正确地添加到了正文字段中,以及我们在请求中指定的文本格式:
图 12.15 – 验证我们通过 POST 请求传递的内容是否正确创建
如果我们在浏览器中查看我们的 JSON:API 路由,我们也会在那里看到这个新节点:
图 12.16 – 使用 HTTP 请求访问新创建的节点
它是如何工作的…
在这种情况下,本例中的 JSON:API 模块在身份验证或实体访问方面没有做任何额外的工作。请求的处理方式与chapter12
用户来到网站,登录,并在管理界面中创建节点没有区别。
请求类型是POST
请求。这与第一部分中用于读取数据的GET
请求不同。发送数据到服务器需要使用POST
请求。你可以在 MDN 上了解更多关于POST
请求(s)的信息:developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST
。
这是一个强大的抽象,可以帮助你构建 API 和解耦的应用程序,因为它需要有效的用户身份验证,但不能执行超出该用户/用户角色允许范围的任何操作。这意味着如果我们移除内容编辑角色的“创建文章节点”权限,这个POST
请求将因为403
禁止响应而失败。因此,在现实世界中创建一个具有有限权限的API 用户
角色是一个好主意,你可以随时打开或关闭它。
永远
不要使用超级用户(user 1
)或具有管理员角色的用户进行 API 请求。如果凭证泄露,恶意行为者可以执行他们想要的任何请求。在阅读本章后,回顾 Drupal.org 上使用 API 时的安全考虑因素会是一个好主意:www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/security-considerations
。
在curl
请求中,当我们传递--user
参数时,它将自动为请求创建一个授权头。此头传递了一个基本认证,Drupal 中的 Basic Auth 模块会拦截并使用它来验证请求。
或者,你可以使用命令行手动获取基本认证令牌;例如:
echo -n "chapter12:chapter12" | base64
这将返回Y2hhcHRlcjEyOmNoYXB0ZXIxMg==
。然后,你可以将其作为标题而不是使用–user
参数传递:
curl \
--header 'Accept: application/vnd.api+json' \
--header 'Content-type: application/vnd.api+json' \
--header 'Authorization:Basic Y2hhcHRlcjEyOmNoYXB0ZX
IxMg=='Y2hhcHRlcjEyOmNoYXB0ZXIxMg==' \
… rest of request
参见
虽然cURL功能强大,但它是一个低级工具,对于更长时间、更大的请求,从命令行使用它可能会很困难。你可以安装 Google 的 Postman 工具来简化与 API 的工作。你可以从www.postman.com/
下载它。
使用 PATCH 通过 JSON:API 更新数据
现在我们已经知道了如何在 Drupal 中创建一些数据,让我们学习如何更新现有数据。
如何做到这一点...
我们的要求将与前几节中的POST
请求类似,但有一些变化。
首先,为了在 Drupal 中更新一个实体,你需要将实体的 UUID 传递到请求负载中,这样它才能正常工作。这与数字实体 ID 不同。你可以从 JSON:API 获取数据来获取 UUID,正如我们在使用 JSON:API 从 Drupal 获取数据部分所看到的。对于我们通过POST
请求创建的节点,UUID 值是1ddf244d-e8e6-40f5-be48-23bc8fa0fa3e
。
让我们继续更改我们之前创建的文章节点的标题和正文。这次,我们必须在 URL 以及请求体中传递 UUID:
curl \
--user chapter12:chapter12 \
--header 'Accept: application/vnd.api+json' \
--header 'Content-type: application/vnd.api+json' \
--request PATCH 'https://localhost/jsonapi/node/
article/1ddf244d-e8e6-40f5-be48-23bc8fa0fa3e' \
--data-binary '{
"data": {
"type": "node--article",
"id": "1ddf244d-e8e6-40f5-be48-23bc8fa0fa3e",
"attributes": {
"title": "Article updated by chapter12",
"body": {
"value": "This node was updated using JSON:API!",
"format": "plain_text"
}
}
}
}'
注意,id
属性位于attributes
数据之外。如果你尝试将其放在attributes
下,你会得到一个HTTP 422 Unprocessable Content
错误。如果你传递无效的属性,你会收到一个500
错误。你必须更新的内容必须与数据模型匹配才能成功。
提交此请求后,我们将在命令行以 JSON 格式收到更新后的节点,我们将在 Drupal 中看到我们的更新已被应用:
图 12.17 – 使用 PATCH 请求更新节点的审查
如果我们想要将节点归因于另一个用户怎么办?使用一个仅用于处理 API 操作的账户是个好主意,但我们当然不希望所有内容都归因于该用户。
例如,假设你有一个外部发布平台,用户正在提交内容。当内容发布时,该系统会向 Drupal 发送一个POST
请求,以便创建内容。让我们将内容归因于一个名为Johnny Editor
的 Drupal 用户,其 UUID 为c1ce9fe6-4eea-4f69-92c2-883415019002
。像节点一样,你可以在/jsonapi/user/user
查看用户实体数据。
我们可以使用PATCH
请求来修复现有内容,并将作者从chapter12
用户更改为Johnny Editor
用户,通过在请求体中传递relationships
数据来实现。不过,在我们能够这样做之前,我们的 API 用户角色需要PATCH
数据。现在,请启用这个权限。
这是因为,在这个例子中,我们请求更改节点的所有权从一位用户到另一位用户,Drupal 在执行操作之前会明确检查这一点。
审查你的权限
再次强调,一旦你阅读了这一章,就很好去 Drupal.org 上回顾一下使用 API 时的安全考虑:www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/security-considerations
。
现在,我们可以发出新的PATCH
请求:
curl \
--user chapter12:chapter12 \
--header 'Accept: application/vnd.api+json' \
--header 'Content-type: application/vnd.api+json' \
--request PATCH 'https://localhost/jsonapi/node/
article/1ddf244d-e8e6-40f5-be48-23bc8fa0fa3e' \
--data-binary '{
"data": {
"type": "node--article",
"id": "1ddf244d-e8e6-40f5-be48-23bc8fa0fa3e",
"attributes": {
"title": "Using Drupal 10 PATCH & JSON:API by Johnny
Editor",
"body": {
"value": "This is how you use Drupal 10 PATCH &
JSON:API.",
"format": "plain_text"
}
},
"relationships": {
"uid": {
"data": {
"type": "user--user",
"id": "c1ce9fe6-4eea-4f69-92c2-883415019002"
}
}
}
}
}'
在这里,我们收到了 Drupal 的成功响应,以及节点的 JSON 表示。我们可以看到内容已更新,节点所有者已被重新分配给Johnny Editor:
图 12.18 – 使用 PATCH 请求重新分配节点所有权
你可以使用关系添加任何你想要添加到实体的实体引用。使用这个相同的公式,我们可以向这篇文章添加一些分类。假设有两个术语在taxonomy
标签中,分别称为Technology
和News
。我们想在文章中标记它们。
就像节点和用户一样,你可以通过导航到/jsonapi/taxonomy_term/tags
来查看分类术语数据:
图 12.19 – 使用其 JSON:API 端点读取分类术语
使用这里的 UUID,我们可以在请求的relationships
部分传递它们:
curl \
--user chapter12:chapter12 \
--header 'Accept: application/vnd.api+json' \
--header 'Content-type: application/vnd.api+json' \
--request PATCH 'https://localhost/jsonapi/node/
article/1ddf244d-e8e6-40f5-be48-23bc8fa0fa3e' \
--data-binary '{
"data": {
"type": "node--article",
"id": "1ddf244d-e8e6-40f5-be48-23bc8fa0fa3e",
"attributes": {
"title": "Using Drupal 10 PATCH & JSON:API by Johnny
Editor",
"body": {
"value": "This is how you use Drupal 10 PATCH &
JSON:API.",
"format": "plain_text"
}
},
"relationships": {
"field_tags": {
"data": [
{
"type": "taxonomy_term--tags",
"id": "4ef201ed-7cb6-49e5-b125-4c2709be1a42"
},
{
"type": "taxonomy_term--tags",
"id": "09504010-8eff-4be0-8205-607f9e74ffa1"
}
]
}
}
}
}'
提交后,我们将看到文章节点已成功更新为指定的两个分类术语:
图 12.20 – 通过 PATCH 请求向节点添加分类标签
现在,如果我们像第一部分那样从 Drupal 获取数据,我们将在节点数据中看到作者和分类关系:
图 12.21 – 节点响应中的分类关系
当你将此与 URL 查询字符串中的 include
结合使用时,你将获得作者数据以及分类数据:include=uid,field_tags
。
如果你想要删除引用项,你可以提交一个没有该项目的 PATCH
请求。如果我们想要删除一个术语,我们只需传递我们想要保留的术语:
"relationships": {
"field_tags": {
"data": [
{
"type": "taxonomy_term--tags",
"id": "4ef201ed-7cb6-49e5-b125-4c2709be1a42"
},
]
}
}
要完全清空 tags
字段,传递一个空值:
"relationships": {
"field_tags": {
"data": {}
}
}
你将看到术语已被从文章节点中删除:
图 12.22 – 在 PATCH 请求中传递空值将删除关系
它是如何工作的…
就像 POST
和 PATCH
一样,PATCH
提供了一种强大的方式,可以从解耦的应用程序或远程系统更新 Drupal 中的实体数据。当你理解你的实体数据和模式时,你可以通过 API 以任何你想要的方式创建和操作它。
同样地,POST
和 PATCH
请求必须遵守 Drupal 的权限系统。相同的规则被尊重,就像这是一个真实用户登录到 Drupal 更新内容一样。
使用这两种方法,你可以创建一个解耦的网站,可以从用户那里收集数据并将其发回 Drupal,例如自助服务亭、调查或评论系统。可能性是无限的。
使用 DELETE 命令通过 JSON:API 删除数据
最后,我们来到了 CRUD 简称的最终动作:delete
方法。此方法从 Drupal 中删除请求的数据,前提是你有 UUID。
如何操作…
与 PATCH
类似,发出 DELETE
需要在 Drupal 中具有适当的权限。在这种情况下,分配给我们的 API 用户的角色需要允许删除文章节点。返回到 Drupal 管理区域的权限部分,并将 Article: Delete any content 授予该角色。
我们需要将delete any
分配而不是delete own
,因为我们已经将节点所有权分配给了另一个用户。
所有的 DELETE
请求需要的只是我们希望删除的实体的 UUID。使用我们创建的节点,它的 UUID 是 1ddf244d-e8e6-40f5-be48-23bc8fa0fa3e
。DELETE
请求看起来是这样的:
curl \
--user chapter12:chapter12 \
--header 'Accept: application/vnd.api+json' \
--header 'Content-type: application/vnd.api+json' \
--request DELETE 'https://localhost/jsonapi/node/
article/1ddf244d-e8e6-40f5-be48-23bc8fa0fa3e'
当你这次提交时,命令行上不会有任何响应输出。当成功时,响应将是一个 HTTP 204
空响应。
JSON:API 路由现在显示没有节点,这意味着 DELETE
操作成功:
图 12.23 – 使用 DELETE 请求后,节点已成功删除
它是如何工作的…
就像 POST
和 PATCH
一样,DELETE
代表指定的用户执行操作。由于此用户有权限 delete any
文章内容,请求被允许,Drupal 从网站上删除了文章节点。
对于某些用例,DELETE
可能对执行 API 或授予权限过于破坏性。在这种情况下,您可以使用PATCH
并将实体状态更改为未发布
或存档
(如果使用`内容审阅模块)。
使用视图提供自定义数据源
RESTful Web Services 模块提供了视图插件,允许您通过视图公开 RESTful API 的数据。这允许您创建一个具有路径并使用序列化插件输出数据的视图。您可以使用此功能以 JSON 或 XML 格式输出实体,并且可以带有适当的头信息发送。
在本菜谱中,我们将创建一个视图,输出 Drupal 站点的用户,提供他们的用户名、电子邮件和图片(如果提供)。
准备工作
确保您已启用以下核心模块:
-
视图
-
视图 UI
-
RESTful Web Services
如何操作...
-
导航到结构然后视图。
-
点击添加新视图。
-
将视图命名为
API Users
并显示/api/users
:
图 12.24 – 为自定义 REST 端点设置视图的路径
- 保存视图并继续。
在创建视图后,进行以下更改:
-
将行插件格式从实体更改为字段,以便我们可以控制特定的输出。
-
在格式设置部分的设置部分,在接受的请求格式下启用json。
-
确保您的视图具有名称、电子邮件和图片作为用户实体字段
-
将用户:名称字段更改为纯文本格式化程序。不要将其链接到用户,以确保响应不包含任何 HTML。
-
将用户:图片字段更改为使用 URL 到图片格式化程序,以便只返回 URL 而不是 HTML。
-
保存更新后的视图。
-
通过访问
/api/users
来访问您的视图。您将收到一个包含用户信息的 JSON 响应。
在您的浏览器中,您将看到以我们在视图中设置的方式格式化的系统用户输出:
[
{
"name": "spuvest",
"mail": "spuvest@example.com",
"user_picture": "\/sites\/default\/files\/pictures\/
2017-07\/generateImage_xIQkfx.jpg"
},
{
"name": "crepathuslus",
"mail": "crepathuslus@example.com",
"user_picture": "\/sites\/default\/files\/pictures\/
2017-07\/generateImage_eauTko.gif"
},
{
"name": "veradabufrup",
"mail": "veradabufrup@example.com",
"user_picture": "\/sites\/default\/files\/pictures\/
2017-07\/generateImage_HsEjKW.png"
}
]
它是如何工作的...
RESTful Web Services 模块提供了一个显示、行和格式插件,允许您以序列化格式导出内容实体。REST Export 显示插件允许您指定路径以访问 RESTful 端点,并为请求的格式正确分配Content-Type
头。
序列化风格作为唯一支持的样式插件提供,用于 REST Export 显示。此样式插件仅支持标识为数据显示类型的行插件。它期望从行插件接收原始数据,以便可以将其传递给适当的序列化器。
然后,您可以选择使用数据实体或数据字段行插件。它们不是从它们的渲染方法返回渲染数组,而是返回将被序列化为正确格式的原始数据。通过行插件返回原始格式数据,并由样式插件进行序列化,显示插件将返回响应,通过序列化模块转换为正确的格式,这是我们之前在本章中看到的。
使用视图是提供 API 读取路由的绝佳方式。您将获得在 Drupal 中使用的视图 UI 的所有便利性,以及 JSON 或 XML 序列化的综合力量。您可以根据需要创建尽可能多的这些视图来满足不同的用例。
更多内容...
视图允许您提供特定的 RESTful 端点。使用视图,您可以添加具有自定义路径的显示,并将字段添加到其中,创建自定义 JSON 响应。这使得在 Drupal 中快速开发自定义只读数据 API 变得简单。
控制 JSON 输出中的键名
数据字段行插件允许您配置字段别名。当数据返回到视图时,它将包含 Drupal 的机器名称。这意味着自定义字段看起来可能像field_my_field
,这可能对消费者来说没有意义。通过点击字段旁边的设置,您可以在模态表单中设置别名:
图 12.25 – 您可以在视图 REST 显示中别名字段名称
当您提供一个别名时,字段将匹配。例如,user_picture
可以改为avatar
,而mail
键可以改为email
:
[
{
"name": "spuvest",
"email": "spuvest@example.com",
"avatar": "\/sites\/default\/files\/pictures\/2017-07\/
generateImage_xIQkfx.jpg"
},
]
控制 RESTful 视图的访问
当您使用视图创建 RESTful 端点时,您不是使用 RESTful Web Services 模块创建的相同权限。您需要在视图中定义路由权限,这样您就可以指定请求的特定角色或权限。
EntityResource
插件提供的默认GET
方法不提供列出实体的方式,并允许通过 ID 检索任何实体。使用视图,您可以提供实体列表,限制它们到特定的捆绑包,等等。
使用视图,您甚至可以提供一个新端点来检索特定实体。使用上下文过滤器,您可以添加路由参数和过滤器来限制和验证实体 ID。例如,您可能希望通过 API 公开文章内容,但不公开页面。
使用 OAuth 方法
使用 RESTful Web Services 模块,我们可以为端点定义特定的支持认证提供者。Drupal 核心提供了一个 cookie 提供者,它通过有效的 cookie 进行认证,例如您的常规登录体验。然后,还有 HTTP Basic Authentication 模块来支持 HTTP 认证头。
一些替代方案提供了更健壮的认证方法。使用基于 cookie 的认证,你需要使用 CSRF 令牌来防止未经授权的第三方加载未请求的页面。当你使用 HTTP 认证时,你会在请求头中发送每个请求的密码。
一个流行且开放的授权框架是 OAuth。OAuth 是一种使用令牌而不是密码的正确认证方法。在这个菜谱中,我们将实现Simple OAuth
模块,为GET
和POST
请求提供 OAuth 2.0 认证。
准备工作
如果你不太熟悉 OAuth 或 OAuth 2.0,它是一个授权标准。OAuth 的实现围绕在 HTTP 头中发送的令牌的使用。有关更多信息,请参阅 OAuth 主页:oauth.net/
。
对于以下部分,你需要贡献的Rest UI模块。Rest UI 将使你更容易在 Drupal 中查看和控制基于 REST 的路由。
如何做到这一点...
继续使用 Composer 获取必要的模块:
composer require drupal/restui:¹.0
在扩展下的管理区域启用REST UI模块。
现在,执行以下操作:
-
首先,我们必须将
Simple OAuth
模块添加到我们的 Drupal 站点中:composer require drupal/simple_oauth:⁶.0@beta
-
前往配置,然后在网络服务下点击REST以配置可用的端点:
图 12.26 – 使用 REST UI 模块,你可以在管理中管理 RESTful 端点
-
现在端点已经启用,必须进行配置。检查
GET
和POST
请求。然后,勾选JSON复选框,以便数据可以以 JSON 格式返回。勾选oauth2复选框,然后保存。 -
在我们能够配置
Simple OAuth
模块之前,我们必须生成一对密钥来加密OAuth
令牌。我们可以通过两种方式之一来完成这项工作:-
在 webroot 之外创建一个目录,然后点击生成密钥。在出现的对话框中,添加服务器上此文件夹的完整路径,然后点击生成。
-
如果管理 UI 不起作用,我们可以使用以下两个命令来生成密钥。将它们放在 webroot 之外的服务器上:
openssl genrsa -out private.key 2048
openssl rsa -in private.key -pubout > public.key
-
-
生成密钥后,转到配置页面,然后转到Simple OAuth。输入刚刚生成的私钥和公钥的路径,然后点击保存配置:
图 12.27 – 在管理区域中管理 Simple OAuth 的密钥
-
从Simple OAuth 设置配置表单中,点击+ 添加客户端。为客户端提供一个标签,并选择管理员范围。然后,点击保存以创建客户端。
-
接下来,我们将通过
/oauth/
令牌端点生成一个令牌。你需要使用你刚刚创建的客户端的 ID。我们必须传递grant_type
、client_id
以及用户名和密码。grant_type
是密码,而client_id
是从创建的客户端中获取的 ID。用户名和密码将是你要使用的账户:curl -X POST https://localhost/oauth/token \
-H 'content-type: application/x-www-form-
urlencoded' \
-d 'grant_type=client_credentials&client_id=
CLIENT_ID&username=chapter12&client_secret=
chapter12'
-
响应将包含一个
access_token
属性。这将在进行 API 请求时用作你的令牌。 -
使用Authorization: Bearer [****token]头请求 REST 节点:
curl -X GET 'https://localhost/node/8?_format=json' \
-H 'accept: application/json' \
-H 'authorization: Bearer ACCESS_TOKEN'
就这样!你现在可以使用 OAuth 设置认证路由,用于 Drupal 中的各种资源,并配置所有不同的 REST 端点,包括它们接受的方法和认证类型。
它是如何工作的…
Simple OAuth
模块是使用社区标准库League\OAuth2
PHP 库构建的,这是一个 OAuth2 实现的社区标准库。
在典型的认证请求中,存在一个认证管理器,该管理器使用authentication_collector
服务来收集所有标记的认证提供者服务器。根据提供者设置的优先级,每个服务被调用以检查它是否适用于当前请求。然后,每个应用的认证提供者被调用以查看认证是否无效。
对于 RESTful Web Services 模块,这个过程更为明确。端点的supported_auth
定义中标识的提供者是唯一通过applies
和authenticates
过程运行的服务。
第十三章:在 Drupal 中编写自动化测试
在前面的章节中,我们回顾了如何使用控制器、路由、响应、自定义模块、自定义实体类型、钩子等来为 Drupal 添加自定义功能。即使只有一点点的代码,你也可以为你的 Drupal 应用程序添加很多功能。
但你怎么知道它真的“工作”了?当然,你可以点击并尝试一些事情——但这并不能保证事情在底层按预期工作。你添加的代码和功能越多,验证现有功能是否仍然完整而不提供测试就越困难。
通过实施自动化测试,你可以确保你添加的代码和功能确实按预期工作。最重要的是,自动化测试可以显著减少错误和回归到你的生产网站,这反过来将有助于增强作为开发者的信心。
Drupal 提供了几种类别的工具来帮助你为你的应用程序提供测试。本章涵盖了以下内容:
-
安装 PHPUnit 测试套件
-
运行 PHPUnit
-
编写单元测试
-
编写内核测试
-
编写功能测试
-
编写功能 JavaScript 测试
-
编写 NightwatchJS 测试
测试类型
你可以在 Drupal 中运行和编写的测试类型有五种——单元测试、内核测试、功能测试、功能 JavaScript 测试和NightwatchJS 测试。你编写哪些将取决于你正在创建的功能类型以及你愿意接受的测试覆盖率水平来证明你的代码正在工作。让我们来看看这些测试类型。你可以在 GitHub 上找到本章中使用的完整代码:github.com/PacktPublishing/Drupal-10-Development-Cookbook/tree/main/chp13
单元测试
单元测试是不需要 Drupal 安装即可评估的测试,因为它们只测试执行代码。这是你可以编写的最低级别的测试。单元测试对于测试插件、服务或其他不要求与数据库交互的代码非常有用。如果你需要编写需要数据库或 Drupal 环境的测试,你将编写内核测试。
内核测试
内核测试是单元测试的下一步。内核测试是一种可以执行和测试需要一定数据库交互级别的功能的测试。如果你想测试在保存实体时触发的功能、测试字段格式化器、检查用户对路由的访问权限,或者测试控制器及其响应的功能,内核测试就是你需要编写的测试。在执行内核测试时,会在测试执行的地方安装一个 Drupal 实例。这种隔离级别允许你在不影响当前工作数据库的情况下测试交互式功能。你可以指定要安装的模块和在内核测试中包含的配置,这使得它成为测试模块的绝佳方式。这些是在 Drupal 中最常见的测试。
功能性测试
功能性测试是你能编写的最高级别的测试。像内核测试一样,功能性测试将安装一个可工作的 Drupal 副本来运行测试。这些测试使用无头浏览器进行评估,并且从用户的角度测试功能非常有用。这类测试适用于测试用户工作流程、用户权限,以及评估页面内容是否符合预期。例如,如果你有一个功能,用户导航到网站,登录,导航到 Drupal 管理界面,并看到其他角色不应看到的部分,你可以通过功能性测试来评估这一点。
功能性 JavaScript 测试
功能性 JavaScript 测试是在真实浏览器中执行的,例如 Chrome 或 Firefox。常规的功能性测试在底层使用名为 Mink 的无头 PHP 浏览器模拟器中运行。由于它不是一个完整的浏览器,因此无法测试像 Chrome 这样的真实浏览器所拥有的任何 JavaScript 相关功能。如果你想在浏览器中测试与 AJAX、cookies 或 DOM 相关的 JavaScript 事件,你需要编写一个功能性 JavaScript 测试。功能性 JavaScript 测试将需要存在一个浏览器,如 Chrome,以及像 Selenium 这样的工具来编排测试中的 Chrome。
NightwatchJS 测试
与功能性 JavaScript 测试类似,可以使用 yarn
运行和与之交互。如果你是一位倾向于编写比 PHP 更多 JavaScript 的开发者,你可能对使用 NightwatchJS 而不是 PHPUnit 感兴趣。另一个额外的优点是,你可以为自定义编写的 JavaScript 编写单元测试,这是你不能用 PHPUnit 来测试的。
功能性 JavaScript 和 NightwatchJS 测试需要最多的努力来设置和实施,但它们非常有价值,因为它们在相同的设置、条件、用户角色(们)和浏览器下运行。它们执行时间最长,但仍然只占任何人类完成相同任务时间的很小一部分。
安装 PHPUnit 测试套件
所有的先前测试类型(除了 NightwatchJS 测试)都由一个测试框架 PHPUnit 执行。我们可以向我们的自定义模块或自定义主题添加任意数量的测试,并使用 PHPUnit 运行它们——我们只需安装它并配置它指向我们的测试文件。
准备工作
在继续之前,首先要安装所有测试依赖项,以便您实际上可以在 Drupal 中运行测试。Drupal 有一个特定的 Composer 包,它带来了 PHPUnit 和其所需的依赖项。
安装时,请按照以下步骤操作:
在项目根目录打开一个终端(命令行)。
运行以下 Composer 命令:
composer require --dev drupal/core-dev:¹⁰
drupal/core-dev
包将把 PHPUnit 和 Drupal 中编写和运行测试所需的各个依赖项引入到您的项目中。
--dev 标志
注意,在安装时,我们使用 --dev
标志。这告诉 Composer 在 composer.json
的 require-dev
部分列出这些包。drupal/core-dev
包不是您想在生产环境中拥有的东西;它仅用于测试。
如何操作...
接下来,我们需要配置 PHPUnit,使其知道我们的测试所在的位置。Drupal 核心在 core
目录中提供了一个示例 phpunit.xml.dist
文件。这是 PHPUnit 执行时读取的文件。您不需要逐行理解它,但有一些区域我们需要调整,以便它可以在您的自定义模块目录中执行。
将 phpunit.xml.dist
文件从 core
目录复制出来,并将其放置在项目根目录中。将文件重命名为 phpunit.xml
并进行以下编辑:
-
在文档顶部
<phpunit>
标签上编辑 bootstrap 属性,使其指向核心测试的bootstrap
文件:bootstrap="./web/core/tests/bootstrap.php"
-
为了运行内核或功能测试,请编辑
<php>
部分中的以下环境设置:<env name="SIMPLETEST_BASE_URL"
value="http://localhost"/>
<env name="SIMPLETEST_DB" value="
mysql://database:database@database/database"/>
<env name="BROWSERTEST_OUTPUT_DIRECTORY" value=""/>
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
<env name="MINK_DRIVER_ARGS_WEBDRIVER" value="
["chrome", {"browserName":"chrome",
"chromeOptions":{"args":["--disable-gpu","--
headless"]}}, "http://chrome:9515"] "/>
DDEV、Lando、Docksal 或 Docker?
如果您使用 DDEV、Lando、Docksal 或其他本地运行 Drupal 的现成工具,请检查它们的文档以及服务名称。根据您使用的工具,SIMPLETEST_BASE_URL
、SIMPLETEST_DB
和 MINK_DRIVER_ARGS_WEBDRIVER
的先前值可能会有所不同。
-
接下来,编辑
testsuites
部分,使其指向您的自定义模块目录。您需要为每种测试类型指定一个testsuite
条目:<testsuites>
<testsuite name="unit">
<directory>
web/modules/custom/*/tests/src/Unit
</directory>
</testsuite>
<testsuite name="kernel">
<directory>
web/modules/custom/*/tests/src/Kernel
</directory>
</testsuite>
<testsuite name="functional">
<directory>
web/modules/custom/*/tests/src/Functional
</directory>
</testsuite>
<testsuite name="functional-javascript">
<directory>
web/modules/custom/*/tests/src/FunctionalJavascript
</directory>
</testsuite>
</testsuites>
这些更改将告诉 PHPUnit 我们为自定义模块(位于 web/modules/custom
)编写的测试的位置。测试位于模块的 tests/src
目录中。
每种测试类型都位于该目录下的独立目录中:
单元测试放入 tests/src/Unit
内核测试放入 tests/src/Kernel
功能测试放入 tests/src/Functional
函数 JavaScript 测试放入 tests/src/FunctionalJavascript
通过在phpunit.xml
中将每种测试类型定义为单独的测试套件,我们可以选择使用 PHPUnit 执行哪种类型的测试,我们将在下一节中探讨这一点。你还可以设置多个testsuite
条目,或者一个testsuite
条目内的多个目录。例如,如果你想运行 PHPUnit 时包含所有贡献模块的所有单元测试,你可以添加另一个directory
条目:
<testsuite name="unit">
<directory>
web/modules/contrib/*/tests/src/Unit
</directory>
<directory>
web/modules/custom/*/tests/src/Unit
</directory>
</testsuite>
我们创建了一个phpunit.xml
文件并将其放置在项目的根目录下,原因在于该文件可以防止在下次更新 Drupal 时覆盖或删除项目。这样,你可以将配置提交到你的仓库中,并与团队共享,同时还能在 GitHub、GitLab 或 CircleCI 提供的持续集成服务中运行测试。
重要的是要知道,这仅仅是一种设置 PHPUnit 配置的可能方式。一旦你对 PHPUnit 更加熟悉,你还可以在这里配置更多设置。
还有一些其他设置可以输出测试覆盖率结果,保存测试失败截图的位置,额外的测试监听器,日志输出等等。请务必查看在线文档(phpunit.readthedocs.io/en/9.5/configuration.html
),以获取更多关于如何配置 PHPUnit 的信息。
它是如何工作的...
当你执行 PHPUnit 时,它会使用phpunit.xml
文件来告知如何执行。它将扫描所有列出的目录以查找测试文件,并相应地执行它们,在命令行窗口(或 IDE)中提供测试反馈。为每个你编写测试的项目,都需要一个像这样的phpunit.xml
文件才能运行任何测试。
还有更多...
如果你计划编写功能 JavaScript 测试,你还需要 Chrome、Chrome WebDriver 和 Selenium。安装和配置这些工具可能因你使用 Lando、DDEV、Docksal、Docker Compose 或其他工具来运行 Drupal 而异。请查阅有关如何安装这些工具以正确执行功能 JavaScript 测试的最佳信息的文档。假设你正在使用.lando.yml
文件中的services
部分:
chrome:
type: compose
services:
image: drupalci/webdriver-chromedriver:production
command: chromedriver --log-path=/tmp/
chromedriver.log --verbose
在phpunit.xml
中的MINK_DRIVER_ARGS_WEBDRIVER
设置中,chrome
成为了一种新的有线服务,这使得所有这些功能都能为功能 JavaScript 测试工作)。如果你想安装并使用 Firefox 作为测试的替代浏览器,你也可以这样做,但 Chrome 是测试中最常用的浏览器。
这些工具的添加方式可能因你所使用的工具而异。请查阅有关如何添加 Chrome 和 Chrome WebDriver 的最佳方法的文档。由于涉及太多特定于堆栈的配置,本书无法涵盖。
运行 PHPUnit
首先,我们可以通过执行phpunit
命令来验证我们是否正确设置了。目前还没有测试,但这没关系。这一步只是为了验证工具已安装,并且我们的配置文件被正确读取。
如何做到这一点...
咨询文档
从这里开始,我们将提供使用 PHPUnit 执行测试的裸骨、详尽的命令。如果你使用 Lando、DDEV、Docksal 或其他工具,请查阅它们的文档,了解如何最佳运行 PHPUnit。它们通常有小型且方便的命令包装器。
在你的命令行中,执行以下操作:
phpunit
这是您将运行的主要命令。PHPUnit 将自动检测项目目录中的phpunit.xml
配置。
找不到 phpunit?
如果你收到关于找不到phpunit
命令的错误,你可能需要使用项目根目录中的完整路径来代替,使用vendor/bin/phpunit
。Composer 应该会自动为你别名命令,但根据项目的设置,这可能是必需的。
你应该看到类似以下输出的内容:
PHPUnit 9.5.26 by Sebastian Bergmann and contributors.
No tests executed!
你也可以通过向phpunit
添加--testsuite
和--filter
参数来运行特定的testsuites
或单个测试:
phpunit --testsuite unit
上述命令将在phpunit.xml
文件中列出的目录中运行所有找到的单元测试:
phpunit --testsuite unit --filter FooBarTest
上述命令将仅运行FooBarTest
单元测试。这两种方法在测试特定testsuites
或测试本身以获得更快反馈时非常有用,尤其是在测试、调试和迭代代码时。
它是如何工作的…
当你执行 PHPUnit 时,它会扫描phpunit.xml
文件中列出的所有目录,并在命令行窗口中提供测试反馈。
在这种情况下,PHPUnit 表示没有找到或执行任何测试,这是正确的,因为我们还没有任何测试。我们现在可以编写第一个单元测试了。
编写单元测试
让我们编写第一个测试。如前所述,单元测试是你能编写的最低级别的测试类型。它只执行和测试没有依赖服务(如连接的数据库或其他集成 API)的原始 PHP 代码。这意味着测试可以使用 PHP 单独执行。
准备工作
让我们创建一个场景,帮助你思考何时以及如何应用单元测试。想象一下,你需要向一个前端组件提供数据。前端开发者要求你在 API 响应中提供所有 JSON 键,格式为camelCase
。camelCase
会将字符串如field_event_date
转换为fieldEventDate
。
在 Drupal 的许多地方使用snake_case
;你最常见的用途是在机器名(如前一个事件日期字段)上。Drupal 中的所有机器名都在snake_case
格式。
这是一个非常简单的例子,但完美地说明了我们如何使用单元测试来测试我们的类。
如何做呢...
现在创建一个名为chapter13
的自定义模块。这是我们将在本章的其余部分编写示例代码及其测试的地方。如果你需要复习如何在 Drupal 中创建自定义模块,请参考第四章,使用自定义代码扩展 Drupal。
自定义模块创建完成后,让我们在 chapter13
模块的 src
目录下添加 CamelCase
类:
<?php
declare(strict_types=1);
namespace Drupal\chapter13;
/**
* Class CamelCase
* @package Drupal\chapter13
*/
class CamelCase {
/**
* Convert snake_case to camelCase.
*
* @param string $input
* @return string
*/
public static function convert(string $input): string {
$input = strtolower($input);
return str_replace('_', '', lcfirst(ucwords
($input, '_')));
}
}
看起来它工作得很好,对吧?让我们通过一个测试来证明它。
在你的自定义模块中,创建一个 tests
目录。在这个目录下,创建一个 src
目录,然后在 src
目录中,创建一个 Unit
目录。现在你应该有一个路径看起来像 chapter13/tests/src/Unit
。请注意,你的 tests
目录中的 src
目录与 chapter13
模块根目录下的 src
目录不同,CamelCase
类就位于该目录。
在 chapter13/tests/src/Unit
目录下,创建一个新文件并将其命名为 CamelCaseTest.php
。所有测试文件都必须以 Test
结尾,以便被 PHPUnit 发现。
我们的测试文件需要做两件事:
-
使用
CamelCase
类提供测试数据 -
断言转换方法返回我们期望的结果
在你的测试类中,任何以 test
开头的方法都将由 PHPUnit 进行评估。考虑到这一点,让我们继续在我们的 CamelCaseTest.php
文件中填写我们的单元测试:
<?php
namespace Drupal\Tests\chapter13\Unit;
use Drupal\Tests\UnitTestCase;
use Drupal\chapter13\CamelCase;
/**
* Class CamelCaseTest
* @package Drupal\Tests\chapter13\Unit
*/
class CamelCaseTest extends UnitTestCase {
/**
* Data provider for testToCamel().
*
* @return array
* An array containing input values and expected output
values.
*/
public function exampleStrings() {
return [
['button_color', 'buttonColor'],
['snake_case_example', 'snakeCaseExample'],
['ALL_CAPS_LOCK', 'allCapsLock'],
];
}
/**
* Tests the ::convert method.
*
* @param $input
* The input values.
*
* @param bool $expected
* The expected output.
*
* @param bool $separator
* The string separator.
*
* @dataProvider exampleStrings()
*/
public function testCamelCaseConversion($input,
$expected) {
$output = CamelCase::convert($input);
$this->assertEquals($expected, $output);
}
}
好的,运行 phpunit
:
phpunit --testsuite unit --filter CamelCaseTest
这将导致以下结果:
Testing
... 3 / 3 (100%)
Time: 00:00.044, Memory: 10.00 MB
OK (3 tests, 3 assertions)
这个输出表明测试被调用了三次(一次对应于 @dataProvider
中的每一组数据)并且每次都通过了。换句话说,$input
等于 $expected
,这是 assertEquals
评估的内容。如果测试失败,这个输出将表明哪个测试失败以及在哪一行。
它是如何工作的…
PHPUnit 报告说所有三个测试都通过了。但是,等等——我们不是只写了 testCamelCaseConversion
测试方法吗?
当测试执行时,PHPUnit 检测到我们测试方法中的一个特殊注释,称为 @dataProvider
,它标记了 exampleStrings
方法。exampleStrings
方法提供了一个数据数组。每个数组包含要发送的值和我们期望得到的值。PHPUnit 将循环数据提供者的值,因此我们的测试方法被调用三次(一次对应于每一组值)并评估测试。这意味着当执行时,PHPUnit 看到的测试方法调用如下:
testCamelCaseConversion("button_color", "buttonColor")
三个值测试确保该方法对两个或更多单词有效,并返回正确的格式。
在测试中,我们将输入传递给 CamelCase::convert
方法。从该方法中,我们将返回的值传递给 assertEquals
,这是 PHPUnit 的许多值断言方法之一。
你添加到数据提供者中的每一个项目都将由使用它们的测试方法进行断言。dataProviders
是测试我们代码在多种场景下的一个极好的方式。因此,我们知道传递给转换的任何字符串都将从 foo_bar
转换为 fooBar
并返回。这个测试证明了这一点。
那么,自动化测试是如何帮助这里的呢?在我们的设置中,团队中的任何开发者都可以运行 testsuites
并查看它们的输出。如果它们失败了,他们可以看到哪些代码失败以及在哪里。这为在代码部署到生产之前修复代码提供了机会。
在持续集成设置下,你可以防止代码在所有测试通过之前合并到你的主分支。正如我们提到的,这是减少在生产环境中向用户暴露错误和缺陷的绝佳方式。
还有更多…
随着时间的推移,需求可能会改变,而且经常是这样。假设你部署了这段代码后,你的前端开发者回来要求将任何类似foo-bar
的字符串也转换为驼峰格式。
我们的测试已经到位,我们可以将这个新案例添加到我们的数据提供者中:
public function exampleStrings() {
return [
['button_color', 'buttonColor'],
['snake_case_example', 'snakeCaseExample'],
['ALL_CAPS_LOCK', 'allCapsLock'],
['foo-bar', 'fooBar'],
];
}
现在我们运行测试,得到以下结果:
Testing
...F 4 / 4 (100%)
Time: 00:00.046, Memory: 10.00 MB
There was 1 failure:
1) Drupal\Tests\chapter13\Unit\CamelCaseTest::
testCamelCaseConversion with data set #3 ('foo-bar',
'fooBar')
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'fooBar'
+'foo-bar'
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
这没问题——我们还没有更新我们的实现。首先添加新案例到测试中,让我们可以迭代和改进实现代码,直到测试通过。这提供了一个快速的反馈循环,因为我们可以在CamelCase
类中随意修改实现。如果所有测试都通过,我们知道我们已经满足了要求,我们的功能是有效的,我们可以继续处理模块中的其他功能。
参见
在编写任何实现代码之前先编写测试被称为测试驱动开发。这意味着测试驱动规范或需求,实现代码使它们通过。关于你是否应该先编写所有测试,有许多不同的观点。
如果你刚开始接触测试,如果你需要在 Drupal 模块中澄清一个想法,写一些初始代码作为概念验证是完全可以的。你可以在开发过程中的任何时候添加测试。重要的是要为你的代码添加测试;不必太担心何时添加。
你练习得越多,写测试也越多,你的技能就会越好,最终你会转向先写测试。
继续实验CamelCase
类及其CamelCaseTest
类。看看你是否可以发明一些新的需求,更改代码,运行测试,并使它们通过。
Drupal 核心有数百个优秀的单元测试示例。最受欢迎的贡献模块也很有用。如果你卡住了或需要指导,一定要查看它们。
现在你已经了解了 Drupal 中单元测试的基础,让我们看看内核测试。
编写内核测试
让我们基于之前的例子继续前进。现在假设利益相关者要求你在屏幕上以驼峰格式输出字段值。好消息是我们有一个工作的实现和单元测试,所以我们可以轻松地在 Drupal 中完成这个任务。
在这种情况下,我们需要为字符串字段创建一个字段格式化类。当格式化器用于字段以显示输出时,我们希望将用户输入通过我们现有的CamelCase
类。如果你需要关于字段格式化和管理实体显示的复习,请参阅第二章,内容 构建经验。
这提供了一个很好的例子,可以进入内核测试。之前,我们提到内核测试创建了一个具有你测试中指定的设置的 Drupal 最小安装,以便运行和评估它们的测试方法。运行内核测试没有危险,因为它们以任何方式都不会接触或干扰你的当前站点数据库。测试完成后,它将被拆除,或从数据库中删除,不留痕迹。
如何做到这一点...
在你的tests/src
目录下创建一个新的目录,命名为Kernel
。这是你的chapter13
模块的内核测试将驻留的地方。这次,我们将先编写测试。
在chapter13/tests/src/Kernel
下创建CamelCaseFormatterTest.php
文件。接下来,我们将用以下代码填充它。设置部分有相当多的模板代码,但我们将回顾正在发生的事情:
<?php
namespace Drupal\Tests\chapter13\Kernel;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
/**
* Tests the formatting of string fields using the Camel
Case field formatter.
*
* @package Drupal\Tests\chapter13\Kernel
*/
class CamelCaseFormatterTest extends KernelTestBase {
use NodeCreationTrait,
ContentTypeCreationTrait;
protected $strictConfigSchema = FALSE;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'field',
'text',
'node',
'system',
'filter',
'user',
'chapter13',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Install required module schema and configs.
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installConfig(['field', 'node', 'filter',
'system']);
$this->installSchema('node', ['node_access']);
// Create a vanilla content type for testing.
$this->createContentType(
[
'type' => 'page'
]
);
// Create and store the field_chapter13_test field.
FieldStorageConfig::create([
'field_name' => 'field_chapter13_test',
'entity_type' => 'node',
'type' => 'string',
'cardinality' => 1,
'locked' => FALSE,
'indexes' => [],
'settings' => [
'max_length' => 255,
'case_sensitive' => FALSE,
'is_ascii' => FALSE,
],
])->save();
FieldConfig::create([
'field_name' => 'field_chapter13_test',
'field_type' => 'string',
'entity_type' => 'node',
'label' => 'Chapter13 Camel Case Field',
'bundle' => 'page',
'description' => '',
'required' => FALSE,
'settings' => [
'link_to_entity' => FALSE
],
])->save();
// Set the entity display for testing to use our
camel_case formatter.
$entity_display = EntityViewDisplay::load
('node.page.default');
$entity_display->setComponent('field_chapter13_test',
[
'type' => 'camel_case',
'region' => 'content',
'settings' => [],
'label' => 'hidden',
'third_party_settings' => []
]);
$entity_display->save();
}
/**
* Tests that the field formatter camel_case formats the
value
* as expected.
*/
public function testFieldIsFormatted() {
$node = $this->createNode(
[
'type' => 'page',
'field_chapter13_test' => 'A user entered string'
]
);
$build = $node->field_chapter13_test->view('default');
$this->assertSame('aUserEnteredString',
$build[0]['#context']['value']);
}
}
不要因为测试中的代码量而气馁。大多数代码是设置我们运行测试本身所需的条件:
-
安装提供字段和
Node
实体的所需模块 -
安装模块配置和实体模式
-
创建一个
page
内容类型 -
创建
field_chapter13_test
并将其分配给页面节点类型 -
修改节点默认视图模式并将
field_chapter13_test
设置为使用camel_case
字段格式化器
另一种方法是,在 Drupal 中创建所有这些项目(内容类型、字段、设置格式化器)并将该配置导出到仅用于此测试的模块中。最终结果大致相同,但你可能会发现,完全用代码创建条件在长期来看比几十个 YAML 文件更易于维护。
测试中的命名
在内核测试中命名内容类型、实体或字段时,你可以随意命名。它们不必与你的系统完全匹配,特别是如果你只需要一个可以包含字段的随机内容类型。我们测试的是返回的输出,而不是名称。
如果我们现在运行测试,它将失败;现在,我们可以实现使测试通过。
为了解决这个问题,我们需要创建一个使用我们现有的CamelCase
类的FieldFormatter
插件。这个练习很简单,因为将输入转换为驼峰格式的大部分工作已经存在。
在chapter13
模块的src
目录下添加一个名为Plugin/Field/FieldFormatter
的目录。在该目录中,添加一个名为CamelCaseFormatter.php
的文件。我们的实现将如下所示:
<?php
declare(strict_types = 1);
namespace Drupal\chapter13\Plugin\Field\FieldFormatter;
use Drupal\chapter13\CamelCase;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\
StringFormatter;
/**
* Plugin implementation of the 'camel_case' field
formatter.
*
* @FieldFormatter(
* id = "camel_case",
* label = @Translation("Camel case"),
* field_types = {
* "string"
* }
* )
*/
class CamelCaseFormatter extends StringFormatter {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface
$items, $langcode) : array {
$elements = [];
foreach ($items as $delta => $item) {
$view_value = $this->viewValue($item);
$elements[$delta] = $view_value;
}
return $elements;
}
/**
* {@inheritdoc}
*/
protected function viewValue(FieldItemInterface $item) {
return [
'#type' => 'inline_template',
'#template' => '{{ value|nl2br }}',
'#context' => ['value' => CamelCase::convert($item
->value)],
];
}
}
通过从核心 Drupal 扩展StringFormatter
类,我们可以利用我们的CamelCase
类进行修改。
现在,让我们运行我们的内核测试:
phpunit --testsuite kernel --filter CamelCaseFormatterTest
这导致以下结果:
Testing
F 1 / 1 (100%)
Time: 00:01.500, Memory: 10.00 MB
There was 1 failure:
1) Drupal\Tests\chapter13\Kernel\CamelCaseFormatterTest::
testFieldIsFormatted
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'aUserEnteredString'
+'a user entered string'
FAILURES!
Tests: 1, Assertions: 9, Failures: 1.
失败了!发生了什么?记得我们之前提到过需求可能会改变吗?我们的测试用例将用户输入的字符串
作为测试数据。我们的CamelCase
类目前无法处理包含空格、破折号或逗号的字符串。由于这是 Drupal 中的一个字段,用户可以输入几乎所有内容。我们需要考虑到这一点。
修改CamelCase
类以适应这个新的要求:
public static function convert(string $input): string {
$input = strtolower($input);
$input = preg_replace('/[, -]/', '_', $input);
return str_replace('_', '', lcfirst(ucwords
($input, '_')));
}
替换逗号、空格和破折号字符的添加现在应该能满足新的用例。让我们再次运行内核测试:
Testing
. 1 / 1 (100%)
Time: 00:01.491, Memory: 10.00 MB
OK (1 test, 9 assertions)
然而,我们不要忘记之前损坏的单元测试——让我们将两个新的示例字符串添加到exampleStrings
数据提供者方法中:
public function exampleStrings() {
return [
['button_color', 'buttonColor'],
['snake_case_example', 'snakeCaseExample'],
['ALL_CAPS_LOCK', 'allCapsLock'],
['foo-bar', 'fooBar'],
['This is a basic string', 'thisIsABasicString'],
];
}
现在,当你运行 PHPUnit 时,单元测试和内核测试都应该通过:
Testing
...... 6 / 6 (100%)
Time: 00:01.419, Memory: 10.00 MB
OK (6 tests, 14 assertions)
我们现在网站新增了一个新的、经过测试的功能,我们可以有信心地部署它。当然,当我们需要时,我们可以自由地在我们 Drupal 应用程序中重用这个功能。
它是如何工作的...
当运行内核测试时,Drupal 将安装一个额外的副本来运行测试。内核测试将使用测试中所需的安装配置文件和模块。这样,你可以确保你在最干净的可能设置中测试你的代码,没有任何不必要的贡献模块或其他代码的干扰。当测试运行完成后,Drupal 将自动清理并删除这个第二个安装,包括测试本身创建的任何数据。
到目前为止,我们已经编写了代码和测试来证明它们确实做了我们需要的——但它们无法测试网站上的用户是否看到了正确的内容。让我们深入进去,添加一个功能测试来做到这一点!
编写功能测试
现在我们知道我们的代码是有效的,让我们证明用户在访问节点时可以看到格式化的字符串。功能测试使用一个模拟浏览器,允许我们模拟用户在网站上导航。功能测试安装 Drupal 并在隔离模式下进行测试,因此没有风险会破坏你当前的 Drupal 安装。
如何做到这一点...
使用功能测试,我们可以像真实用户一样导航浏览器,执行所有各种断言,这是单元测试和内核测试无法做到的。像之前一样,我们需要设置一些配置才能进行测试。在你的tests/src
目录中,创建一个名为Functional
的新目录,然后在其中创建一个名为CamelCaseFormatterDisplayTest.php
的文件。
在这个新的测试文件中,我们将借鉴之前内核测试的一些设置:
<?php
namespace Drupal\Tests\chapter13\Functional;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
/**
* Class CamelCaseFormatterDisplayTest
*
* @package Drupal\Tests\chapter13\Functional
*/
class CamelCaseFormatterDisplayTest extends
BrowserTestBase {
/**
* @var bool Disable schema checking.
*/
protected $strictConfigSchema = FALSE;
/**
* @var string The theme to use during test.
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'field',
'text',
'node',
'system',
'filter',
'user',
'chapter13',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a vanilla content type for testing.
$this->createContentType(
[
'type' => 'page'
]
);
// Create and store the field_chapter13_test field.
FieldStorageConfig::create([
'field_name' => 'field_chapter13_test',
'entity_type' => 'node',
'type' => 'string',
'cardinality' => 1,
'locked' => FALSE,
'indexes' => [],
'settings' => [
'max_length' => 255,
'case_sensitive' => FALSE,
'is_ascii' => FALSE,
],
])->save();
FieldConfig::create([
'field_name' => 'field_chapter13_test',
'field_type' => 'string',
'entity_type' => 'node',
'label' => 'Chapter13 Camel Case Field',
'bundle' => 'page',
'description' => '',
'required' => FALSE,
'settings' => [
'link_to_entity' => FALSE
],
])->save();
// Set the entity display for testing to use our
camel_case formatter.
$entity_display = EntityViewDisplay::load
('node.page.default');
$entity_display->setComponent('field_chapter13_test',
[
'type' => 'camel_case',
'region' => 'content',
'settings' => [],
'label' => 'hidden',
'third_party_settings' => []
]);
$entity_display->save();
}
/**
* Test that a site visitor can see a string formatted
with our custom
* field CamelCaseFieldFormatter.
*
* @return void
*/
public function testUserCanSeeFormattedString() {
$this->drupalCreateNode(
[
'type' => 'page',
'field_chapter13_test' => 'A user entered string'
]
);
$this->drupalGet('/node/1');
$this->getSession()->getPage()->hasContent
('aUserEnteredString');
}
}
在功能测试中这里有一些差异。
在功能测试中,您可以指定两个属性,$profile
和 $defaultTheme
。$profile
指定用于此测试的安装配置文件,而 $defaultTheme
指定用于此测试要激活的主题。默认情况下,使用 testing 配置文件,这是安装 Drupal 所需的最小绝对值。您还可以指定 minimal 或 standard,这将安装这些配置文件之一以用于测试。
安装配置文件和测试
如果您使用带有配置或额外内容的安装配置文件,例如创建内容类型和角色,请确保不要尝试在测试中再次使用 createContentType
等方法创建它们。在这些情况下,我们将收到错误,因为安装配置文件已经创建了它们。
最后,您还可以创建自己的安装配置文件进行测试,其中包含您需要的 Drupal 配置文件 – 如果您不想在测试的 setUp
方法中编写代码,这将很有用。
为了使此测试运行,我们只需要默认设置,所以运行 PHPUnit:
phpunit --testsuite functional --filter
CamelCaseFormatterDisplayTest
这导致以下结果:
Testing
. 1 / 1 (100%)
Time: 00:07.390, Memory: 10.00 MB
OK (1 test, 3 assertions)
它通过了,但请注意,执行功能测试的总时间远高于单元测试或内核测试。这是由于必须安装 Drupal 的完整版本来运行功能测试。在此机器上,使用核心 testing 配置文件需要 7 秒。如果我们切换到安装更多内容的核心 standard 配置文件,我们可以看到时间的增加:
protected $profile = 'standard';
Testing
. 1 / 1 (100%)
Time: 00:18.441, Memory: 10.00 MB
OK (1 test, 2 assertions)
所用时间比较小的安装配置文件翻倍。这不一定是一个问题,但在编写测试时请记住这一点。最终,有价值的测试和具体的反馈值得花一点额外的时间来确保我们用自定义代码正确地完成事情。
它是如何工作的...
功能测试比内核测试更健壮。与内核测试一样,Drupal 将安装另一个副本来运行测试,但安装比内核测试更 完整 或更完整。这些测试使用名为 Mink 的浏览器模拟器运行,这就是它能够导航 URL、检查 HTML 元素以及执行类似真实用户导航网站的操作的原因。
参见
您可以使用功能测试做很多事情(请参阅本章末尾关于如何扩展我们之前所涵盖的内容的想法)。然而,Mink 并不是一个真正的浏览器,并且没有运行或对 JavaScript 交互做出反应的能力。例如,如果我们需要在 5 秒后隐藏页面上的字段文本,或者使用 AJAX 显示字段值,我们需要使用功能 JavaScript 测试来完成。
编写功能 JavaScript 测试
假设您现在有一个最后的请求。您被要求使用 AJAX 在用户访问页面 3 秒后显示 Camel Case 格式化器的值。
测试这需要使用实际的浏览器,例如 Google Chrome。检查涉及 AJAX、cookies、用户交互或 DOM 的任何内容都不能用常规功能测试完成。
幸运的是,编写功能 JavaScript 测试并没有太大的不同;我们只是扩展了不同的基类进行测试——WebDriverTestBase
而不是 BrowserTestBase
。
需要 Chrome 和 Selenium
如果你使用 DDEV、Lando、Docksal 或其他现成的工具在本地运行 Drupal,请检查它们的文档,了解如何最佳地集成 Chrome 和 Selenium 以进行功能 JavaScript 测试。它们在安装方法上都有所不同。
如何做…
在你的 tests/src
目录中,创建一个名为 FunctionalJavascript
的新目录。在该新目录中,创建一个名为 CamelCaseFormatterDisplayAjaxTest.php
的文件。你可以将之前的函数测试代码复制到这个文件中,并进行以下更改:
-
将
namespace
从Functional
更改为FunctionalJavascript
-
使用
WebDriverTestBase
而不是BrowserTestBase
扩展 -
类名应更改为
CamelCaseFormatterDisplayAjaxTest
由于我们现在需要在页面显示此文本周围添加 JavaScript,因此可以删除旧的功能测试,因为它将失败。文本最初不会出现在页面上,旧测试无法监听或等待 AJAX,所以我们不再需要这个测试。
现在,我们可以更新测试方法以适应新的要求。假设 JavaScript 已经编写好,并且字段值是通过 AJAX 获取和输出的。实现细节在这里并不重要,但我们可以更改测试方法以 等待 AJAX 完成后再检查页面上的文本:
public function testUserCanSeeFormattedString() {
$this->drupalCreateNode(
[
'type' => 'page',
'field_chapter13_test' => 'A user entered string'
]
);
$this->drupalGet('/node/1');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertSession()->
pageTextContains('aUserEnteredString');
}
在运行我们的新测试后,这是结果:
Testing
. 1 / 1 (100%)
Time: 00:19.721, Memory: 10.00 MB
OK (1 test, 2 assertions)
它是如何工作的…
功能 JavaScript 测试是功能测试,增加了在真实浏览器(如 Google Chrome)中运行的功能。这使得可以对 HTML 元素、交互性、AJAX、cookies 以及其他需要使用真实浏览器执行操作的功能进行更多测试。
编写 NightwatchJS 测试
与功能 JavaScript 测试类似,NightwatchJS 使用 Google Chrome 来评估测试。然而,测试文件完全是用 JavaScript 编写的,而不是 PHP 文件,并且需要 NodeJS 和 yarn
来运行和交互。
准备工作
NightwatchJS 不是作为 PHPUnit 的一部分包含的,因此你需要使用 yarn
安装它:
-
在
/core
文件夹中,运行yarn install
。 -
配置
.env.example
为.env
并根据需要编辑。这些设置将大致与你的本地环境设置相同。DRUPAL_TEST_BASE_URL
将是本地站点的 URL 值,例如。
如何做…
NightwatchJS 在以下目录中查找测试:
-
mymodule/tests/src/Nightwatch/Tests
-
mymodule/tests/src/Nightwatch/Commands
-
mymodule/tests/src/Nightwatch/Assertions
-
mymodule/tests/src/Nightwatch/Pages
在这里,mymodule
是你的自定义模块名称,例如 chapter13
,我们一直在本章的所有代码中使用它。
你可以使用 yarn
运行 NightwatchJS 测试:
yarn test:nightwatch (args)
或者,您可以运行单个测试——例如,您自定义模块中的测试:
yarn test:nightwatch mymodule/tests/src/Nightwatch/Tests/
exampleTest.js
让我们将CamelCase
PHP 类转换为 JavaScript 函数,并使用nightwatch
进行测试。
在chapter13/js
目录下创建一个名为js
的目录。在其内部,创建一个名为camelCase.js
的文件。该文件将包含以下代码:
module.exports = (inputText) => {
let text = inputText.toLowerCase();
text = text.replace(/[, -]/g, '_');
let extractedText = text.split('_').map(function(word,
index) {
if (index !== 0) {
return word.charAt(0).toUpperCase() +
word.slice(1).toLowerCase();
} else {
text = word;
}
}).join('');
text = text.toLowerCase() + extractedText;
return text.replace('_', '');
}
我们可以像在章节开头对 PHP 类进行单元测试一样对这个 JavaScript 函数进行单元测试。为此,在chapter13/tests/src/Nightwatch
目录下创建一个新的目录,并在该目录内创建一个名为Tests
的目录。
在Tests
目录下创建一个名为CamelCaseTest.js
的文件。它看起来像这样,得到与之前功能 JavaScript 测试相同的结果:
const assert = require('assert');
const camelCase = require('../../../js/camelCase');
const dataProvider = [
{input: 'button_color', expected: 'buttonColor'},
{input: 'snake_case_example', expected:
'snakeCaseExample'},
{input: 'ALL_CAPS_LOCK', expected: 'allCapsLock'},
{input: 'foo-bar', expected: 'fooBar'},
];
module.exports = {
'@tags': ['chapter13'],
'@unitTest' : true,
'Strings are converted to camelCase' : function (done) {
dataProvider.forEach(function (values) {
assert.strictEqual(camelCase(values.input),
values.expected);
});
setTimeout(function() {
done();
}, 10);
}
};
现在执行测试,使用以下命令:
yarn test:nightwatch chapter13/tests/src/Nightwatch/Tests/
CamelCaseTest.js
Nightwatch 将执行 JavaScript 函数,遍历dataProvider
值,并断言函数返回我们期望的结果:
yarn test:nightwatch ../modules/custom/chapter13/tests/src/
Nightwatch/CamelCaseTest.js
yarn run v1.22.19
[Nightwatch/CamelCaseTest]
✔ Strings are converted to camelCase
Done in 3.77s.
这对于测试您为自定义模块或主题编写的 JavaScript 函数非常有用。手动测试 JavaScript 函数或行为可能是一项耗时的工作。NightwatchJS 可以方便地自动化这项任务。
它是如何工作的...
NightwatchJS 也能够执行基于浏览器的测试断言,例如我们在本章前面讨论的功能 JavaScript 测试。然而,也有一些限制,因为 JavaScript 的作用域比 PHP 等语言要有限得多——根据您试图测试的内容,测试设置可能比在 PHP 中要长得多。您当然可以通过在测试中使用 JavaScript 来登录为管理员并导航 Drupal 来创建字段、节点和元素来编写脚本,但这需要大量的 JavaScript,在这种情况下,创建自己的安装配置文件可能更合适。
话虽如此,Drupal 核心中有各种使用 NightwatchJS 的测试,因此请务必参考它们以获取提示、示例和想法。
参见
您并不严格限于使用 PHPUnit 或 NightwatchJS 为 Drupal 编写测试。有许多第三方测试框架可用于在 Drupal 中编写和运行针对不同用例的测试。请务必查看以下内容:
-
Behat Drupal 扩展
-
Drupal 测试特性
第十四章:将外部数据迁移到 Drupal 中
无论您已经开发了一段时间还是刚开始您的职业生涯,您都会遇到一个非常常见的情况,那就是需要从外部来源引入数据。这可能包括从旧版本的 Drupal 迁移,从非 Drupal 网站迁移,不同的数据库引擎,静态 HTML 文件,或者从 CSV 或 HTTP API 中整合 JSON 或 XML 数据。
无论哪种场景,Drupal 都包含几个强大的工具,通过核心 Migrate 模块解决这些需求。在底层,它还包含一个强大的插件系统,允许您扩展和定义自己的数据源或处理插件,以及一个健康的贡献模块生态系统,以增强 Drupal 中的迁移体验。
在本章中,我们将探讨 Drupal 10 中的 Migrate 模块,您将学习如何实现以下内容:
-
从旧版本的 Drupal 迁移
-
从逗号分隔值(CSV)文件迁移数据
-
从远程 HTTP API 迁移数据
-
编写自定义迁移源插件
-
为迁移编写自定义处理插件
技术要求
您可以在 GitHub 上找到本章中使用的完整代码:github.com/PacktPublishing/Drupal-10-Development-Cookbook/tree/main/chp14
从旧版本的 Drupal 迁移
Drupal 随带了一些核心模块,可以帮助您将网站从 Drupal 6 或 7 更新到 Drupal 10。在版本 8 之前的 Drupal 旧版本之间的架构在设计上有着根本的不同,您不能像从 8 或 9 版本那样升级。
为了减轻从 Drupal 6 或 7 升级的挑战,Migrate Drupal 模块帮助为您的旧 Drupal 数据库准备一个新的迁移环境,并包含在 Drupal 10 的核心版本中。
自定义模块、自定义主题和自定义 Drush 命令
关于从版本 8 之前的 Drupal 版本升级的一个重要注意事项是,您将需要手动移植您可能创建的自定义模块、自定义主题和自定义 Drush 命令。没有工具可以自动化此过程,并且它们不会工作,直到您将它们移植以与 Drupal 10 兼容。这必须在升级之前完成,否则在尝试迁移或审查您的进度时可能会遇到多个错误。
准备工作
在您从旧版本的 Drupal 执行任何迁移之前,准备以下内容是很重要的:
-
运行 Drupal 10 的工作环境
-
用于升级的 Drupal 6/7 数据库副本
-
您要迁移的网站上使用的模块和主题清单
-
一个数据库服务器,其中既有 Drupal 10 数据库,也有旧 Drupal 6/7 数据库
-
连接到您服务器上的 Drupal 6/7 数据库所需的数据库凭据
-
从旧网站复制所有公共和私有文件到新网站可访问的位置
-
在您的 Drupal 10 网站中启用 Migrate Drupal 和 Migrate Drupal UI 模块
-
对当前 Drupal 10 安装的 数据库备份
我们将在下一节中逐步介绍这些步骤。
在迁移完成之前,你可能需要迭代这个过程,尤其是对于老旧、复杂的网站。因此,在继续之前,请确保您备份了 Drupal 10 网站的 数据库备份。这样,如果在迁移过程中出现问题,您可以恢复数据库,进行更改,并再次尝试。这比完全重新安装 Drupal 10 来重新开始迁移要快得多。始终保持一个干净的数据库备份可用。让我们开始吧!
如何操作……
在您的 Drupal 10 网站安装并准备就绪后,您需要采取的第一步是审查您在 Drupal 6/7 网站上使用的模块。您应该登录到您要迁移的网站,并转到管理后台的模块列表部分。如果您更喜欢使用 Drush 命令行工具,您也可以获取此列表。
记录下在旧 Drupal 网站中使用中的贡献模块和主题列表。您将不得不针对每个模块评估以下问题:
-
我是否还需要在 Drupal 10 上使用此模块?
-
贡献模块是否已迁移到核心?
-
贡献模块是否有 Drupal 10 版本可用?如果没有,我是否还需要它?是否有具有类似功能的替代模块?
-
此模块是否提供升级路径和迁移集成,以将数据从之前的 Drupal 版本迁移过来?
在从版本 8 之前的 Drupal 版本升级时,需要保持这样的清单
在回答列表中的每个问题的时候,您可以通过 Composer 选择所需的模块来更新您的 Drupal 10 网站。想法是首先通过使之前 Drupal 网站中使用的模块和主题可用,来准备好 Drupal 10 环境。
一些模块,如 views,已被移入 Drupal 核心,并且没有贡献版本可用。其他模块,如 Pathauto,为 Drupal 提供了更新的版本,并且还包括迁移插件。这些迁移插件在迁移您的旧网站时由 Migrate Drupal 模块自动使用。
不幸的是,并非所有模块都有与 Drupal 10 兼容的版本。这可能发生在它们被竞争性模块(例如,Field Collections 与 Paragraphs)取代,或者维护者决定不将模块迁移到 Drupal 7 以外。如果您发现您的 Drupal 6/7 网站中有一个您绝对需要在 Drupal 10 网站上使用的模块,在这种情况下您的选择有限。您可以执行以下操作:
-
检查模块问题队列,以查看是否已经对较新版本进行了工作
-
您自己和您的开发团队检查该模块,以评估迁移的难度
-
查看是否存在与 Drupal 10 类似的替代模块,并做出您需要的配置更改(可能还可以提供迁移路径)
-
使用像 Drupal Module Upgrader(
www.drupal.org/project/drupalmoduleupgrader
)这样的工具来帮助您了解如何移植模块 -
咨询官方 Drupal Slack 频道、Drupal Stack Exchange 网站,或开发机构以帮助您将功能移植到 Drupal 10,如果您或您的团队无法完成此操作
是否选择移植模块?
如果您最终将模块移植到 Drupal 10,请确保将工作贡献回社区在 Drupal.org。这不仅是一个很好的学习经验,了解如何开发模块和学习 Drupal 10 的新 API,而且将知识传递下去也有助于其他可能遇到困难的人。您可以申请成为模块的维护者,确保错误修复和功能增强被发布。
现在您已经通过了模块列表,请使用 Composer 将您需要的模块添加到您的 Drupal 10 网站中:
composer require drupal/(module_name):^VERSION
一旦您拥有了所有必需的模块和主题,请确保启用它们。不必担心配置所有新增的模块;迁移过程会为您处理这些。如果它们未被启用,迁移过程将无法看到或使用它们可能拥有的任何迁移插件,并且您可能无法迁移所有数据。
如果这些模块包含 Drupal 6/7 的迁移插件,它们将在运行迁移到您的 Drupal 10 网站时自动合并。
运行 Drupal 迁移
现在我们已经准备好了环境,并且拥有了所有需要的模块,让我们开始迁移过程。
导航到 /upgrade
。您将看到以下屏幕:
图 14.1 – 升级屏幕提供了一个从旧版本升级的向导
Migrate Drupal UI 模块提供了这个界面,帮助您在管理部分内执行迁移。再次审查准备步骤,然后点击继续。
在下一屏幕上,我们需要设置要迁移的 Drupal 版本,以及连接到旧数据库所需的数据库凭据。如果您不确定数据库主机或凭据应该输入什么,请检查平台文档了解如何连接到第二个数据库(Lando、DDEV、Docksal 等)。
图 14.2 – 迁移向导将提示输入先前版本 Drupal 的数据库凭据
哪里可以找到 Drupal 8 或 9 的选项?
如果您是从 Drupal 8 或 9 更新,您不需要利用 Migrate Drupal 或此界面。相反,您应该继续使用 Drupal 的常规更新过程,从 Drupal 8 或 9 升级到 Drupal 10。如果您正在从 Drupal 8 或 9 站点迁移部分数据并在 Drupal 10 中重新开始,您将需要编写自己的迁移脚本。请参阅本章后面的部分以获取示例。
设置公共/私有文件源
在上一屏幕的底部,您可以设置上传到旧站点的任何公共和私有文件的源。
图 14.3 – 您可以指定上传的文件在旧站点上的位置,以便迁移可以找到它们
您可以选择设置文件的本地路径或添加到公共网站的 URL。
迁移将从旧路径源文件,因此如果您的文件之前上传到 sites/default/files
,您需要在该位置提供它们。对于公共文件的值,您然后输入 /var/www/web
,这是您网站 web 根目录的位置。
文档根目录
注意,在某些情况下,webroot 可以命名为 docroot
而不是 web
。这取决于您的 Lando、DDEV、Docksal 等设置,以及/或如果您使用 Acquia、Pantheon 或 Platform.sh 等托管主机提供商,可能会有所不同。如果您不确定,请检查适用于您情况的文档。
您也可以对私有文件做同样处理。
如果您选择使用网址,这也会起作用。然而,运行从互联网拉取文件的迁移可能会导致迁移所需的时间比您预期的要长得多。此外,您可能会因为多个文件请求而使您的实时网站离线。虽然您可以选择这条路线,但对于较大的迁移来说,通常不建议这样做。
当您已添加所有适用的设置后,点击 审查升级。此屏幕将显示将要升级的项目列表,这是执行迁移前的最后一步。
运行迁移
在这个阶段,系统正在从旧版本的 Drupal 迁移到 Drupal 10。批处理过程将一直运行,直到完成(或遇到错误)。根据您旧站点的规模和构建,这可能需要一段时间。您可以离开电脑休息一下;只需确保浏览器窗口保持打开即可。
如果迁移出现错误,则过程将不会完成。虽然不幸,但这可能会发生,具体取决于您之前站点的复杂程度。在这种情况下,您应该检查站点日志以查看错误是什么。这些问题可以解决,然后可以再次执行迁移。您可能需要调整一些迁移设置,可能需要为模块(以支持迁移)提供补丁,或者可能是其他类型的错误。
记得我们为准备 Drupal 10 站点所做的数据库备份吗?在处理了你在站点日志中找到的问题之后,继续恢复数据库。你应该能够回到第一个升级界面并重新开始迁移,这比从头开始设置 Drupal 10 要快。
如果一切顺利,迁移将完成,你将成功将你的旧 Drupal 站点迁移到 Drupal 10!
迁移完成!
当它完成时,环顾一下你的管理界面。你应该会看到熟悉的项目,例如内容类型、分类法、媒体类型、用户角色、上传的文件、重定向以及其他来自你之前站点的项目,以及所有你的内容和用户账户都已恢复。
请注意,某些项目,如视图配置,无法自动迁移。这是由于视图本身的复杂性质。幸运的是,在管理界面中构建视图非常容易,一旦你验证迁移成功,你总是可以处理这个问题。
它是如何工作的……
Drupal 核心包含数十个插件和迁移路径,用于从旧版本的 Drupal 6 或 7 安装迁移。这些帮助解释了新版本的 Drupal 如何访问、转换并在迁移过程中将数据保存到新系统中。
例如,核心的过滤器模块在 Drupal 的几个版本中都存在。然而,它的模式、配置和数据结构在多年中发生了变化。它并不与 Drupal 10 一一对应。如果你查看core/modules/filter
模块目录,你会注意到一个名为migrations
的目录。这里有一些文件帮助 Drupal 理解如何将旧过滤器从 Drupal 6 和 7 映射到你的新 Drupal 10 安装中。你可以在d7_filter_format.yml
中看到这个示例:
id: d7_filter_format
label: Filter format configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: d7_filter_format
process:
format: format
status: status
name: name
cache: cache
weight: weight
filters:
plugin: sub_process
source: filters
key: '@id'
process:
id:
plugin: filter_id
bypass: true
source: name
map:
editor_caption: filter_caption
editor_align: filter_align
settings:
plugin: filter_settings
source: settings
status:
plugin: default_value
default_value: true
weight: weight
destination:
plugin: entity:filter_format
在深入细节之前,这个定义有助于迁移过程理解如何转换和移动即将进入 Drupal 10 的过滤器和过滤设置。这些文件在使用迁移 Drupal 模块时自动加载和使用,你可以在 Drupal 核心中看到它们的几个示例。
贡献模块也可以提供这些功能。这就是我们能够从旧版本的 Drupal 迁移而不丢失数据的原因。大多数流行的贡献模块都能顺利迁移且没有问题,但有些模块不包括文件。你可以检查问题队列以获取帮助,但也可以在自定义模块中编写自己的代码。
在后面的章节中,我们将探讨自定义迁移源和迁移过程插件。
从 CSV 文件迁移数据
有时,你需要将数据迁移到 Drupal 中,这些数据以各种格式存在。其中一种流行的格式是CSV,或称为逗号分隔值。CSV 文件可以从各种数据库客户端和电子表格软件中导出,是迁移的优秀数据源候选者。
准备工作
从这里,我们需要添加两个模块来从 CSV 文件进行迁移。使用 Composer,下载以下模块:
你还需要创建一个自定义模块,我们将在这里放置迁移定义、源插件和过程插件类。到这一点,你应该熟悉创建自定义模块。如果需要刷新,请参考前面的章节。
如何做到这一点...
当你必须执行无法使用 Migrate Drupal 的 Drupal 迁移时,你必须在一个自定义模块中编写自己的迁移 YAML 配置文件。这是因为 Migrate Drupal 模块专门用于从旧版本 Drupal 迁移,将那个旧版本 Drupal 视为源。
在这个例子中,CSV 文件将是迁移的源。每次迁移都由三个主要部分组成:
-
源:这是为迁移提供数据的提供者。
-
目标:这是每个记录将被迁移到并存储的地方——通常是一个实体(节点、用户、媒体、分类法等)。
-
过程:此管道定义了源数据如何转换和保存为迁移项。在这里,你可以定义
目标
上的各种字段和属性,并使用多个过程
插件来使源
数据适合你想要的字段(或 Drupal 可能要求的方式)。
在我们继续之前,让我们看看一个从 CSV 文件迁移的迁移定义的快速示例:
id: redirects
migration_tags: {}
migration_dependencies: {}
migration_group: default
label: Old website redirects.
source:
plugin: csv
path: data/redirects.csv
ids: [id]
constants:
uid: 1
status: 301
destination:
plugin: 'entity:redirect'
process:
redirect_source: old_path
redirect_redirect: new_path
uid: constants/uid
status_code: constants/status
假设我们需要使用 CSV 文件将旧网站的 URL 迁移到 Drupal,并使用重定向贡献模块(www.drupal.org/project/redirect
)将它们存储为重定向。通过这样做,我们可以确保旧网站上存在的 URL 可以成功重定向到 Drupal 中的新 URL,这样我们就不会失去访客。
为了做到这一点,我们在自定义模块的config/install
目录中定义了一个名为migrate_plus.migration.redirects.yml
的迁移定义文件。这是为需要 Migrate Plus 贡献模块来执行迁移(如 Migrate Source CSV)的迁移。
在这里,你可以清楚地看到源
、目标
和过程
部分。
源
部分告诉迁移我们将使用 CSV 插件(由 Migrate Source CSV 模块提供),CSV 文件的路径,ID 键(Migrate 将使用它来跟踪唯一行值),以及在过程管道中使用的某些常量值。
目标
部分告诉迁移我们想要使用entity:redirect
插件来保存数据。此插件确保迁移的值被保存为重定向实体。
process
部分将实体上的字段和属性映射到迁移源中的数据值。在这种情况下,redirect_source
、redirect_redirect
、uid
和status_code
被映射到 CSV 文件中的old_url
和new_url
,而uid
和status_code
使用常量值(在先前的源部分定义)。
模块下的/data/redirects.csv
CSV 文件包含迁移的所有数据。文件包含以下内容:
ID | old_path | new_path |
---|---|---|
1 | /``foo |
/``node/1 |
2 | /``foo/bar |
/``node/2 |
3 | /``foo/bar/baz |
/``node/3 |
表 14.1 – /data/redirects.csv
下的 CSV 文件
CSV 文件包含了几百条与前面类似的记录。
别名怎么办?
当将重定向的 URL 迁移到 Drupal 时,目的地(在这种情况下,new_path
)需要是 Drupal 实体路径(例如/node/1
)。Drupal 将正确保存重定向。如果您已安装 Pathauto 自动别名模式,它们将在迁移保存每行数据时生成。安装了重定向贡献模块后,将自动创建一个 301 重定向并将其与我们要重定向到的节点相关联。
当我们启用我们的自定义模块时,迁移将出现在结构 | 迁移下的管理部分迁移列表中。您定义的任何迁移都将出现在此部分。您可以从此界面运行迁移,或者您可以使用 Drush 从命令行运行,这是由于Migrate Tools贡献模块的恩赐。
图 14.4 – Migrations 屏幕列出了 Drupal 中所有注册的迁移以及您可以执行的任务
然后,我们可以执行迁移:
图 14.5 – 在管理界面中运行的迁移,显示进度条
我们可以看到,现在在 Drupal 中显示的重定向是从 CSV 文件迁移过来的:
图 14.6 – 迁移创建了从我们的 CSV 文件中预期的所有 URL 重定向
从 CSV 迁移到节点
假设我们还有一个包含我们想要迁移到 Drupal 节点类型中的数据的 CSV 文件。迁移定义可能看起来会怎样?实际上并没有太大的不同!
id: chapter14csvnodes
label: Old website articles.
source:
plugin: csv
path: /data/articles.csv
ids: [id]
constants:
uid: 1
destination:
plugin: 'entity:node'
default_bundle: article
process:
title: old_title
body/value: old_body
body/format:
plugin: default_value
default_value: full_html
uid: constants/uid
field_one: old_field_one
field_two: old_field_two
created:
plugin: format_date
from_format: 'Y-m-d\TH:i:sP'
to_format: 'U'
source: created_date
再次使用 CSV 源插件,我们进行了一些修改,现在我们定义了另一个可以使用的迁移。在destination
部分,我们将entity:redirect
替换为entity:node
,并提供了我们想要保存迁移数据的节点类型(bundle
)。
在过程
部分,我们添加了从文章节点类型中映射迁移数据所需的具体节点属性和字段。这里也有关于如何处理数据的配置附加到过程插件的示例。
你将在迁移文件中看到如下映射:
field_one: old_field_one
这是对 Migrate 模块提供的get
插件的简写。get
插件直接使用源提供的值存储在 Drupal 中。使用get
插件的长写方式如下:
field_one:
plugin: get
source: old_field_one
创建的属性展示了另一个过程插件format_date
。format_date
过程插件允许你在保存到 Drupal 之前对日期进行格式化。在上面的示例中,它将日期时间值转换为时间戳,这是 Drupal 在节点上存储创建和更改日期的方式。
它是如何工作的...
Migrate Source CSV模块提供了一个 CSV 源插件,它会为你解析和读取提供的 CSV 文件,解析出标题和记录。这使得创建迁移定义并快速将记录映射到 Drupal 中的目的地成为可能。
有数十个类似这些的迁移过程插件,你可以在迁移中使用它们来简化过程。有关你可以使用的插件的完整列表,请查阅在线文档:
-
迁移过程插件(
www.drupal.org/docs/8/api/migrate-api/migrate-process-plugins/list-of-core-migrate-process-plugins
) -
Migrate Plus 过程插件(
www.drupal.org/docs/8/api/migrate-api/migrate-process-plugins/list-of-core-migrate-process-plugins
)
有其他贡献的模块添加了更多的过程插件,但在这两者之间,核心的 Migrate 和贡献的 Migrate Plus 模块几乎涵盖了你需要做的所有迁移。当没有过程插件满足在迁移过程中转换数据的需求时,你可以自己创建。请参阅本章后面的创建过程插件部分。
从 HTTP API 迁移数据
CSV 文件和 SQL 数据库并不是迁移中唯一可以使用的数据源。Migrate Plus贡献模块附带了一个 URL 源插件。通过使用 URL 插件作为迁移源,迁移可以获取并解析以下格式的数据:
-
JSON
-
XML
-
SOAP
这意味着你可以从任何互联网 API 迁移数据,这使得 Migrate Plus 在需要通过线迁移数据时成为一个不可或缺的工具。
让我们看看如何使用它从 HTTP API 迁移数据。
如何做...
到目前为止,我们已经在本章中给出了两个迁移定义的示例。尽管我们是从不同类型的源迁移,但迁移定义本身的格式不会改变太多。我们仍然有我们的 source
、destination
和 process
部分。
假设我们想要从返回 JSON 响应的公共 API 中抓取数据并将其保存为 Drupal 中的节点。本例中的 JSON 响应看起来像这样:
{
"data":[
{
"id": 1,
"title": "Item One",
"body": "Lorem ipsum dolor sit amet",
"archived": false
},
{
"id": 2,
"title": "Item Two",
"body": "Lorem ipsum dolor sit amet",
"archived": true
},
{
"id": 3,
"title": "Item Three",
"body": "Lorem ipsum dolor sit amet",
"archived": false
}
]
}
我们需要消费该响应并为 data
下的每个项目插入节点。我们的迁移定义看起来如下:
id: chapter14httpjson
label: Migrates external API data.
migration_tags: {}
migration_dependencies: {}
source:
plugin: url
urls:
- 'http://example.com/api/v1/content'
data_fetcher_plugin: http
data_parser_plugin: json
item_selector: data
fields:
-
name: id
label: 'ID'
selector: id
-
name: title
label: 'Title'
selector: title
-
name: body
label: 'Body content'
selector: body
-
name: archived
label: 'Archived status'
selector: archived
ids:
id:
type: integer
destination:
plugin: entity:node
default_bundle: article
process:
title: title
body/value: body
body/format:
plugin: default_value
default_value: basic_html
status: archived
uid:
plugin: default_value
default_value: 1
目标和过程部分与本章开头其他迁移相同。到现在为止,你应该看到一种模式——迁移定义总是看起来很相似,无论源或目标如何。每个源插件都有不同的配置值。让我们分析一下 Url
插件正在做什么。
它是如何工作的...
Url
插件有一些可设置的配置属性。其中最重要的有 urls
、data_fetcher_plugin
和 data_parser_plugin
。在使用 Url
插件时,这些属性必须始终设置。
urls
属性接受一个或多个用于迁移的 URL。每个 URL 将逐个迁移其内容。如果你有一个多个位置且需要将数据迁移到同一位置的场景(假设响应格式相同),这很有用。
data_fetcher_plugin
和 data_parser_plugin
属性是 Migrate Plus 模块的独特属性。Migrate Plus 引入了 DataFetcher
和 DataParser
插件类型以及它们的插件管理器。它还包括 File
和 Http
数据获取插件,以及 Json
、Xml
和 Soap
数据解析插件。
当迁移执行时,配置被读取。这加载了 Url
插件,其中包含以下内容:
/**
* Returns the initialized data parser plugin.
*
* @return \Drupal\migrate_plus\DataParserPluginInterface
* The data parser plugin.
*/
public function getDataParserPlugin() {
if (!isset($this->dataParserPlugin)) {
$this->dataParserPlugin = \Drupal::service
('plugin.manager.migrate_plus.data_parser')->
createInstance($this->configuration[
'data_parser_plugin'], $this->configuration);
}
return $this->dataParserPlugin;
}
/**
* Creates and returns a filtered Iterator over the
documents.
*
* @return \Iterator
* An iterator over the documents providing source rows
that match the
* configured item_selector.
*/
protected function initializeIterator() {
return $this->getDataParserPlugin();
}
任何迁移源插件都必须提供 initializeIterator
方法。这告诉迁移如何解析数据,并将其传递给 Json
插件。然后 Json
插件从迁移定义中获取 data_fetcher_plugin
值,我们将其设置为 http
。Xml
和 Soap
数据解析器有类似的实现。当你结合这些时,这就是迁移知道如何通过互联网调用以获取数据以及如何解析这些数据以准备迁移的方式。
一旦数据被检索和解析,它需要知道如何 访问 响应中的项目。有时,你会得到 API 响应,其中你想要的结果是深度嵌套的。
我们源定义中的 item_selector
属性通知解析器如何读取和从响应中提取项目。由于示例 JSON 结构简单,我们只需为项目选择器提供 data
。如果我们想要的项目以某种方式嵌套,我们会输入结果的路径,例如 foo/bar/data
。
在 source
部分下的字段描述了我们要映射的字段,以及我们希望在过程部分如何引用这些字段。对于 CSV 迁移来说这不是必要的,因为标题记录会自动成为在过程管道映射中使用的参考字段。在这种情况下,我们需要映射它们:
Name: title
label: 'Title'
selector: title
name
属性是我们希望在过程部分中如何引用这个属性的。label
属性是它的名称(在 Drupal 迁移源区域中可见),而 selector
属性是我们 JSON 结果中的实际名称。你可以将 name
和 label
设置为你想要的任何内容,但 selector
必须与 API 返回的响应字段匹配。
再次,我们可以导航到 Drupal 管理中的 结构 | 迁移 来查看这个新的迁移并执行它。
还有更多…
关于使用 Url
插件的一个最后的注意事项。之前,我们提到 Migrate Plus 包含两个数据提取插件。我们介绍了 Http
;另一个是 File
。如果你想从磁盘上的 JSON 或 XML 文件中迁移数据,你可以这样做。如果你将 data_fetcher_plugin
从 http
更改为 file
,Url
插件将找到它并将其用作迁移源。
编写自定义迁移源插件
到目前为止,你已经看到了 Drupal 使用可用的源插件进行数据迁移的几种强大方式。当没有插件满足你的需求时会发生什么?当然,你可以编写一个迁移源插件!
考虑以下场景。你需要从 MySQL 数据库迁移数据到 Drupal 的节点中。虽然 Drupal 的迁移系统可以理解如何连接到数据库,但它并不理解如何查询你试图获取的数据。在这些情况下,你可以编写一个源插件。
如何操作...
假设数据库中有一个名为 articles
的表,我们在迁移中需要从中提取数据,它有 id
、title
、body
、is_published
和 published_on
等字段。在我们能够编写源插件之前,我们首先需要建立连接以访问这个数据库。
在你的 settings.php
文件中,添加以下 MySQL 数据库连接:
$databases['migrate']['default'] = array (
'database' => 'DATABASE_NAME',
'username' => 'DATABASE_USERNAME',
'password' => 'DATABASE_PASSWORD',
'prefix' => '',
'host' => 'DATABASE_HOSTNAME',
'port' => '',
'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
'driver' => 'mysql',
);
注意 $databases
数组中的 migrate
键名。这个条目将用于我们的源插件以建立连接。这个键可以是任何你想要的名字,除了 default
,这是 Drupal 用于其默认数据库连接的名称。如果你选择使用除 migrate
之外的其他键名,请确保记住它,因为它将在你的迁移定义中被引用。
在你的自定义模块中,回到 src/Plugin/migrate/source
目录,创建一个新的目录。然后,在这个目录中创建一个名为 ArticlesSource.php
的文件。这将是我们为迁移提供数据检索功能的源插件。
我们需要满足我们的源插件三个方法——一个 query
方法,一个 fields
方法和一个 id
方法。对于你创建的每个 SQL 源插件,这三个方法是必需的:
-
query
方法将包含我们实际用于检索数据的 SQL 查询 -
fields
方法将返回一个数组,包含源上的可用字段,以它们的机器名称和描述为键 -
id
方法定义了用于唯一标识源行的源字段
在我们的新文件 ArticlesSource.php
中,我们可以开始定义我们的源插件:
<?php
namespace Drupal\chapter14\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
/**
* Get the article records from the legacy database.
*
* @MigrateSource(
* id = "legacy_articles",
* source_module = "chapter14",
* )
*/
class ArticlesSource extends SqlBase {
我们的插件类扩展了 SqlBase
,这是一个核心迁移插件,用于处理数据库源连接。在我们提供上述三个方法之后,它将完成大部分繁重的工作。
我们的插件类也具有 @MigrateSource
注解。这是必需的。如果没有这个注解,Drupal 将不会发现这个类作为可用的源插件,迁移将无法执行任何操作。
注解的 id
属性定义了插件 ID。source_module
属性标识了提供源插件将从中读取数据的系统。对于贡献的源,这几乎总是它们定义的模块。
接下来,我们提供 query
方法。如果你以前在 Drupal 中使用过数据库 API,这将很熟悉。它使用相同的 API。它看起来像任何其他 Drupal SQL 查询;唯一的区别是它将在另一个数据库中执行——即我们在之前的 settings.php
文件中定义的那个。
填充这部分很简单:
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('articles', 'art');
$query->fields('art', [
'id',
'title',
'body',
'is_published',
'published_on',
]);
$query->orderBy('art.id');
$query->orderBy('art.published_on');
return $query;
}
它是如何工作的…
Drupal 的数据库 API 比较容易使用。这里的 query
方法选择所有文章及其字段,按它们的 ID 和发布日期排序。这个查询的结果将在这个部分的后面为我们的迁移提供动力。
对于 fields
方法,我们需要列出我们在迁移中使用的字段:
/**
* {@inheritdoc}
*/
public function fields() {
return [
'id' => $this->->t('The article id.'),
'title' => $this->->t('The article title.'),
'body' => $this->->t('The article body content.'),
'is_published' => $this->->t('The published state.'),
'published_on' => $this->->t('The published date.'),
];
}
最后,对于 id
方法,我们需要指定哪个字段是迁移的唯一标识符:
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'id' => [
'type' => 'integer',
'alias' => 'art',
],
];
}
在这种情况下,它只是 id
,来自旧数据库文章表的唯一字段。
在迁移中使用自定义源插件
在我们的源插件就绪后,我们现在可以专注于迁移本身。
就像我们的其他示例一样,我们需要定义一个迁移定义 YAML 文件:
id: articles
label: Migrates articles from the legacy database.
Migration_tags: {}
migration_dependencies: {}
source:
plugin: legacy_articles
key: migrate
destination:
plugin: entity:node
default_bundle: article
process:
title: title
body/value: body
body/format:
plugin: default_value
default_value: basic_html
status: is_published
created: published_on
uid:
plugin: default_value
default_value: 1
Drupal 中迁移的最好部分之一是 API 定义得很好,所以无论你如何获取或处理数据,定义总是遵循相同的模式。
在这个迁移中,我们指定了我们的新源插件 legacy_articles
,这是我们在 ArticlesSource
类中提供的插件 ID。源部分中的 key
属性与我们在 settings.php
中添加的数据库键同名。由于我们扩展了 SqlBase
,键属性用于在执行查询时建立数据库连接。如果你好奇,可以查看 SqlBase
的 getDatabase
方法,看看它是如何使用 key
属性的。
由于我们在源插件的fields
方法中定义了我们的字段,我们可以在迁移定义中完全跳过字段部分,并按提供的方式使用它们。在这种情况下,迁移已经知道字段是什么。对于ids
属性也是如此;源插件已经定义了它,所以我们不需要像本章中的其他迁移那样在这里列出它。
从这里开始,迁移的其余部分看起来与本章中其他示例中的相同。destination
部分通知迁移创建article
节点,而process
部分定义了如何将我们的源字段映射到节点字段。
再次提醒,你可以在管理界面中的结构 | 迁移下查看此迁移并运行它。你可以自由地创建一个与 Drupal 数据库并行的数据库来实验源插件——一旦你知道如何操作,你就能将任何内容拉入 Drupal,并成为一名真正的迁移大师。
为迁移编写自定义流程插件
到目前为止,我们已经涵盖了从 CSV、JSON 和数据库源迁移数据的情况,但如果是这些源中的数据并不完全符合在 Drupal 中存储的方式,该怎么办呢?
迁移可能是一件棘手的事情。虽然 Drupal 提供了多个途径来获取迁移所需的数据源,但仍然会有许多情况需要你操纵这些传入的数据,以便将其处理到满意的状态,无论是为了存储还是清理目的。幸运的是,创建流程插件非常简单,你很快就能在迁移中操纵数据。
如何做到这一点...
让我们看看编写流程插件的示例。使用之前的示例,一个从数据库表获取数据的自定义源插件,假设我们现在需要为迁移拉取一个额外的字段no_index
。虽然查询数据很容易,但数据本身不适合存储在元标签字段中(www.drupal.org/project/metatag
),因为其值要么是 0 要么是 1。当值为 1(true
)时,作者表示他们不希望搜索引擎抓取此页面。在之前的系统中,此值的出现会在页面头部添加额外的元标签。
贡献的 Metatag 模块能够复制这种功能。然而,元标签字段以序列化的方式在数据库中存储这些数据。我们不能直接使用源数据中的值,但我们可以添加一个流程插件来转换数据为我们所需的形式。
假设你在 Drupal 中为文章内容类型添加了一个 Metatag 字段,你可以通过以下步骤进行数据迁移。
首先,让我们更新我们的源插件以考虑一个新字段:
$query->fields('art', [
'id',
'title',
'body',
'is_published',
'published_on',
'no_index',
]);
然后,我们将它添加到我们的fields
列表中:
public function fields() {
return [
'id' => $this→t('The article id.'),
'title' => $this→t('The article title.'),
'body' => $this→t('The article body content.'),
'is_published' => $this→t('The published state.'),
'published_on' => $this→t('The published date.'),
'no_index' => $this→t('Indicates this article should
not be crawled.'),
];
}
现在它已经被添加到源插件中,迁移正在接收每条记录的值。
在我们的迁移定义中,让我们映射元标签字段,并设置好编写自定义流程插件:
field_metatags:
- plugin: set_no_index
_source: no_index
在这里,我们将 no_index
字段的值通过管道传递到一个 ID 为 set_no_index
的插件。现在,我们可以在迁移过程中开始操作数据,以确保它能够正确存储。
在自定义模块中,我们在 src/Plugin/migrate/process
目录下创建一个目录。在这个目录中,我们将创建一个名为 SetNoIndex.php
的文件。这是我们新的自定义处理插件。
自定义处理插件至少需要实现 transform
方法。该方法负责处理并返回管道中此步骤的数据。
过程插件代码看起来如下:
<?php
namespace Drupal\chapter14\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* @MigrateProcessPlugin(
* id = "set_no_index",
* )
*/
class SetNoIndex extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInter
face $migrate_executable, Row $row, $destination
_property) {
return (bool) $value ? ['robots' => 'noindex, nofollow,
noarchive, nosnippet'] : [];
}
}
与自定义源插件一样,注意类顶部的注释。这是必需的,以便插件能够被 Drupal 发现。id
值与我们在迁移定义中使用的是相同的。
它是如何工作的……
当迁移运行时,如果传入的值是 1,我们将返回一个值数组。这正是我们所需要的!然而,在成功保存到 Drupal 之前,我们还需要做一件事。
记得我们之前提到数据是以序列化数组的形式存储的吗?仅从我们的自定义处理插件传递一个普通的 PHP 数组是不够的。在迁移中,你可以在字段映射中使用多个处理插件。它们按照列表中的顺序运行,这提供了一种可组合的方式来转换源数据。
幸运的是,核心 Migrate 模块提供了一个将帮助我们处理的过程插件,即 callback
过程插件。callback
过程插件使用来自先前过程插件(我们 set_no_index
的结果)的值调用 PHP 函数,并返回回调提供的值。
在管道中将这两个结合起来看起来如下:
field_metatags:
- plugin: set_no_index
_source: no_index
- plugin: callback
_callable: serialize
更新迁移定义后,我们需要将这些更改引入 Drupal。这可以通过 Drush 完成:
php vendor/bin/drush config-import --partial --source=/var/www/
html/web/modules/custom/chapter14/config/install -y
此命令将重新导入 chapter14
模块 config/install
目录中的配置文件。这是一个很好的方法,可以在你逐步处理迁移定义的同时持续引入定义的更改。
迁移定义是配置
迁移 YAML 文件配置方式与任何其他 Drupal 配置文件相同。当你对迁移 YAML 文件进行更新并使用上述命令导入更改时,确保在准备部署或提交工作到仓库时执行 config-export
。
当我们运行迁移时,我们将在元标签字段中有新的数据。在这个例子中,你可以通过两种方式验证它是否工作:
- 首先,你可以在管理界面中查看你网站上迁移的内容,编辑一个迁移节点,并看到元标签字段在节点表单上有正确的数据:
图 14.7 – 节点表单上的元标签字段
- 其次,你可以在数据库中检查元标签字段,看看原始数据是否在那里:
图 14.8 – 数据库中的元标签表反映了我们要迁移的值
如果你还记得上一章的内容,我们讨论了使用单元测试来知道我们编写的代码实际上是否工作的重要性。处理插件编写测试相对简单。我们可能正在从迁移中查看 Drupal 中的数据,但让我们 100%确信我们的处理插件正在做正确的事情。
我们为set_no_index
插件编写的单元测试看起来像这样:
<?php
namespace Drupal\Tests\chapter14\Unit\Plugin\migrate\
process;
use Drupal\chapter14\Plugin\migrate\process\SetNoIndex;
use Drupal\Tests\migrate\Unit\process\
MigrateProcessTestCase;
/**
* Tests the set_no_index process plugin.
*/
class SetNoIndexTest extends MigrateProcessTestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->plugin = new SetNoIndex([], 'set_no_index', []);
parent::setUp();
}
/**
* Data provider for testPluginValue().
*
* @return array
* An array containing input values and expected output
values.
*/
public function valueProvider() {
return [
[1, ['robots' => 'noindex, nofollow, noarchive,
nosnippet']],
[0, []],
[NULL, []],
];
}
/**
* Test set_no_index plugin.
*
* @param $input
* The input values.
*
* @param $expected
* The expected output.
*
* @dataProvider valueProvider
*/
public function testPluginValue($input, $expected) {
$output = $this->plugin->transform($input, $this->
migrateExecutable, $this->row,
'destinationproperty');
$this->assertSame($output, $expected);
}
}
valueProvider
方法为我们的测试方法提供值,以及当评估时我们期望set_no_index
插件返回的结果。现在我们确信我们的迁移将始终提供正确的数据,并且没有迁移的文章会被搜索引擎错误地抓取。有关 Drupal 中的单元测试的更多信息,请务必参考第十三章,使用 Drupal运行和编写测试。
参见
在本章中,我们展示了 Drupal 的 Migrate 和 Migrate Plus 模块的强大功能和灵活性。掌握这两个模块将使你能够从几乎任何来源迁移数据并相应地处理它。我们还展示了如何编写自定义源和自定义处理插件来实现这一目标。
请务必检查Drupal.org上 Migrate 的模块贡献生态系统。这里有几个模块提供了无数的数据源和处理插件,覆盖了各种数据源,例如 CSV、JSON、XML、XLS、HTML 和 HTTP API。