精通-Yii-全-
精通 Yii(全)
原文:
zh.annas-archive.org/md5/e04b5155417b0f1bfc19f1cfd8e116d4译者:飞龙
前言
Yii 框架 2(Yii2)是流行的 Yii 框架的继任者。像它的继任者一样,Yii2 是一个开源、高性能的快速开发框架,旨在创建现代、可扩展和性能卓越的 Web 应用程序和 API。
本书旨在为没有接触过 Yii 和 Yii2 的开发者以及希望成为 Yii2 专家的 Yii 框架开发者提供指导,这本书将成为您成为 Yii 大师的指南。从初始化和配置到调试和部署,这本书将成为您掌握这个强大框架所有方面的指南。
本书涵盖内容
第一章,Composer、配置、类和路径别名,涵盖了 Yii2 应用程序的基础知识。在本章中,您将学习 Yii2 的核心约定以及如何将其配置为多环境应用程序。您还将发现如何使用 Composer,这是一个用于管理应用程序软件依赖项的依赖项管理工具。
第二章,控制台命令和应用,专注于如何使用内置的 Yii2 控制台命令,因为它引导您创建自己的命令。
第三章,迁移、DAO 和查询构建,教您如何在 Yii2 中创建迁移,以及如何使用数据库访问对象(DAO)与数据库交互,以及如何使用 Yii2 的查询构建器。
第四章,活动记录、模型和表单,教您如何创建和使用活动记录,以轻松地与数据库交互。此外,您还将发现如何创建模型来表示不在数据库中存储的信息,以及如何根据活动记录模型和普通模型创建 Web 表单。
第五章,模块、小部件和助手,涵盖了如何在我们的应用程序中集成模块。本章还将介绍如何创建和使用动态小部件,并额外介绍 Yii2 的强大助手类。
第六章,资产管理,专注于如何使用资产包创建和管理我们的资产,以及如何使用资产命令管理我们的资产。本章还涵盖了使用 Node Package Manage 和 Bower 等强大工具构建和生成我们的资产库的几种策略。
第七章, 验证用户身份和授权,教你如何在 Yii2 中使用几种常见的身份验证方案(如 OAuth 身份验证、基本 HTTP 身份验证和头部身份验证)来验证用户的真实性,并展示如何授予他们访问应用程序特定部分的权利。
第八章, 路由、响应和事件,专注于 Yii2 的路由和响应类如何在 Yii2 中工作。在本章中,我们将介绍如何处理应用程序内外部的数据,并发现如何利用 Yii2 强大的事件系统。
第九章, RESTful API,讨论了如何使用 Yii2 的 ActiveController 类快速轻松地通过 RESTful JSON 和 XML API 扩展你的应用程序。
第十章, 使用 Codeception 进行测试,帮助你学习如何使用名为 Codeception 的强大测试工具为你的应用程序创建单元、功能性和验收测试。在本章中,你还将学习如何创建用于测试目的的数据固定值。
第十一章, 国际化与本地化,介绍了如何本地化我们的应用程序并构建它们以支持多种语言。此外,你还将掌握如何使用 Yii2 控制台命令创建和管理翻译文件。
第十二章, 性能和安全,涵盖了多种提高你的 Yii2 应用程序性能的方法以及如何使其免受现代网络应用程序攻击的保障措施。
第十三章, 调试和部署,帮助你熟练掌握如何使用应用程序日志和 Yii2 调试工具来调试你的 Yii2 应用程序。此外,你还将发现无缝且不中断地部署你的 Yii2 应用程序的基本原则。
你需要这本书的内容
为了确保开发环境的统一并防止对主机操作系统的不必要的更改,强烈建议你在 Linux 虚拟机中运行所有命令。这将确保你的输出,无论是从你的网页浏览器还是从你的命令行,都与本书中展示的输出相匹配。由于自己设置这个环境可能是一项艰巨的任务,因此提供了使用 VirtualBox 和 Vagrant 的预构建虚拟机,以简化设置过程。
要开始使用这本书,你应该运行 Microsoft Windows 7、8、8.1 或 10、Apple OS X 10.9 或更高版本,或者能够运行虚拟机的 Linux 操作系统,例如 Ubuntu 14.04 LTS。此外,你还需要安装 VirtualBox 的最新版本(可在www.virtualbox.org/wiki/Downloads找到)和 Vagrant(可在www.vagrantup.com/downloads.html找到)。
注意
安装这些软件依赖项后,你可能需要重新启动你的计算机以使更改生效。
安装好 VirtualBox 和 Vagrant 后,你可以在新的命令行或终端窗口中打开,为章节创建一个新的目录,然后运行以下命令来创建你的虚拟机开发环境。这些命令将下载一个预构建的虚拟机,其中包含所有启动所需的软件,并启动你的新开发环境:
vagrant init charlesportwoodii/php56_trusty64
vagrant up --provider virtualbox
vagrant ssh
注意
关于这个特定 Vagrant 虚拟机的更多信息可以在atlas.hashicorp.com/charlesportwoodii/boxes/php56_trusty64找到。
注意,如果你使用的是 Windows 操作系统,你可能需要像 PuTTy 这样的工具通过 SSH 连接到你的虚拟机。有关如何在 Windows 上通过 SSH 连接到你的新虚拟机的更多信息,可以在docs-v1.vagrantup.com/v1/docs/getting-started/ssh.html找到。
一旦你的新 Vagrant 虚拟机启动,你就可以通过 SSH 访问这个虚拟机的文件,并通过打开一个新的浏览器窗口并导航到http://localhost:8080来访问你的webroot目录。默认情况下,当你打开这个网页时,你会看到phpinfo()的输出。
小贴士
根据你的操作系统安全设置,你的计算机可能会提示或阻止你访问计算机上的 8080 端口。如果你遇到问题,请确保配置你的防火墙设置,并确保计算机上的 8080 端口是开放的,并且 VirtualBox 可以从主机操作系统转发连接到虚拟操作系统。
由于 Yii2 完全兼容 PHP7,强烈建议你在 PHP7 上开发和测试你的 Web 应用程序。以下命令将允许你配置 PHP7 的 Vagrant 虚拟机:
vagrant init charlesportwoodii/php7_trusty64
vagrant up --provider virtualbox
vagrant ssh
小贴士
由于这些虚拟机自动配置端口转发,建议您一次只运行一个虚拟机。有关完整命令和配置选项的列表,请参阅 Vagrant 文档:docs.vagrantup.com/v2。
本书面向对象
掌握 Yii 适合中级到高级的软件开发人员,他们希望快速掌握 Yii2。本书假设您对 PHP 5、HTML5 以及基本的软件开发实践和方法有所了解。
术语
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:"此脚本告诉 Composer,当运行create-project命令时,它应该运行postCreateProject静态函数。"
代码块设置如下:
"scripts": {
"post-create-project-cmd": [
"yii\\composer\\Installer::postCreateProject"
]
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
// Define our application_env variable as provided by nginx/apache
if (!defined('APPLICATION_ENV'))
{
if (getenv('APPLICATION_ENV') != false)
define('APPLICATION_ENV', getenv('APPLICATION_ENV'));
else
define('APPLICATION_ENV', 'prod');
}
$env = require(__DIR__ . '/config/env.php');
任何命令行输入或输出都如下所示:
$ ./yii fixture/load <FixtureName>
$ ./yii fixture/unload <FixtureName>
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"一旦我们指定了所有必要的属性,我们就可以点击预览按钮来预览我们的表单,然后我们可以点击生成按钮来生成源代码。"
注意
警告或重要注意事项以如下框中显示。
小贴士
小技巧和窍门如下所示。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要发送一般反馈,请简单地发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书的标题。
如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请参阅我们的作者指南:www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 图书的骄傲拥有者,我们有一些东西可以帮助您从您的购买中获得最大收益。
下载示例代码
本书最新和最完整的源代码副本维护在 Packt 网站上:www.packtpub.com,以及 GitHub 上的github.com/masteringyii,适用于每个适用的章节。
错误更正
尽管我们已经尽一切努力确保内容的准确性,但错误仍然会发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
侵权
在互联网上侵犯版权材料是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供疑似侵权材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您对本书的任何方面有问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。
第一章。Composer、配置、类和路径别名
在深入了解 Yii Framework 2 之前,我们需要看看它是如何安装的,如何配置的,以及框架的核心构建块是什么。在本章中,我们将介绍如何通过名为 Composer 的包管理工具安装框架本身和预构建的应用程序。我们还将涵盖 Yii Framework 2 和我们的 Web 服务器的一些常见配置,包括使我们的应用程序了解它们正在运行的环境,并相应地对此环境做出反应。
注意
引用 Yii Framework 2 最常见的方式是 Yii Framework 2、YF2 和 Yii2。本书中我们将交替使用这些术语。
Composer
安装 Yii2 有几种不同的方式,从从源代码控制(通常是从 GitHub 的 github.com/yiisoft/yii2)下载框架到使用包管理器(如 Composer)。在现代 Web 应用程序中,Composer 是安装 Yii2 的首选方法,因为它使我们能够以自动化的方式安装、更新和管理我们应用程序的所有依赖项和扩展。此外,使用 Composer,我们可以确保 Yii Framework 2 保持最新状态,包括最新的安全和错误修复。Composer 可以通过遵循 getcomposer.org 上的说明进行安装。通常,这个过程如下所示:
curl -sS https://getcomposer.org/installer | php
或者,如果你系统上没有 cURL,可以通过 PHP 本身进行安装:
php -r "readfile('https://getcomposer.org/installer');" | php
安装完成后,我们应该将 Composer 移动到一个更集中的目录,这样我们就可以从系统上的任何目录调用它。与基于每个项目的安装相比,从集中式目录安装 Composer 有几个优点:
-
它可以从任何项目中进行调用。当处理多个项目时,我们可以确保每次和每个项目都使用相同的依赖管理器。
-
在集中式目录中,Composer 只需要更新一次,而不是在我们正在工作的每个项目中都更新。
-
依赖管理器很少被认为是应该推送到你的 DCVS 仓库的代码。将
composer.phar文件保留在仓库之外可以减少你需要提交和推送的代码量,并确保你的源代码与包管理器代码保持隔离。 -
通过从集中式目录安装 Composer,我们可以确保 Composer 总是可用的,这样每次克隆依赖于 Composer 的项目时,我们都可以节省一个步骤。
将 Composer 移动到 /usr/local/bin 是一个不错的选择,如下面的示例所示:
mv composer.phar /usr/local/bin/composer
chmod a+x /usr/local/bin/composer
小贴士
在本书中,当我们引用命令行参数时,将使用 Unix 风格的命令。因此,一些命令可能在 Windows 上无法工作。如果你决定设置 Windows 环境,你可能需要使用Composer-Setup.exe(可在getcomposer.org/Composer-Setup.exe找到)来为你的系统配置 Composer。如果你在系统上运行 Composer 时遇到任何问题,请确保查看在getcomposer.org/doc/提供的 Composer 文档。
或者,如果你已经在你的系统上安装了 Composer,确保通过运行以下命令将其更新到最新版本:
composer self-update
提示
本书中所使用的命令基于假设你有足够的权限来运行它们。在类 Unix 系统中,你可能需要在一些命令前加上sudo以便以高权限集执行命令。如果你在 Windows 上运行这些命令,你应该确保你在具有提升权限的命令提示符中运行列出的命令。确保在使用sudo和使用提升的命令提示符时遵循最佳实践,以确保你的系统保持安全。
一旦安装了 Composer,我们还需要安装一个名为The Composer Asset Plugin的全局插件(可在github.com/francoispluchino/composer-asset-plugin找到)。此插件使 Composer 能够为我们管理资产文件,而无需安装额外的软件(这些程序是 Bower,由 Twitter 创建的资产依赖管理器,以及 Node Package Manager,或 NPM,它是一个 JavaScript 依赖管理器)。
composer global require "fxp/composer-asset-plugin:1.0.0"
提示
由于 GitHub API 的速率限制,在安装过程中,Composer 可能会要求你输入你的 GitHub 凭据。输入凭据后,Composer 将从 GitHub 请求一个专用的 API 密钥,该密钥可用于未来的安装。确保查看getcomposer.org/doc/上的 Composer 文档以获取更多信息。
安装 Composer 后,我们现在可以实例化我们的应用程序。如果我们想安装现有的 Yii2 包,我们可以简单地运行以下命令:
composer create-project --prefer-dist <package/name> <foldername>
以 Yii2 基本应用程序为例,此命令将看起来像这样:
composer create-project --prefer-dist yiisoft/yii2-app-basic basic
运行命令后,你应该看到类似以下内容的输出:
Installing yiisoft/yii2-app-basic (2.0.6)
- Installing yiisoft/yii2-app-basic (2.0.6)
Downloading: 100%
Created project in basic
Loading composer repositories with package information
Installing dependencies (including require-dev)
- Installing yiisoft/yii2-composer (2.0.3)
- Installing ezyang/htmlpurifier (v4.6.0)
- Installing bower-asset/jquery (2.1.4)
- Installing bower-asset/yii2-pjax (v2.0.4)
- Installing bower-asset/punycode (v1.3.2)
- Installing bower-asset/jquery.inputmask (3.1.63)
- Installing cebe/markdown (1.1.0)
- Installing yiisoft/yii2 (2.0.6)
- Installing swiftmailer/swiftmailer (v5.4.1)
- Installing yiisoft/yii2-swiftmailer (2.0.4)
- Installing yiisoft/yii2-codeception (2.0.4)
- Installing bower-asset/bootstrap (v3.3.5)
- Installing yiisoft/yii2-bootstrap (2.0.5)
- Installing yiisoft/yii2-debug (2.0.5)
- Installing bower-asset/typeahead.js (v0.10.5)
- Installing phpspec/php-diff (v1.0.2)
- Installing yiisoft/yii2-gii (2.0.4)
- Installing fzaninotto/faker (v1.5.0)
- Installing yiisoft/yii2-faker (2.0.3)
Writing lock file
Generating autoload files
> yii\composer\Installer::postCreateProject
chmod('runtime', 0777)...done.
chmod('web/assets', 0777)...done.
chmod('yii', 0755)...done.
注意
你的输出可能会因系统上缓存的数据和子包的版本而略有不同。
此命令将安装 Yii2 基础应用程序到名为 basic 的文件夹中。在创建新的 Yii2 项目时,你通常会使用 create-project 命令来克隆 "yii2-app-basic",然后从那里开始开发你的应用程序,因为基础应用程序已经预先填充了几乎所有你需要开始新项目的东西。然而,你也可以从头开始创建一个 Yii2 项目,虽然这更复杂,但它让你对你的应用程序结构有更多的控制。
让我们看看当我们运行 create-project 命令时创建的 composer.json 文件:
{
"name": "yiisoft/yii2-app-basic",
"description": "Yii 2 Basic Application Template",
"keywords": ["yii2", "framework", "basic", "application template"],
"homepage": "http://www.yiiframework.com/",
"type": "project",
"license": "BSD-3-Clause",
"support": {
"issues": "https://github.com/yiisoft/yii2/issues?state=open",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
"source": "https://github.com/yiisoft/yii2"
},
"minimum-stability": "stable",
"require": {
"php": ">=5.4.0",
"yiisoft/yii2": "*",
"yiisoft/yii2-bootstrap": "*",
"yiisoft/yii2-swiftmailer": "*"
},
"require-dev": {
"yiisoft/yii2-codeception": "*",
"yiisoft/yii2-debug": "*",
"yiisoft/yii2-gii": "*",
"yiisoft/yii2-faker": "*"
},
"config": {
"process-timeout": 1800
},
"scripts": {
"post-create-project-cmd": [
"yii\\composer\\Installer::postCreateProject"
]
},
"extra": {
"yii\\composer\\Installer::postCreateProject": {
"setPermission": [
{
"runtime": "0777",
"web/assets": "0777",
"yii": "0755"
}
],
"generateCookieValidationKey": [
"config/web.php"
]
},
"asset-installer-paths": {
"npm-asset-library": "vendor/npm",
"bower-asset-library": "vendor/bower"
}
}
}
虽然这些项目中的大多数(如名称、描述、许可证和 require 块)相当直观,但这里也有一些特定于 Yii2 的项目需要注意。我们首先想要查看的部分是 "scripts" 部分:
"scripts": {
"post-create-project-cmd": [
"yii\\composer\\Installer::postCreateProject"
]
}
此脚本告诉 Composer 当运行 create-project 命令时,应该运行 postCreateProject 静态函数。查看框架源代码,我们看到此文件在 yii2-composer 包中被引用(参考 github.com/yiisoft/yii2-composer/blob/master/Installer.php#L232)。然后此命令运行几个项目创建后的操作,包括设置本地磁盘权限、生成唯一的 cookie 验证密钥以及为 composer-asset-plugin 设置一些资产安装器路径。
接下来,我们有 "extra" 块:
"extra": {
"yii\\composer\\Installer::postCreateProject": {
"setPermission": [
{
"runtime": "0777",
"web/assets": "0777",
"yii": "0755"
}
],
"generateCookieValidationKey": [
"config/web.php"
]
},
"asset-installer-paths": {
"npm-asset-library": "vendor/npm",
"bower-asset-library": "vendor/bower"
}
}
此部分告诉 Composer 在运行 postCreateProject 命令时使用这些选项。这些预配置选项为我们创建应用程序提供了一个良好的起点。
配置
现在我们已经安装了基本应用程序,让我们看看 Yii2 自动为我们生成的几个基本配置和引导文件。
需求检查器
从 yii2-app-basic 创建的项目现在自带一个名为 requirements.php 的内置需求脚本。此脚本检查几个不同的值,以确保 Yii2 可以在我们的应用服务器上运行。在运行我们的应用程序之前,让我们先运行需求检查器:
php requirements.php
你将得到类似以下内容的输出:
Yii Application Requirement Checker
This script checks if your server configuration meets the requirements for running Yii application.
It checks if the server is running the right version of PHP, if appropriate PHP extensions have been loaded, and if php.ini file settings are correct.
Check conclusion:
-----------------
PHP version: OK
[... more checks here ...]
-----------------------------------------
Errors: 0 Warnings: 6 Total checks: 21
通常情况下,只要错误计数设置为 0,我们就可以继续前进。如果需求检查器发现错误,它将在 Check conclusion 部分报告错误,以便你进行纠正。
小贴士
作为你的部署过程的一部分,建议你的部署工具运行需求检查器。这有助于确保你的应用服务器满足 Yii2 的所有要求,并且你的应用程序不会被部署到不支持它的服务器或环境中。
入口脚本
与其前身一样,Yii Framework 2 附带两个独立的入口脚本:一个用于网页应用程序,另一个用于控制台应用程序。
网页入口脚本
在 Yii2 中,Web 应用的入口脚本已从根(/)文件夹移动到web/文件夹。在 Yii1 中,我们的 PHP 文件存储在protected/目录中。通过将我们的入口脚本移动到web/目录,Yii2 通过减少我们需要运行应用程序的 Web 服务器配置来增加了我们应用程序的安全性。此外,所有公共资产(JavaScript 和 CSS)文件现在完全与我们的源代码目录隔离。如果我们打开web/index.php,我们的入口脚本现在看起来如下所示:
<?php
// comment out the following two lines when deployed to production
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
require(__DIR__ . '/../vendor/autoload.php');
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
$config = require(__DIR__ . '/../config/web.php');
(new yii\web\Application($config))->run();
提示
下载示例代码
本书最新和最完整的源代码副本维护在 Packt Publishing 网站上,www.packtpub.com,以及 GitHub 上的github.com/masteringyii,适用于每一章的相关内容。
虽然适用于基本应用,但默认的入口脚本需要我们在迁移到不同环境时手动注释和更改代码。由于在非开发环境中更改代码不符合最佳实践,我们应该更改此代码块,这样我们就不必触摸我们的代码来将其移动到不同的环境。
我们将首先创建一个新的应用程序范围常量APPLICATION_ENV。该变量将由我们的 Web 服务器或控制台环境定义,并允许我们根据我们正在工作的环境动态加载不同的配置文件:
-
在
web/index.php中的<?php标签之后,添加以下代码块:// Define our application_env variable as provided by nginx/apache/console if (!defined('APPLICATION_ENV')) { if (getenv('APPLICATION_ENV') != false) define('APPLICATION_ENV', getenv('APPLICATION_ENV')); else define('APPLICATION_ENV', 'prod'); }我们的应用程序现在知道如何从环境变量中读取
APPLCATTION_ENV变量,该变量将通过我们的命令行或我们的 Web 服务器配置传递。默认情况下,如果没有设置环境,APPLICATION_ENV变量将被设置为 prod。接下来,我们希望加载一个包含多个环境常量的单独环境文件,我们将使用这些常量来动态更改我们的应用程序在不同环境中的运行方式:
$env = require(__DIR__ . '/../config/env.php');接下来,我们将配置 Yii 以根据我们的应用程序设置
YII_DEBUG和YII_ENV变量:defined('YII_DEBUG') or define('YII_DEBUG', $env['debug']); defined('YII_ENV') or define('YII_ENV', APPLICATION_ENV); -
然后,按照
web/下的我们的index.php文件的其余部分进行操作:require(__DIR__ . '/../vendor/autoload.php'); require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php'); (new yii\web\Application($config))->run();
通过这些更改,我们的 Web 应用程序现在已配置为能够了解其环境并加载适当的配置文件。
注意
不要担心;在本章的后面部分,我们将介绍如何为我们的 Web 服务器(Apache 或 NGINX)和命令行定义APPLICATION_ENV变量。
配置文件
在 Yii2 中,配置文件仍然分为控制台和 Web 特定的配置。由于这两个文件之间有许多共同点(例如我们的数据库和环境配置),我们将存储常见元素在其自己的文件中,并在我们的 Web 和控制台配置中包含这些文件。这将帮助我们遵循 DRY 标准,并减少我们应用程序中的重复代码。
注意
软件开发中的DRY(不要重复自己)原则指出,我们应该避免在应用程序的多个地方出现相同的代码块。通过保持应用程序 DRY,我们可以确保应用程序的性能,并减少应用程序中的错误。通过将数据库和参数的配置移动到它们自己的文件中,我们可以在 Web 和控制台配置文件中重用相同的代码。
网络和控制台配置文件
Yii2 支持两种不同类型的配置文件:一个用于 Web 应用程序,另一个用于控制台应用程序。在 Yii2 中,我们的 Web 配置文件存储在config/web.php中,我们的控制台配置文件存储在config/console.php中。如果你熟悉 Yii1,你会看到这两个文件的基本结构并没有发生太大的变化。
数据库配置
我们接下来要查看的下一个文件是我们的数据库配置文件,存储在config/db.php中。此文件包含我们的 Web 和控制台应用程序连接到数据库所需的所有信息。
在我们的基本应用程序中,此文件看起来如下:
<?php
return [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=yii2basic',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
];
然而,对于一个了解其环境的程序,我们应该用以下配置替换此文件,以使用我们之前定义的APPLICATION_ENV变量:
<?php return require __DIR__ . '/env/' . APPLICATION_ENV . '/db.php';
小贴士
目前,我们只是在设置一些基本配置。我们将在下一节中介绍如何设置我们的目录。
通过这个更改,我们的应用程序现在知道它需要查看位于config/env/<APPLICATION_ENV>/下的一个名为db.php的文件,以获取该文件的正确配置环境。
参数配置
类似于我们的数据库配置文件,Yii 还允许我们使用一个参数文件,我们可以将应用程序的所有非组件参数存储在这个文件中。此文件位于config/params.php。由于基本应用程序没有使此文件了解其环境,我们将对其进行更改,如下所示:
<?php return require __DIR__ . '/env/' . APPLICATION_ENV . '/params.php';
环境配置
最后,我们有之前在处理入口脚本时定义的环境配置。我们将此文件存储在config/env.php中,并且它应该编写如下:
<?php return require __DIR__ . '/env/' . APPLICATION_ENV . '/env.php';
大多数现代应用程序都有几个不同的环境,这取决于它们的需求。通常,我们会将它们分为四个不同的环境:
-
我们通常遇到的第一种环境被称为DEV。这个环境是我们所有本地开发发生的地方。通常,开发者可以完全控制这个环境,并根据需要对其进行更改,以构建他们的应用程序。
-
我们通常遇到的第二种环境是一个名为TEST的测试环境。通常,我们会将应用程序部署到这个环境中,以确保我们的代码在类似生产的环境中能够正常工作;然而,当我们使用这个环境时,我们通常仍然可以访问高日志级别和调试信息。
-
我们通常拥有的第三个环境被称为 UAT,即用户验收测试环境。这是一个独立的环境,我们将将其提供给我们的客户或业务利益相关者,以便他们可以测试应用程序,以验证它是否按他们期望的方式工作。
-
最后,在我们的典型设置中,我们会有一个 PROD 或生产环境。这是我们的代码最终部署的地方,也是所有用户最终与我们的应用程序交互的地方。
如前几节所述,我们已经将所有环境配置文件指向了 config/env/<env> 文件夹。由于我们的本地环境将被命名为 DEV,我们将首先创建它:
-
我们将首先从命令行创建我们的
DEV环境文件夹:mkdir –p config/env/dev -
接下来,我们将在
config/env/dev/下的db.php中创建我们的dev数据库配置文件。目前,我们将坚持使用基本的 SQLite 数据库:<?php return [ 'dsn' => 'sqlite:/' . __DIR__ . '/../../../runtime/db.sqlite', 'class' => 'yii\db\Connection', 'charset' => 'utf8' ]; -
接下来,我们将在
config/env/dev下的env.php中创建我们的环境配置文件。如果您还记得本章前面的内容,这就是我们存储debug标志的地方,因此这个文件将如下所示:<?php return [ 'debug' => true ]; -
最后,我们将在
config/env/dev/下创建我们的params.php文件。到目前为止,这个文件将简单地返回一个空数组:<?php return [];
现在,为了简单起见,让我们将此配置复制到我们的其他环境中。从命令行,我们可以这样做:
cp –R config/env/dev config/env/test
cp –R config/env/dev config/env/uat
cp –R config/env/dev config/env/prod
设置我们的应用程序环境
现在我们已经告诉 Yii 它需要使用哪些文件和配置来为每个环境,我们需要告诉它使用哪个环境。为此,我们将在我们的 Web 服务器配置中设置自定义变量,这将把这个选项传递给 Yii。
设置 NGINX 的网络环境
在我们的控制台应用程序正确配置后,我们现在需要配置我们的 Web 服务器以将 APPLICATION_ENV 变量传递给我们的应用程序。在典型的 NGINX 配置中,我们有一个如下所示的位置块:
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
fastcgi_pass 127.0.0.1:9000;
#fastcgi_pass unix:/var/run/php5-fpm.sock;
try_files $uri =404;
}
要将 APPLICATION_ENV 变量传递给我们的应用程序,我们只需定义一个新的 fastcgi_param,如下所示:
fastcgi_param APPLICATION_ENV "dev";
在进行此更改后,只需重新启动 NGINX。
设置 Apache 的网络环境
我们也可以轻松地配置 Apache 将 APPLICATION_ENV 变量传递给我们的应用程序。使用 Apache,我们通常有一个如下所示的 VirtualHost 块:
# Set document root to be "basic/web"
DocumentRoot "path/to/basic/web"
<Directory "path/to/basic/web">
# use mod_rewrite for pretty URL support
RewriteEngine on
# If a directory or a file exists, use the request directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Otherwise forward the request to index.php
RewriteRule . index.php
# ...other settings...
</Directory>
要将 APPLICATION_ENV 变量传递给我们的应用程序,我们只需使用如下所示的 SetEnv 命令,这个命令可以放置在我们的 VirtualHost 块的任何位置:
SetEnv APPLICATION_ENV dev
在进行此更改后,只需重新启动 Apache 并导航到您的应用程序。
在最基本的层面上,我们的应用程序与我们第一次运行 composer create-project 命令时并没有做任何不同的事情。尽管没有做任何不同的事情,但我们的应用程序现在比之前的变化要强大得多,也更加灵活。在本书的后面部分,我们将探讨这些特定的变化如何使我们的应用程序的自动化部署变得无缝且简单。
组件和对象
Yii2 中几乎所有内容都扩展自两个基类:Component 类和 Object 类。
组件
在 Yii2 中,Component 类取代了 Yii1 中的 CComponent 类。在 Yii1 中,组件作为服务定位器,托管一组特定的应用程序组件,为请求处理提供不同的服务。在 Yii2 中,每个组件都可以使用以下语法进行访问:
Yii::$app->componentID
例如,可以使用以下方式访问数据库组件:
Yii::$app->db
可以使用以下方式访问缓存组件:
Yii::$app->cache
Yii2 通过我们之前提到的应用程序配置自动在运行时按名称注册每个组件。
为了提高 Yii2 应用程序的性能,组件是延迟加载的,或者只有在第一次访问时才实例化。这意味着如果缓存组件在你的应用程序代码中从未使用过,缓存组件将永远不会被加载。然而,有时这并不理想,因此为了强制加载一个组件,你可以通过将其添加到配置文件中的 bootstrap 配置选项来引导它。例如,如果我们想引导日志组件,我们可以这样做:
<?php return [
'bootstrap' => [
'log'
],
[…]
]
bootstrap 选项的行为类似于 Yii1 中的预加载选项——任何你想要或需要在引导时实例化的组件,如果它在配置文件的 bootstrap 部分中,将会被加载。
注意
关于服务定位器和组件的更多信息,请确保阅读位于 www.yiiframework.com/doc-2.0/guide-concept-service-locator.html 和 www.yiiframework.com/doc-2.0/guide-structure-application-components.html 的 Definitive Guide to Yii 指南。
对象
在 Yii2 中,几乎所有不扩展自 Component 类的类都扩展自 Object 类。Object 类是实现了属性特征的基类。在 Yii2 中,属性特征允许你访问有关对象的大量信息,例如 __get 和 __set 魔法方法,以及其他实用函数,如 hasProperty()、canGetProperty() 和 canSetProperty()。结合这些功能,使得 Yii2 中的对象非常强大。
小贴士
object 类非常强大,许多 Yii 类都从它扩展。尽管如此,自己使用魔法方法 __get 和 __set 并不是最佳实践,因为它比原生 PHP 方法慢,并且与你的 IDE 自动完成工具和文档工具的集成不佳。
路径别名
在 Yii2 中,路径别名用于表示文件路径或 URL 路径,这样我们就不需要在我们的应用程序中直接硬编码路径或 URL。在 Yii2 中,别名始终以 @ 符号开头,这样 Yii 就知道如何将其与文件路径或 URL 区分开来。
别名可以通过多种方式定义。定义新别名的最基本方法是调用 \Yii::setAlias():
\Yii::setAlias('@path', '/path/to/example');
\Yii::setAlias('@example, 'https://www.example.com');
也可以通过在应用程序配置文件中设置别名选项来定义别名,如下所示:
return [
// ...
'aliases' => [
'@path => '/path/to/example,
'@example' => 'https://www.example.com',
],
];
此外,可以使用 \Yii::getAlias() 轻松检索别名:
\Yii::getAlias('@path') // returns /path/to/example
\Yii::getAlias('@example') // returns https://www.example.com
Yii 中的几个地方是别名感知的,并且会接受别名作为输入。例如,yii\caching\FileCache 接受文件别名作为 $cachePath 参数的别名:
$cache = new FileCache([
'cachePath' => '@runtime/cache',
]);
注意
关于路径别名的更多信息,请查看 Yii 文档,网址为 www.yiiframework.com/doc-2.0/guide-concept-aliases.html。
摘要
在本章中,我们介绍了如何通过 composer 创建新的 Yii2 应用程序。我们还介绍了 Yii2 伴随的基本配置文件,以及如何配置我们的 Web 应用程序以加载特定环境的配置文件。最后,我们还涵盖了组件、对象和路径别名,这些都是掌握 Yii 的基础。
在下一章中,我们将涵盖你需要知道的一切,以便成为控制台命令和应用程序的大师。
第二章. 控制台命令和应用
在构建现代 Web 应用程序时,我们通常需要编写后台和运维任务来支持我们的主应用程序。这些任务可能包括生成报告、通过队列系统发送电子邮件,甚至运行可能导致基于 Web 的端点超时的数据分析。使用 Yii2,我们可以通过编写控制台命令甚至完整的控制台应用程序将这些工具和脚本直接构建到我们的应用程序中。
配置和使用
Yii2 控制台应用程序的基本结构与在 Web 应用程序中使用的结构非常相似。在 Yii2 中,从 yii\console\Controller 继承的控制台命令几乎与 yii\web\Controller 相同。
入口脚本
在继续配置文件本身之前,让我们先看看控制台入口脚本,它是名为 yii 的文件的一部分。这个入口脚本作为所有控制台命令的启动器,通常可以通过调用以下命令来运行:
$ ./yii
此命令将输出系统当前所有可用的命令。尽管如此,与 web/index.php 入口脚本一样,它还没有意识到其环境。我们可以通过将 yii 替换为以下代码块来改变这一点:
#!/usr/bin/env php
<?php
/**
* Yii console bootstrap file.
*/
// Define our application_env variable as provided by nginx/apache
if (!defined('APPLICATION_ENV'))
{
if (getenv('APPLICATION_ENV') != false)
define('APPLICATION_ENV', getenv('APPLICATION_ENV'));
else
define('APPLICATION_ENV', 'prod');
}
$env = require(__DIR__ . '/config/env.php');
defined('YII_DEBUG') or define('YII_DEBUG', $env['debug']);
// fcgi doesn't have STDIN and STDOUT defined by default
defined('STDIN') or define('STDIN', fopen('php://stdin', 'r'));
defined('STDOUT') or define('STDOUT', fopen('php://stdout', 'w'));
require(__DIR__ . '/vendor/autoload.php');
require(__DIR__ . '/vendor/yiisoft/yii2/Yii.php');
$config = require(__DIR__ . '/config/console.php');
$application = new yii\console\Application($config);
$exitCode = $application->run();
exit($exitCode);
注意
此脚本适用于类 Linux 环境。Yii2 还提供了一个 yii.bat 文件,可以在 Windows 上运行。如果您在 Windows 计算机上操作,请确保除了 yii 文件外,还要更改 yii.bat。
在配置了入口脚本文件后,我们就可以查看我们的应用程序配置文件了。
小贴士
您可能还会注意到在 web/ 文件夹中,有一个名为 index-test.php 的单独入口脚本。此脚本由 Codeception 测试框架使用,该框架用于在 Yii2 中运行单元、功能性和验收测试。我们将在 第十章 使用 Codeception 进行测试 中介绍如何配置和使用此入口脚本以及 Codeception。
配置
在 Yii2 中,控制台配置文件位于 config/console.php,并且几乎与我们的 Web 配置文件相同:
<?php
Yii::setAlias('@tests', dirname(__DIR__) . '/tests');
return [
'id' => 'basic-console',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'controllerNamespace' => 'app\commands',
'components' => [
'cache' => [
'class' => 'yii\caching\FileCache',
],
'log' => [
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
],
],
],
'db' => require(__DIR__ . '/db.php'),
],
'params' => require(__DIR__ . '/params.php'),
];
与我们的 Web 配置文件一样,我们可以使用我们在 第一章 Composer、配置、类和路径别名 中编写的环境感知配置来包含我们的数据库和参数配置文件。实际上,我们 Web 和控制台配置之间的主要区别是显式声明我们的控制台命令命名空间和显式声明 @test 别名,它定义了我们的测试文件将位于何处。
小贴士
由于 Yii 极其灵活的结构,我们可以重新组织我们的引导和入口脚本文件,使它们位于文件系统的许多不同物理位置。正因为这种灵活性,控制台配置文件期望我们显式声明 @test 别名,这样我们就可以运行我们的控制台测试。
设置控制台环境
按照我们为我们的 Web 应用程序设置的相同约定,我们现在需要指示我们的控制台将 APPLICATION_ENV 变量传递给我们的控制台应用程序。从命令行,我们可以轻松地通过导出一个变量来更改环境:
export APPLICATION_ENV="dev"
小贴士
如果我们想将此更改永久应用于我们正在工作的服务器,我们可以将此变量存储在我们的 ~/.bash_profile 文件中,或者我们可以将其存储在 /etc/profile 中,以便为所有用户全局存储。通过将这些命令添加到这些文件之一,下次我们登录到 shell 时,此变量将自动导出。请注意,如果你使用 Windows,你需要将此变量导出到你的 %path% 变量中。
好吧,试试看!退出并再次登录到你的 shell,然后运行以下命令:
echo $APPLICATION_ENV
如果你的计算机配置正确,你应该会在屏幕上看到环境输出。
dev
运行控制台命令
现在我们已经配置了控制台应用程序,我们可以通过运行以下命令轻松运行我们的控制台命令:
$ ./yii
小贴士
在 Windows 上,此命令是 yii.bat。
如果你熟悉 Yii1,此命令已取代了 /yiic 命令。
如果没有提供任何参数,这等同于运行 /yii help 并输出帮助菜单,列出我们应用程序的所有内置控制台命令:
$ ./yii

Yii 为每个默认命令提供了额外的帮助信息。例如,如果我们想查看缓存命令的子命令,我们可以运行以下命令:
$ ./yii help cache

通常,我们可以将 Yii 控制台的使用减少到以下模式:
$ ./yii <route> [--option1=value1 --option2=value2 ... \
argument1 argument2 ...]
在这里,<route> 指的是我们想要运行的特定控制器和操作。例如,如果我们想从控制台刷新我们应用程序的整个缓存,我们可以运行以下命令:
$ ./yii cache/flush-all
这是我们的输出结果:
The following cache components were processed:
* cache (yii\caching\FileCache)
./yii 命令还允许你从同一命令使用替代的控制台配置文件:
$ ./yii <route> --appconfig=path/to/config.php
不需要修改我们的代码,我们只需指示 Yii 使用一个替代的配置文件,这个文件可以包含任何内容,从简单的引用另一个数据库或缓存到更复杂的,如完全不同的控制器命名空间。这个选项在创建具有前端和后端的应用程序时特别有用,因为它们可能包含不同的缓存或数据库组件。
内置控制台命令
现在我们知道了如何运行控制台命令,让我们来看看内置命令的工作方式。如前所述,Yii2 有七个内置控制台命令:help、asset、cache、fixtures、gii、message和migrate。在我们应用程序的开发过程中,我们可能会使用所有七个命令来使我们的应用程序更加健壮。让我们更详细地查看每一个。
帮助命令
建入 Yii2 的第一个命令是help命令。在运行控制台命令时,您可能不知道某个命令需要哪些选项。与其参考 Yii2 文档,您可以使用help命令来提供您所需的所有核心信息。
在最基本的情况下,help命令将输出所有当前可用的控制台命令:
$ ./yii help
一些命令包含可以运行的附加子命令。要查看给定命令的所有可用子命令的列表,您可以运行此命令:
$ ./yii help <command>
一些子命令,例如在 Gii 工具中找到的,需要传递额外的选项以便它们能够运行。要查看给定子命令的所有必需和可选标志的列表,您可以运行以下命令:
$ ./yii help <command/sub>
随着我们进入下一部分,请确保您使用help命令来查看每个命令的所有可能选项和需求。
资产命令
我们工具箱中的第二套默认命令集是asset命令集,包括asset/template和asset/compress。
第一个命令asset/template用于生成一个配置文件来自动化压缩和最小化 JavaScript 和 CSS 资源,其用法如下:
$ ./yii asset/template path/to/asset.php
运行此命令将在path/to/asset.php生成一个新文件,其中包含由下一个命令asset/compress使用的构建指令。此文件概述了要使用的 CSS 和 JavaScript 压缩器、要压缩的资源包列表、压缩资源将输出的目标集合以及针对我们的assetManager的任何自定义配置。
下一个命令asset/compress读取我们生成的配置文件,构建压缩的资源文件和一个可引用的资源包配置,我们可以将其加载到我们的布局和/或视图中。此命令的调用方式如下:
$ ./yii asset/compress path/to/asset.php path/to/asset-bundle.php
注意
在第六章中,我们将深入探讨我们如何使用这些命令以及assetManager类来更详细地管理我们的资源。
缓存命令
我们工具箱中的第三个内置命令是cache命令。cache命令提供了清除由我们的应用程序生成的缓存的功能。这些命令是cache、cache/flush、cache/flush-all和cache/flush-schema。
第一个命令cache返回配置文件中定义的所有可用缓存的命名列表,可以使用以下命令运行:
$ ./yii cache
这里是输出结果:
The following caches were found in the system:
* cache (yii\caching\FileCache)
此命令的输出格式如下,以便我们可以识别正在使用的缓存。在我们的默认应用程序中,只有一个缓存被预定义:我们的文件缓存。
<cache_name> (<cache_type>)
一旦我们知道正在使用哪些缓存,我们就可以使用cache/flush命令通过名称刷新该缓存。使用上一条命令的输出,我们可以通过运行以下命令来按名称清除缓存组件:
./yii cache/flush cache
这里是输出结果:
The following cache components will be flushed:
* cache
The following cache components were processed:
* cache (yii\caching\FileCache)
小贴士
Yii2 中的一些命令是交互式的,在运行前会提示确认。当你需要自动化命令的使用时,例如在部署时,这可能会出现问题。你可以通过在命令后附加--interactive=0来绕过此行为。在非交互式运行命令时,可能需要额外的参数。确保你参考help命令,以确定在运行非交互式命令时需要传递哪些参数。
或者,如果我们想刷新我们应用程序的整个缓存,我们可以使用cache/flush-all选项:
$ ./yii cache/flush-all
在我们的生产环境中,我们希望通过缓存数据库模式来减少数据库服务器的负载。当被指示时,Yii2 将维护当前活动的db组件(数据库)和数据库模式的缓存。当进行模式更改,例如应用新的迁移时,我们需要清除此缓存,以便 Yii2 能够意识到我们的更新后的数据库结构。我们可以通过运行以下命令来清除数据库模式缓存:
$ ./yii cache/flush-schema
小贴士
我们将在下一章中介绍如何启用模式缓存并提高数据库的性能。
固定数据命令
在测试我们的应用程序时,我们通常会想要设置数据库,以便我们的测试始终以可预测和可重复的方式进行。我们可以做到这一点的一种方式是创建固定数据,这将在我们的应用程序中代表测试中的数据库对象。Yii2 提供了一套命令来加载和卸载数据固定数据;这些命令是fixture/load和fixture/unload,它们确实如你所期望的那样工作。
当使用固定数据时,我们的典型测试流程如下:
-
应用数据库迁移。
-
按以下方式执行我们的测试用例:
-
加载我们的数据库固定数据。
-
执行特定的测试。
-
卸载我们的数据库固定数据。
-
-
按需重复,直到所有测试都运行完毕。
fixture/load和fixture/unload命令以相同的方式从命令行调用:
$ ./yii fixture/load <FixtureName>
$ ./yii fixture/unload <FixtureName>
小贴士
固定数据是创建我们应用程序可重复测试的强大方式。此外,yii2-codeception包在测试运行时提供了加载和卸载固定数据的额外支持。在第十章,使用 Codeception 进行测试中,我们将介绍如何创建新的固定数据以及如何将其与 Codeception 集成。
Gii 命令
我们工具箱中的下一组命令是 Gii 命令。如果你熟悉 Yii1,Gii 提供了生成控制器、模型、表单以及基本的 CRUD 功能性。在 Yii2 中,Gii 已从 Web 应用程序模块扩展到 Web 和控制台应用程序,并增加了额外的功能。
Yii2 中的 Gii 模块提供了以下控制台命令来自动生成代码:gii/controller、gii/model、gii/crud、gii/form、gii/extension 和 gii/module。这些命令中的每一个,当提供正确的选项时,都会生成由子命令标识的相应项目。有关完整的要求和选项列表,请确保使用 Gii 子命令上的 help 命令。
小贴士
作为开发工具,Gii 有能力任意生成和覆盖你应用程序中的现有代码。出于安全考虑,你应该只在开发环境中有条件地加载 Gii 模块。此外,Gii 模块本身永远不应该部署到你的生产环境中。因此,建议你只在 composer.json 文件的 require-dev 部分加载 Gii 模块。
require-dev 部分是 composer.json 文件中的一个特殊部分,它允许我们将我们的开发依赖项与生产依赖项分开。默认情况下,运行 Composer 会安装 require 和 require-dev 部分中的所有包。在生产环境中,我们希望通过将 --no-dev 标志传递给 Composer 安装命令来排除开发环境。有关 Composer CLI 的更多信息,请确保参考 Composer 文档:getcomposer.org/doc/03-cli.md。
消息命令
下一个命令集是 message 命令,它提供了自动为我们的应用程序以各种不同格式生成消息翻译的功能性。
第一个子命令是 message/config 命令,它生成一个配置文件,message/extract 命令将使用该文件输出翻译文件。在生成任何翻译之前,我们必须按照以下方式运行 message/config 命令:
$ ./yii message/config /path/to/translation/config.php
此命令在 /path/to/translation/config.php 生成一个配置文件,其中包含 message/extract 生成消息输出文件所需的所有信息。
在配置好你的消息配置文件后,你可以运行以下 message/extract 命令:
$ ./yii message /path/to/translation/config.php
根据你的配置文件和 \Yii::t() 的使用,此命令将生成包含消息列表的 PHP 文件、.po 文件和命令翻译文件格式,或者将必要的消息列表填充到你的数据库中的指定表中。
小贴士
在第十一章中,我们将更深入地介绍如何使用这些命令生成 PHP 消息文件和.po文件,以及如何填充我们的数据库。我们还将详细介绍Yii::t()方法的使用。
迁移命令
Yii2 的最终内置命令集是migration命令。migration命令提供了生成、应用、回滚和审查数据库迁移的功能。这个工具提供了以下子命令:migrate/create、migrate/history、migrate/mark、migrate/up、migrate/down、migrate/to、migrate/new和migrate/redo。
小贴士
我们将在第三章中更详细地介绍如何完全使用这个工具以及如何一般性地与数据库工作,迁移、DAO 和查询构建。现在,请使用./yii help migrate命令来查看有关迁移工具的更多信息。
创建控制台命令
现在我们知道了 Yii2 提供的内置命令,让我们开始添加我们自己的命令。在 Yii2 中,我们编写的任何自定义命令都将存储在我们应用程序的/commands子文件夹中。如果这个文件夹还不存在,请创建它:
mkdir commands
现在,让我们编写一个基本的控制台命令,它只是输出一些文本:
-
首先,我们将在
commands文件夹中创建一个名为BasicController.php的新文件:touch commands/BasicController.php -
现在,让我们编写一些 PHP 代码。首先,我们需要声明我们的
BasicController所在的命名空间。这个命名空间直接对应于我们在config/console.php中定义的controllerNamespace参数:<?php namespace app\commands; -
然后,我们将声明我们想在新的控制器中使用
\yii\console\Controller类:use \yii\console\Controller; -
接下来,我们将声明我们的控制器类如下:
class BasicController extends Controller { } -
最后,在我们的类中,我们将创建一个
actionIndex()方法,该方法将简单地输出HelloWorld,然后优雅地返回一个成功的错误代码。默认情况下,actionIndex()方法是当未指定控制器中的操作时被调用的方法:public function actionIndex() { echo "HelloWorld"; return 0; }
我们已经有了第一个控制台命令!现在,如果我们运行help命令,你可以在可用命令列表中看到我们的命令:
$ ./yii help

此外,我们现在可以执行我们的命令来验证它是否正常工作:
$ ./yii basic
这是输出:
HelloWorld
生成帮助信息
虽然我们现在可以运行我们的命令,但全局帮助菜单和操作帮助菜单的help命令目前不提供任何有用的信息。在 Yii2 中,这些信息直接从我们在BasicController类和actionIndex()方法之前使用的文档块注释(也称为DocBlock注释)中提取。例如,考虑我们在类声明之前添加以下内容:
/**
* A basic controller for our Yii2 Application
*/
class BasicController extends \yii\console\Controller {}
我们还可以通过在方法之前指定DocBlock注释来向我们的actionIndex()方法提供更多信息:
/**
* Outputs HelloWorld
*/
public function actionIndex() {}
在基本控制器上运行help命令将显示以下内容:
$ ./yii help basic

传递命令行参数
就像我们的 Web 控制器(yii\web\Controller)一样,我们也可以通过命令行将参数传递给我们的控制台命令。而不是使用$_GET参数来确定正在使用的参数,Yii2 将直接从命令行界面提取参数。以我们的BasicController的以下方法为例:
/**
* Outputs "$name lives in $city"
* @param string $name The name of the person
* @param string $city The city $name lives in
* @return 0
*/
public function actionLivesIn($name, $city="Chicago")
{
echo "$name lives in $city.\n";
return 0;
}
help命令现在显示我们所需的新方法的必需参数和可选参数:
$ ./yii help basic/lives-in

小贴士
到现在为止,你可能已经注意到控制台命令可以接受两种类型的输入:参数,(在这个例子中,name和city),和选项。参数作为我们提供给动作的数据。另一方面,选项允许我们为我们的控制器指定额外的配置。例如,如前所述,我们可以通过传递--interactve=0标志选项来非交互式地运行我们的命令。我们创建和使用的每个控制台应用程序都可能有自己的选项,我们可以设置。确保你参考该类的 Yii2 文档,并使用help命令来确定每个命令可用的选项。
如果没有参数,此命令将抛出以下错误,指示name参数是必需的:
Error: Missing required arguments: name
一旦我们提供了名称,控制台就会按预期输出结果:
$ ./yii basic/lives-in Alice
这就是输出结果:
Alice lives in Chicago.
通过为city参数提供默认值,该选项对于我们的命令执行不是必需的。然而,如果我们作为第二个参数传递一个值,它将按预期覆盖我们的默认值:
$ ./yii basic/lives-in Alice California
这里是输出结果:
Alice lives in California.
小贴士
根据您的 shell 配置,您可能无法从命令行传递某些字符(如$或*)。确保将使用特殊字符的任何字符串用引号括起来,以确保完整的参数传递给您的应用程序。
除了简单的字符串外,Yii2 还将接受以逗号分隔列表形式的数组。以以下方法为例:
/**
* Outputs each element of the input $array on a new line
* @param array $array A comma separated list of elements
* @return 0
*/
public function actionListElements(array $array)
{
foreach ($array as $$k)
echo "$$k\n";
return 0;
}
通过使用数组type-hint对第一个参数进行类型提示,我们可以通知 Yii 将命令行参数转换为可用的 PHP 数组。从命令行,我们可以通过将其表示为逗号分隔列表来指定元素为数组:
$ ./yii basic/list-elements these,are,separate,items
这将是出现的输出结果:
these
are
separate
items
小贴士
Yii2 不支持从命令行使用多维数组。如果您需要从命令行传递多维数组的数据,您可以传递配置文件的路径,然后在控制器动作中加载该文件。
存储这些数据的选项从返回数据数组的 PHP 文件,到在控制器操作中加载并转换为 PHP 数组的 JSON 或 YAML 格式文件。
退出代码
如我们之前的示例所示,我们迄今为止编写的每个操作都有一个返回值0。虽然从控制器操作返回不是强制性的,但被认为是一种最佳实践,以便我们的 shell 可以通知我们的控制台命令是否已成功执行。按照惯例,退出代码0表示我们的命令没有错误地运行,而任何大于零的正整数都会表示发生了特定的错误。返回的数字将是返回给 shell 的错误代码,并且可以由我们的最终用户用来参考我们的应用程序文档或支持论坛以确定出了什么问题。
假设,例如,我们想要验证我们的输入而不需要深入自定义表单和验证器。在这个例子中,我们希望我们的$shouldRun输入是一个正的非零整数。如果这个整数小于零,我们可以返回一个错误代码,我们的文档将能够引用:
/**
* Returns successfully IFF $shouldRun is set to any
* positive integer greater than 0
*
* @param integer $shouldRun
* @return integer
*/
public function actionConditionalExit($shouldRun=0)
{
if ((int)$shouldRun < 0)
{
echo 'The $shouldRun argument must be an positive non-zero integer' . "\n";
return 1;
}
return 0;
}
此外,Yii2 为我们提供了一些预定义的常量,我们可以使用它们:Controller::EXIT_CODE_NORMAL,其值为0,以及Controller::EXIT_CODE_ERROR,其值为1。如果您有多个返回代码,定义有意义的常量以识别您的错误代码是一种良好的做法。
格式化
Yii2 为我们提供了对控制台命令输出格式的支持。这是通过yii\helpers\Console辅助器提供的。在我们能够使用这个辅助器之前,我们需要将其导入到我们的类中:
<?php
namespace app\commands;
use yii\helpers\Console;
在加载了这个辅助器之后,我们现在可以使用\yii\console\Controller中的stdout()方法或ansiFormat()方法。虽然两种方法都会格式化文本,但ansiFormat()方法可以用来动态地组合具有不同格式的多个字符串:
/**
* Outputs text in bold and cyan
* @return 0
*/
public function actionColors()
{
$this->stdout("Waiting on important thing to happen...\n", Console::BOLD);
$yay = $this->ansiFormat('Yay', Console::FG_CYAN);
echo "$yay! We're done!\n";
return 0;
}
然后,如果我们运行我们的新控制台命令,我们可以看到我们的输出文本是如何变化的:
$ ./yii basic/colors

注意
可用的完整常量列表可在 Yii2 文档中找到,链接为www.yiiframework.com/doc-2.0/yii-helpers-baseconsole.html。
摘要
在本章中,我们介绍了如何配置 Yii 以与我们的 Web 应用程序一致的方式运行控制台命令。我们还简要介绍了七个内置的控制台命令。此外,我们还介绍了如何创建自己的控制台命令,如何向我们的命令传递参数,如何在代码中正确返回值,以及如何格式化命令的输出。
在下一章中,我们将通过学习如何使用和编写迁移、如何使用数据库访问对象(DAO)以及如何使用 Yii 的内置查询构建器来扩展我们对 Yii 的掌握。
第三章。迁移、DAO 和查询构建
编写现代 Web 应用程序的最基本方面之一是与数据库一起工作。通过 PHP 的 PDO 驱动程序,Yii2 可以与许多不同类型的关联数据库一起工作。在本章中,我们将介绍如何连接到不同的数据库,编写数据库迁移以实例化我们的数据库,使用数据库访问对象(DAO),以及使用 Yii2 内置的查询构建器。我们还将介绍数据提供者和数据小部件等强大工具的基础知识,以及如何使用 Yii2 复制和负载均衡对数据库的访问。
连接到数据库
为了与数据库一起工作,所需的主要组件是 yii\db\Connection 类。通过这个类,我们可以连接到各种不同的数据库类型,从本地的 SQLite 数据库到集群化的 MySQL 数据库。建立数据库连接的最简单方法就是创建一个 SQLite 数据库连接,如下所示:
$connection = new \yii\db\Connection([
'dsn' => 'sqlite:/' . \Yii::getAlias('@app') . '/runtime/db.sqlite',
'charset' => 'utf8'
]);
$connection->open();
然而,通常我们希望在应用程序的整个范围内使用单个数据库连接。我们可以通过将数据库配置放入 Web 或控制台配置文件的 db 组件中来使我们的应用程序保持 DRY。按照前几章中的示例,此组件将引用 config/env/<ENV>/db.php 文件。例如,在此文件中建立 SQLite 连接将如下所示:
<?php return [
'dsn' => 'sqlite:/' . \Yii::getAlias('@app') . '/runtime/db.sqlite',
'class' => 'yii\db\Connection',
'charset' => 'utf8'
];
通过将我们的数据库配置存储在我们的应用程序的 db 组件中,它可以很容易地在我们的 Web 和控制台应用程序之间共享,而无需我们做任何额外的工作。此外,由于 Yii2 仅在需要时加载组件,它可以保持我们的应用程序精简且性能良好。
小贴士
在 Yii2 中,组件仅在需要时才会被加载。这个过程通常被称为延迟加载。除非组件被预先加载,否则 Yii2 不会在首次使用之前创建该组件的实例。一旦被初始化,Yii 将在整个应用程序中重用相同的组件,而不是创建该组件的多个实例。延迟加载是 Yii 性能出色的主要原因之一。
将我们的数据库配置存储在我们的配置文件中后,我们现在可以访问数据库连接,如下所示:
\Yii::$app->db;
此连接也将共享到我们应用程序中使用的任何 Active Record 模型,我们将在第四章Active Record,模型和表单中讨论。
如前所述,Yii2 可以连接到多种不同的数据库类型。因为 Yii2 是基于 PHP 的 PDO 库构建的,所以它可以连接到原生 PDO 驱动程序可以连接到的相同来源。以下是 Yii2 支持的一些数据源名称(DSN)的示例:
| 数据库类型 | DSN 方案 |
|---|---|
| MySQL、Percona、MariaDB 等等 | mysql:host=localhost;dbname=mydatabase |
| SQLite | sqlite:/path/to/database/file.sqlite |
| PostgreSQL | pgsql:host=localhost;port=5432;dbname=mydatabase |
| CUBRID | cubrid:dbname=demodb;host=localhost;port=33000 |
| MS SQL Server (via the sqlsrv driver) | sqlsrv:Server=localhost;Database=mydatabase |
| MS SQL Server (via the dblib driver) | dblib:host=localhost;dbname=mydatabase |
| MS SQL Server (via the mssql driver) | mssql:host=localhost;dbname=mydatabase |
| Oracle | oci:dbname=//localhost:1521/mydatabase |
提示
如果你正在连接到 MS SQL 服务器,你需要在你的系统上安装 sqlsrv、dblib 或 mssql PHP 驱动程序。有关这些基础驱动程序的更多信息可以在 PHP 手册中找到,链接为 php.net/manual/en/pdo.drivers.php。
此外,Oracle 连接将需要安装 Oracle 的 OCI8 驱动程序。有关此驱动程序的更多信息可以在 PHP 手册中找到,链接为 php.net/manual/en/book.oci8.php。
注意,除非适当的 PHP 驱动程序已正确安装和配置,否则 Yii2 将无法连接到任何数据库。如果你不确定已安装了哪些驱动程序,原生的 phpinfo() 函数可以输出所有当前已安装的 PHP 扩展列表。
除了前面列出的基础驱动程序之外,Yii2 还可以连接到通过 开放数据库连接(ODBC)的数据库。当你通过 ODBC 连接到数据库时,你需要在你的 db 连接组件中指定 $driverName 属性,以便 Yii2 能够正确连接到你的数据库:
'components' => [
// [...]
'db' => [
'class' => 'yii\db\Connection',
'driverName' => 'mysql', 'dsn' => 'odbc:Driver={MySQL};Server=localhost;Database=test',
'username' => 'username',
'password' => 'password',
]
]
如前所述,某些数据库配置可能需要你指定用户名或密码才能连接到它们。在 db 组件中,只需指定适合你数据库的 username 和 password 属性。
额外的配置选项
除了前面列出的基本 db 组件选项之外,Yii2 还提供了几个额外的选项,这些选项可以用来提高你应用程序的性能或解决原生 PHP 驱动程序中的已知问题。虽然许多这些选项可以在 Yii 指南和 API 文档中找到,但其中一些可能会比其他选项更频繁地使用。这些属性是 $emulatePrepare、$enableQueryCache 和 $enableSchemaCache。
提示
yii\db\Connection 类的所有可用方法和属性的完整列表可以在 www.yiiframework.com/doc-2.0/yii-db-connection.html 找到。
第一个常见的属性$emulatePrepare可以用来减轻 Yii 团队在准备数据库语句时发现的一些常见问题。默认情况下,Yii2 将尝试使用内置在原生 PDO 驱动程序中的原生准备支持。为了帮助减轻与一些原生 PDO 驱动程序(主要是 MS SQL 驱动程序)的问题,可能需要将$emulatePrepare属性设置为true,以便允许 Yii2 处理准备语句。
在我们的db组件中通常启用的下一个常见属性是$enableQueryCache。为了提高应用程序的性能,我们可以将其设置为true,并允许 Yii 缓存常用查询。在一个主要执行读取操作的应用程序中,启用此属性可以显著提高应用程序的性能。
然而,要完全启用此组件,我们现在将提到的附加属性也必须设置。第一个属性是$queryCache,它指定查询缓存应使用的命名缓存对象。如果未设置,则默认为应用程序中的缓存组件。第二个属性是$queryCacheDuration,它决定了任何数据库查询结果将被缓存多长时间。默认情况下,查询缓存将有效期为 3,600 秒,即 60 分钟:
'components' => [
//[...
'db' => [
'dsn' => 'sqlite:/' . \Yii::getAlias('@app') . '/runtime/db.sqlite',
'class' => 'yii\db\Connection',
'charset' => 'utf8',
'enableQueryCache' => true,
'queryCache' => 'filecache',
'queryCacheDuration' => 60
],
'filecache' => [
'class' => 'yii\caching\FileCache',
],
]
最后一个常见的属性,通常会被添加到我们的db组件中是$enableSchemaCache。在 Yii 访问数据库之前,它通常需要确定数据库模式。此模式信息用于帮助 Yii 在运行验证器和处理关系模型(如相关 Active Record 模型)时。我们不必让 Yii 在每次请求时都尝试确定我们的数据库模式,我们可以通过将$enableSchemaCache设置为true来告诉它我们的模式不会改变。
与之前概述的$enableCache参数类似,我们还需要定义$schemaCache参数,这将告诉 Yii 使用哪个缓存组件。我们还需要定义$schemaCacheDuration参数,以便 Yii2 知道模式缓存在秒内有效的时间:
'components' => [
// [...]
'db' => [
'dsn' => 'sqlite:/' . \Yii::getAlias('@app') . '/runtime/db.sqlite',
'class' => 'yii\db\Connection',
'charset' => 'utf8',
'enableSchemaCache' => true,
'schemaCache' => 'filecache',
'schemaCacheDuration' => 3600
],
'filecache' => [
'class' => 'yii\caching\FileCache',
],
]
由于我们的大多数控制器操作很可能会导致数据库操作,启用这些属性可以大大提高应用程序的性能。
小贴士
记住,因为$enableSchemaCache和$enableQueryCache已被启用,Yii2 将不会对数据库执行常见检查。您数据库中底层数据或模式的任何更改都可能使您的应用程序返回错误数据或完全崩溃。如果您直接更改数据库中的数据而不是通过 Yii2,或者更改数据库模式,请确保刷新由$enableSchemaCache或$enableQueryCache定义的相关缓存组件,以确保您的应用程序正常运行。
编写数据库迁移
当构建和维护现代网络应用程序时,我们数据库的底层结构可能需要根据需求或范围的变化而改变。为了确保我们的数据库模式可以与源代码同步发展,Yii2 提供了内置支持来管理数据库迁移。使用数据库迁移,我们可以将数据库视为源代码的扩展,并在源代码更改时轻松地对其进行更改。
概述模式
当与数据库迁移一起工作时,我们通常会使用 yii\db\Schema 类。当正确配对时,我们通常可以编写迁移,以便它们能够在各种数据库类型上运行。例如,当本地工作时,我们可能需要使用本地 SQLite 数据库,即使我们的应用程序最终将在 MySQL 数据库上运行。
这个类的核心是多种不同的模式类型,Yii2 将能够正确地将它们映射到我们数据库中的适当数据类型。这些包括 INT、DATETIME 和 TEXT 等数据类型。
小贴士
要获取 Schema 类提供的所有可用常量的完整列表,请确保您参考 Yii2 指南 www.yiiframework.com/doc-2.0/yii-db-schema.html#constants。
在我们的迁移中,我们可以通过运行以下命令来调用这些常量:
Schema::<CONSTANT>
在整数的示例中,我们可以使用以下内容:
Schema::TYPE_INTEGER
在我们的迁移中使用这些常量,我们可以确保我们的迁移映射到数据库中的适当数据类型,并在各种数据库类型上工作。
编写迁移
如前一章所示,我们可以通过从 yii 命令行工具调用 migrate/create 命令来创建一个新的迁移。以前一章的源代码作为起点,我们将在命令行中运行以下操作:
./yii migrate/create init

运行此命令将在我们应用程序的 migrations 文件夹中创建一个新的迁移。
小贴士
根据您系统的文件权限,如果 migrations 文件夹不存在,Yii2 可能无法创建它。如果 migrations 文件夹尚未存在,请在运行 migrate/create 命令之前确保创建它。
当运行迁移时,Yii2 将按照它们创建的顺序执行它们。为了确定这个顺序,Yii2 将查看文件名或包含从 migrate/create 命令指定的迁移名称以及迁移创建的确切时间的迁移。
在我们的例子中,文件名是 m150523_194158_init.php,这意味着这个迁移是在 2015 年 5 月 23 日晚上 7:41:58 UTC 创建的。
小贴士
由于这种命名约定,你创建的任何迁移都将具有独特且唯一的文件名。如果你正在跟随教程,请确保你在由 ./yii 命令创建的文件中工作。
运行migrate/create命令后,Yii2 为我们提供了一个类似以下代码块的骨架迁移:
<?php
use yii\db\Schema;
use yii\db\Migration;
class m150523_194158_init extends Migration
{
public function up() {}
public function down()
{
echo "m150523_194158_init cannot be reverted.\n";
return false;
}
/*
// Use safeUp/safeDown to run migration code within a transaction
public function safeUp() {}
public function safeDown() {}
*/
}
在 Yii2 中,迁移可以以两种方式操作:我们可以将迁移提升,或者将其降级。这两个操作对应于四个函数之一:up()、safeUp()、down()和safeDown()。up()和down()方法是运行迁移所需的基本方法,即使有错误,也会执行它们内部发出的任何数据库命令。或者,我们可以使用safeUp()和safeDown()方法,这两个方法在功能上与up()和down()方法相同,唯一的区别是整个操作都被包裹在一个事务中。如果我们的数据库支持事务,使用安全方法运行迁移可以帮助我们在错误导致整个数据库出现问题之前在运行时捕获迁移错误。
小贴士
由于它们提供的额外安全性,safeUp()和safeDown()应该是我们编写迁移时的首选方法。此外,如果使用了safeUp()或safeDown(),则不能使用不安全的方法。
让我们从向我们的数据库添加一个简单的表开始,以便存储我们的用户信息。我们将从简单地存储一个 ID、电子邮件地址、密码、用户名以及一些时间戳元数据开始,这些元数据表示我们的用户何时被创建以及最后更新。在我们的迁移中,我们可以这样写:
class m150523_194158_init extends Migration
{
public function safeUp()
{
return $this->createTable('user', [
'id' => Schema::TYPE_PK, // $this->primaryKey()
'email' => Schema::TYPE_STRING, // $this->string(255) // String with 255 characters
'password' => Schema::TYPE_STRING,
'name' => Schema::TYPE_STRING,
'created_at' => Schema::TYPE_INTEGER, // $this->integer()
'updated_at' => Schema::TYPE_INTEGER
]);
}
public function safeDown()
{
return $this->dropTable('user');
}
}
小贴士
如前所述,Yii2 支持两种不同的方式来声明列的架构类型。我们可以直接使用由Schema类定义的常量,或者使用原生的迁移方法,如primaryKey()、integer()、string()和text()。使用迁移方法更受青睐,因为它允许我们向列添加额外的属性,例如列的大小和长度。有关迁移类提供的所有方法的完整列表,请参阅 Yii2 指南www.yiiframework.com/doc-2.0/yii-db-migration.html。
在前面的例子中,我们概述了两种方法:createTable(),它将在我们的应用程序中创建一个新的数据库表,以及dropTable(),它将从我们的数据库中删除表。
小贴士
在与数据库一起工作时,一个常见的约定是使用下划线编写字段名,并为表和列名使用单数名称。虽然 Yii2 足够智能,可以处理你指定的任何字段名,但遵循此约定可以使你的代码更易于阅读,与数据库的交互也更简单。虽然你不必明确遵循此约定,但遵循约定可以在未来为你节省大量时间。
运行迁移
我们可以通过yii命令运行我们的迁移,如前一章所示:
./yii migrate/up

由于我们在示例中使用的是 SQLite 数据库,我们可以轻松地探索运行 migrate/up 命令后发生了什么。使用 sqlite 命令行工具,我们可以探索我们的 SQLite 数据库:
sqlite3 /path/to/runtime/db.sqlite
小贴士
如果您的包管理器不提供 sqlite3,您可以从 www.sqlite.org/download.html 下载二进制可执行文件。
通过从我们的 SQLite 提示符运行 .tables 命令,我们可以看到在运行 migrate/up 命令时创建了两个表,migration 和 user:
sqlite> .tables

第一个表,migration,包含所有已应用迁移的列表以及它们被应用的时间。

第二个表,user,显示了 Yii 从我们的迁移类创建的结果模式。

例如,通过指定我们的 ID 属性的 TYPE_PK 模式,Yii2 知道它需要向我们的 SQLite 模式添加 AUTOINCREMENT 和 NOT NULL 属性。
小贴士
虽然数据库迁移适用于大多数数据库更改,但针对大型数据集运行它们可能会导致您的数据库对应用程序不可用,从而导致停机时间。确保在您通过 Yii2 运行数据库迁移之前,您的应用程序应该能够处理临时停机时间。如果即使是临时的停机时间也不适合您的应用程序,您可能需要考虑以其他方式将数据迁移到更新的模式。
修改数据库模式
在本地开发时,我们可以简单地使用 migrate/down 命令来撤销特定的迁移(假设我们实现了 down() 或 safeDown() 方法)。然而,在我们提交并将代码推送到我们的分布式版本控制系统(DCVS)系统,如 Git 或 SVN 之后,其他人可能会使用或与我们合作代码。在这种情况下,我们希望在不会损害他们本地实例的情况下更改我们的迁移;我们可以创建新的迁移,以便我们的代码用户可以应用这些迁移,以使他们的应用程序保持最新。
以我们为我们创建的用户模式为例:
CREATE TABLE `user` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`email` varchar(255),
`password` varchar(255),
`name` varchar(255),
`created_at` integer,
`updated_at` integer
);
而不是为我们的用户名设置一个单独的字段,我们可能希望有两个字段:一个用于他们的名字,另一个用于他们的姓氏。我们可能还希望对其他字段进行一些更改,例如我们的 email 字段,以防止它们为 NULL。我们可以通过编写一个新的迁移并更改数据库本身的模式来实现这一点。
我们将首先创建一个新的迁移:
./yii migrate/create name_change --interactive=0
小贴士
记住,--interactive=0 标志告诉 Yii 在没有提示的情况下运行我们的控制台命令。
在我们的新 migrations/…name_change.php 迁移中,我们可以编写一个 safeUp() 方法来为我们更改这些列:
public function safeUp()
{
$this->renameColumn('user', 'name', 'first_name');
$this->alterColumn('user', 'first_name', SCHEMA::TYPE_STRING);
$this->addColumn('user', 'last_name', SCHEMA::TYPE_STRING);
$this->alterColumn('user', 'email', SCHEMA::TYPE_STRING . ' NOT NULL');
$this->createIndex('user_unique_email', 'user', 'email', true);
}
在 Yii2 中,迁移命令在执行时具有自解释性。例如,第一个方法 renameColumn() 将简单地将 name 列重命名为 first_name。同样,addColumn() 将在数据库中添加一个具有指定名称和模式的新的列,alterColumn() 将修改指定列的模式,而 createIndex() 将在我们的数据库中的电子邮件字段上创建一个唯一索引,这将确保不会有两个用户共享相同的电子邮件地址。
小贴士
可以在 Yii2 指南 www.yiiframework.com/doc-2.0/yii-db-migration.html 中找到可以在迁移调用中运行的命令的完整列表。
但是,如果我们尝试在 SQLite 数据库上运行这些迁移,我们会遇到类似于以下错误的错误,表明 SQLite 不支持这些方法:
./yii migrate/up
这是输出结果:
*** applying m150523_203944_name_change
> rename column name in table user to first_name \
...Exception: yii\db\sqlite\QueryBuilder::renameColumn is not \
supported by SQLite. \
(/var/www/ch3/vendor/yiisoft/yii2/db/sqlite/QueryBuilder.php:201)
虽然之前列出的迁移可以在 MySQL 或 PostgreSQL 上工作,但我们的 SQLite 驱动程序不提供对这些命令的支持。然而,由于我们使用 SQLite,因此我们必须重写我们的初始迁移命令,并通知应用程序用户关于更改的信息。对于 SQLite,我们可以将新创建的 migrations/…name_change.php 迁移重写如下:
public function safeUp()
{
$this->dropTable('user');
$this->createTable('user', [
'id' => Schema::TYPE_PK,
'email' => Schema::TYPE_STRING . ' NOT NULL',
'password' => Schema::TYPE_STRING . ' NOT NULL',
'first_name' => Schema::TYPE_STRING,
'last_name' => Schema::TYPE_STRING,
'created_at' => Schema::TYPE_INTEGER,
'updated_at' => Schema::TYPE_INTEGER
]);
$this->createIndex('user_unique_email', 'user', 'email', true);
}
public function safeDown()
{
return true;
}
小贴士
yii\db\Migration 没有我们可以用来检索数据的 query() 方法。因此,如果我们需要在迁移中查询数据,我们需要使用 Yii2 的查询构建器来完成此操作,我们将在本章后面介绍。如果我们的应用程序被广泛采用,可能最好使用查询构建器查询所有用户并将它们临时存储在内存中(或者如果我们有大量记录,则存储在临时存储中)。然后,在为我们的用户表创建新的表模式之后,我们可以使用 insert() 方法将它们重新插入到我们的数据库中。
在更新了我们的新迁移之后,我们可以重新运行我们的迁移命令。由于我们的第一次迁移已经应用,当执行 migrate/up 命令时,该迁移将被跳过,并且只会运行我们的 migrations/m150523_203944_change.php 迁移:
./yii migrate/up

运行我们的迁移后,我们可以查询我们的数据库以查看在 SQLite 中我们的完整模式看起来是什么样子:
sqlite3 /path/to/runtime/db.sqlite

小贴士
Yii2 中的迁移功能非常强大。请查看 Yii2 文档 www.yiiframework.com/doc-2.0/yii-db-migration.html,了解您可以使用 yii\db\Migration 做到的一切。
数据库访问对象
Yii 数据库访问对象,通常称为 DAO,提供了一个强大的面向对象 API,用于与关系数据库一起工作。作为更复杂数据库访问(如查询构建器和活动记录)的基础,DAO 使我们能够通过 SQL 语句和 PHP 数组直接与数据库交互。因此,与使用活动记录或查询构建器相比,使用 DAO 语句的性能要高得多。
DAO 的核心是我们的 yii\db\Connection 类,或者更常见的是我们的 db 组件 \Yii::$app->db。由于我们的 db 组件已经为 SQLite 正确配置,我们将继续使用它。使用 DAO,我们可以运行两种类型的查询:返回数据的查询,如 SELECT 查询,以及执行数据的查询,如 DELETE 或 UPDATE。
提示
如果你直接使用 yii\db\Connection 类,你需要在运行任何针对该连接的查询之前显式调用 open() 方法。
查询数据
我们可以使用 DAO 的第一种方式是查询数据。用于查询数据的主要方法有四个:queryAll()、queryOne()、queryScalar() 和 queryColumn()。
第一种方法,queryAll(),用于根据 createCommand() 方法中使用的 SQL 语句查询特定表中的所有数据。以我们的用户表为例,我们可以通过运行以下命令来查询我们数据库中的所有用户:
$users = \Yii::$app->db
->createCommand('SELECT * FROM user;')
->queryAll();
运行此命令后,我们的 $users 变量将填充一个用户数组:
Array
(
[0] => Array
(
[id] => 1
[email] => test@example.com
[password] => test123
[first_name] => test
[last_name] => user
[created_at] => 0
[updated_at] => 0
)
)
下一种方法,queryOne(),用于从数据库中检索单个记录。
$user = \Yii::$app->db
->createCommand('SELECT * FROM user WHERE id = 1;')
->queryOne();
queryOne() 方法返回单个元素的数据数组。如果没有找到数据,此方法将返回 false:
Array
(
[id] => 1
[email] => test@example.com
[password] => test123
[first_name] => test
[last_name] => user
[created_at] => 0
[updated_at] => 0
)
第三种方法,queryScalar(),用于返回返回单个值的 SELECT 查询的结果。例如,如果我们想计算我们数据库中用户的数量,我们可以使用 queryScalar() 来获取值:
$count = \Yii::$app->db
->createCommand('SELECT COUNT(*) FROM user;')
->queryScalar();
运行此命令后,我们的 $count 变量将填充我们数据库中用户的数量。
最后一种方法,queryColumn(),用于查询我们数据库中的特定列。例如,如果我们想了解我们数据库中所有用户的电子邮件地址,我们可以使用 queryAll() 来获取所有这些数据,或者我们可以使用 queryColumn(),这将更高效,因为它将查询更少的数据:
$user = \Yii::$app->db
->createCommand('SELECT email FROM user;')
->queryColumn();
与 queryAll() 类似,queryColumn() 将返回结果数组:
Array
(
[0] => test@example.com
)
如果没有找到结果,queryColumn() 将返回一个空数组。
在了解这些方法之后,作为一个练习,让我们回到我们之前的迁移并将它们重写以保留我们的用户跨模式更改:
-
首先,让我们回滚我们的迁移以正确模拟场景:
./yii migrate/down -
然后,我们将使用
migrate/to命令迁移我们的初始迁移:./yii migrate/to m150523_194158_init -
接下来,让我们用一些测试数据填充我们的数据库:
sqlite3 /path/to/runtime/db.sqlite INSERT INTO user (email, password, name) VALUES ('test@example.com', 'test1', 'test user'); INSERT INTO user (email, password, name) VALUES ('test2@example.com', 'test2', 'test user 2'); -
如果我们查看我们的数据库,我们会看到初始模式和数据现在已经就绪。
![查询数据]()
-
然后,让我们重写我们的
migrations/…name_change.php迁移,在运行我们创建的初始迁移之前从数据库中获取我们的用户,然后将我们的用户重新插入到数据库中。我们将使用queryAll()DAO 方法来获取数据,并使用yii\db\Migration的insert()方法将其放回数据库。新的代码块已被突出显示以便于查看:public function safeUp() { $users = \Yii::$app->db ->createCommand('SELECT * FROM user') ->queryAll(); $this->dropTable('user'); $this->createTable('user', [ 'id' => Schema::TYPE_PK, 'email' => Schema::TYPE_STRING . ' NOT NULL', 'password' => Schema::TYPE_STRING . ' NOT NULL', 'first_name' => Schema::TYPE_STRING, 'last_name' => Schema::TYPE_STRING, 'created_at' => Schema::TYPE_INTEGER, 'updated_at' => Schema::TYPE_INTEGER ]); $this->createIndex('user_unique_email', 'user', 'email', true); foreach ($users as $user) { $this->insert('user', [ 'id' => $user['id'], 'email' => $user['email'], 'password' => $user['password'], 'first_name' => $user['name'], 'created_at' => $user['created_at'], 'updated_at' => $user['updated_at'] ]); } } -
现在,我们可以重新运行我们的迁移。如果成功,我们应该看到我们的原始迁移运行,并且为数据库中的每个用户执行一个插入调用。
./yii migrate/up –interactive=0![查询数据]()
-
最后,我们可以查询我们的 SQLite 数据库以预览更新的模式并查看更新的用户:
sqlite3 /path/to/runtime/db.sqlite![查询数据]()
如您所见,DAO 的查询方法为我们提供了快速有效地从数据库中获取数据的能力。
引用表和列名
当编写数据库无关的 SQL 查询时,正确引用字段名可能会出现问题。为了避免这个问题,Yii2 提供了自动为您使用特定数据库的正确引用规则来引用表和列名的功能。
要自动引用列名,只需将列名放在方括号中:
[[column name]]
要自动引用表,只需将表名放在花括号中:
{{table name}}
下面展示了这两个工具在操作中的示例:
$result = \Yii::$app->db
->createCommand("SELECT COUNT([[id]]) FROM {{user}}")
->queryScalar();
执行查询
虽然查询方法提供了从我们的数据库中选择数据的能力,但我们经常需要执行UPDATE或DELETE命令,这些命令不会返回数据。为了执行这些命令,我们通常可以使用execute()方法:
\Yii::$app->db
->createCommand('INSERT INTO user (email, password) VALUES ("test3@example.com", "test3");')
->execute();
如果成功,execute()方法将返回true,如果失败,则返回false。
Yii2 还提供了insert()、update()和delete()的便捷包装器,这使得我们能够编写命令而无需编写原始 SQL。这些方法代表您正确转义和引用表和列名以及绑定参数。
例如,我们可以按照以下方式将新用户插入到数据库中:
// INSERT ( tablename, [ attributes => attr ] )
\Yii::$app->db
->createCommand()
->insert('user', [
'email' => 'test4@example.com',
'password' => 'changeme7',
'first_name' => 'Test',
'last_name' => 'User',
'created_at' => time(),
'updated_at' => time()
])
->execute();
我们可以使用update()方法更新我们数据库中的所有用户:
// UPDATE (tablename, [ attributes => attr ], condition )
\Yii::$app->db
->createCommand()
->update('user', [
'updated_at' => time()
], '1 = 1')
->execute();
小贴士
我们更新命令中列出的最后一个参数定义了查询命令的where条件,我们将在本章后面更详细地介绍。1=1是一个常见的 SQL 成语,用于更新所有记录。
我们也可以使用delete()方法从我们的数据库中删除用户:
// DELETE ( tablename, condition )
\Yii::$app->db
->createCommand()
->delete('user', 'id = 3')
->execute();
此外,如果您需要同时插入多行,可以使用batchInsert()方法,这比逐行插入要高效得多:
// batchInsert( tablename, [ properties ], [ rows ] )
\Yii::$app->db
->createCommand()
->batchInsert('user', ['email', 'password', 'first_name', 'last_name', 'created_at', 'updated_at'],
[
['james.franklin@example.com', 'changeme7', 'James', 'Franklin', time(), time()],
['linda.marks@example.com', 'changeme7', 'Linda', 'Marks', time(), time()]
['roger.martin@example.com', 'changeme7', 'Roger', 'Martin', time(), time()]
])
->execute();
小贴士
Yii2 没有提供batchUpdate()或batchDelete()方法,因为批量更新和删除可以通过update()和delete()方法使用常规 SQL 来处理。
参数绑定
与用户提交的数据打交道时,最重要的规则是永远不要信任用户提交的数据。任何通过我们的数据库传递并来自最终用户的数据都需要在执行数据库操作之前进行验证、清理和正确绑定到我们的语句中。
以以下查询为例:
\Yii::$app->db
->createCommand("UPDATE user SET first_name = 'Tom' WHERE id = " . $_GET['id'])
->execute();
在正常情况下,Yii 会生成以下 SQL,假设$_GET['id']的值为1:
UPDATE user SET first_name = 'Tom' WHERE id = 1;
虽然这看起来是无害的,但任何可以操作$_GET['id']变量的用户都可以将我们的查询重写为更危险的内容。例如,他们可以通过将$_GET['id']替换为1; DROP TABLE user; --来删除我们整个用户表:
UPDATE user SET first_name = 'Tom' WHERE id = 1; DROP TABLE user; --
这种攻击被称为 SQL 注入。为了帮助防止 SQL 注入,Yii2 提供了几种不同的方法来将参数绑定到我们的查询中,从而过滤注入的 SQL。这三种方法是bindValue()、bindValues()和bindParam()。
第一种方法,bindValue(),用于将单个参数绑定到我们的 SQL 语句中的标记上。例如,我们可以将之前的查询重写如下:
\Yii::$app->db
->createCommand("UPDATE user SET first_name = :name WHERE id = :id)
->bindValue(':name', 'Tom')
->bindValue(':id', $_GET['id'])
->execute();
或者,我们可以使用bindValues()方法在单个调用中绑定多个参数:
\Yii::$app->db
->createCommand("UPDATE user SET first_name = :name WHERE id = :id)
->bindValues([ ':name' => 'Tom', ':id' => $_GET['id'] ])
->execute();
为了方便,可以将之前的查询重写,使参数与createCommand()方法一致:
$params = [ ':name' => 'Tom', ':id' => $_GET['id'] ];
\Yii::$app->db
->createCommand("UPDATE user SET first_name = :name WHERE id = :id, $params)
->execute();
最后一种方法,bindParam(),通过引用绑定参数而不是通过值绑定:
$id = 1;
$name = 'Tom';
$q = \Yii::$app->db
->createCommand("UPDATE user SET first_name = :name WHERE id = :id)
->bindParam(':name', $name)
->bindParam(':id', $id);
由于bindParam()通过引用绑定参数,我们可以更改绑定的值以执行多个查询。在先前的例子中,我们可以写出以下内容来更新多个用户,而无需每次都重写查询:
$q->execute();
$id = 2;
$name = 'Kevin';
$q->execute();
小贴士
记住,与用户数据打交道时最重要的规则是永远不要信任用户提交的数据。即使在您 100%确信 SQL 注入不会发生的情况下,也建议您使用参数绑定而不是直接编写 SQL 语句。这将保护您免受未来代码更改的影响。
事务
当连续运行多个查询时,我们通常希望确保数据库状态在这些查询之间保持一致。大多数现代数据库都支持使用事务来实现这一点。在事务中,更改以这种方式写入数据库,如果一切顺利则可以提交,如果事务中的任何给定查询失败则可以无后果地回滚。在 Yii2 中,这看起来如下所示:
$transaction = \Yii::$app->db->beginTransaction();
try {
\Yii::$app->db->createCommand($sql)->execute();
\Yii::$app->db->createCommand($sql)->execute();
//[ … more queries …]
$transaction->commit();
} catch (\Exception $e) {
$transaction->rollBack();
}
查询构建器
在 DAO(数据访问对象)奠定基础之上,诞生了 Yii 的查询构建器。Yii 的查询构建器允许我们以编程方式编写数据库无关的查询。因此,通过查询构建器编写的查询比其 DAO 对应物要易于阅读得多。
查询构建器的基本原理包括创建 yii\db\Query 实例、构建语句,然后执行该查询语句。例如,我们可以在查询构建器中使用以下代码简单地查询我们数据库中的所有用户:
$users = (new \yii\db\Query())
->select(['id', 'email'])
->from('user')
->all();
小贴士
当使用查询构建器时,我们实际上使用的是 yii\db\Query 类而不是 yii\db\QueryBuilder。虽然 yii\db\QueryBuilder 可以生成与 yii\db\Query 类似的 SQL 语句,但 yii\db\Query 允许这些语句对数据库不可知。通常,当使用查询构建器时,你将想要使用 yii\db\Query。
查询构建方法
查询构建器的基本原理涉及将多个查询方法链接在一起。这些方法名称直接对应于它们命名的 SQL 部分。当使用查询构建器时,你将最常使用的方法将是 select()、from() 和 where() 方法。
今后,我们将使用以下变量来表示我们的查询构建器对象:
$query = (new \yii\db\Query());
选择方法
select() 方法直接对应于我们的 SQL 查询的 SELECT 部分,接受列名字符串或列数组,以指定我们想要从数据库中选择的列。例如,以下查询是相同的:
$query->select('id, first_name)->from('user');
$query->select(['id', 'last_name'])->from('user');
小贴士
当使用 select() 方法时,数组格式通常易于阅读和操作。如果你选择将列名作为字符串列出,确保你在整个应用程序中保持一致性。在以下示例中,我们将使用数组格式。
select() 方法还支持列别名和表前缀,如下一个示例所示:
$query->select([
'id' => 'user_id',
'user.first_name' => 'fName']
)->from('user');
除了列名外,select 方法还支持表达式。例如,如果我们想检索用户的完整姓名作为一个单独的字段,我们可以执行以下查询:
$query->select([
"id",
"CONCACT(first_name, ' ', last_name)" => 'full_name'
])->from('user');
select 方法还可以用来执行子查询,例如 COUNT():
$query->select('COUNT(*)')->from('user');
最后,select 语句可以与 distinct() 方法链接,以检索唯一记录。例如,如果我们想列出我们用户数据库中的所有第一个名字,我们可以执行以下查询:
$query->select('first_name')->distinct()->from('user');
小贴士
如果你的查询中省略了 select() 方法,将执行 SELECT * 查询。
from 方法
我们之前的示例已经展示了 from() 方法的基本用法。from() 方法也可以用来指定表别名,如下面的示例所示:
$query->select('first_name')->from(['u' => 'users']);
小贴士
与 select() 方法类似,from() 方法也可以接受字符串作为输入,而不是数组。前面的查询可以重写为 $query->select('first_name')->from(['users u']);。
where 方法
where() 方法指定了我们的 SQL 查询的 where 部分,可以使用字符串格式、哈希格式或运算符格式。
字符串格式
where()方法的字符串格式应始终与addParams()方法链式连接,以防止 SQL 注入:
$query->select(['first_name', 'last_name'])
->from('user')
->where('id = :id')
->addParams([':id' => 1]);
或者,可以将参数重写为where()方法的第二个参数:
$query->select(['first_name', 'last_name'])
->from('user')
->where('id = :id', [':id' => 1]);
小贴士
记住,为了避免 SQL 注入,不要在where()方法中直接添加 PHP 变量。
哈希格式
哈希格式提供了一种更好的方法来在where语句中链式连接多个AND条件。而不是传递一个字符串作为参数,我们可以传递一个表示列名和值的键值对的数组。当使用哈希格式时,所选字段将通过 SQL AND连接在一起。
例如,我们可以通过运行以下查询来找到所有名为 John 的用户,他们在 20 多岁,并且没有列出宠物名称:
$query->from('user')
->where([
'first_name' => 'John',
'pets_name' => NULL,
'age' => [20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
]);
小贴士
我们当前的数据库中没有age或pets_name字段。我们将不得不通过迁移调整我们的模式来向数据库中添加这些字段。
这将导致以下查询:
SELECT *
FROM user
WHERE first_name = "John" AND
pets_name IS NULL AND
age IN (20, 21, 22, 23, 24, 25, 26, 27, 28, 29);
小贴士
如前所述,哈希格式允许您生成更复杂的WHERE查询,例如在指定值数组时使用IN,以及在传递数组值为 null 时使用IS NULL。
操作符格式
使用where()方法的最后一种方式是使用操作符格式。操作符格式允许我们构建包含条件如LIKE、OR、BETWEEN和EXISTS等更复杂的 SQL 查询,仅举几个例子。
通常,operator格式采用以下格式:
where([ operator, condition1, condition2 ]);
例如,如果我们想从我们的数据库中获取所有名为 John 或 Jill 的用户,我们可以执行以下操作:
$query->where(['or', 'John', 'Jill']);
小贴士
要查看操作符格式支持的完整操作符列表,请查看 Yii2 API 文档中的www.yiiframework.com/doc-2.0/yii-db-query.html#where()-detail。
如您所想象,where()方法可能会迅速变得非常庞大和复杂。与其使用操作符,您可能会发现使用andWhere()或orWhere()方法来链式连接多个条件会使代码更易读:
$query->andWhere(['in', 'id', [1,2,3,4,5]);
排序结果
查询构建器还可以使用orderBy()方法根据给定字段对结果进行排序。例如,为了按年龄对数据库中的所有用户进行排序,我们可以构建以下查询:
$query->from('user')
->orderBy('age ASC');
限制和偏移数据
通常与where()方法一起使用的是limit()和offset()方法,这些方法用于限制结果数量并偏移给定数量的结果。当这两个方法正确使用时,它们构成了通过结果分页的基本方法:
$query->from('user')
->limit(5)
->offset(5);
分组和聚合
在处理多样化的数据集时,我们通常需要对我们的数据进行一些分析。聚合函数如 GROUP BY 和 HAVING 可以极大地帮助我们从数据中提取更多信息。Yii2 通过 groupBy() 和 having() 方法支持这些方法。
例如,如果我们想列出我们数据库中每个年龄组的用户数量,我们可以执行以下查询:
$query->select(['age', 'COUNT(*)' => 'users'])
->from('user')
->groupBy('age');
这将生成以下 SQL 语句:
SELECT age, COUNT(*) AS users FROM user GROUP BY age;
groupBy() 方法的行为类似于 select() 方法,因为它接受一个数组或字符串作为参数;然而,当使用数据库表达式时,你需要使用数组语法。
在使用 groupBy() 对我们的结果进行分组后,我们可以使用 having() 方法来过滤我们的结果,该方法的行为与 where() 方法相同。以下示例将仅显示我们数据集中年龄超过指定年龄的用户数量:
$query->select(['age', 'COUNT(*)' => 'users'])
->from('user')
->groupBy('age')
->having('>', 'age', 30');
连接和并集
当在多个表上工作时,你可能经常需要在你的数据集上执行连接或并集。可以通过查询构建器使用 join() 和 union() 方法来执行连接和并集。
连接方法具有以下方法语法:
$query->join( $type, $table, $on, $params );
第一个参数 $type 指定了你想要执行的连接类型(例如,INNER JOIN、LEFT JOIN 或 OUTER JOIN)。$table 参数指定要连接的表。第三个参数 $on 指定了表应该连接的条件,并采用 where() 方法的语法,而 $params 参数指定了要绑定到连接的可选参数。
例如,假设我们有一个帖子表和用户表。我们可以使用 join() 方法将它们连接如下:
$query->join('LEFT JOIN', 'post', 'post.user_id = user.id');
假设我们的数据库中既有用户表又有帖子表,这将返回一个包含所有用户及其帖子的连接。结果将包括所有与所有他们拥有的帖子连接的用户。
也可以通过类型使用快捷方法 rightJoin()、leftJoin() 和 innerJoin() 来执行连接。
同样地,可以通过首先构建两个不同的 yii\db\Query 对象,然后使用它们上的 union() 方法来构造两个不同查询的并集,如下所示:
$query1->union($query2);
执行查询
在使用查询构建器构建我们的查询后,我们需要指定查询的执行。Yii2 提供以下查询方法来执行使用查询构建器构建的查询。查询方法简单地链接到现有的 $query 对象,这将立即导致它们的执行。
在使用查询构建器的大多数情况下,我们希望获取数据库中的所有记录。这可以通过将 all() 方法链接到我们的查询对象来完成,这将检索满足我们的 $query 对象要求的所有记录:
$results = $query->all();
$result 变量将填充一个包含行的数组,每个行包含一个与结果数据关联的名称-值键对数组。
提示
如果你有大量数据集,请仔细考虑使用all()方法,因为查询执行可能需要很长时间才能完成,并且可能会导致你的应用程序挂起或出错。
在其他情况下,可能只需要获取查询的第一行。要获取第一行,我们可以使用one()方法:
$row = $query->one();
在其他时候,我们可能只想知道查询是否会返回任何数据。为了实现这一点,我们可以使用exists()方法,它将返回true或false,指示结果查询将返回数据。
例如,如果我们想知道我们的数据库中是否有任何用户,我们可以使用exists()查询来检查我们在执行任何更复杂的查询之前是否有用户:
$areUsersInDb = (new \yii\db\Query)
->from('user')
->exists();
或者,我们可以在运行查询之前使用count()方法来确定我们数据库中存在多少用户。count()方法将在SELECT片段中执行COUNT(*)方法,并将返回一个标量值:
$count = (new \yii\db\Query)
->from('user')
->count();
当处理数据库表达式,如MIN()和MAX(),或更复杂的查询时,你可能发现从查询构建器中检索标量值而不是关联数组是有用的。要使用查询构建器检索标量值,我们可以使用scalar()方法。例如,如果我们想使用MAX() SQL 方法知道我们数据库中最年长的用户有多老,我们可以使用以下代码返回代表他们年龄的整数:
$age = (new \yii\db\Query)
->select('MAX(age)')
->from('user')
->scalar();
最后,我们可能发现检索数据库结果的第一个列是有益的,例如在使用groupBy()或having()方法的情况下。要获取结果的第一行,我们可以使用column()方法:
$result = (new \yii\db\Query)
->from('user')
->column();
在上一个示例中,我们用户表的第一个列是ID字段。因此,将返回我们数据库中所有 ID 的数组。
小贴士
选择所有列(*)将导致所有记录被加载到内存中,这取决于表的大小,可能会导致性能下降。在查询数据时,请记住你只查询你需要的数据。如果你需要所有数据,你可以以迭代方式查询,例如限制每个查询所需的内存。
查询检查
在构建查询后,你可能想检查生成的查询。为此,可以使用createCommand()方法将查询构建器对象转换为 DAO 命令:
$command = $query->select(['first_name', 'last_name'])
->from('user')
->where('id = :id', [':id' => 1])
->createCommand();
// Show the generated SQL statement
echo $command->sql;
// Show the bound parameters
var_dump($command->params);
// Execute the query via normal DAO commands
$rows = $command->queryAll();
遍历查询结果
通常在处理大量数据集时,结果数据集可能太大而无法加载到内存中。为了保持内存消耗低并防止我们的应用程序挂起,我们可以使用batch()或each()方法。默认情况下,这两种方法将从数据库中检索 100 行。要更改要检索的行数,只需更改每个方法的第一参数:
$query->from('user');
// $users will ben an array of 100 or fewer rows from the database
foreach ($query->batch() as $users) {}
// Whereas the each() method allows you to iterate over the first 50 or fewer users one by one
foreach ($query->each(50) as $user) {}
batch() 方法支持分批获取数据,这可以降低内存消耗。将此方法视为附加了限制和偏移参数的查询,这将限制返回的行数。batch() 查询的每次迭代将包含多个结果。与batch() 方法类似,each() 方法也可以用来减少内存消耗,但它将逐行迭代查询,这意味着该方法的每次迭代将只产生我们数据的一个实例。
数据提供者和数据小部件
在 Yii2 中,数据提供者是辅助类,用于通过查询构建器提取数据,并将其传递给数据小部件。使用数据提供者和数据小部件而不是通过查询构建器构建查询的好处是,它们提供了一个接口来自动处理排序和分页。
与数据提供者一起工作的最常见方式是使用yii\data\ActiveDataProvider类。通常,yii\data\ActiveDataProvider将与 Active Record 模型一起使用:
$provider = new ActiveDataProvider([
'query' => User::find(),
'pagination' => [
'pageSize' => 20,
],
]);
提示
我们将在第四章中介绍如何创建和使用 Active Record 和模型,活动记录、模型和表单。
活动数据提供者也可以通过查询构建器填充,如下例所示:
$query = new yii\db\Query();
$provider = new ActiveDataProvider([
'query' => $query->from('user'),
'pagination' => [
'pageSize' => 20,
],
]);
注意
Yii2 提供了两种额外的数据提供者类型:yii\data\ActiveDataProvider 和 yii\data\SqlDataProvider。有关这些数据提供者的更多信息,请参阅 Yii2 指南中的www.yiiframework.com/doc-2.0/guide-output-data-providers.html。
一旦我们使用数据提供者获取了数据,我们可以将结果数据传递给数据小部件。Yii2 中的数据小部件是可重用的构建块,用于在视图中创建与数据交互的复杂界面。最常见的数据小部件是DetailView、ListView和GridView,它们的行为类似于它们的 Yii1 对应物。
例如,我们可以将之前的数据提供者输出到GridView中,如下所示:
$query = new yii\db\Query();
$provider = new yii\data\ActiveDataProvider([
'query' => $query->from('user'),
'pagination' => [
'pageSize' => 2,
],
]);
echo yii\grid\GridView::widget([
'dataProvider' => $provider
]);
仅就其本身而言,我们的结果GridView小部件将显示我们数据库表中的所有字段。在某些情况下,可能存在我们不希望在页面上显示的敏感数据。或者,可能数据太多,无法在GridView中显示。为了限制在GridView小部件中显示的字段数,我们可以使用columns属性:
echo yii\grid\GridView::widget([
'dataProvider' => $provider,
'columns' => [
'id',
'email',
'first_name',
'last_name',
'created_at',
'updated_at'
]
]);

我们可以使用yii\data\Sort类进一步增强我们的数据提供者,该类为我们提供数据提供者的排序功能。要向我们的数据提供者添加排序,我们需要在yii\data\ActiveDataProvider中指定yii\data\Sort实例,该实例指定了可以排序的属性:
$query = new yii\db\Query();
$provider = new yii\data\ActiveDataProvider([
'query' => $query->from('user'),
'sort' => new yii\data\Sort([
'attributes' => [
'email',
'first_name',
'last_name'
]
]),
'pagination' => [
'pageSize' => 2,
],
]);

如上图所示,sort 属性中列出的属性现在可以通过我们的数据提供者进行点击和排序。
小贴士
更多有关输出数据小部件的信息可以在 Yii2 指南中找到,请参阅www.yiiframework.com/doc-2.0/guide-output-data-widgets.html。
虽然一些小部件,如 GridView,允许我们处理和显示多行,但我们也可以使用数据提供者和数据小部件来显示单行的信息。使用 DetailView 小部件,我们可以动态配置一个简单的界面来显示数据库中特定用户的信息。我们数据提供者的 getModels() 方法将数据提供者拆分为 DetailView 小部件可以理解的单独模型:
echo yii\widgets\DetailView::widget([
'model' => $user,
'attributes' => [
'id',
'first_name',
'last_name',
'email',
// Format the updated dates as datetime object
// Rather than an integer
'updated_at:datetime'
]
]);
小贴士
通常在处理 DetailView 小部件时,我们会向其提供一个 Active Record 实例,而不是从数据提供者生成的模型,这将在第四章中介绍,Active Record,模型和表单。
这将在我们的屏幕上显示:

除了简单地显示数据库中的结果外,DetailView 小部件还支持某些行的自定义格式化。在我们的上一个示例中,我们能够通过在更新字段中指定 :datetime 格式化器,将存储在 updated_at 字段中的 Unix 时间戳格式化为可读的日期和时间字段:
'attributes' => [
[...],
'updated_at:datetime'
]
小贴士
这里列出的格式化器是一个强大的工具,它允许我们快速将原始数据转换为有用的可读信息。有关格式化器的更多信息,请参阅www.yiiframework.com/doc-2.0/yii-i18n-formatter.html。
数据复制和负载均衡
当我们开始处理更大和更复杂的系统时,我们经常发现需要在系统中构建额外的冗余,以便实现高可用性和防止意外停机。当处理大型系统时,我们将数据库拆分为一个读写主数据库和一组从数据库的只读从数据库。通常,我们的应用程序对我们的数据库架构一无所知,这可能会在需要从新主数据库迁移时引入问题。使用 Yii2,我们可以配置我们的数据库连接,使其不仅了解我们的主从数据库配置,而且可以智能地处理从数据库不可用的情况。
在 Yii2 中,我们可以使用以下数据库配置来配置单个主数据库和多个从数据库。这将导致所有写入操作都发送到我们声明的主数据库,所有读取操作都发送到我们声明的从数据库之一:
$config = [
'class' => 'yii\db\Connection',
// configuration for the master
'dsn' => '<master_dns>',
'username' => 'master',
'password' => '<master_password>',
// common configuration for slaves
'slaveConfig' => [
'username' => 'slave',
'password' => '<slave_password>',
'attributes' => [
// Use a small connection timeout
PDO::ATTR_TIMEOUT => 10,
],
],
// List of slave configurations.
'slaves' => [
['dsn' => '<slave1_dsn>'],
['dsn' => '<slave2_dsn>'],
['dsn' => '<slave3_dsn>'],
]
];
$db = Yii::createObject($config);
// Would execute against an available slave
$users = $db->createCommand('SELECT * FROM user')->queryAll();
// Would execute against the master
$db->createCommand('UPDATE user SET updated_at = NOW()')->execute();
小贴士
通常,使用 execute() 方法执行的查询将针对主数据库运行,而所有其他查询将针对从数据库之一运行。
在此配置中,Yii 将会对主节点执行写查询(如 UPDATE、INSERT 和 DELETE),并对可用的从节点之一执行读查询(如 SELECT)。当与从节点协同工作时,Yii 将会尝试连接列表中的从节点,直到从节点响应,并对每个可用的从节点进行查询的负载均衡。通过将 PDO::ATTR_TIMEOUT 设置为 10 秒,如果 Yii 在 10 秒内没有收到从节点的响应,它将终止尝试从从节点检索数据,并且会记住从节点的状态,直到配置生效期间。
或者,使用以下配置,我们可以配置我们的应用程序同时与多个主节点和多个从节点协同工作。当使用多个主节点时,Yii 将会对任何可用的主节点执行写操作,并在可用的主节点之间进行写操作的负载均衡:
$config = [
'class' => 'yii\db\Connection',
'masterConfig' => [
'username' => 'master',
'password' => '<master_password>',
'attributes' => [
// use a smaller connection timeout
PDO::ATTR_TIMEOUT => 10,
],
],
// list of master configurations
'masters' => [
['dsn' => '<master1_dsn>'],
['dsn' => '<master2_dsn>'],
],
'slaveConfig' => [...],
'slaves' => [...]
];
小贴士
如果 Yii2 无法连接到任何可用的主节点,将会抛出异常。
当与主从拓扑结构协同工作时,我们可能希望对某个主节点发起一个读查询。为此,我们需要明确告诉 Yii2 将我们的查询运行在主节点上而不是从节点上:
$rows = $db->useMaster(function ($db) {
return $db->createCommand('SELECT * FROM user')->queryAll();
});
当与事务协同工作时,Yii2 默认会尝试在我们的主节点上运行事务。如果我们需要在从节点上发起一个事务,我们需要在从节点上显式地开始事务,如下所示:
$transaction = $db->slave->beginTransaction();
摘要
在本章中,我们介绍了使用 Yii2 处理数据库的基础。通过使用数据库访问对象,我们展示了如何执行针对数据库的原始 SQL 语句,以及如何使用事务来保护数据库的完整性。我们还展示了如何使用查询构建器,它能够以编程方式使我们能够编写数据库无关的查询。然后我们发现如何使用查询构建器构建智能数据提供者,这些提供者用于向可重用的数据小部件提供数据。最后,我们学习了如何配置 Yii2 以识别主从和多主数据库集群配置,以及如何在这些连接之间进行负载均衡。
在下一章中,我们将发现使用 Yii2 处理数据库的基石——Active Record,这是一个强大的工具,用于处理我们的数据和建模数据库结构。我们还将深入了解 Active Record 的相关内容,基本模型和表单,并学习如何使用一个名为 Gii 的强大工具来自动化构建现代应用程序将使用的大部分代码。
第四章:Active Record、模型和表单
与许多现代 Web 框架一样,Yii2 自带了一些强大的类来表示数据库内外部的数据。这些类使我们能够将数据管理代码从 DAO 和查询构建器中抽象出来,进入一个易于使用的程序接口。在本章中,我们将介绍 Active Record 的使用和实现,学习如何创建数据模型和自定义表单。我们还将介绍如何配置一个名为Gii的强大代码生成工具,以自动化 Active Record 模型和表单的创建。
配置 Gii
虽然 Active Record 模型和表单可以手动生成,但在大多数情况下,我们希望自动化这一代码的创建。为了实现这一点,Yii2 提供了一个名为 Gii 的代码生成工具,它可以从命令行和 Web 界面执行,以创建与我们的数据库结构兼容的 Active Record 模型以及与我们的基础模型和 Active Record 模型兼容的表单。
与 Yii1 不同,Gii 在 Yii2 中不是预打包的。在 Yii2 中,几乎每个模块都可以作为一个独立的 Composer 包提供,可以从命令行界面进行安装。因此,我们必须使用 Composer 将 Gii 包含到我们的应用程序中。由于 Gii 作为一个 Composer 包提供,我们可以通过在命令行中运行以下命令来将其包含到我们的应用程序中:
$ composer require yiisoft/yii2-gii --dev

小贴士
由于 Gii 是一个开发工具,并且具有向我们的应用程序写入新代码的能力,因此我们应该使用--dev标志,以便 Composer 将其添加到composer.json文件的require-dev部分。通常,在我们的部署过程中,我们将使用--no-dev标志,以确保开发包不会被部署到我们的生产环境中。
安装 Gii 后,我们现在需要配置它,使其既能与 Yii2 控制台协同工作,也能在我们的 Web 浏览器中运行。
Gii 用于 Web 应用程序
要启用 Gii 的 Web 界面,我们需要在config/web.php配置文件中的module部分指定,并引导 Gii 模块,以确保它正确加载:
return [
'bootstrap' => ['gii'],
'modules' => [
'gii' => [
'class' => 'yii\gii\Module',
'allowedIPs' => ['*']
]
// [...]
],
// [...]
];
小贴士
默认情况下,Gii 仅在您的机器的 loopback 接口上可用。如果您正在使用远程开发服务器或虚拟机,您需要将您的宿主 IP 地址添加到allowedIPs块中,或者将allowedIPs块设置为通配符字符*,以便授予您的计算机访问 Gii 的权限。
虽然这个基本配置可以正确加载 Gii 模块,但它并不遵循我们关于环境感知的约定。例如,如果我们带着这个配置进入生产环境,并使用前面描述的composer install --no-dev进行部署,那么我们的应用程序将会崩溃,因为 Composer 没有在我们的 vendor 文件夹中安装 Gii 模块。
幸运的是,因为我们之前在我们的引导文件中定义了 APPLICATION_ENV 常量而不是返回包含我们的配置文件的静态数组,我们可以将配置存储为变量,并根据我们正在工作的环境有条件地修改它以包含 Gii 模块:
<?php
$config = [
'id' => 'basic',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'components' => [
'request' => [
'cookieValidationKey' => '<random_key>',
],
'cache' => [
'class' => 'yii\caching\FileCache',
],
'user' => [
'identityClass' => 'app\models\User',
'enableAutoLogin' => true,
],
'errorHandler' => [
'errorAction' => 'site/error',
],
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
],
],
],
'db' => require(__DIR__ . '/db.php'),
],
'params' => require(__DIR__ . '/params.php'),
];
if (APPLICATION_ENV == "dev")
{
$config['bootstrap'][] = 'gii';
$config['modules'] = [
'gii' => [
'class' => 'yii\gii\Module',
'allowedIPs' => ['*']
]
];
}
return $config;
小贴士
作为 APPLICATION_ENV 的替代方案,您可以使用通常在您的引导文件中定义的 YII_ENV_DEV 常量有条件地加载 Gii:
if (YII_ENV_DEV) // YII_ENV_DEV = true. Define in ./yii
{ // to enable this constant
$config['bootstrap'][] = 'gii';
$config['modules'] = [
'gii' => 'yii\gii\Module'
];
}
对于我们的配置文件,使用任一常量都是合适的。然而,大多数开发者发现,允许他们的 Web 服务器或命令行定义 APPLICATION_ENV 常量比手动管理 YII_ENV_DEV 常量需要更少的维护。
现在可以通过在 Web 浏览器中导航到我们的应用程序路径并将 URI 更改为 /index.php?r=gii 来访问 Gii。

小贴士
如果您已经为您的应用程序启用了美观的 URL,可以通过导航到 /gii 端点来访问 Gii。
控制台应用的 Gii
与 Yii1 不同,Yii2 的 Gii 提供了一个新的命令行界面来与 Gii 一起工作。在 Yii2 中,我们现在可以从命令行界面生成活动记录模型、表单甚至扩展的源代码。
启用 Gii 的最简单方法是将我们的 config/console.php 文件修改为在配置文件的 module 部分包含 Gii 模块,然后启动 Gii 模块本身,如下所示:
return [
'bootstrap' => ['gii'],
'modules' => [
'gii' => 'yii\gii\Module',
// [...]
],
// [...]
];
与我们的 Web 应用程序一样,这种基本配置并不能确保我们的应用程序在所有环境中都能正常工作。我们可以以与我们的 Web 配置文件相同的方式重新配置我们的 config/console.php 文件,以确保 Gii 模块仅在开发环境中加载:
<?php
Yii::setAlias('@tests', dirname(__DIR__) . '/tests');
$config = [
'id' => 'basic-console',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log'],
'controllerNamespace' => 'app\commands',
'components' => [
'cache' => [
'class' => 'yii\caching\FileCache',
],
'log' => [
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
],
],
],
'db' => require(__DIR__ . '/db.php'),
],
'params' => require(__DIR__ . '/params.php'),
];
if (APPLICATION_ENV == "dev")
{
$config['bootstrap'][] = 'gii';
$config['modules'] = [
'gii' => 'yii\gii\Module'
];
}
return $config;
如前一个代码块所示,如果不需要在我们的模块中注册其他选项,我们可以使用更短的语法来加载配置文件的 module 部分,这通常是加载模块的首选方式:
$config['modules'] = [
'gii' => 'yii\gii\Module'
];
在配置我们的控制台环境后,现在我们可以通过在 ./yii 命令中调用帮助命令来从命令行运行 Gii:
$ ./yii help gii

在配置我们的控制台应用程序使用 Gii 后,我们现在可以使用 Gii 工具来创建代码。在我们继续本章的其余部分时,我们将介绍如何从 Web 界面以及控制台界面使用 Gii。
活动记录
在构建丰富的 Web 应用程序时,最重要的任务之一是确保我们在代码中正确地建模和表示我们的数据。从简单的博客网站到像 Twitter 这样的大型应用程序,数据建模和表示对于确保我们的应用程序易于使用并且可以按需扩展至关重要。为了帮助我们建模数据,Yii2 实现了活动记录模式,也称为 yii/db/ActiveRecord 类中的活动记录。
活动记录模式
由马丁·福勒在他的 2003 年出版的《企业应用架构模式》一书中命名,Active Record 模式是一种 对象关系映射(ORM)模式,用于在对象中表示数据库行和列。在 Active Record 模式中,每个数据库列都由一个单独的 Active Record 类表示。实例化后,该对象提供了一个简单的接口来管理我们代码中的单个行或行集合。可以创建新行,删除旧行,以及更新现有行——所有这些都在一个简单且一致的 API 中完成。Active Record 还使我们能够以编程方式引用和交互相关数据,这些数据通常由外键关系在我们的数据库中表示。
在 Yii2 中,Active Record 由 yii/db/ActiveRecord 类实现,通常被认为是表示和操作数据库中数据的首选类。虽然许多框架和 ORM 只为关系数据库实现 Active Record,但 Yii2 还为搜索工具(如 Sphinx 和 ElasticSearch)以及 NoSQL 数据库(如 Redis 和 MongoDB)实现了 Active Record。在本节中,我们将介绍如何创建新的 Active Record 类,如何在我们的代码中实现它们,以及一些与 Active Record 一起工作的常见陷阱。
在我们开始使用 Active Record 之前,我们首先需要创建一些我们可以工作的表。本章的项目资源中包含一个基础迁移,它将创建几个新表并将一些示例数据填充到它们中:
$ ./yii migrate/up –interactive=0

运行迁移后,您可以通过从 sqlite3 工具运行 .schema 命令来验证以下模式是否存在于我们的数据库中。
创建 Active Record 类
要在 Yii2 中开始使用 Active Record,我们首先需要在我们的应用程序中声明一个 yii/db/ActiveRecord 的实例。由于 Yii2 中的 Active Record 实例是从 yii/base/Model 类扩展出来的,并且被视为模型,因此我们通常将它们存储在我们的应用程序的 models/ 目录下,并在 app/models 命名空间下。
注意
在 Yii2 中,@app 是一个预定义的别名,它指向我们的应用程序基本路径。因此,我们应用程序中声明的任何命名空间通常采用 app<文件夹> 的形式,这使得 Yii2 的内置自动加载器能够自动引用在 /文件夹/ClassName.php 中找到的类。如果我们想的话,我们可以声明额外的别名,例如 @frontend 和 @backend,将我们的应用程序划分为不同的部分,这将使我们能够在不同的命名空间中创建多个 Active Record 实例。
在本章中,为了保持简单,我们只声明位于 app\models 命名空间内的那些 Active Record 类。
为了说明一个例子,让我们为我们在 第三章 中创建的 user 表创建一个 Active Record 类,迁移、DAO 和查询构建:
-
首先,我们需要在我们的应用程序的
models/目录中创建一个新文件,命名为User.php:touch models/User.php -
接下来,我们需要声明 Active Record 实例将存在的命名空间,并扩展
yii/db/ActiveRecord类:<?php namespace app\models; use yii\db\ActiveRecord; class User extends ActiveRecord {} -
最后,我们需要在我们的类中实现静态方法
tableName(),它定义了我们的 Active Record 模型将使用的表名。由于我们的 Active Record 模型将使用user表,我们将定义此方法如下:/** * @return string the string name of the database table */ public static function tableName() { return 'user'; }
使用 Gii 创建活动记录类
虽然我们可以手动创建 Active Record 实例,但通常我们希望使用 Gii 来为我们创建这些类。使用 Gii 创建我们的 Active Record 类有几个优点:除了创建类之外,它还会为我们的字段创建属性标签,根据我们的数据库模式创建验证规则,并根据我们数据库的外键结构生成与另一个 Active Record 类的模型关系。
使用 Gii 的网页界面
与 Yii1 类似,Gii 提供了一个友好且易于使用的网页界面来创建我们的 Active Record 实例。要开始使用 Gii,请导航到我们应用程序的 /gii 端点,并在 模型生成器 部分下点击 开始 按钮。

从这个页面,我们可以根据我们的数据库模式生成 Active Record 类。例如,让我们为我们的 user 表创建一个 active record 实例:
-
首先,我们需要在数据库中的
user表的名称字段中填写user,这是我们的数据库中user表的名称。随着你输入,Gii 将尝试显示与我们的文本输入匹配的可能数据库表,这在处理大型数据库时非常有用。 -
接下来,我们需要按键盘上的 Tab 键,或者将鼠标焦点放在
模型名称字段上,该字段应自动填充为User,这将是我们将生成的类的名称。 -
然后,我们需要确保选中 生成关系 复选框。这将自动将所需的代码添加到我们的类中,以便为我们在下一节中创建的
Post和Role类生成模型关系。在选中此框后,我们的表单应填写如下:![使用 Gii 的网页界面]()
-
然后,我们可以在页面底部的 预览 按钮上点击,这将允许我们在确认创建我们的类之前预览 Gii 将为我们生成的代码。
![使用 Gii 的网页界面]()
-
在预览了类之后,我们可以点击 生成 按钮来生成我们的
User类,该类将位于models/User.php。![使用 Gii 的网页界面]()
小贴士
为了为我们创建新的类,运行我们服务器的网络用户需要对我们models/目录有写权限。如果 Gii 返回错误,表明它无法写入models/目录,您需要调整目录的权限。在我们的 Linux 环境中,这可以通过将www-data组添加到文件夹并调整权限,使用户能够写入它来完成:
chown –R <me>:www-data /path/to/models/
chmod –R 764 /path/to/models/
作为一种替代方案,您可以使用chmod工具调整models/目录的权限为777。只需确保在使用 Gii 创建模型后,将权限重新调整到一个更合理的值。
默认情况下,Gii 被配置为将新模型添加到我们应用程序的models/文件夹中,并在app/models命名空间下创建模型。此外,yii/db/ActiveRecord类被配置为自动使用我们应用程序的db组件。所有这些字段都可以在 Gii 网页界面中配置,以便我们进行更改。
使用 Gii 的命令行界面
作为 Gii 网页界面的替代方案,Gii 可以从命令行生成 Active Record 类。当从命令行运行 Gii 时,我们只需提供两个属性:我们正在处理的表名和模型名。这具有以下形式:
./yii gii/model --tableName=<tablename> --modelClass=<ModelName>
例如,我们可以通过运行以下命令创建我们的帖子表类:
./yii gii/model --tableName=post --modelClass=Post

当我们在这里时,让我们也为我们role表创建一个类:
./yii gii/model --tableName=role --modelClass=Role
与 Active Record 一起工作
现在我们已经生成了模型,让我们看看 Gii 实际上写入磁盘的内容。我们将首先打开models/User.php,它应该与以下代码块相同:
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "user".
*
* @property integer $id
* @property string $email
* @property string $password
* @property string $first_name
* @property string $last_name
* @property integer $role_id
* @property integer $created_at
* @property integer $updated_at
*
* @property Post[] $posts
* @property Role $role
*/
class User extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
{
return 'user';
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['email', 'password'], 'required'],
[['role_id', 'created_at', 'updated_at'], 'integer'],
[['email', 'password', 'first_name', 'last_name'], 'string', 'max' => 255],
[['email'], 'unique']
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'email' => 'Email',
'password' => 'Password',
'first_name' => 'First Name',
'last_name' => 'Last Name',
'role_id' => 'Role ID',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
];
}
/**
* @return \yii\db\ActiveQuery
*/
public function getPosts()
{
return $this->hasMany(Post::className(), ['author_id' => 'id']);
}
/**
* @return \yii\db\ActiveQuery
*/
public function getRole()
{
return $this->hasOne(Role::className(), ['id' => 'role_id']);
}
}
模型验证规则
在我们生成的 Active Record 类中,我们应该注意的第一个部分是rules()方法,这是 Gii 为我们生成的。由于yii/db/ActiveRecord扩展了yii/base/Model,它继承了yii/base/Model所有的验证逻辑和工具:
public function rules()
{
return [
[['email', 'password'], 'required'],
[['role_id', 'created_at', 'updated_at'], 'integer'],
[['email', 'password', 'first_name', 'last_name'], 'string', 'max' => 255],
[['email'], 'unique']
];
}
当 Gii 创建我们的模型时,它扫描我们的数据库模式以确定任何我们认为默认需要的验证规则。如前一个代码块所示,它已将email和password属性标记为required,将email字段标记为unique,并且正确地识别了我们的名称字段以及时间戳的适当数据类型。
Yii 中的rules()方法包含一个验证规则数组,其格式如下:
[
// Specifies which attributes should be validated, REQUIRED
['attr', 'attr2', ...],
// Specifies the validator to be used, REQUIRED
// Can be either a built in core validator,
// a custom validator method name, or a validator alias
'validator',
// Specifies the scenarios that the validator should
// run on, OPTIONAL
'on' => ['scenario1', 'scenario2', ...],
// Specifies additional properties to be passed
// to the validator, OPTIONAL
'property1' => 'value1', 'property2' => 'value2'
]
小贴士
内置验证器的完整列表可以在 Yii2 指南中找到,网址为www.yiiframework.com/doc-2.0/guide-tutorial-core-validators.html。
添加自定义验证器
除了 Yii2 内置的许多核心验证器外,我们可能还需要为我们自己的类编写自定义验证器。自定义验证器可以是使用匿名函数编写的内联代码,也可以是在我们的类中作为单独的方法编写的。
例如,假设我们只想在业务的核心时段允许更改我们的用户信息。作为一个匿名函数,这可以写成如下:
public function rules()
{
return [
// [... other validators ..],
// an inline validator defined as an anonymous function
['email', function ($attribute, $params) {
$currentTime = strtotime('now');
$openTime = strtotime('9:00');
$closeTime = strtotime('17:00');
if ($currentTime > $openTime && $currentTime < $closeTime)
return true;
else
$this->addError('email', 'The user\'s email address can only be changed between 9 AM and 5 PM');
}],
];
}
或者,我们可以通过提供一个验证器的名称来将其作为单独的方法编写,然后在我们的类中使用该名称作为方法:
public function rules()
{
return [
// [... other validators ..],
// a custom validator
['email', 'validateTime']
];
}
public function validateTime($attributes, $params)
{
$currentTime = strtotime('now');
$openTime = strtotime('9:00');
$closeTime = strtotime('17:00');
if ($currentTime > $openTime && $currentTime < $closeTime)
return true;
else
$this->addError('email', 'The user\'s email address can only be changed between 9 AM and 5 PM');
}
此外,可以通过创建和扩展 yii\validators\Validator 类,并在该类中实现 validateAttribute($model, $attribute) 方法来编写自定义验证器:
// app/models/User.php::rules()
public function rules()
{
return [
// [... other validators ..],
// a custom validator
['email', 'EditableTime']
];
}
// app/components/EditableTimeValidator.php
<?php
namespace app\components;
use yii\validators\Validator;
class EditableTimeValidator extends Validator
{
public function validateAttribute($model, $attribute)
{
$currentTime = strtotime('now');
$openTime = strtotime('9:00');
$closeTime = strtotime('17:00');
if ($currentTime > $openTime && $currentTime < $closeTime)
return true;
else
$this->addError($model, $attribute, 'The user\'s email address can only be changed between 9 AM and 5 PM');
}
}
}
自定义验证器错误消息
几乎所有的 Yii2 验证器都自带内置的错误消息。然而,如果我们想更改特定属性的错误消息,我们可以通过为特定验证器指定消息参数来实现。例如,我们可以通过更改验证器的最后一行来调整我们唯一验证器的错误消息,如下所示:
[['email'], 'unique', 'That email address is already in use by another user!']
与验证错误一起工作
Yii2 提供了多种方式来交互和自定义错误,这些错误会在发生时出现。正如你可能在前面的示例中注意到的,我们可以使用 yii/base/Model 方法以及 addError() 在我们的工作流程中为模型属性添加新的错误。正如前一个示例所示,这通常采取以下形式:
$this->addError($attribute, $message);
此外,我们可以使用 getError() 方法来检索我们模型的全部错误或特定属性的错误。此方法将返回一个包含适用于每个属性的错误消息数组的错误数组:
[
'email' => [
'Email address is invalid.',
'The user\'s email address can only be changed between 9 AM and 5 PM'
],
'password' => [
'Password is required.'
],
]
手动执行验证规则
在 Yii2 中,验证规则是在调用 yii/db/ActiveRecord 上的 validate() 方法时执行的。虽然这可以在我们的控制器中手动完成,但通常是在 save() 方法执行之前执行的。验证器方法将返回 true 或 false,指示验证是否成功。
validate() 方法可以通过覆盖 beforeValidate() 和 afterValidate() 方法或监听 yii\base\Model::EVENT_BEFORE_VALIDATE 或 yii\base\Model::EVENT_AFTER_VALIDATE 事件来扩展。
小贴士
我们将在第八章路由、响应和事件中更详细地介绍事件。
模型属性标签
Gii 为我们自动实现的下一个方法是attributeLabels()方法。attributesLabels()方法使我们能够用更描述性的名称命名我们的模型属性,这些名称可以用作表单标签。默认情况下,Gii 会根据我们的列名自动为我们生成标签。此外,通过遵循在我们的user表中使用下划线的约定,Gii 已经自动为我们创建了标题化和可读的属性标签:
public function attributeLabels()
{
return [
'id' => 'ID',
'email' => 'Email',
'password' => 'Password',
'first_name' => 'First Name',
'last_name' => 'Last Name',
'role_id' => 'Role ID',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
];
}
由于我们的attributeLabels()方法仅仅返回一个键值对数组,我们可以通过使用\Yii::t()方法将我们的属性标签翻译成多种语言来增强我们的应用程序:
public function attributeLabels()
{
return [
'id' => 'ID',
'email' => \Yii::t('app', 'Email'),
// [ ... other attribute labels ... ]
];
}
假设我们的应用程序已经正确配置以使用翻译,我们可以使用getAttributeLabel()方法为我们的email属性获取翻译后的文本:
$user->getAttributeLabel('email'); // returns "Email"
如果我们的应用程序配置为英语区域设置,它将简单地返回字符串"Email";然而,如果我们的应用程序在不同的语言中运行,比如说西班牙语,这个方法将返回字符串"Correo"而不是"Email"。
小贴士
我们将在第十一章国际化与本地化中介绍Yii::t()方法以及一般化的国际化与本地化。
Active Record 关系
假设我们已经正确配置了数据库模式,包括主键和外键,Yii2 也会为我们生成模型关系。与 Yii1 不同,Yii2 已经废弃了relations()方法,并用魔法__getter()方法取代了它们。我们的User模型展示了Post和Role关系:
/**
* @return \yii\db\ActiveQuery
*/
public function getPosts()
{
return $this->hasMany(Post::className(), ['author_id' => 'id']);
}
/**
* @return \yii\db\ActiveQuery
*/
public function getRole()
{
return $this->hasOne(Role::className(), ['id' => 'role_id']);
}
Yii2 也简化了关系方法,现在它只支持两种类型的关系:由hasOne()方法使用的单对一关系,以及由hasMany()方法定义的多对一关系。尽管如此,与 Yii1 一样,可以通过简单地调用__getter()方法来访问相关数据。例如,如果我们想获取我们正在处理的一个用户的角色名称,我们可以简单地执行以下操作:
$user = new User::findOne(4); // Fetch a user in our db
echo $user->role->name; // "Admin"
小贴士
Gii 根据你的数据库模式做出几个推断来创建模型关系。在执行代码之前,请检查你的关系是否映射到正确的类,并且具有正确的关联类型。
使用 Active Record 的多数据库连接
默认情况下,所有活动记录实例都将使用db组件连接到我们的数据库。在我们有多个数据库连接到我们的应用程序的情况下,我们可以通过在 Active Record 类中定义静态方法getDb()来配置活动记录以使用备用数据库:
public static function getDb()
{
// the "db2" component
return \Yii::$app->db2;
}
Active Record 中的行为
Yii2 支持多种行为,这些行为可以用来自动处理模型管理中的一些更繁琐的任务,例如管理创建和更新时间、自动为我们的应用程序创建 URL 拼接,以及记录哪个用户创建了特定记录。
要在 Yii2 中使用行为,我们只需在 PHP 文件顶部指定我们想要使用的行为类,然后将行为添加到模型的 behaviors() 方法中。例如,由于我们的 User 和 Post 类都有 created_at 和 updated_at 属性,我们可以添加以下内容,让 Yii2 为我们管理这些属性:
<?php
use Yii;
use yii\behaviors\TimestampBehavior
class User extends yii\db\ActiveRecord
{
/**
* Allow yii to handle population of
* created_at and updated_at time
*/
public function behaviors()
{
return [
TimestampBehavior::className(),
];
}
// [... other methods ...]
}
默认情况下,yii\behaviors\TimestampBehavior 类将使用从原生 PHP time() 函数提取的当前时间填充 created_at 和 updated_at 属性。像 Yii2 中的大多数事情一样,这是完全可配置的。例如,如果我们的数据库中创建了使用 MySQL TIMESTAMP 列类型的创建和更新字段,我们可以按以下方式调整行为:
public function behaviors()
{
return [
[
'class' => TimestampBehavior::className(),
'createdAtAttribute' => 'created',
'updatedAtAttribute' => 'updated',
'value' => new \yii\db\Expression('NOW()'),
],
];
}
小贴士
更多关于行为的信息可以在位于 www.yiiframework.com/doc-2.0/guide-concept-behaviors.html 的 Yii2 指南中找到。
使用 Active Record
现在我们已经了解了 Gii 在创建新的 Active Record 类时为我们自动提供了什么,以及我们可以向我们的类添加哪些附加选项来增强它们,让我们看看我们如何可以使用 Active Record 实例来执行基本的创建、读取、更新和删除(CRUD)操作。
查询数据
要使用 Active Record 查询数据,我们可以使用 yii/db/ActiveRecord::find() 方法来查询数据,这将返回一个 yii/db/ActiveQuery 实例。由于 yii/db/ActiveQuery 扩展了 yii/db/Query,我们可以利用我们在 第三章 中学习到的几乎所有方法和查询对象,即 迁移、DAO 和查询构建。让我们看看使用 yii/db/ActiveRecord::find() 方法的几个不同示例。
// Find the user in our database with the ID of 1\.
// one() returns an instance of User model, for the user with id=1
$user = User::find()->where(['id' => 1])
->one();
// Find all users in our database and order them by ID
// Returns an array of User objects
$users = User::find()->orderBy('id'])
->all();
// Returns the number of users in our database
$userCount = User::find()->count();
作为 yii/db/ActiveQuery 的替代方案,yii/db/ActiveRecord 还提供了两个额外的查询数据方法,findOne(),它将返回查询的第一个 Active Record 实例,以及 findAll(),它将返回 Active Record 实例的数组。这两个方法都接受一个标量参数、一个标量参数数组或一个关联对数组来查询数据:
// Fetches user with the ID of 1
User::findOne(1);
// Fetches users with the ID of 1, 2, 3, and 4
User::findAll([1, 2, 3, 4]);
// Fetches admin users (role_id = 2 from migration)
// with the last name of Doe
User::findOne([
'role_id' => 2,
'last_name' => 'Doe'
]);
// Retrieves users with the last name of Doe
User::findAll([
'last_name' => 'Doe'
]);
注意
yii/db/ActiveRecord::findOne() 方法不会将 LIMIT 1 添加到生成的 SQL 查询中,这可能会导致查询运行时间更长,因为 yii/db/ActiveRecord::findOne() 将简单地从查询结果中获取第一行。如果您在使用 yii/db/ActiveRecord::findOne() 时遇到性能问题,请尝试使用 yii/db/Activequery::find() 方法,并配合 limit() 和 one() 方法,如下所示:
User::find()->limit(1)->one();
有时使用yii/db/ActiveQuery可能会非常消耗内存,这取决于正在访问的记录数量。绕过这种限制的一种方法是将我们的结果数据转换为数组格式,使用asArray()方法:
$users = User::find()->asArray()
->all();
与返回 Active Record 实例的数组相比,asArray()方法将返回包含 Active Record 数据属性的数组数组。
小贴士
虽然asArray()方法可以用来提高大型查询的性能,但它有几个缺点。返回的数据将不是 Active Record 的实例,因此它将没有与之相关的任何方法或有用的属性。此外,由于数据是直接从 PDO 返回的,数据将不会自动类型转换,而是以字符串的形式返回。
数据访问
当使用 Active Record 时,数据库查询的每一行都会生成一个 Active Record 实例。Active Record 实例的列值可以通过该 Active Record 实例的模型属性来访问:
$user = User::findOne(1);
echo $user->first_name; // "Jane"
echo $user->last_name; // "Doe"
此外,可以通过相关对象的属性访问关系信息。例如,要从给定的帖子中检索作者的姓名,我们可以运行以下代码:
$post = Post::findOne(1);
echo $post->id; // "1"
// "Site Administrator"
echo $post->author->first_name . ' ' . $post->author->last_name;
注意
Active Record 属性以列名命名。如果您不希望 Active Record 属性包含下划线,不符合您的编码风格,您应该重命名您的列名。
我们的数据也可以通过在 Active Record 类中创建自定义获取器和设置器方法来操作。例如,如果我们想显示用户的完整姓名而不更改我们的数据库模式,我们可以在我们的 User Active Record 类中添加以下方法:
/**
* Returns the user's full name
* @return string
*/
public function getFullName()
{
return $this->first_name . ' ' . $this->last_name;
}
然后,可以直接通过getFullName()方法或作为伪属性直接访问这些数据:
$user = User::findOne(1);
echo $user->fullName; // "Jane Doe"
echo $user->getFullName(); // "Jane Doe"
同样地,我们也可以创建自定义设置器。例如,以下方法接受用户的完整姓名作为输入,并为我们填充first_name和last_name属性:
/**
* Set the users first and last name from a single variable
* @param boolean
*/
public function setFullName($name)
{
list($firstName, $lastName) = explode(" ", $name);
$this->first_name = $firstName;
$this->last_name = $lastName;
return true;
}
然后,我们的设置器使我们能够将用户的完整姓名视为可设置的属性:
$user = User::findOne(1);
$user->fullName = 'Janice Doe'; // or $user->setfullName('Janice Doe');
echo $user->first_name; // "Janice"
echo $user->last_name; // "Doe"
保存数据
一旦我们对 Active Record 实例进行了更改,我们可以通过在实例上调用save()方法来将这些更改保存到数据库中,如果模型成功保存到数据库中,它将返回true,如果发生错误,它将返回false。
$user = User::findOne(1);
$user->first_name = "Janice";
$user->last_name = "Doe";
$user->save();
小贴士
如果在保存或验证过程中发生错误,您可以通过yii/db/ActiveRecord::getErrors()方法检索错误。
如果我们再次从数据库中检索用户信息,我们会看到结果被存储:
$user = User::findOne(1);
echo $user->first_name; // "Janice"
echo $user->last_name; // "Doe"
数据也可以通过yii/db/ActiveRecord::load()方法批量分配。通常当使用load()方法时,我们会提供来自表单提交的数据,我们将在本章后面介绍。
$user = User::findOne(1);
$user->load(\Yii::$app->request->post());
$user->save();
小贴士
\Yii::$app->request 表示请求对象,并在我们的 config/web.php 文件中进行配置。post() 方法表示通过 POST 请求提交的任何数据。
创建新记录
在我们的数据库中创建新记录可以通过使用 new 关键字实例化一个活动记录类的实例,用数据填充模型,然后在模型上调用 save() 方法来完成。
$user = new User;
$user->load(\Yii::$app->request->post());
/**
$user->attributes = [
'first_name' => 'Janice',
'last_name' => 'Doe',
// ... and so forth
];
*/
$user->save();
删除数据
也可以通过在模型上调用 delete() 方法从我们的数据库中删除数据。delete() 方法将从数据库中永久删除数据,并在删除成功时返回 true,如果发生错误则返回 false。
$user = User::findOne(1);
$user->delete(); // return true;
可以通过调用 yii/db/ActiveRecord::deleteAll() 静态方法来删除多行数据:
Post::deleteAll(['author_id' => 4]);
小贴士
使用 deleteAll() 方法时要小心,因为它将永久删除任何由条件语句指定的数据。条件语句中的错误可能导致整个表被截断。
Active Record 事件
作为创建 beforeSave() 和 afterDelete() 等前后方法处理程序的替代方案,Yii2 支持我们的应用程序可以监听的不同事件。Active Record 支持的事件在以下表中概述:
| 事件 | 描述 |
|---|---|
EVENT_INIT |
当通过 init() 方法初始化 Active Record 实例时触发的事件 |
EVENT_BEFORE_UPDATE |
在记录被更新之前触发的事件 |
EVENT_BEFORE_INSERT |
在记录被插入之前触发的事件 |
EVENT_BEFORE_DELETE |
在记录被删除之前触发的事件 |
EVENT_AFTER_UPDATE |
在记录被修改后触发的事件 |
EVENT_AFTER_INSERT |
在记录被插入后触发的事件 |
EVENT_AFTER_DELETE |
在记录被删除后触发的事件 |
EVENT_AFTER_FIND |
在记录被创建并用查询结果填充后触发的事件 |
小贴士
我们将在第八章(Chapter 8)中介绍事件的确切含义以及如何使用它们,路由、响应和事件。
模型
在 Yii1 中,基础模型和表单模型是两个分离的类(CModel 和 CFormModel)。在 Yii2 中,这两个类已经被合并为一个单一的类,yii/base/Model。这个类在 Yii2 中用于数据表示,并且当我们无法使用 yii/db/ActiveRecord 表示数据时,它应该是我们的首选类。
小贴士
由于 yii/db/ActiveRecord 扩展了 yii/base/Model,我们已经熟悉了 yii/base/Model 提供的大多数方法和属性,例如 getAttributes()、rules()、attributeLabels() 和 getErrors()。有关 yii/base/Model 支持的所有方法的完整列表,请参阅 Yii2 API 文档,链接为 www.yiiframework.com/doc-2.0/yii-base-model.html。
模型属性
在 yii/db/ActiveRecord 中,数据属性和属性名称直接从我们的数据库列名称中获取。在 yii/base/Model 中,数据属性和属性名称被定义为模型类中的公共属性。例如,如果我们想创建一个名为 UserForm 的模型来收集用户信息,我们可以编写以下类:
<?php
use Yii;
class UserForm extends yii/base/Model
{
public $email;
public $password;
public $name;
}
与活动记录实例不同,存储在基本模型中的信息不会被持久化。在类上调用 unset() 或创建该类的新实例不会授予用户访问其他模型实例中存储的数据的权限。由于我们的模型属性是 PHP 类的公共属性,我们可以像访问任何公共属性一样访问它们。
场景
当与模型或活动记录类一起工作时,我们可能希望在不同情况下重用相同的模型,例如登录用户或注册用户。为了帮助我们编写更少的代码,Yii2 提供了 scenarios() 方法来定义每个场景应执行哪些业务逻辑和验证规则。默认情况下,场景由我们的验证规则使用 on 属性确定:
public function rules()
{
return [
[['email', 'password'], 'required'],
[['email'], 'email'],
[['email', 'password', 'name'], 'string', 'max' => 255],
[['email', 'password'], 'required', 'on' => 'login'],
[['email', 'password', 'name'], 'required', 'on' => 'register'],
];
}
这种行为可以通过覆盖 scenarios() 方法并使用我们的自定义逻辑来定制:
public function scenarios()
{
return [
'login' => ['email', 'password'],
'register' => ['email', 'password', 'name']
];
}
或者,如果我们想在模型中添加新的场景而不更改模型验证规则中定义的当前场景,我们可以通过获取类的父场景,添加我们想要添加的新场景,然后返回更新后的场景数组来简单地添加它们:
public function scenarios()
{
$scenarios = parent::scenarios();
$scenarios['login'] = ['email', 'password'];
$scenarios['register'] = ['email', 'password', 'name'];
return $scenarios;
}
然后,我们可以在实例化我们的模型或定义模型在运行时的场景属性时控制哪个场景是活动的:
// Instantiate a model with a specific scenario
$model = new UserForm(['scenario' => 'login']);
// Set scenario at runtime
$model = new UserForm;
$model->scenario = 'register';
小贴士
当在运行时或模型实例化期间未指定场景时,将使用默认场景。默认场景将所有模型属性标记为对大量赋值和模型验证都处于活动状态。
表单
在 Yii2 中,我们可以使用 yii/widgets/ActiveForm 类根据我们的模型动态生成丰富的 HTML5 表单。与手动管理表单相比,yii/widgets/ActiveForm 类具有多个优势。除了提供几个有用的辅助方法并与 HTML 辅助类 yii/helpers/Html 配合良好外,我们还可以使用 Gii 工具通过我们的模型数据生成表单。当与模型和活动记录实例一起工作时,这是生成表单的首选方式。
使用 Gii 生成表单
与 Active Record 类一样,表单可以从网络 Gii 工具和控制台 Gii 工具自动为我们生成。让我们看看如何生成一个用于身份验证的表单,我们将它称为 LoginForm,以及一个用于处理注册的表单,我们将它称为 RegisterForm。
使用 Gii 网络界面生成表单
对于我们的 LoginForm 表单,让我们首先通过导航到应用程序的 /gii 端点并点击 表单生成器 部分下方的 开始 按钮来打开 Gii 网络工具。

与我们的模型生成器一样,要生成表单,我们只需要提供一些字段。对于表单,我们只需要知道视图名称(它将转换为文件名)和模型类。对于我们的视图名称,让我们使用 site/forms/LoginForm,对于我们的模型类,我们可能想要使用我们之前生成的 UserForm 类。由于我们只想使用表单进行登录,我们还应该指定我们想要使用 登录 场景。
小贴士
当指定模型类时,我们需要指定命名空间和类名,以便 Yii 能够找到我们的类。对于我们的 UserForm 类,我们需要提供 app\models\UserForm。

一旦我们指定了所有必要的属性,我们就可以点击 预览 按钮来预览我们的表单,然后我们可以点击 生成 按钮来生成源代码。

与我们的模型生成器不同,在生成后,我们的表单生成器还将为我们提供一个模板操作,我们可以将其放入我们的控制器中:
<?php
// app\controllers\SiteController.php::actionLogin()
public function actionLogin()
{
$model = new \app\models\UserForm(['scenario' => 'login']);
if ($model->load(Yii::$app->request->post())) {
if ($model->validate()) {
// form inputs are valid, do something here
return;
}
}
return $this->render('LoginForm', [
'model' => $model,
]);
}
使用 Gii 控制台界面生成表单
作为使用 Gii 网络界面生成表单的替代方案,我们还可以使用 Gii 的控制台界面为我们的模型类生成基本表单。要使用控制台界面生成表单,我们可以运行 gii/form 工具,如下例所示:
./yii gii/form --modelClass=app\\models\\UserForm --viewName=site/forms/RegisterForm --scenarioName=register --enableI18N=1
小贴士
大多数控制台外壳将反斜杠字符视为转义字符。为了将反斜杠字符传递给 Gii,我们需要用第二个反斜杠转义反斜杠字符。
这是输出:

我们生成的 RegisterForm 视图将如下所示:
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/* @var $this yii\web\View */
/* @var $model app\models\UserForm */
/* @var $form ActiveForm */
?>
<div class="site-forms-RegisterForm">
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'email') ?>
<?= $form->field($model, 'password') ?>
<?= $form->field($model, 'name') ?>
<div class="form-group">
<?= Html::submitButton(Yii::t('app', 'Submit'), ['class' => 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end(); ?>
</div><!-- site-forms-RegisterForm -->
小贴士
记住,Gii 的控制台界面只提示你提供生成类所需的最基本信息。请记住使用 help 工具来发现其他命令行参数以进行额外定制。
使用表单
现在我们已经创建了表单,让我们简要地看看它们是如何工作的。如前所述,我们的 yii\widgets\ActiveForm 类期望有一个模型来与之一起工作。在大多数情况下,这将定义在我们的控制器中,然后传递到我们的视图中:
$model = new app\models\UserForm(['scenario' => 'login']);
你可能会注意到我们生成的表单只包含核心表单逻辑,不包含额外的 HTML,例如 html、head 和 body 标签。在 Yii2 中,生成的表单旨在作为部分视图渲染,而不是完整视图。我们不是直接在我们的控制器中指定我们的表单 LoginForm,而是将我们的模型传递给父视图,然后该视图将渲染我们的表单。例如,我们控制器中的登录操作将更改为以下内容:
// app\controllers\SiteController::actionLogin()
return $this->render('login', [
'model' => $model,
]);
然后,我们将在 views/site/login.php 创建一个新的视图文件,该文件将渲染我们的 LoginForm:
// views/site/login.php
<div class="site-login" style="margin-top: 100px";>
<div class="body-content">
<?php echo $this->render('forms/LoginForm', [ 'model' => $model ]); ?>
</div>
</div>
小贴士
与 Yii1 不同,Yii2 没有用于渲染部分视图的 renderPartial() 方法。相反,它有两个独立的 render() 方法:一个在 yii/base/Controller 中,另一个在 yii/base/View 中。我们之前示例中调用的 render() 方法是从 yii/base/View 调用的,用于渲染任何视图文件,无论我们是否将其视为部分视图或完整视图。
如果我们导航到应用程序的 site/login 端点,我们的渲染链产生的视图将如下所示:

小贴士
第八章,路由、响应和事件,将帮助我们更好地理解 Yii2 中的路由工作原理以及我们如何轻松地找出哪些控制器操作与哪些视图操作匹配。
ActiveForm 和输入类型
现在我们知道了如何渲染我们的表单,让我们分解我们的表单视图。由于在 Yii2 中视图文件和控制器是分开的,我们首先需要确保我们在视图文件中使用我们的活动表单类:
<?php use yii\widgets\ActiveForm; ?>
我们的活动表单元素随后被包含在 ActiveForm 类的静态调用 begin() 和 end() 方法中。
<?php $form = ActiveForm::begin(); ?>
<?php ActiveForm::end(); ?>
默认情况下,我们的 begin() 方法会为我们提供一些内置的 HTML 默认值,例如 ID 和类属性。要自定义这些,我们可以向 begin() 方法提供一个参数数组以手动指定这些属性:
$form = ActiveForm::begin([
'id' => 'login-form',
'options' => [
'class' => 'form-horizontal'
]
]) ?>
我们需要注意的下一个关于我们的表单的事项是,模型属性被包裹在 $form->field() 的调用中:
<?= $form->field($model, 'email') ?>
<?= $form->field($model, 'password') ?>
field() 方法是一个可链式方法,用于指定模型属性的 <input> 标签,添加一些基本的客户端验证(例如 required 属性),并在模型验证错误实例的 POST 提交中填充 form 字段。由于该方法可链式,我们可以将其他属性附加到我们的表单上。例如,如果我们想为我们的 email 字段添加客户端验证,以便我们的浏览器可以验证我们的文本字段是否为电子邮件地址,我们可以链式以下内容:
<?= $form->field($model, 'email')->input('email') ?>
除了我们的必需验证器外,我们的视图现在还会验证我们的电子邮件是否为有效的电子邮件地址。

同样,我们可以自定义我们的 password 字段,通过指定该字段应为密码输入来遮挡我们的密码:
<?= $form->field($model, 'password')->passwordInput() ?>
使用 ActiveForm,我们还可以使用 hint() 方法和 label() 方法为任何属性添加内联提示或修改标签:
<?= $form->field($model, 'name')->textInput()->hint('Please enter your name')->label('Your Name') ?>

小贴士
虽然 $form 属性是 yii/widgets/ActiveForm 的一个实例,但 field() 方法返回的是 yii/widgets/ActiveField 的一个实例。有关 yii/widgets/ActiveField 所有的可用方法和选项的列表,请参阅 Yii2 文档中的www.yiiframework.com/doc-2.0/yii-widgets-activefield.html。
摘要
在本章中,我们涵盖了大量的信息!我们介绍了如何正确设置和配置 Gii,这是 Yii2 的代码生成工具。然后,我们介绍了如何使用 Gii 的网络和命令行界面自动创建基于我们数据库模式的 Active Record 类,以及我们可以绑定到我们的 Active Record 类的许多常见方法和属性,例如验证规则、属性标签和行为。接下来,我们介绍了如何创建不依赖于我们数据库的基本模型,以及如何向我们的模型和 Active Record 类添加场景。最后,我们介绍了如何使用 Gii 工具根据我们的模型创建 HTML 表单,并探讨了 ActiveForm 类附带的一些功能。
在下一章中,我们将扩展我们对 Yii2 所提供的可用帮助器和组件的了解。我们还将深入研究 Yii2 的模块,并探讨我们如何使用它们来创建可重用的自包含应用程序,这些应用程序将在整本书中不断构建。
随着我们继续前进,我们将基于我们迄今为止所获得的大部分知识进行构建。在继续前进之前,请确保您已经回顾了我们所学的类和信息。
第五章:模块、小部件和辅助工具
与其前身一样,Yii2 提供了几个有用的工具和可重用的代码块,帮助我们快速开发应用程序,这些工具和代码块被称为小部件和辅助工具。Yii2 还为我们提供了构建和包含称为模块的迷你应用程序的能力,这使我们能够快速向应用程序添加新功能,同时在我们的主要应用程序和任何扩展功能中保持关注点的清晰分离。在本章中,我们将介绍在我们的应用程序中构建和使用模块的基本知识。我们还将介绍 Yii2 的一些内置小部件和辅助工具,并学习我们如何实现自己的定制小部件。
模块
在 Yii2 中,模块被视为包含完整 MVC 栈的迷你自包含软件包。当与应用程序配对时,模块通过在不向我们的主要代码库中添加代码的情况下添加新功能和工具,为扩展应用程序提供了一种方法。因此,模块是创建和重用代码的绝佳方式。在用 Yii2 创建应用程序时,你很可能会使用预构建的模型,例如 Gii 或 Yii2 开发模块;然而,模块也可以是专门为构建特定目的而创建的定制应用程序,用于在特定目的上构建和分离代码。在本节中,我们将介绍 Yii2 中的基本模块,以及如何在我们的应用程序中创建和实现它们。
模块组件
与 Yii1 相比,Yii2 中的模块变化不大。在核心上,它们仍然由相同的结构组成,并共享许多相同的思想。在 Yii2 中,模块存储在我们的应用程序根目录的modules目录中,并通过我们的网络或控制台配置文件与我们的应用程序注册。如果我们分解一个基本模块,其目录结构和核心文件如下:
app/
modules/
mymodule/
Module.php
controllers/
DefaultController.php
models/
views/
layouts/
default/
index.php
我们应用程序中注册的每个模块都位于其自己的专用模块文件夹中,该文件夹默认通过其对应的路由与我们的 URL 管理器注册(在本例中,mymodule文件夹将对应于/mymodule URI 路由)。因此,除非在 URL 管理器中另行注册,否则模块中的任何控制器都将作为模块本身的专用控制器路由可用。例如,DefaultController.php控制器将映射到我们模块的根路由(/mymodule),而任何其他控制器将映射到/mymodule URI 中的控制器名称。
此外,模块在 Yii2 中提供了对基本 MVC 架构的全面支持。每个模块可能都有自己的模型、视图、控制器,甚至组件。像完整的应用程序一样,模块也有自己的视图和布局的支持,允许它们以不同于我们的主要应用程序的方式被样式化和管理。作为 Yii2 应用程序的一部分,它们也完全访问我们主要应用程序中实现的模型和类。
模块类结构
模块最重要的部分是在我们模块根目录下的Module.php文件中定义的模块类。在最基本层面上,一个模块必须简单地扩展yii\base\Module类:
<?php
namespace app\modules\mymodule;
class Module extends \yii\base\Module {}
然而,就像 Yii2 中的所有内容一样,模块可以通过覆盖我们类中的公共init()方法来定义它们自己的初始化代码和配置文件。至少,当我们覆盖这个方法时,我们想要确保调用父类yii\base\Module中的init()方法。
public function init()
{
parent::init();
// Set custom parameters
$this->params['a'] = 'b';
// Register a custom Yii config for our module
\Yii::configure($this, require __DIR__ . '/config/config.php');
}
我们还可以通过向yii\base\Module::$params数组中添加值来定义我们模块的附加自定义参数。此外,可以使用Yii::configure()静态方法将自定义配置注册到我们的模块中。这种配置可以是一个简单的键值对,也可以是一个完整的配置文件,例如我们在 Web 和控制台配置文件中使用的那些。
小贴士
可以在位于www.yiiframework.com/doc-2.0/guide-concept-configurations.html的 Yii2 指南中详细了解 Yii2 的配置语法。
控制器
在一个模块中,控制器被放置在主模块的controllers/目录中,并且根据 Yii2 的约定,它们位于模块的命名空间中。例如,为了创建我们mymodule模块的默认控制器,我们会在app/modules/mymodule/controllers/DefaultController.php中添加以下内容:
<?php
namespace app\modules\mymodule\controllers;
class DefaultController extends \yii\web\Controller
{
public function actionIndex()
{
return $this->render('index');
}
}
就像我们项目中的其他控制器一样,我们控制器中的默认操作是索引操作。由于我们模块中的控制器扩展了yii\web\controller,我们可以通过设置yii\web\controller::$defaultAction参数来调整我们的默认操作。
默认情况下,Yii2 会将/mymodule URI 路由路由到DefaultController类。但是,如果我们想改变这个设置,我们可以调整我们Module类的$defaultRoute参数。例如,如果我们有一个名为UserController的控制器来处理用户,我们可以使默认路由映射到我们的控制器,如下所示:
<?php
namespace app\modules\mymodule;
class Module extends \yii\base\Module
{
public $defaultRoute = 'user'; // user maps to UserController
}
因此,在模块内导航到/mymodule将导致我们的UserController类被执行而不是DefaultController。
小贴士
记住,除非另有说明,否则控制器将始终在其命名的 URI 中可用。在我们的例子中,/mymodule和/mymodule/user都将映射到相同的控制器并执行类似操作。如果您在调整$defaultRoute参数后不希望启用命名的控制器路由,请相应地调整您的路由器。
视图和布局
由于模块中的控制器扩展自yii\web\controller,我们可以利用模块中的视图和布局渲染。要开始渲染我们的视图,我们首先需要定义我们想要使用哪个布局。默认情况下,模块将使用父模块的布局文件,直到达到主布局文件,然后它将默认到app/views/layouts中定义的布局文件。
如果我们不想使用我们应用程序的布局文件,我们可以通过设置yii\base\Module::$layout属性来为我们的模块定义一个自定义布局文件,如下所示:
然后,我们将在app/modules/mymodules/views/layouts文件夹中定义一个名为main.php的布局文件:
<?php use yii\helpers\Html; ?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<?php echo Html::csrfMetaTags() ?>
<title><?php echo Html::encode($this->title) ?></title>
<?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
<?php echo $content ?>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>
小贴士
在布局文件中渲染我们的视图文件所需的唯一组件是<?php echo $content ?>。然而,当与视图一起工作时,你可能会发现许多你期望在视图中工作的事情却不会工作,除非使用beginPage()、endPage()、beginBody()、endBody()和head()方法定义一个完整的 HTML 文档,这些方法来自yii\base\view。有关这些方法的更多信息,请参阅 Yii2 文档中的布局部分[http://www.yiiframework.com/doc-2.0/guide-structure-views.html#layouts]和yii\base\view类[http://www.yiiframework.com/doc-2.0/yii-base-view.html]。
定义我们的布局后,我们需要定义我们的DefaultController::actionIndex()方法的视图文件,其中我们之前声明了我们要渲染索引视图。在模块中,视图是与我们的render()方法中请求的视图同名的 PHP 文件,并且它们映射到app/modules/mymodule/views/<controller>路径。在我们的例子中,这个视图映射到app/modules/mymodule/views/default/index.php。现在,让我们简单地添加以下内容到这个视图文件中:
<?php echo "MyModule: Hello World!"; ?>
注册模块
一旦我们创建了我们的模块,我们需要通过在app/config/web.php文件中定义一个modules部分来将其注册到我们的配置文件中:
'modules' => [
'mymodule' => 'app\modules\mymodule\Module'
],
或者,如果我们想向我们的模块传递额外的参数,我们可以定义我们的配置如下:
'modules' => [
'mymodule' => [
'class' => 'app\modules\mymodule\Module',
'foo' => 'bar' // Maps to app\modules\mymodule\Module::$foo, assuming $foo is declared
]
]
小贴士
与 Yii2 中的许多配置选项一样,模块可以通过之前提到的配置文件接收额外的参数。任何键值对都会将数组中列出的值填充到指定类的公共属性中。
动态注册模块
在处理大型项目时,通常会将几个组件分解成需要注册到我们的应用程序中的模块。此外,可能存在只有某些模块需要在特定时间注册的情况。一次自动注册多个不同模块的过程的一种方法是为我们的应用程序创建一个动态配置脚本,并让我们的应用程序为我们扫描模块。
要做到这一点,我们首先需要调整app/config/web.php文件中的模块部分,为我们的模块加载自定义配置,如下所示:
'modules' => require(__DIR__ . '/module.php'),
然后,我们将定义app/config/module.php,如下所示:
-
首先,我们将想要设置我们想要扫描的目录,以及尝试加载一个预先缓存的配置文件,如果存在的话。
<?php // Set the scan directory $directory = __DIR__ . DS . '..' . DS . 'modules'; $cachedConfig = __DIR__.DS.'..'.DS.'runtime'.DS.'modules.config.php'; -
然后,我们将尝试返回我们的缓存配置文件,如果它存在的话。
// Attempt to load the cached file if it exists if (file_exists($cachedConfig)) return require_once($cachedConfig); -
如果我们没有预先缓存的配置文件,我们将迭代我们的
app/modules目录中的所有文件夹,然后动态构建一个模块配置数组。此外,我们还将尝试加载位于app/modules/<module>/config/main.php的模块特定配置文件。这将使我们能够将配置与我们的模块打包在一起,而无需更改我们的app/config/web.php文件:else { // Otherwise generate one, and return it $response = array(); // Find all the modules currently installed, and preload them foreach (new IteratorIterator(new DirectoryIterator($directory)) as $filename) { // Don't import dot files if (!$filename->isDot() && strpos($filename->getFileName(), ".") === false) { $path = $filename->getPathname(); if (file_exists($path.DS.'config'.DS.'main.php')) { $config = require($path.DS.'config'.DS.'main.php'); $module = [ 'class' => 'app\\modules\\' . $filename->getFilename() . '\Module' ]; foreach ($config as $k=>$v) $module[$k] = $v; $response[$filename->getFilename()] = $module; } else $response[$filename->getFilename()] = 'app\\modules\\' . $filename->getFilename() . '\Module'; } } -
最后,我们生成生成配置文件的缓存版本,以消除每个请求上的重复工作。现在,当向我们的应用程序添加新模块时,我们只需简单地删除
runtime/modules.confg.php文件,而不是繁琐地更新我们的网络配置文件:$encoded = serialize($response); file_put_contents($cachedConfig, '<?php return unserialize(\''.$encoded.'\');'); // return the response return $response; }
总体而言,我们的动态配置文件将如下所示:
<?php
// Set the scan directory
$directory = __DIR__ . DS . '..' . DS . 'modules';
$cachedConfig = __DIR__.DS.'..'.DS.'runtime'.DS.'modules.config.php';
// Attempt to load the cached file if it exists
if (file_exists($cachedConfig))
return require_once($cachedConfig);
else
{
// Otherwise generate one, and return it
$response = array();
// Find all the modules currently installed, and preload them
foreach (new IteratorIterator(new DirectoryIterator($directory)) as $filename)
{
// Don't import dot files
if (!$filename->isDot() && strpos($filename->getFileName(), ".") === false)
{
$path = $filename->getPathname();
if (file_exists($path.DS.'config'.DS.'main.php'))
{
$config = require($path.DS.'config'.DS.'main.php');
$module = [ 'class' => 'app\\modules\\' . $filename->getFilename() . '\Module' ];
foreach ($config as $k=>$v)
$module[$k] = $v;
$response[$filename->getFilename()] = $module;
}
else
$response[$filename->getFilename()] = 'app\\modules\\' . $filename->getFilename() . '\Module';
}
}
$encoded = serialize($response);
file_put_contents($cachedConfig, '<?php return unserialize(\''.$encoded.'\');');
// return the response
return $response;
}
通过使用配置文件和模块注册过程,我们可以大幅减少我们的配置文件管理开销,并使我们的应用程序极其灵活,如果我们将功能打包到可能或可能不会同时安装的单独模块中。
引导模块
一些模块,如debug模块,在启用时需要在每个请求上执行。为了确保这些模块在每次请求上运行,我们可以通过将它们添加到配置文件的引导部分来引导它们。如果你熟悉 Yii1,bootstrap选项的使用方式与 Yii1 的预加载配置选项类似:
[
'bootstrap' => [
'debug',
],
'modules' => [
'debug' => 'yii\debug\Module',
],
]
小贴士
由于 Yii2 按需懒加载新对象的方式,你可能会遇到 Yii2 自动加载类和实际填充该对象之间的竞争条件。我们的配置选项的Bootstrap参数将确保 Yii2 在执行流程的早期自动加载并注册对象,而不是等待所需的类。
然而,在bootstrap部分添加项目时要小心,因为强制 Yii2 在需要之前注册对象可能会降低你的应用程序的性能。
访问模块
当与模块一起工作时,你可能需要获取当前正在运行的模块的实例,以便访问模块 ID 和参数或与模块关联的组件。要检索当前活动的模块实例,可以直接在模块类上使用getInstance()方法:
$module = \app\modules\mymodule\Module::getInstance();
或者,如果你知道模块的名称,你可以通过\Yii实例来访问它:
$module = \Yii::$app->getModule('mymodule');
此外,如果你在一个控制器中工作,你可以使用以下方法在运行中的控制器内访问一个模块:
$module = \Yii::$app->controller->mymodule;
一旦你有了模块的实例,你可以访问与该模块相关的任何公共属性、参数和组件:
echo $module->foo;
var_dump($module->params);
使用 Composer 管理模块
在打包项目时,通常有益于独立于我们的主应用程序来管理和版本化我们的模块。使用 Composer 和语义版本化,我们可以管理我们的模块,使它们在我们的应用程序中的特定时间点进行版本化,同时仍然允许开发者与我们合作。此外,我们还可以配置我们的主项目,在部署时自动为我们安装模块,这可以大大减少管理模块的开销:
-
要开始使用 Composer 管理我们的模块,我们首先需要将我们的模块源代码从主应用程序中移出,并将其推送到我们的 DCVS 仓库。
-
接下来,我们需要在我们新模块的仓库中创建一个
composer.json文件:{ "name": "masteringyii/chapter5-mymodule", "description": "The mymodule module for Chapter 5 of the book Mastering Yii", "license": "MIT", "type": "drupal-module", "keywords": [ "mastering yii", "book", "packt", "packt publishing", "chapter 5" ], "authors": [ { "name": "Charles R. Portwood II", "homepage": "https://www.nasteringyii.com" } ], "support": { "source": "https://github.com/masteringyii/chapter5-mymodule" }, "homepage": "https://www.masteringyii.com" }小贴士
我们用来管理模块安装的工具称为 composer-installers。为了自动将模块安装到我们的模块目录中,我们需要明确声明我们的 Composer 包的类型。composer-installers 项目目前不支持 Yii 特定的模块;然而,为了我们的目的,
drupal-module类型可以满足我们的需求。 -
接下来,我们需要对我们的主项目的
composer.json文件进行一些修改。我们需要做的第一个修改是在composer.json文件的 require 块中包含 composer-installers 依赖项。我们可以通过在composer.json文件的 require 块中添加以下内容来实现:"composer/installers": "v1.0.21" -
我们需要对主项目的
composer.json文件进行的第二个修改是引用我们模块的 DCVS 仓库。我们可以通过创建一个包含我们模块repository的 DCVS 信息的 repositories 块,然后将模块添加到我们的require块中来实现:"repositories": [ { "type": "vcs", "url": "https://github.com/masteringyii/chapter5-mymodule" }, ], "require": { "php": ">=5.4.0", "yiisoft/yii2": "*", "yiisoft/yii2-bootstrap": "*", "yiisoft/yii2-swiftmailer": "*", "composer/installers": "v1.0.21", "masteringyii/chapter5-mymodule": "dev-master" }, -
然后,我们需要将安装信息添加到
composer.json文件的 extras 部分。这为 composer-installers 包提供了所需的信息:"installer-paths": { "modules/mymodule/": [ "masteringyii/chapter5-mymodule" ], } -
然后,我们想要确保我们的模块目录被排除在我们的 DCVS 仓库之外。我们可以通过在我们的模块目录中添加一个包含以下信息的
.gitignore文件来实现:* -
最后,我们可以运行 Composer 来更新和自动安装我们的模块:
composer update –o小贴士
由于我们指定了想要使用
mymodule仓库的dev-master分支,Composer 将将项目克隆到我们的应用程序中,这将允许我们像往常一样独立于主应用程序开发它。然而,在部署期间,你应该对模块进行语义版本化,以便下载模块的版本化副本而不是克隆。
我们的主模块现在已通过 Composer 安装。
模块概述
模块最适合在大型应用程序中使用,在这些应用程序中需要创建某些功能或可重用的组件。正如本节所展示的,模块非常强大,可以用来扩展我们的应用程序。
小部件
在 Yii2 中,小部件是可重用的代码块,用于在视图中以面向对象的方式添加可配置的用户界面逻辑到我们的应用程序中。Yii2 自带了多种不同类型的小部件,其中一些我们在前面的章节中已经见过。也可以创建自定义小部件来创建可以在多个项目中重用的工具。在本节中,我们将介绍小部件的基本类型、如何使用它们以及如何在我们的应用程序中实现它们。
使用小部件
作为表示层工具,小部件通常在我们的视图文件中使用。在 Yii2 中,小部件可以通过两种不同的方式使用。使用小部件的第一种方式是在视图中的一个支持小部件上调用yii\base\Widget::widget()方法。此方法接受一个配置数组作为选项,并返回一个渲染的小部件作为结果。例如,要在我们的页面上显示 Twitter Bootstrap 3 样式的警报,我们可以使用以下yii\bootstrap\Alert小部件:
<?php use yii\bootstrap\Alert; ?>
<?php echo Alert::widget([
'options' => [
'class' => 'alert-info',
],
'body' => 'This is a bootstrap alert widget using widget()',
]);
或者,我们可以使用yii\base\widget::begin()和yii\base\widget::end()来构造特定的小部件实例。使用我们之前的例子,这将如下所示:
<?php use yii\bootstrap\Alert; ?>
<?php $widget = Alert::begin([
'options' => [
'class' => 'alert-warning',
],
]);
echo 'This is an bootstrap3 alert widget warning using begin() and end()';
$widget->end();

两个警报小部件渲染后的样子
小贴士
作为视图对象,小部件负责注册和加载它们自己的资产以确保它们被正确呈现。这就是为什么我们可以创建yii\bootstrap\Alert的实例并看到带有所有适当样式和功能的警报被渲染。
常用内置小部件
为了帮助快速开发应用程序,Yii2 内置了几个强大的小部件,我们可以使用它们来加速开发。
Bootstrap 小部件
Yii2 提供的主要小部件类型之一是针对 Twitter Bootstrap 3 样式的,它为我们提供了快速简单地向应用程序添加功能的方法。当使用 Bootstrap 小部件时,Yii2 会自动将必要的 HTML、CSS 和 JavaScript 对象注入 DOM 中。然而,这可以通过在我们的应用程序的资产管理者AppAsset.php中包含核心 Bootstrap 资产来优化,该文件位于您的@app/assets目录中:
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap\BootstrapAsset', // this line
];
小贴士
我们将在第六章中更详细地介绍AssetManager,资产管理。
所有 Bootstrap 特定的小部件都属于\yii\bootstrap命名空间。这些核心小部件如下:
| 小部件 | 结果 |
|---|---|
| ActiveForm | 一个样式化的 ActiveForm 实例 |
| 警报 | 一种样式警报 |
| 按钮 | 一个样式化的按钮 |
| 按钮下拉 | 一个按钮下拉组 |
| 按钮组 | 一个按钮组 |
| 轮播 | 图片或文本轮播 |
| 折叠 | 一个手风琴折叠 JavaScript 小部件 |
| 下拉菜单 | 一个下拉菜单 |
| 模型 | 一个模型 |
| 导航 | 一个导航菜单 |
| NavBar | 一个导航顶部栏 |
| 进度条 | 一个样式化的进度条 |
| 标签页 | 一个样式化的标签页 |
小贴士
更多关于 Bootstrap 特定小部件的信息可以在 Yii 指南中找到,链接为 www.yiiframework.com/doc-2.0/guide-widget-bootstrap.html。更多关于 Twitter Bootstrap 3 的信息可以在 getbootstrap.com 找到。
jQuery UI 小部件
通过官方 Yii2 扩展,Yii2 还提供了一些 jQuery-UI 特定的小部件。可以通过在我们的应用程序中包含 yii2-jui Composer 包来将 jQuery UI 小部件的支持添加到我们的应用程序中:
php composer.phar require --prefer-dist yiisoft/yii2-jui "*"
安装完成后,jQuery UI 包在 \yii\jui 命名空间下提供了以下小部件的支持:
| 小部件 | 结果 |
|---|---|
| 折叠面板 | 一个折叠面板元素 |
| 自动完成 | 一个自动完成元素 |
| 日期选择器 | 一个日期时间选择器对象 |
| 对话框 | 一个对话框 |
| 可拖动 | 一个可拖动元素 |
| 可丢弃 | 一个可丢弃元素 |
| 菜单 | 一个菜单 |
| 进度条 | 一个样式化的进度条 |
| 可调整大小 | 一个可调整大小的元素 |
| 可选择 | 一个可选择元素 |
| 滑块 | 一个滑块 |
| 滑块输入 | 一个输入滑块 |
| 可排序 | 一个可排序元素 |
提示
更多关于 jQuery-UI 特定小部件的信息可以在 Yii 指南中找到,链接为 www.yiiframework.com/doc-2.0/guide-widget-jui.html。更多关于 jQuery UI 的信息可以在 jqueryui.com 找到。
Yii 特定小部件
Yii2 支持熟悉的 Yii1 小部件,如 ActiveForm 和 GridView,这两个小部件我们在前面的章节中都有探讨。所有特定于 Yii2 的小部件都命名空间在 \yii\widget 下。
| 小部件 | 结果 |
|---|---|
| ActiveForm | 用于显示 Yii2 表单的 ActiveForm 实例 |
| 网格视图 | 一个用于在网格表格视图中显示模型和数据提供者数据的小部件 |
| 详细视图 | 一个用于显示特定模态数据的视图 |
| 列表视图 | 一个列表视图,用于在单个页面上显示多个模态框 |
| Ajax 表单 | 一个用于构建 Ajax 表单的小部件 |
| 链接分页器 | 一个用于显示多个记录的分页小部件 |
| 链接排序器 | 一个用于对数据提供者中的数据进行排序的小部件 |
| Pjax | Yii2 中 jQuery 的 pjax 功能的实现 |
| 面包屑 | 用于显示面包屑路径的小部件 |
| 内容装饰器 | 内容装饰器小部件用于捕获 begin() 和 end() 方法之间的所有输出,并将其传递到 $content 变量中对应的视图中。 |
| 片段缓存 | 用于缓存视图片段 |
| 输入小部件 | 用于显示输入字段的 小部件。 |
| 马赛克输入 | 一个输入小部件,用于强制用户输入格式正确的数据 |
| 菜单 | 用于显示 Yii 菜单的小部件 |
| 空间删除 | 一个用于删除 HTML 标签之间空白字符的小部件 |
提示
还存在一些在线项目,旨在扩展 Yii2 的 widget 集合。在尝试实现自己的小部件之前,请尝试搜索 Yii2 的扩展,看看是否有人已经实现了你所需要的功能。
创建自定义小部件
在某些情况下,创建我们自己的小部件来处理特定任务可能更有意义。要在 Yii2 中创建自定义小部件,我们需要创建一个扩展 yii\base\Widget 并实现 init() 或 run() 方法的类。例如,假设我们想要创建一个根据一天中的时间显示带有用户名字符的小部件。我们可以通过实现以下内容来创建这个小部件:
<?php
namespace app\components;
use yii\base\Widget;
use yii\helpers\Html;
class GreetingWidget extends Widget
{
public $name = null;
public $greeting;
public function init()
{
parent::init();
$hour = date('G');
if ( $hour >= 5 && $hour <= 11 )
$this->greeting = "Good Morning";
else if ( $hour >= 12 && $hour <= 18 )
$this->greeting = "Good Afternoon";
else if ( $hour >= 19 || $hours <= 4 )
$this->greeting = "Good Evening";
}
public function run()
{
if ($this->name === null)
return HTML::encode($this->greeting);
else
return HTML::encode($this->greeting . ', ' . $this->name);
}
}
然后,我们可以在视图文件中添加以下内容来实现我们的小部件:
<?php
use app\components\GreetingWidget;
echo GreetingWidget::widget( 'name' => ' Charles' );
![创建自定义小部件
我们还可以编写我们的小部件,使其使用 begin() 和 end() 格式。作为一个例子,让我们创建一个输出 HTML div 元素中 begin() 和 end() 标签内任何内容的小部件。我们可以将这个类编写如下:
<?php
namespace app\components;
use yii\base\Widget;
use yii\helpers\Html;
class EchoWidget extends Widget
{
public function init()
{
parent::init();
ob_start();
}
public function run()
{
$content = ob_get_clean();
echo Html::tag('div', $content, ['class' => 'echo-widget']);
}
}
然后,我们可以在视图中如下使用我们的小部件:
<?php use app\components\EchoWidget; ?>
<?php EchoWidget::begin(); ?>
<?php echo "Echo this!"; ?>
<?php EchoWidget::end(); ?>
当使用小部件时,你可能需要将信息传递给视图文件以处理更复杂的视图逻辑。在 Yii2 中,小部件原生支持 render() 方法,允许我们渲染视图文件。
public function run()
{
return $this->render('greeting');
}
默认情况下,视图文件应存储在 WidgetPath\views 文件夹中。在我们的例子中,由于 GreetingWidget 类位于 app\components 命名空间下,我们的问候视图文件将位于 @app\components\views\greeting.php。
小部件总结
小部件是强大的面向对象的可重用代码块,我们可以将其添加到视图中,以快速轻松地为应用程序添加额外的功能。作为自包含的对象,小部件遵循 MVC 模式,并处理小部件功能所需的任何和所有资产和外部脚本的依赖管理。
辅助函数
Yii2 有几个内置的辅助类,用于简化常见的编码任务,如 HTML、数组和 JSON 操作。这些辅助函数以静态类(这意味着它们应该静态调用而不是实例化)的形式存在,并位于 \yii\helpers 命名空间下。在本节中,我们将介绍几个更常见的辅助函数。
小贴士
所有 Yii2 支持的辅助函数的完整文档可以在 www.yiiframework.com/doc-2.0/ 的辅助函数部分下的 Yii2 API 文档页面找到。
URL 辅助函数
在 Yii2 中最常用的第一个辅助函数是 URL 辅助函数。URL 辅助函数帮助我们检索特定的 URL,例如基本 URL 和主页 URL,并生成到特定路径的 URL 路由。URL 辅助函数位于 yii\helpers\Url 命名空间下。
要检索应用程序的主页 URL,请使用 home() 静态方法。可以通过传递不同的参数来获取不同类型的 URL:
$relativeHomeUrl = Url::home();
$absoluteHomeUrl = Url::home(true);
$httpsAbsoluteHomeUrl = Url::home('https');
或者,您可以使用base()方法来检索您应用程序的基本 URL:
$relativeBaseUrl = Url::base();
$absoluteBaseUrl = Url::base(true);
$httpsAbsoluteBaseUrl = Url::base('https');
小贴士
home()方法返回我们应用程序的首页路由,而base()方法返回我们应用程序的基本 URL,用于内部使用。
URL 辅助工具也可以使用toRoute()和to()方法生成应用程序其他部分的路线。通常,toRoute()方法具有以下形式:
// Generate a relative URL to controller/action
$url = Url::toRoute(['controller/action', 'foo' => 'bar', 'let' => 'asl']);
或者,toRoute()可以通过在第一个数组参数前添加一个斜杠来生成绝对 URL:
// Generate an absolute URL to controller/action with multiple params.
$url = Url::toRoute(['/controller/action', 'foo' => 'bar', 'let' => 'asl']);
此外,如果不需要额外的参数,方法可以简化为一个单字符串:
// Navigate to controller/action
$url = Url::toRoute('controller/action');
作为toRoute()方法的替代,可以使用to()方法。to()方法与toRoute()方法相同,只是它始终期望一个数组而不是一个字符串:
// Generates a URL to controller/action
echo Url::to(['controller/action']);
// Generates a URL to controller/action with params
// controller/action?foo=bar#name
echo Url::to(['controller/action', 'foo' => 'bar', '#' => 'name']);
// the currently requested URL
echo Url::to();
此外,如果我们想检索当前 URL,可以使用current()方法。如果没有传递参数,将返回当前 URL。传递给方法的任何数组参数都将生成带有其参数的当前 URL:
// The current URL
echo Url::current();
// The current URL with params
echo Url::current([ 'foo' => 'bar' ]);
HTML 辅助工具
在 Yii 中,另一个常见的辅助工具是 HTML 辅助工具。HTML 辅助工具提供了许多不同的静态方法来生成安全的 HTML 标签。通常,可以通过调用tag()方法来生成 HTML 标签,如下所示:
use \yii\helpers\Html; // HTML Helper namespace
// Generates an HTML encoded span tag with the class name, and the users name HTML encoded.
// <span class="name">Charles</span>
Html::tag('span', Html::encode($user->name), ['class' => 'name']);
如前一个示例所示,数据也可以使用Html::encode()方法进行 HTML 编码,使其在客户端查看时安全:
小贴士
任何由最终用户提交的数据都应该用encode()方法包装,以防止 XSS 注入。
我们 HTML 标签的 CSS 样式也可以通过removeCssClass()和addCssClass()方法由我们的 HTML 辅助工具管理。addCssClass()方法可以与字符串或类定义的数组一起工作,并且如果类已存在,则不会添加该类:
$options = ['class' => 'btn btn-default'];
Html::removeCssClass($options, 'btn-default');
Html::addCssClass($options, 'btn-success');
Html::addCssClass($options, 'btn'); // Has no effect
Html::tag('span', Html::encode($user->name), $options);
HTML 辅助工具也可以用来生成链接:
// Generate a link to the user's profile
// <a href="profile/view/id/$id" class="profile">My Profile</a>
Html::a('My Profile', ['profile/view', 'id' => $id], ['class' => 'profile']);
Html::mailto('Contact me', 'admin@masteringyii.com');
辅助工具也可以用来生成图像标签:
// Generates an IMG tag
// <img src="img/logo.png" alt="masteringyii logo" />
Html::img('@web/images/logo.png', ['alt' => 'masteringyii logo']);
此外,HTML 辅助工具可以用来包含内联 CSS 样式和 JavaScript:
// <style>.greeting { color: #2d2d2d; }</style>
Html::style('.greeting { color: #2d2d2d; }');
//<script defer>alert("Hello World!");</script>
Html::script('alert("Hello World!");');
CSS 文件和 JavaScript 也可以通过 HTML 辅助工具包含:
//<link href="@web/css/styles.css" />
Html::cssFile('@web/css/styles.css');
// <!--[if IE 9]>
// <link href="http://example.com/css/ie9.css" />
// <![endif]-->
Html::cssFile('@web/css/ie9.css', ['condition' => 'IE 9']);
// <script type="text/javascript src="img/main.js"></script>
Html::jsFile('@web/js/main.js');
小贴士
HTML 辅助工具也可以用来生成许多不同类型的 HTML 标签。完整的方法列表可在www.yiiframework.com/doc-2.0/yii-helpers-html.html找到。
JSON 辅助工具
与 JSON 对象一起工作通常很复杂。为了帮助减轻复杂 JSON 对象的一些问题,Yii2 提供了yii\helpers\Json类,该类提供了对复杂 JSON 对象的编码和解码的支持:
$data = [
'foo' => 'bar,
'a', => 'b',
'param' => [
'param2' => [ 'a' => 'b'],
'foo' => 'bar'
]
];
// Encodes an array to JSON
$json = Json::encode($data);
// Decodes JSON to a PHP array
$decoded = Json::decode($json);
小贴士
yii\helpers\Json类基于原生的 PHP json_encode()和json_decode()类,为复杂 JSON 对象提供更强大的支持。在 Yii 中使用时,建议使用此类而不是原生 PHP 函数。
Markdown 辅助工具
Markdown 是一种将文本转换为 HTML 的工具,用于在网络上写作。旨在取代有问题的 WYSIWYG 编辑器,Markdown 已经迅速成为全球专业人士首选的写作方法。为了帮助您使用 Markdown,Yii2 提供了yii\helpers\Markdown辅助类,支持一些最常用的 Markdown 风味,包括 GitHub 风味 Markdown 和 Markdown Extra。
use \yii\helpers\Markdown;
$html = Markdown::process($markdown); // use original markdown flavor
$html = Markdown::process($markdown, 'gfm'); // use github flavored markdown
$html = Markdown::process($markdown, 'extra'); // use markdown extra
变量转储
经常在调试时,我们需要探索给定的数组或对象。大多数开发者会使用原生的 PHP 函数var_dump()或print_r(),这两个函数在大数组或对象中可能会存在问题。为了帮助处理对象和数组,Yii2 在yii\helpers\VarDumper命名空间中提供了 VarDumper 辅助类。
虽然这个类复制了var_dump()和print_r()的大部分功能,但它可以识别递归结构以避免重复显示相同的对象。VarDumper 可以使用如下:
yii\helpers\VarDumper;
VarDumper::dump($var);
抽象化
经常在处理字符串时,我们需要对字符串应用抽象化以获得适当的时态或复数形式。yii\helpers\Inflector类使我们能够做到这一点。以下是一些抽象化示例:
use \yii\helpers\Inflector;
// WhoIsOnline
echo Inflector::camelize('who is online?');
// person => people
echo Inflector::classify('person');
// Who is online
echo Inflector::humanize('WhoIsOnline');
// 26 => 26th
echo Inflector::ordinalize(26);
// person => People
echo Inflector::pluralize('person');
// People => Person
echo Inflector::singularize('People');
// SendEmail => send_email
echo Inflector::underscore('SendEmail');
// SendEmail => Send Email
echo Inflector::titlize('SendEmail');
小贴士
抽象类将仅与英语单词一起工作。
FileHelper
为了帮助我们处理文件,Yii2 提供了yii\helpers\FileHelper类。要搜索给定目录中的文件,我们可以使用FileHelper,如下所示:
use \yii\helpers\FileHelper;
$files = FileHelper::findFiles('/path/to/search/');
现在,你已经在$files变量中以数组的形式列出了所有文件。
使用查找文件方法,我们可以指定我们想要有或排除的文件类型:
// Only .php and .txt files
FileHelper::findFiles('.', ['only' => ['*.php', '*.txt']]);
// Exclude .php and .txt files
FileHelper::findFiles('.', ['except' => ['*.php', '*.txt']]);
默认情况下,fileHelper()将执行递归搜索。要禁用此行为,我们可以将recursive属性设置为false:
FileHelper::findFiles('.', ['recursive' => false]);
FileHelper也可以用来确定特定文件或文件扩展名的 MIME 类型:
// image/jpeg
FileHelper::getMimeType('/path/to/img.jpeg');
// image/jpeg
FileHelper::getMimeTypeByExtension('jpeg');
摘要
在本章中,我们介绍了许多可以帮助我们在 Yii2 中更快地开发应用程序并扩展 Yii2 的不同工具。我们首先介绍了模块的基础知识、其 MVC 结构以及如何将其集成到我们的主应用程序中。我们还介绍了如何使用 Composer 来自动化模块在我们项目中的包含,无论是开发还是部署。然后,我们介绍了 Yii2 中的小部件,并学习了如何在我们的应用程序中使用它们。我们还介绍了如何创建我们自己的小部件。最后,我们介绍了 Yii2 中的一些内置辅助类,并学习了如何使用它们。
在下一章中,我们将探讨在 Yii2 中如何管理资源以及如何使用yii\web\AssetManager来优化我们资源的用法。我们还将介绍如何集成其他工具,例如 Grunt、Node 和 Bower,以简化我们在 Yii2 中资源的使用。
第六章:资产管理
现代 Web 应用程序由许多不同的组件组成。除了功能之外,我们应用程序的展示可能被认为是其最重要的方面。用户界面的展示和相应的用户体验对于构建优秀的 Web 应用程序至关重要。在 Web 应用程序中,展示和体验通常由层叠样式表(CSS)和 JavaScript 文件定义。使用原始 HTML,我们可以包含所需的任何脚本和样式,但通常我们需要以编程方式处理我们的资产(例如,当使用模块、组件或小部件时)。为了帮助管理我们的资产,我们可以使用第三方工具和 Yii2 内置的资产管理器的组合。在本章中,我们将介绍如何使用 Yii2 的资产管理工具,以及介绍我们可以使用的几个第三方工具,以简化资产文件的管理。
资产包
Yii2 中通过资产包管理资源。在 Yii2 中,资产包简单来说是一个声明我们希望在应用程序中使用的所有资源的类,它位于我们应用程序的assets/目录中,通常位于声明AppAsset类的AppAsset.php文件中,该类扩展了yii\web\AssetBundle。由于我们的默认应用程序包含一个预定义的AppAsset类,让我们看看该文件中已经定义了什么。
<?php
namespace app\assets;
use yii\web\AssetBundle;
class AppAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
'css/site.css',
'//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js'
];
public $js = [ ];
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap\BootstrapAsset',
];
}
我们的示例资产包文件声明了几个公共属性。第一个属性是应用程序的基路径和基 URL,它们定义了我们的资产应该从哪里加载。第二个属性是 CSS 和 JavaScript 文件的数组,它们定义了应该与我们的资产包注册哪些资产。最后,我们的资产包定义了当前资产包依赖于哪些资产包。以下是最常见属性的详细说明:
| 属性 | 说明 |
|---|---|
basePath |
包含资产文件的 Web 服务器公共目录的字符串或路径别名。 |
baseUrl |
JS 或 CSS 属性中列出的相对资源的基 URL。 |
css |
要包含在资产包中的 CSS 文件数组。 |
cssOptions |
将与生成的<link>标签一起渲染的选项和条件数组。 |
depends |
依赖于此资产包的资产包数组。 |
js |
要包含在资产包中的 JavaScript 文件数组。 |
jsOptions |
将与生成的<script>标签一起渲染的选项和条件数组。 |
publishOptions |
要传递给yii\web\AssetManager的publish()方法的选项。 |
sourcePath |
定义包含我们想要包含在包中的资产文件的目录。设置此属性将覆盖basePath和baseUrl。 |
使用资产包
在定义了我们的资源包之后,我们还需要将它们包含到我们的布局文件中。我们可以通过在主布局文件的开头添加以下内容来实现(在我们的例子中这是views/layouts/main.php)。
<?php
use app\assets\AppAsset;
AppAsset::register($this);
在页面加载时,我们的资源包将注册所有依赖的资源包,并将所有非 Web 可访问的文件发布到 Web 可访问的目录。然后在视图渲染阶段,它将生成所有必要的 HTML 标记以包含在我们的视图中。
小贴士
在前面的例子中,$this是yii\web\View的一个实例。当在组件或小部件中工作时,您可以通过使用$this->view在组件或小部件中检索视图对象。
配置
内部,Yii2 通过assetManager应用程序组件来管理资源包及其配置,该组件由yii\web\AssetManager类实现。通过配置此组件的$bundles属性,我们可以自定义资源包的行为。以yii\web\JQueryAsset资源包为例;默认情况下,它提供从Bower(我们将在本章后面讨论的第三方资源依赖管理器)提供的 jQuery 版本,当我们的 Yii2 项目安装时。如果我们想使用这个资源包的不同版本的 jQuery,或者想通过使用第三方 CDN 来提高性能,我们可以像以下这样覆盖 jQuery 资源包选项。
// config/web.php
return [
// [...],
'components' => [
// [...],
'assetManager' => [
'bundles' => [
'yii\web\JqueryAsset' => [
// Prevents the asset bundle from publishing this file
'sourcePath' => null,
'js' => [
'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.js',
]
],
],
],
],
];
在这个例子中,我们通过将js参数设置为 CloudFlare CDN,并告诉我们的JQueryAsset资源包不要将资源作为从第三方 CDN 渲染的内容来重新定义资源包的 JavaScript 文件。
或者,我们也可以有条件地重新定义正在渲染的文件,比如在我们有一个想要在生产环境中显示的脚本压缩版本,但在其他环境中我们希望使用非压缩版本的情况下。
// config/web.php
return [
// [...],
'components' => [
// [...],
'assetManager' => [
'bundles' => [
'yii\web\JqueryAsset' => [
'js' => [
APPLICATION_ENV == 'prod' ?
'jquery.min.js' : 'jquery.js'
]
],
],
],
],
];
小贴士
作为提醒,我们的APPLICATION_ENV常量依赖于我们在第一章中建立的多环境设置,Composer, 配置, 类和路径别名。
此外,我们可以通过将特定的资源包设置为false来禁用特定的资源包,如下面的示例所示。
// config/web.php
return [
// [...],
'components' => [
// [...],
'assetManager' => [
'bundles' => [
'yii\web\BootstrapAsset' => false
],
],
],
];
此外,我们可以通过将bundles属性设置为false来完全禁用应用程序中包含的所有资源包。
// config/web.php
return [
// [...],
'components' => [
// [...],
'assetManager' => [
'bundles' => false
],
],
];
资源映射
在某些情况下,多个资源包可能定义了同一脚本的不同版本。例如,一个资源包可能包含 jQuery 版本 2.1.3,另一个可能定义 2.1.4。为了解决这些冲突,我们可以将配置文件的assetMap属性设置为将任何命名的资源文件实例解析为单个依赖项,该依赖项将被包含在我们的视图中。
// config/web.php
return [
// [...],
'components' => [
// [...],
'assetManager' => [
'assetMap' => [
'jquery.js' => 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.js',
'jquery.min.js' => 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js'
]
],
],
];
在这种情况下,任何在资产包的js部分定义了jquery.js和jquery.min.js实例的资产包,都将将该资产重新映射到我们的 CloudFlare CDN 资产。
小贴士
assetMap属性将资产文件在包中的最后一部分作为键值对进行匹配。
资产类型和位置
根据它们的位置,Yii2 将资产分为三种不同的方式。资产将被分类为源资产、发布资产或外部资产。源资产是混合在我们源代码中的资产文件,并且不在可访问的 Web 目录中。这类资产通常包含在模块、小部件、扩展或组件中。任何 Yii2 定义为源资产的资产都需要由 Yii2 发布到可访问的 Web 目录。发布资产是已发布到可访问的 Web 目录的源资产。最后,外部资产是位于可访问位置上的资产,例如在我们的当前服务器上、另一个服务器或 CDN 上。与发布资产不同,Yii2 不会将这些资产发布到我们的资产目录,而是直接将它们作为外部资源引用。
当与资产包一起工作时,如果指定了sourcePath属性,Yii2 会将任何带有相对路径列出的资产视为源资产,并在运行时尝试发布这些资产。如果没有指定sourcePath属性,Yii2 将假设列出的资产位于一个可访问的 Web 目录中,并且已发布。在这种情况下,有必要指定basePath属性或baseUrl属性,以告诉 Yii2 资产所在的位置。
小贴士
不要使用@webroot/assets别名作为sourcePath属性,因为这个目录由资产管理器用于保存从其源位置发布的资产文件。存储在这个目录中的任何数据都可能随时被 Yii2 删除。
资产选项
与yii\web\View方法registerJsFile()和registerCssFile()一样,我们可以通过设置资产包的相应$jsOptions和$cssOptions属性,以一组给定的选项来渲染资产包。
例如,我们可以在视图中的<body>标签末尾包含我们的列出的 JavaScript 文件,使我们的资产包包含在内。
public $jsOptions = ['position' => \yii\web\View::POS_END];
小贴士
yii\web\View类还提供了定位方法,用于身体的开头(yii\web\View::POS_BEGIN),身体的末尾(yii\web\View::POS_END),在jQuery(window).load()事件中(yii\web\View::POS_LOAD),以及在jQuery(window).ready()事件中(yii\web\View::POS_READY)。
在 CSS 中,我们还可以定义如下<noscript>块:
public $cssOptions = ['noscript' => true];
此外,我们还可以在条件语句中包裹我们的 CSS 块:
public $cssOptions = ['condition' => 'IE 11'];
这将导致以下 HTML 被渲染:
<!--[if IE11]>
<link rel="stylesheet" href="path/to/ie11.css">
<![endif]-->
小贴士
设置 $jsOptions 或 $cssOptions 属性将把指定的选项应用到资产包中定义的所有 CSS 和 JavaScript 文件上。为了对每个资产应用不同的条件,您需要创建一个包含这些条件的单独资产包,或者使用 theregisterCssFile() 或 registerJsFile() 方法在视图中内联资产。
资产发布
如前所述,如果资产包引用的资产位于一个从网络浏览器无法公开访问的目录中(或者设置了 sourcePath 属性),其资产将作为资产管理器在将包注册到视图时执行的自动发布过程的一部分被复制到 @webroot/assets(对应于 @web/assets 的网络路径)。如前所述,可以通过设置资产包的 baseUrl 和 basePath 属性来更改发布路径。
如您所预期的那样,在 Web 请求上复制文件的过程可能相当昂贵,如果允许其持续运行,可能会在生产环境中引起与性能相关的问题。为了帮助减轻这个问题,Yii2 提供了两种替代方案。
而不是复制文件,Yii 的资产管理器可以通过设置 assetManager 的 linkAssets 属性来配置在原始资产文件和可访问的网页目录之间创建一个符号链接,如下所示:
// config/web.php
return [
// [...],
'components' => [
'assetManager' => [
'linkAssets' => true,
],
],
// [...],
];
小贴士
发布过程通常只发生一次。一旦 Yii2 发布了我们的资产,它就不会再次发布,除非我们删除我们的资产目录或告诉 Yii2 重新发布我们的资产。
默认情况下,Yii2 将在 sourcePath 属性中列出的每个文件上运行发布过程,这意味着如果您有一个大目录,那么无论文件是否实际使用,每个文件都会被复制。为了使 Yii2 的资产管理器只复制您需要的文件,您可以修改资产包的 publishOptions 属性。
以我们使用雅虎流行的 CSS 库 purecss 为例。要从源代码构建 purecss,我们需要运行 Bower、NPM 和 Grunt,这将留下我们不希望发布到我们的网页目录中的构建文件。
通过设置如以下示例所示的 publishOptions 属性,我们可以确保只发布构建文件,这可以在初始发布期间显著提高性能。
<?php
namespace app\assets;
use yii\web\AssetBundle;
class PureCssAsset extends AssetBundle
{
public $sourcePath = '@bower/purecss';
public $css = [
'build/base-min.css',
];
public $publishOptions = [
'only' => [
'build/'
]
];
}
资产包的客户端缓存管理
在生产环境中运行应用程序时,我们通常会在我们的 JavaScript 和 CSS 资产上设置长期缓存过期日期,以提高性能。当推送新代码时,我们的资产通常会发生变化,但它们的文件位置不会变,这将在我们进行更改时阻止客户端接收我们的更新资产。克服这个问题的最简单方法是在我们的资产末尾附加一个版本号或时间戳,这样浏览器就可以缓存我们资产的特定版本,并且在我们推送新资产时能够重新缓存。
使用 Yii2,我们可以通过设置 assetManager 的 appendTimestamp 属性来配置资产管理器,使其自动将最后修改时间戳附加到我们的资产上,如下所示:
// config/web.php
return [
// [...],
'components' => [
'assetManager' => [
'appendTimestamp' => true,
],
],
// [...],
];
使用资产包的预处理器
为了使资产开发更加简单且易于管理,许多开发者已经转向使用扩展语法语言,如 LESS 和 CoffeeScript,并依赖它们相应的工具将这些资产转换为 CSS 和 JavaScript 文件。Yii2 可以通过启用资产管理器来帮助简化此过程,使其为您处理构建过程。使用 Yii2 的资产包,您可以直接在资产包中列出 LESS、SCSS、Stylus、CoffeeScript 和 TypeScript 文件,Yii2 将识别它们,并自动通过相应的预处理器运行它们。以下是一个资产包的示例:
<?php
namespace app\assets;
use yii\web\AssetBundle;
class AppAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
'css/app.less',
];
public $js = [
'js/app.ts'
];
public $depends = [
'yii\web\YiiAsset'
];
}
当我们的资产包注册到我们的视图中时,Yii2 将自动运行适当的预处理器工具,将资产转换为 CSS 和 JavaScript 以包含在我们的视图中。
注意
Yii2 依赖于您的计算机上安装的相应预处理器软件才能使此功能正常工作。
当与预处理器一起工作时,可能需要为您的资产指定额外的参数,以确保它们能够正确生成。在 Yii2 中,您可以通过以下方式设置 assetManager 实例的 converter 属性来实现这一点。
// config/web.php
return [
// [...],
'components' => [
'assetManager' => [
'converter' => [
'class' => 'yii\web\AssetConverter',
'commands' => [
'less' => ['css', 'lessc {from} {to}'],
'ts' => ['js', 'tsc --out {to} {from}'],
],
],
],
],
// [...],
];
小贴士
虽然使用起来很方便,但在生产环境中让 Yii2 构建我们的资产文件通常不是一个好主意,因为它会将不必要的软件引入生产环境,这些软件可能与您的开发环境不匹配或存在安全漏洞,并且可能会严重阻碍应用程序的性能,因为 Yii2 需要在其首次运行时构建资产文件。在生产环境中工作,通常在将应用程序推送到生产之前,在构建服务器上构建所有资产文件是一个更好的选择。我们将在本章后面介绍如何使用 Grunt、NodeJS 和 Bower 构建资产文件,并在第十三章调试和部署中介绍一些基本的部署策略。
资产命令行工具
对于 HTTP/1.1 应用程序,为了节省带宽和请求,通常最好将多个资产文件合并并压缩在一起。Yii2 可以通过 asset 命令帮助简化此过程,该命令可以帮助您使用 Yii2 以及一些第三方 Java 工具来压缩和合并您的资产文件。
小贴士
由于 HTTP/2 协议的变化,通常单独提供资产文件比合并它们更有益。随着更多像 Nginx 和 Apache 这样的网络服务器开始支持 HTTP/2 协议,您应该进行自己的实验,以确定对于您的应用程序来说,合并资产文件是否是最佳选择。
asset命令行工具提供了两个选项asset/template,该选项用于生成一个名为asset.php的指令文件,供第二个命令asset/compress使用,该命令用于压缩文件。第一个工具asset/template的调用方式如下:
./yii asset/template config/assets.php
运行此命令后,将在我们应用程序的config目录中生成一个名为assets.php的文件,默认输出如下。
<?php
/**
* Configuration file for the "yii asset" console command.
*/
// In the console environment, some path aliases may not exist. Please define these:
// Yii::setAlias('@webroot', __DIR__ . '/../web');
// Yii::setAlias('@web', '/');
return [
// Adjust command/callback for JavaScript files compressing:
'jsCompressor' => 'java -jar compiler.jar --js {from} --js_output_file {to}',
// Adjust command/callback for CSS files compressing:
'cssCompressor' => 'java -jar yuicompressor.jar --type css {from} -o {to}',
// The list of asset bundles to compress:
'bundles' => [
// 'app\assets\AppAsset',
// 'yii\web\YiiAsset',
// 'yii\web\JqueryAsset',
],
// Asset bundle for compression output:
'targets' => [
'all' => [
'class' => 'yii\web\AssetBundle',
'basePath' => '@webroot/assets',
'baseUrl' => '@web/assets',
'js' => 'js/all-{hash}.js',
'css' => 'css/all-{hash}.css',
],
],
// Asset manager configuration:
'assetManager' => [
//'basePath' => '@webroot/assets',
//'baseUrl' => '@web/assets',
],
];
小贴士
为了压缩资产,Yii2 默认会尝试使用 Closure Compiler(developers.google.com/closure/compiler/)和 YUI Compressor(github.com/yui/yuicompressor/)。您需要安装这两个工具,以便asset命令按预期工作。
此配置文件定义了几个不同的选项。前两个选项jsCompressor和cssCompressor定义了压缩 JavaScript 和 CSS 文件应运行的命令。默认情况下,这些工具将尝试使用 Closure Compile 和 YUI Compressor;如果您希望使用其他工具,这两个工具都可以按需进行配置。
第二个选项bundles定义了您希望一起压缩的资产包。第三个选项assetManager定义了资产管理器组件应使用的一些基本选项,例如压缩资产的basePath和baseUrl。最后,targets选项定义了将生成的输出资产包。默认情况下,Yii2 将创建一个名为all的目标,并为列出的所有资产包生成压缩资产。
在许多情况下,我们通常会将资产分散在几个不同的资产包中,例如共享的、前端的和后端的工具。由于前端资产不需要与我们的后端资产一起包含,我们可以定义多个目标,压缩后生成单独的资产,这样我们就可以专门包含这些资产,从而为我们的最终用户节省带宽。以下是一个示例:
<?php
/**
* Configuration file for the "yii asset" console command.
*/
// In the console environment, some path aliases may not exist. Please define these:
// Yii::setAlias('@webroot', __DIR__ . '/../web');
// Yii::setAlias('@web', '/');
return [
// [...],
'targets' => [
'shared' => [
'js' => 'js/shared-{hash}.js',
'css' => 'css/shared-{hash}.css',
'depends' => [
'yii\web\YiiAsset',
'app\assets\AppAsset',
],
],
'backend' => [
'js' => 'js/backend-{hash}.js',
'css' => 'css/backend-{hash}.css',
'depends' => [
'yii\web\YiiAsset',
'app\assets\AdminAsset'
],
],
'frontend' => [
'js' => 'js/frontend-{hash}.js',
'css' => 'css/frontend-{hash}.css',
'depends' => [],
],
]
];
在编写我们的资产配置文件后,我们可以通过运行资产命令来生成我们的压缩资产文件,如下所示:
./yii asset/compress config/asset.php
小贴士
资产配置文件作为便利性提供,以便尽可能地将所有内容保持在 Yii2 中。虽然 Closure Compiler 和 YUI Compressor 是很好的工具,但像 Grunt 和 NodeJS 这样的工具通常可以提供更易于使用和开发的解决方案,同时消除在 Yii2 中编译和压缩资产所需的大部分配置。在处理资产时,务必找到最适合您的开发工作流程、团队和构建过程的工具。
第三方资产工具
当处理现代 Web 应用程序时,我们经常需要从各种来源包含许多不同类型的资产。直接将这些资产包含在我们的应用程序中可能会导致几个问题,具体如下:
-
第三方资产的许可
-
版本和安全管理
-
仓库大小
-
构建过程
而不是直接在我们的应用程序中包含资产,我们可以利用第三方资产管理系统,如 NodeJS 和 Bower,这可以缓解之前概述的所有问题。
使用 Yii2,我们可以直接使用 Node 和 Bower 包。对于简单的应用程序,我们可以在 composer.json 文件中直接包含这些包,通过在 require 部分包含 bower-asset/PackageName 和 npm-asset/PackageName。Yii2 的后置脚本将自动处理将这些资产包含在 @bower 文件夹和 @npm 文件夹中,然后我们可以在我们的资产包中引用它们。在一个典型的 Yii2 实例中,这分别对应于 vendor/bower 和 vendor/npm。
对于更复杂的项目,直接在我们的应用程序中利用这些第三方工具可能更有意义,稍后包含必要的 CSS 和 JavaScript 文件。在下一节中,我们将探讨三个工具:NodeJS、Bower 和 Grunt,并探讨我们如何与 Yii2 结合使用它们。
NodeJS
我们经常用来管理我们的资产的第一和最重要的工具被称为 NodeJS,这是一个我们可以用来安装其他两个包(Bower 和 Grunt)的工具。要开始使用 NodeJS,我们首先需要从 nodejs.org/download/ 下载软件并在我们的系统上安装它。
对于我们的目的,NodeJS 将为我们提供自动下载和构建我们的资产文件所需的工具和包。要开始使用 NodeJS,我们首先需要在我们的应用程序中包含一个 package.json 文件。此文件将定义我们想要使用的所有依赖项。一个典型的用于资产管理 NodeJS 文件将如下所示:
{
"name": "masteringyii-ch6",
"description": "Chapter 6 source code for the book 'Mastering Yii'",
"repository": {
"type": "git",
"url": "https://www.github.com/masteringyii/chapter6"
},
"dependencies": {
"ansi-styles": "¹.1.0",
"bower": "1.3.12",
"grunt": "⁰.4.4",
"grunt-cli": "⁰.1.13",
"grunt-contrib-concat": "⁰.4.0",
"grunt-contrib-cssmin": "0.6.1",
"grunt-contrib-uglify": "0.2.0"
}
}
小贴士
在 NodeJS 中与其他包(如 Bower 和 Grunt)一起工作的有两种不同的方式。第一种方式是在我们的 package.json 文件中将它们作为依赖项包含。这样做的好处是我们可以将构建工具的版本锁定到我们的应用程序中。或者,我们可以全局安装这些工具,这样我们就可以直接通过命令行运行它们。当与许多开发者和团队一起工作时,通常最好使用 package.json 文件中定义的工具。
在我们的 package.json 文件中,我们定义了一些关于我们的存储库的详细信息,例如名称、描述和存储库详细信息,以及我们想要使用的几个工具,例如 Bower、Grunt 和一些用于连接和压缩 CSS 和 JavaScript 文件的 Grunt 工具。
在我们的 NodeJS 配置文件设置完成后;我们现在可以使用 NodeJS 通过运行以下命令将这些工具添加到我们的存储库中:
npm install
这将安装我们的构建工具到 node_modules 目录。
小贴士
由于此目录包含构建工具,我们应该通过将其添加到我们的 .gitignore 文件中来排除它。
Bower
要管理 CSS 和 JavaScript 库,我们可以利用一个名为 Bower 的资产依赖管理工具。要开始使用 Bower,我们首先需要在应用程序的根目录中创建一个 bower.json 文件,并填充我们想要包含的库。例如,让我们在我们的应用程序中包含流行的 CSS 库 PureCSS。我们可以通过编写以下基本的 bower.json 文件来实现这一点:
{
"name": "masteringyii-ch6",
"dependencies": {
"pure": "~0.6.0"
}
}
提示
包名称的完整列表可以在 bower.io/ 查找。
要安装这些包,然后我们可以从我们的 node_modules 目录运行 Bower,如下所示:
./node_modules/.bin/bower install
这将把我们的库和 CSS 添加到应用程序根目录下的 vendor/bower 目录。
提示
默认情况下,Bower 会将其自身安装到 bower_components 目录。然而,由于 Yii2 已经定义了安装目录,它被重新映射到 vendor/bower。
Grunt
由于我们已经知道如何使用 YUI Compressor 和 Closure Compiler 以及 Yii2 的 asset 命令,此时我们有一个选项是将我们的资产包和资产配置文件直接指向 node_modules 和 bower_components 目录。虽然这消除了之前列出的许多问题,但我们还可以使用另一个名为 Grunt 的第三方工具来处理压缩和合并我们的文件。
简而言之,Grunt 是一个 JavaScript 任务运行器,旨在帮助自动化许多需要重复执行的任务,例如构建资产文件。使用像 Grunt 这样的工具的主要好处是,您可以自动化开发和构建服务器的整个工作流程。
要开始使用 Grunt,我们首先需要创建一个名为 Gruntfile.js 的文件,它将包含我们应用程序的所有构建指令。
-
创建我们的
Gruntfile.js文件的第一个步骤是声明我们正在使用 Grunt,并指定我们想要使用的 Grunt 模块(这些名称我们在package.json文件中指定)。module.exports = function(grunt) { // Register the NPM tasks we want grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-contrib-uglify'); }; -
在本节中,我们将通过指定在运行 Grunt 时想要运行的任务来声明我们的默认任务。在我们的例子中,我们想要连接我们的 JavaScript 和 CSS 文件,然后最小化我们的 JavaScript 和 CSS 文件。
// Register the tasks we want to run grunt.registerTask('default', [ 'concat', 'cssmin:css', 'uglify:js' ]); -
然后,我们开始配置我们的 Grunt 任务,告诉 Grunt 它可以在哪里找到我们的
package.json文件,并设置一些基本的路径别名。grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), paths: { assets: 'web', bower: 'vendor/bower', css : '<%= paths.assets %>/css', js: '<%= paths.assets %>/js', dist: '<%= paths.assets %>/dist', }, } -
在本节中,我们定义我们的任务以连接我们的 JavaScript 和 CSS 文件。
concat: { css: { src: [ '<%= paths.bower %>/pure/pure-min.css', '<%= paths.css %>/*' ], dest: '<%= paths.dist %>/app.css' }, js : { src: [ '<%= paths.js %>/*.js' ], dest: '<%= paths.dist %>/app.js' } }, -
我们的任务是在连接我们的 CSS 资产后最小化它们。
cssmin : { css:{ src: '<%= paths.dist %>/app.css', dest: '<%= paths.dist %>/app.min.css' } }, -
最后,压缩我们的 JavaScript 文件的任务。
uglify: { js: { files: { '<%= paths.dist %>/app.min.js' : ['<%= paths.dist %>/app.js'] } } },
在我们的 Gruntfile.js 文件配置完成后,我们可以通过以下方式运行 Grunt 来构建我们的资产文件:
./node_modules/.bin/grunt
如果一切顺利,我们应该看到以下输出:
Running "concat:css" (concat) task
File web/dist/app.css created.
Running "concat:js" (concat) task
File web/dist/app.js created.
Running "cssmin:css" (cssmin) task
File web/dist/app.min.css created.
Running "uglify:js" (uglify) task
File "web/dist/app.min.js" created.
Done, without errors.
如 Grunt 输出所示,我们为我们生成了四个文件,包括压缩和解压缩的 JavaScript 和 CSS 文件,这些文件包含了我们想要包含在我们网站中的所有资源。从这一点出发,我们就可以有条件地将我们的资源文件包含在我们的资源包中,并切换掉我们的APPLICATION_ENV或YII_ENV_<ENV>环境,以便在生产环境中使用压缩版本,在非生产环境中使用非压缩版本。
小贴士
NodeJS、Bower 和 Grunt 各自提供了强大的工具来自动完成某些任务,并且与 Yii2 配合良好。然而,在决定使用特定技术之前,务必咨询您的团队,以确定对他们来说什么是最有效的。
摘要
在本章中,我们介绍了资源在 Yii2 中的工作方式和管理工作。我们探讨了资源包文件的基本知识及其与 Yii2 资源管理器的集成。我们还探讨了如何使用asset命令来构建配置文件,以及如何组合和压缩我们的资源。最后,我们探讨了三个第三方工具:NodeJS、Bower 和 Grunt,并说明了如何结合我们的资源包使用这些工具来自动化我们的资源文件构建。
在探索了 Yii 的前端方面之后,在下一章中,我们将回到后端,学习我们如何在应用程序中处理用户认证和授权,以及如何在我们的应用程序中设置访问控制过滤器以及基于规则的认证。
第七章. 认证和授权用户
当与现代 Web 应用一起工作时,我们经常需要认证我们的用户,以确保他们是他们所声称的那个人,并且他们拥有访问信息的适当权限(授权)。在本章中,我们将介绍使用 Yii2 认证用户的基础知识,并探讨如何使用基本访问控制过滤器以及更复杂的基于角色的访问控制过滤器,让他们访问我们应用中的特定页面。
小贴士
在本章中,我们将基于我们在第四章中创建的迁移脚本和模型,即活动记录、模型和表单。在开始本章之前,请确保您已经很好地理解了在该章中创建的模型和迁移。
用户认证
几乎对于每一个足够规模的 Web 应用,我们最终都需要我们的应用支持用户存储和认证,以确保与我们的应用一起工作的用户是他们所声称的那个人。在 Web 应用中,我们通常通过公开的身份(如电子邮件地址)和用户知道的秘密(如密码)来处理认证。根据我们数据的安全性和我们的威胁模型,我们还可以扩展我们的认证过程,包括通过短信文本消息或 Authy 或 Google Authenticator 等双因素认证应用发出的双因素认证代码。在本节中,我们将介绍如何使用 Yii2 实现基本认证,并探讨我们如何通过认证过程增强我们用户的网络安全。
在 Yii2 中,认证是通过用户组件管理的,并在我们的config/web.php应用配置文件中定义。在我们开始在我们的应用中认证用户之前,我们首先需要在配置文件中定义此组件。具体来说,我们需要告诉 Yii2 它可以在哪里找到我们将用于处理应用内认证逻辑的身份类。在下面的代码块中,我们已经将我们的身份类定义为我们在第三章中创建的用户模型,即Migrations, DAO, and Query Building:
return [
// [...],
'components' => [
'user' => [
'identityClass' => 'app\models\User',
],
],
// [...],
];
在接下来的章节中,我们将介绍如何扩展我们的User类以支持认证。
实现用户身份接口
要实现具有所需认证逻辑的我们的身份类,我们首先必须让我们的身份类(app\models\User,在models\User.php中定义)实现yii\web\IdentityInterface。
小贴士
记住,在 PHP 5+中,接口是 PHP 构造,用于定义实现类必须包含哪些方法。
在 PHP 5+中,我们可以通过在我们的类中使用implements关键字来增强我们的User对象,如下所示:
class User extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface
然后,我们可以实现IdentityInterface接口中概述的方法。这些方法是findIdentity($id)、findIdentityByAccessToken()、getId()、getAuthKey($token, $type)和validateAuthKey($authKey):
-
我们需要实现的第一种方法是
findIdentity($id)。此方法负责找到具有指定$id属性的identity类的实例,并且主要用于当 Yii2 需要从会话数据中验证用户时。 -
要实现此方法,我们需要定义静态方法并返回我们的
User类的一个实例,如下面的示例所示:/** * @inheritdoc */ public static function findIdentity($id) { return static::findOne($id); } -
在
yii\web\IdentityInterface中定义的下一个我们需要定义的方法是findIdentityByAccessToken($token, $type)。在 Yii2 中,认证可以通过前端网页表单、Cookie(如果我们使用基于 Cookie 的认证)或 RESTful API 来处理。findIdentityByAccessToken方法用于我们使用 RESTful 认证时。由于我们的应用程序还没有 REST API,我们可以简单地定义此方法为空体,如下所示:/** * @inheritdoc */ public static function findIdentityByAccessToken($token, $type=null) { }提示
如果我们想要添加基于令牌的认证的基本支持,我们需要执行以下步骤:
-
添加一个新的迁移来存储与我们的用户数据一起的访问令牌。
-
创建一个基于 API 的认证方法,该方法生成访问令牌并将其存储在我们的用户数据旁边
-
实现以下
findIdentityByAccessToken()方法:
public static function findIdentityByAccessToken($token, $type=null) { return static::findOne(['access_token' => $token]); }我们将在第九章中更详细地介绍 RESTful API 认证,RESTful APIs。
-
-
接下来,我们需要明确定义
getId()方法,它将返回我们用户的 ID:/** * @inheritdoc */ public function getId() { return $this->id; }提示
虽然
yii\base\Object,yii\base\ActiveRecord从中扩展,为我们在ActiveRecord实例中定义的所有公共属性定义了一个魔法方法__getter,但 PHP 5+中的接口要求显式定义接口中列出的所有方法。 -
最后,我们需要在我们的应用程序中实现
getAuthKey()和validateAuthKey()方法。如前所述,这两个方法专门用于基于 Cookie 的认证。由于我们本章不会使用基于 Cookie 的认证,我们可以简单地定义这两个方法,如下所示:/** * @return string current user auth key */ public function getAuthKey() {} /** * @param string $authKey * @return boolean if auth key is valid for current user */ public function validateAuthKey($authKey) { return true; }
基于 Cookie 的认证
当与用户一起工作时,我们经常需要在我们的应用程序中包含一个类似于记住我功能的功能,以便我们的用户在离开一段时间后可以无缝地登录到我们的应用程序。为了使基于 Cookie 的认证在 Yii2 中工作,我们需要对我们的应用程序进行一些更改:
-
首先,我们需要在我们的网络配置文件中将用户组件的
enableAutoLogin属性设置为true。这将允许 Yii2 在用户设置了适当的 cookie 后自动登录:return [ 'components' => [ // [...], 'user' => [ 'identityClass' => 'app\models\User', 'enableAutoLogin' => true, ], // [...], ] ]; -
接下来,我们需要定义一个位置来存储和持久化我们的基于 cookie 的认证令牌。一种实现方式是添加一个额外的迁移,为我们的用户表添加一个
auth_key列。在创建我们的用户时,我们可以设置此值,如下所示:public function beforeSave($insert) { if (parent::beforeSave($insert)) { if ($this->isNewRecord) { $this->auth_key = \Yii:: $app->security->generateRandomString(); } return true; } return false; }提示
或者,我们可以将此值持久化到二级存储系统中,例如 Memcached 或 Redis。我们将在 第十二章 中介绍如何使用 Redis 和 Memcached 缓存数据,性能和安全。
-
最后,当我们定义我们的登录表单方法以实例化我们的
IdentityInterface对象时,我们需要以如下方式记录用户的登录时长:Yii::$app->user->login($identity, 3600*24*30);Yii2 将创建一个 cookie,它将用于内部操作,并且只要 cookie 有效,就会自动登录用户。如果没有设置时长,将使用基于会话的认证而不是基于 cookie 的认证,这意味着当用户关闭浏览器时,我们的用户会话将过期,而不是当用户的 cookie 过期时。
与用户身份交互
现在我们已经定义了我们的身份接口所需的方法,让我们更详细地看看 yii\web\User 对象。
提示
记住,yii\web\User 类与 app\models\User 类是不同的。
yii\web\User 对象在 Yii2 中通过 \Yii::$app->user 来引用,它包含了当前用户的信息。我们可以通过 \Yii::$app->user->identity 属性来检索我们用户的信息。如果一个用户未认证,这个属性将是 NULL。然而,如果一个用户已认证,它将包含当前用户的信息。例如,如果我们想获取用户在 第四章 中定义的完整姓名,即 活动记录、模型和表单,我们可以这样做:
$name = \Yii::$app->user->identity->getFullName(); // "Jane Doe";
或者,我们可以通过检查 yii\web\User 的 isGuest 属性来检测用户是否已登录。如果用户未认证,此属性将返回 true;如果已认证,则返回 false:
\Yii::$app->user->isGuest;
此外,如果我们想获取用户的 ID,我们可以通过我们在 User 类中定义的 getId() 方法来访问它:
\Yii::$app->user->getId();
最后,我们可以使用Yii::$app->user中的相应login()和logout()方法在我们的应用程序中登录和注销用户。要登录用户,我们首先需要创建我们之前建立的标识实例。在下面的示例中,我们正在从用户的电子邮件地址获取标识信息。如前所述,我们还可以将持续时间参数作为login()方法的一部分提供,用于基于 cookie 的认证:
$identity = User::findOne([ 'email' => $emailAddress ]);
Yii::$app->user->login($identity);
在我们认证之后,我们可以通过调用\Yii::$app->user->logout()来将用户从我们的应用程序中注销。默认情况下,此参数将销毁与当前用户关联的所有会话数据。如果我们想保留这些数据,我们可以将false作为logout()方法的第一个参数传递。
使用表单验证用户
现在我们已经实现了我们的身份接口,并且了解了yii\web\User组件的基础知识,让我们将这些组件与我们在第三章中创建的用户数据、迁移、DAO 和查询构建以及我们在第四章中创建的UserForm类和场景组合在一起。作为提醒,以下是我们在第四章中开始的UserForm类:
<?php
namespace app\models;
use Yii;
class UserForm extends \yii\base\Model
{
public $email;
public $password;
public $name;
public function rules()
{
return [
[['email', 'password'], 'required'],
[['email'], 'email'],
[['email', 'password', 'name'], 'string', 'max' => 255],
[['email', 'password'], 'required', 'on' => 'login'],
[['email', 'password', 'name'], 'required', 'on' => 'register']
];
}
public function scenarios()
{
return [
'login' => ['email', 'password'],
'register' => ['email', 'password', 'name']
];
}
}
为了增强我们的UserForm类以方便登录,我们需要进行一些更改:
-
首先,由于我们将在多个地方使用我们的身份对象,我们应该创建一个私有变量来存储它。这将有助于减少我们在使用表单时对数据库进行的查询次数。我们还将想要定义一个方法来检索这个属性:
private $_user = false; /** * Finds user by [[email]] * @return User|null */ public function getUser() { if ($this->_user === false) $this->_user = User::findOne(['email' => $this->email]); return $this->_user; } -
接下来,我们需要实现一个方法来验证用户的密码。如第三章中所述,迁移、DAO 和查询构建,我们使用 PHP 5 的
password_hash方法对用户的密码进行散列。为了验证以这种方式散列的密码,我们可以使用 PHP 5 的password_verify方法。对于我们的应用程序,让我们在我们的app\models\User类中添加一个verifyPassword()方法:/** * Validates password * * @param string $password password to validate * @return boolean if password provided is valid for current user */ public function validatePassword($password) { return password_verify($password, $this->password); } -
要调用此方法,我们将向我们的
UserForm类的rules()方法添加一个新的验证器,该验证器仅在之前定义的登录场景中执行:public function rules() { return [ // [...], [['password'], 'validatePassword', 'on' => 'login'], ]; } -
回想我们在第四章中介绍的信息,即Active Record, Models, and Forms,我们知道在登录场景中,
validatePassword方法将被调用以满足我们添加到rules()方法中的新验证规则。我们可以定义此方法如下:/** * Validates the password. * This method serves as the inline validation for password. * * @param string $attribute the attribute currently being validated * @param array $params the additional name-value pairs given in the rule */ public function validatePassword($attribute, $params) { if (!$this->hasErrors()) { if (!$this->getUser() || !$this->getUser()->validatePassword($this->password)) { $this->addError($attribute, 'Incorrect email or password.'); } } } -
我们将通过添加一个
login()方法来最终化我们的UserForm类,该方法将验证用户提交的电子邮件和密码,然后登录用户。/** * Logs in a user using the provided email and password. * @return boolean whether the user is logged in successfully */ public function login() { if ($this->validate()) { if (Yii::$app->user->login($this->getUser())) return true; } return false; } -
在我们的表单最终化后,我们可以在控制器中实现登录操作,以完成工作流程。在我们的情况下,让我们让登录操作将用户重定向到一个页面,在用户登录后显示一些关于用户的信息。由于我们已经在第四章中定义了此操作的绝大部分,因此对此操作只需要进行小的修改:
public function actionLogin() { $model = new \app\models\UserForm(['scenario' => 'login']); if ($model->load(Yii::$app->request->post())) { if ($model->login()) return $this->redirect('secure'); } return $this->render('login', [ 'model' => $model, ]); }为了说明目的,让我们也在这页上显示
\Yii::$app->user->identity的信息,以便我们可以看到它。我们可以通过创建之前提到的安全操作,然后使用VarDumper辅助程序来打印这些信息。public function actionSecure() { echo "<pre>"; \yii\helpers\VarDumper::dump(\Yii::$app->user->identity->attributes); echo "</pre>"; }
由于我们已经在第四章中创建了我们的登录视图,即Active Record, Models, and Forms,因此我们可以使用该章节中列出的凭据来验证我们的应用程序。例如,我们可以使用以下凭据登录为管理员:
-
用户名:
admin@example.com -
密码:
admin
如果身份验证成功,我们将被重定向到显示我们用户属性的安全页面。
[
'id' => 4
'email' => 'admin@example.com'
'password' => '$2y$13$f.1jE/cSFP42bHbqjtmJ5.6VkcOtKPp7Vu3UBC6clL7cHj84fltUC'
'first_name' => 'Site'
'last_name' => 'Administrator'
'role_id' => 2
'created_at' => 1439233885
'updated_at' => 1439233885
]
授权
虽然我们现在能够对我们的数据库进行身份验证,但我们需要实现必要的方法以确保正确的人可以访问正确的页面。为此,我们需要实现访问控制过滤器或基于角色的访问控制过滤器。
访问控制过滤器
控制对某些页面的访问的一种方法是通过创建访问控制过滤器。在 Yii2 中,访问控制过滤器是我们可以绑定到我们的控制器上的行为,以确保正确的人有权访问正确的内容。访问控制过滤器通过yii\filter\AccessControl实现,主要用于需要简单访问控制的情况,例如需要确保用户是否已登录(尽管它可以配置为更复杂的规则)。作为一个过滤器,yii\filter\AccessControl在我们的控制器中的behaviors()方法中实现,如下例所示:
<?php
namespace app\controllers;
use yii\web\Controller;
use yii\filters\AccessControl;
class SiteController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['login', 'logout', 'register'],
'rules' => [
[
'allow' => true,
'actions' => ['login', 'register'],
'roles' => ['?'],
],
[
'allow' => true,
'actions' => ['logout'],
'roles' => ['@'],
],
],
],
];
}
}
之前提到的代码执行了几个操作,让我们来分解一下:
-
如前几章所述,行为返回一个选项数组。在这种情况下,我们返回的第一个行为是访问行为,它指定了
yii\filter\AccessControl过滤器作为此行为应使用的类:return [ 'access' => [ 'class' => AccessControl::className(), // [...] ] ]; -
接下来,我们定义我们希望我们的过滤器应用的操作。在这种情况下,我们只想将
yii\filter\AccessControl应用于我们的SiteController对象的登录、注销和注册操作。'only' => ['login', 'logout', 'register'], -
最后,我们定义我们的过滤器应遵守的规则。在下面的代码片段中,我们声明我们希望未经认证的用户(在角色部分由特殊字符
?指定)可以访问登录和注册操作,并允许任何已认证的用户(在角色部分由特殊字符@指定)可以访问注销操作:'rules' => [ [ 'allow' => true, 'actions' => ['login', 'register'], 'roles' => ['?'], ], [ 'allow' => true, 'actions' => ['logout'], 'roles' => ['@'], ], ]
默认情况下,如果用户未认证,我们的访问控制过滤器将重定向用户到我们的登录页面,如果他们没有访问权限,将抛出yii\web\ForbiddenHttpException。由于这并不总是期望的,我们可以通过设置过滤器的denyCallback参数来修改我们的过滤器。此外,我们可以在过滤器的规则部分中,通过设置matchCallback属性来定义错误可能发生的情况。例如,如果我们想使我们的安全操作仅对管理员可访问,我们可以编写以下代码:
<?php
namespace app\controllers;
use Yii;
use yii\filters\AccessControl;
use yii\web\Controller;
use yii\web\HttpException;
use yii\helpers\Url;
class SiteController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
// Specifies the actions that the rules should be applied to
'only' => ['secure'],
// The rules surrounding who should and should not have access to the page
'rules' => [
[
'allow' => true,
'matchCallback' => function($rule,
$action) {
return !\Yii::$app->user->isGuest &&
\Yii::$app->user->identity->role->id === 2;
}
],
],
// The action that should happen if the user shouldn't have access to the page
'denyCallback' => function ($rule, $action) {
if (\Yii::$app->user->isGuest)
return $this->redirect
(Url::to('/site/login'));
else
throw new HttpException('403', 'You are
not allowed to access this page');
},
],
];
}
}
在本节中,用户只有在其角色为2(这是我们将在第三章中指定的管理员角色)时才能使用安全操作。如果他们未认证,我们将重定向他们到登录页面;如果他们已认证但权限不足,我们将抛出 HTTP 403 错误。
小贴士
之前显示的示例是为了说明我们可以使用访问控制过滤器的matchCallback和denyCallback属性做什么。
通过访问控制过滤器,我们可以在规则部分设置ips参数来通过 IP 地址限制对某些操作的访问,如下所示。IP 地址可以通过特定的 IP 或使用通配符字符的子网进行限制,如下面的示例所示:
return [
'access' => [
'class' => AccessControl::className(),
// [..]
'rules' => [
[
'allow' => true,
'ips' => [
'10.0.0.5', // Allow 10.0.0.5
'192.168.*' // Allow 192.168.0.0/24 subnet
]
]
]
],
];
此外,我们可以通过指定允许的 HTTP 动词来限制对操作的访问,使用yii\filter\VerbFilter过滤器。例如,如果我们想确保只有GET请求可以针对我们的安全操作运行,我们可以定义以下行为:
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\filters\VerbFilter;
class SiteController extends Controller
{
public function behaviors()
{
return [
// [...]
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'secure' => ['get'],
],
],
];
}
}
默认情况下,我们的访问控制过滤器将尝试将自身应用于我们控制器中的每个操作。为了指定我们的过滤器应限制的操作,我们可以设置过滤器的only属性:
'only' => ['secure'],
此外,我们可以通过设置rules数组的actions属性来指定我们的访问控制规则应应用的操作:
'rules' => [
[
'allow' => true,
'actions' => [ 'secure' ],
'matchCallback' => function($rule, $action) {
return !\Yii::$app->user->isGuest && \Yii::$app->user->identity->role->id === 2;
}
],
[
'allow' => true,
'actions' => [ 'authenticated' ],
'roles' => ['@']
]
],
类似于only参数,我们可以通过设置except过滤器来排除某些操作从身份验证过滤器:
'except' => ['secure'],
提示
访问控制过滤器被分解为规则,如前一个示例所示。每个规则仅适用于一组特定的操作,这允许我们为这些规则指定自定义的允许或拒绝回调。然而,only和except的父选项指定了父访问控制过滤器何时应用。
基于角色的访问控制
作为使用用户身份对象管理访问的替代方案,我们也可以通过在我们的应用程序中配置基于角色的访问控制(RBAC)来管理对操作的访问。在 Yii2 中,RBAC 通过创建代表一组权限的角色,然后将这些角色分配给特定用户来实现。角色通过检查来确定给定的角色或权限是否适用于相关用户。在本节中,我们将介绍在 Yii2 中配置和使用 RBAC 的基本知识。
提示
Yii2 对 RBAC 的实现通过authManager组件遵循 NIST RBAC 模型。NIST RBAC 模型的完整实现细节位于csrc.nist.gov/rbac/sandhu-ferraiolo-kuhn-00.pdf。
配置 RBAC
要开始使用 RBAC,我们首先需要为 RBAC 配置我们的authManager组件并定义我们想要使用的授权管理器。Yii2 提供了两种不同的授权管理器,第一种是yii\rbac\PhpManager,它使用 PHP 脚本来存储授权数据,第二种是yii\rbac\DbManager,它利用应用程序数据库来管理授权数据。对于具有非动态权限和角色的简单应用程序,yii\rbac\PhpManager可能更受欢迎。
要配置authManager,我们只需定义我们想要使用的类,如下所示:
return [
// [...],
'components' => [
'authManager' => [
'class' => 'yii\rbac\PhpManager',
],
],
// [...],
];
提示
默认情况下,yii\rbac\PhpManager将授权数据存储在@app/rbac目录中,该目录必须可由您的 Web 服务器写入。
或者,如果我们使用数据库来管理我们的授权数据,我们将按如下方式配置authManager:
return [
// [...],
'components' => [
'authManager' => [
'class' => 'yii\rbac\DbManager',
],
],
// [...],
];
当使用我们的数据库来管理我们的授权数据时,我们需要运行 RBAC 迁移来适当地配置我们的数据库,这可以通过从我们的命令行界面运行以下命令来完成:
./ yii migrate --migrationPath=@yii/rbac/migrations
这将导致输出类似于以下内容:
Yii Migration Tool (based on Yii v2.0.6)
Total 1 new migration to be applied:
m140506_102106_rbac_init
*** applying m140506_102106_rbac_init
> create table {{%auth_rule}} ... done (time: 0.006s)
> create table {{%auth_item}} ... done (time: 0.005s)
> create index idx-auth_item-type on {{%auth_item}} (type) ... /
done (time: 0.006s)
> create table {{%auth_item_child}} ... done (time: 0.005s)
> create table {{%auth_assignment}} ... done (time: 0.005s)
*** applied m140506_102106_rbac_init (time: 0.050s)
Migrated up successfully.
配置 RBAC 后,我们可以通过\Yii::$app->authManager访问authManager组件。
创建权限和权限关系
在配置我们的authManager组件之后,我们需要定义我们希望用户拥有的权限以及它们之间的关系。对于大多数具有固定权限层次结构的应用程序,这可以通过编写 RBAC 控制台命令来初始化数据库中的数据来实现。在以下示例中,我们将为虚构的问题管理应用程序创建三个权限:一个用户创建新问题的权限,支持新创建的问题,一个监督员监督监督员,以及管理员权限:
// Save to @app/commands
<?php
namespace app\commands;
use Yii;
use yii\console\Controller;
class RbacController extends Controller
{
public function actionInit()
{
$auth = \Yii::$app->authManager;
// Create the user permissions
$user = $auth->createPermission('createIssue');
$user->description = 'A permission to create a new issue within our incident management system';
$auth->add($user);
// Create the supporter permissions
$supporter = $auth->createPermission('supportIssue');
$supporter->description = 'A permission to apply supporter specific actions to an issue';
$auth->add($supporter);
// A supporter should have all the permissions of a user
$auth->addChild($supporter, $user);
// Create a permission to manage issues
$supervisor = $auth->createPermission('manageIssue')
$supervisor->description = 'A permission to apply management specific actions to an issue';
$auth->add($supervisor);
// A supervisor should have all the permissions of a supporter and a end user
$auth->addChild($supervisor, $supporter);
$auth->addChild($supervisor, $user);
$admin = $auth->createRole('admin');
$admin->description = 'A permission to perform admin actions on an issue';
$auth->add($admin);
// Allow an admin to perform all related tasks.
$auth->addChild($admin, $supervisor);
$auth->addChild($admin, $supporter);
$auth->addChild($admin, $user);
}
}
然后,我们可以通过从我们的命令行运行rbac/init命令来初始化我们新创建的权限方案:
./yii rbac/init
在定义了我们的角色之后,我们可以在注册步骤或管理仪表板中将其应用于我们的用户,如下所示。在这个例子中,我们正在获取管理员角色并将其分配给具有用户 ID 4的管理员用户:
$auth = \Yii::$app->authManager;
$role = $auth->getRole('admin');
$auth->assign($role, User::findOne([ 'email' => 'admin@example.com' ]));
或者,我们可以在authManager组件中定义一个隐含的默认角色。这样,我们就不需要明确地将新用户分配到最低级别的用户角色。这可以通过以下方式实现:
return [
// [...],
'components' => [
'authManager' => [
'class' => 'yii\rbac\PhpManager',
'defaultRoles' => ['user'],
],
// [...],
],
];
自定义授权规则
除了基本的身份验证角色和权限之外,我们还可以通过扩展yii\rbac\Rule并实现execute()方法来定义自定义规则,如下所示:
// Save to @app/rbac
<?php
namespace app\rbac;
use yii\rbac\Rule;
/**
* Checks if a user can edit their own issue
*/
class SupervisorRule extends Rule
{
public $name = 'isAuthor';
/**
* @param string|integer $user the user ID.
* @param Item $item the role or permission that this rule is associated with
* @param array $params parameters passed to ManagerInterface::checkAccess().
* @return boolean a value indicating whether the rule permits the role or permission it is associated with.
*/
public function execute($user, $item, $params)
{
return isset($params['issue']) ? $params['issue']->author == $user : false;
}
}
我们可以将自定义规则添加到我们的authManager组件中,如下所示:
$auth = Yii::$app->authManager;
// Add a rule
$rule = new \app\rbac\SupervisorRule;
$auth->add($rule);
// Create a permission and associate the rule to it
$modifyOwnIssue = $auth->createPermission('modifyOwnIssue');
$modifyOwnIssue->description = 'Modify a issue that was self submitted';
$modifyOwnIssue->ruleName = $rule->name;
$auth->add($modifyOwnIssue);
// Assign the supervisor role to the superviseIssue permissions
$superviseIssue = $auth->getRole('superviseIssue');
$auth->addChild($modifyOwnIssue, $superviseIssue);
检查用户是否有访问角色的权限
在配置了 RBAC、创建了所需的角色并将用户分配到这些角色之后,我们可以使用yii\web\User::can()方法来检查用户是否有访问特定角色的权限,如下所示:
if (\Yii::$app->user->can('createIssue'))
{
// Create a new issue
}
我们也可以通过检查父角色并传递所需数据来验证我们的新创建的规则是否可访问,如下所示:
if (\Yii::$app->user->can('superviseIssue', ['issue' => Yii::$app->request->post('Issue')]))
{
// Can modify an issue that they created
}
小贴士
虽然通过角色和规则的命名更加明确,但使用 RBAC 可能会很快变得令人困惑。当使用 RBAC 时,请彻底记录权限、关系和规则以供以后参考。
闪存消息
而不是盲目地重定向用户而不提供信息,我们可以利用 Yii2 中的闪存消息向用户显示一次性有用的信息,例如他们需要执行什么操作才能完成另一个操作(例如,他们必须登录才能查看受保护的页面)。
在 Yii1 中,用户指定的闪存消息可以直接绑定到用户组件。在 Yii2 中,它们完全由会话对象管理。在本节中,我们将通过增强我们的登录视图来举例说明如何使用闪存消息。我们还将利用我们在前几章中介绍的其他小部件和辅助工具。
如前节所示,当用户是访客且尝试访问受保护页面时,我们只需将他们重定向回登录页面,不提供任何信息。为了提供良好的用户体验,我们可以在重定向用户之前设置一个闪存消息,然后在登录视图中显示该消息。例如,我们控制器的behaviors()方法将变为以下内容。注意setFlash()方法的使用:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'denyCallback' => function ($rule, $action) {
if (\Yii::$app->user->isGuest)
{
\Yii::$app->session->setFlash('warning', 'You must be authenticated to access this page');
return $this->redirect(Url::to('/site/login'));
}
else
throw new HttpException('403', 'You are not allowed to access this page');
},
'only' => ['secure'],
'rules' => [
[
'allow' => true,
'matchCallback' => function($rule, $action) {
return !\Yii::$app->user->isGuest && \Yii::$app->user->identity->role->id === 2;
}
],
],
],
];
}
在我们的登录视图文件中,我们可以使用hasFlash()方法检查是否存在特定类型的闪存消息,然后使用getFlash()方法显示特定的闪存消息,如下所示:
<?php use yii\bootstrap\Alert; ?>
<div class="site-login">
<?php if (\Yii::$app->user->isGuest): ?>
<div class="body-content">
<?php if (\Yii::$app->session->hasFlash('warning')): ?>
<?php echo Alert::widget([
'options' => [
'class' => 'alert alert-warning'
],
'body' => \Yii::$app->session->getFlash('warning')
]); ?>
<?php endif; ?>
<?php echo $this->render('forms/LoginForm', [ 'model' => $model ]); ?>
</div>
<?php else: ?>
<?php echo Alert::widget([
'options' => [
'class' => 'alert alert-info'
],
'body' => 'You are already logged in. To login as a different user, logout first'
]); ?>
<?php endif; ?>
</div>
现在,如果我们未经过认证就导航到 site/secure,我们会看到以下内容。此外,如果我们再次刷新页面,闪存消息会消失,因为闪存消息只打算显示一次。

哈希和加密
在处理用户信息时,必须注意最佳安全实践,以确保用户信息(如密码)以某种方式存储,如果您的数据库被破坏,用户的明文密码不会被暴露。如第三章所示,“迁移、DAO 和查询构建”,我们正在使用原生的 PHP password_hash()和password_verify()函数来加密和解密我们的用户密码。虽然这些标准易于使用,但在您应用程序的开发过程中,您可能会发现利用 Yii2 安全组件来哈希用户密码和加密敏感数据更容易:
Yii::$app->getSecurity();
哈希和验证密码
使用 Yii2,我们可以通过安全组件的generatePasswordHash()和validatePassword()方法来哈希和验证用户密码。与password_hash()和password_verify()函数一样,generatePasswordHash()和validatePassword()方法使用bcrypt来哈希用户密码:
$hash = \Yii::$app->getSecurity()->generatePasswordHash($password);
然后,密码可以按以下方式验证:
if (Yii::$app->getSecurity()->validatePassword($plainTextPassword, $hashedPassword))
{
// Valid Password
}
else
{
// Invalid Password
}
默认情况下,Yii2 将使用 PHP 的crypt()函数生成密码哈希,但可以通过设置应用程序配置中安全组件的passwordHashStrategy属性,选择性地配置为使用原始的password_hash()方法,使用PASSWORD_DEFAULT算法:
return [
// [...],
'security' => [
'passwordHashStrategy' => 'password_hash'
],
// [...],
];
提示
强烈建议您使用password_hash策略而不是加密,因为 PHP 将继续增强PASSWORD_DEFAULT的哈希算法,以增加 PHP 的安全性。
然而,Yii2 实现的密码哈希方法是原生 PHP 函数的包装。原生函数和 Yii2 实现将相互保持向后兼容。为了更面向对象的方法,建议您使用 Yii2 方法。
数据加密和解密
为了方便起见,Yii2 提供了一种使用密钥或用户密码来加密和解密数据的方法。要使用 Yii2 加密数据,我们可以使用安全组件的encryptByPassword()方法,如下例所示:
$encrypted = \Yii::$app->getSecurity()->encryptByPassword($data, $secretPassword);
可以使用decryptByPassword()方法解密数据:
$data = \Yii::$app->getSecurity()->decryptByPassword($encrypted, $secretPassword);
小贴士
用于加密和解密方法的秘密密码应仅对用户唯一,并以一种格式存储,如果我们的数据库遭到破坏,秘密密码本身不会受到损害。一个好的秘密是使用用户提交的单独密码。
数据散列
除了散列密码和加密数据外,我们还可以使用hashData()和validateData()方法对数据进行散列以进行完整性验证。这些方法将有助于展示和验证文件或原始数据的校验和:
$hash = Yii::$app->getSecurity()->hashData($data, $secretKey);
$data = Yii::$app->getSecurity()->validateData($hash, $secretKey);
小贴士
与加密数据不同,散列数据无法恢复到其原始状态。散列在验证信息未被篡改方面很有益处,并且确保文件或数据在传输后的一致性。
摘要
在本章中,我们介绍了验证用户身份的基本知识,以及根据我们在用户身份界面中设置的属性授予他们访问某些页面的权限,以及如何实现 Yii2 的基于角色的分层认证。我们还探讨了如何使用闪存消息来增强用户体验。此外,我们还探讨了安全组件的一些组件,使我们能够散列用户的密码、散列和验证数据,以及使用用户的密码加密和解密信息。
在下一章中,我们将介绍我们应用程序中更复杂的路由,如何使用和修改我们的响应,以及监听和响应事件的基础知识。
第八章。路由、响应和事件
与许多现代 Web 框架一样,Yii2 使用了一个强大的路由组件,我们可以利用它来处理来自我们端用户和应用程序的各种 URI。这种功能通过 Yii2 强大的请求和响应处理器进一步增强,我们可以使用它们来操作请求和响应体。在本章中,我们将介绍如何操作 Yii2 的 URL Manager 以调整路由,探讨如何配置 Yii2 以以不同的方式响应,以及学习如何在我们的应用程序中发送和监听事件。
路由
如前几章所述,Yii2 中的路由由我们应用程序配置中定义的 UrlManager 组件管理。Yii2 中的路由器负责确定将 Yii2 的外部 URI 请求路由到内部控制器和动作的位置。在第五章模块、小部件和助手中,我们介绍了如何使用yii\helpers\Url助手创建和操作 URL 路由的基本方法。在本节中,我们将通过更详细地探讨 Yii2 的 UrlManager 来了解 Yii2 如何在应用程序内部路由这些请求。
Yii2 中的路由可以分为两个基本步骤。第一个步骤是解析传入的请求和查询参数(默认情况下存储在请求的GET参数中,带有r参数,但如果我们启用了美观的 URL,也可以从请求 URI 中检索)。第二个步骤是创建相应控制器动作的实例,它最终将处理请求。
默认情况下,Yii2 会将路由分解为 URL 中的正斜杠,将其映射到适当的模块、控制器和动作对。例如,site/login 路由将匹配默认应用程序实例中的site控制器和名为login的动作。内部,Yii2 将采取以下步骤来路由请求:
-
默认情况下,Yii2 会将当前模块设置为应用程序。
-
检查应用程序的控制器映射,看它是否包含当前路由。如果是,将根据模块内定义的控制器映射创建一个控制器实例。此时,将根据步骤4中定义的动作映射创建动作。默认情况下,Yii2 将根据
@app/controllers文件夹中找到的控制器创建控制器映射,但可以在模块(或 UrlManager)中进行自定义:yii\base\Module::$controllerMap = [ 'account' => 'app\controllers\UserController', // Different syntax for the previous example //'account' => [ // 'class' => 'app\controllers\AccountController' //], ] -
如果应用程序模块的控制器映射不在应用程序模块中找到,Yii2 将遍历应用程序模块的
module属性中的模块列表,以查看是否有匹配的路由。如果找到模块,Yii2 将使用提供的配置实例化该模块,然后根据前一步中概述的详细信息创建控制器。 -
然后,Yii2 将在模块配置中定义的动作映射中查找动作。如果找到,它将根据该配置创建一个动作;如果没有找到,它将尝试创建与给定动作对应的
action方法中定义的内联动作。
如果在此过程中任何一点发生错误,Yii2 将抛出yii\web\NotFoundHttpException。
默认和捕获所有路由
当 Yii2 收到一个解析为空路由的请求时,将使用默认路由。默认路由设置为site/index,它引用site控制器的index动作。可以通过设置application组件的defaultRoute属性来更改此行为,如下所示:
[
// index action of main controller
'defaultRoute' => 'main/index'
]
此外,可以通过设置yii\web\application的catchAll属性来配置 Yii2 将所有请求转发到单个路由。当需要执行应用程序维护时,这可能是有益的。
[
// Display a maintenance message
'catchAll' => 'site/maintenance'
]
自定义路由和 URL 规则
而不是依赖于 Yii2 内部生成的默认控制器/动作路由,我们可以编写自定义 URL 规则来定义我们自己的 URL 路由。Yii2 中的 URL 路由是通过yii\web\UrlRule的一个实例实现的,它由用于修补给定路由的路径信息和查询参数的模式组成。当使用自定义 URL 规则时,Yii2 将根据伴随请求将请求路由到第一个匹配的规则。此外,匹配规则确定如何分割请求参数。此外,使用yii\helpers\Url辅助器也将依赖于规则列表来内部路由请求。
可以通过设置yii\web\UrlManager:$rules属性为一个包含要匹配的 URL 模式作为键和相应的路由作为值的数组来在应用程序配置中定义 Yii2 的 URL 规则。例如,假设我们有一个用于管理已发布内容的控制器,我们可以编写如下自定义规则,将posts和post路由到我们的内容:
[
'posts' => 'content/index',
'post/<id:\d+>' => 'content/view',
]
现在当我们导航到应用程序的/posts端点时,内容/index 控制器动作对将被触发。正如前一个示例所示,URL 规则可以超出简单的字符串,并且可以包含复杂的正则表达式,我们可以使用这些正则表达式来条件性地路由规则。在前一个示例中,到/post端点的路由后跟一个整数 ID 将路由到内容/view 控制器动作对。此外,Yii2 将自动将$id参数传递给动作:
class ContentController extends yii\web\Controller
{
// Responds to content/index and /posts
public function actionIndex() {}
// Responds to content/view/id/<id> or /post/<id>
public function actionView($id) {}
}
小贴士
正则表达式只能指定给参数。然而,正如我们将在本节后面看到的,我们可以参数化我们的路由,使控制器和动作更加动态。
这些正则表达式可以进一步定制以包含更复杂的路由。例如,将以下内容添加到我们的 URL 路由中,将使我们能够向内容/index 动作传递额外的信息,例如我们想要显示的已发布条目的年份、月份和日期。
// Creates a route that includes the year, month, and date of a post
// eg: https://www.example.com/posts/2015/09/01
[
'posts/<year:\d{4}>/<month:\d{2}>/<day:\d{2}>' => 'content/index',
]
如您从表达式中预期的那样,此路由将仅匹配四位数的年份和两位数的月份和日期。此外,如前所述,通过将此信息添加到我们的 URL 规则中,yii\helper\Url助手将理解使用此模式创建的任何 URL:
// Routes to posts/2014/09/01
Url::to(['posts/index', 'year' => 2015, 'month' => 09, 'day' => 01]);
URL 路由也可以定义为路由域名和方案。例如,可以编写以下路由以确保不同的域名路由到网站的不同部分:
[
'https://dashboard.example.com/login' => 'dashboard/default/login',
'https://www.example.com/login' => 'site/login'
]
在同一代码库中处理多个前端应用时,这很有益处。
参数化路由
除了上一节中描述的命名参数之外,参数还可以嵌入到 URL 规则本身中。这种方法使 Yii2 能够将单个规则匹配到多个路由,这可以大大减少 URL 规则的数量,从而提高路由器的性能。例如,以下路由:
[
'<controller:(content|comment)>/<id:\d+>/<action:(create|list|delete)>' => '<controller>/<action>',
]
此路由将匹配具有给定 ID 的内容和评论控制器,并传递给相应的操作。然而,为了使路由匹配,必须定义所有命名参数。如果给定的路由不包含给定的参数,Yii2 将无法匹配该路由,这很可能会导致路由遇到 404 错误。绕过这种限制的一种方法是为路由提供默认参数,如下例所示:
[
// ...other rules...
[
// :\d+ is a regular expression for integers
'pattern' => 'content/<page:\d+>/<name>',
'route' => 'content/index',
'defaults' => ['page' => 1, 'name' => NULL],
],
]
在此示例中,page将默认为1,而name将默认为NULL。此 URL 规则可以匹配多个路由。在这个特定的情况下,将匹配多个路由:
-
/content, page=1, name=NULL -
/content/215, page=215, name=NULL -
/content/215/foo, page=215, name=foo -
/content/foo, page=1, name=foo
URL 后缀
作为为 URL 路由声明键值对的替代方法,路由可以定义为包含模式、路由甚至特定 URL 后缀的键值对数组的数组,以专门响应。
[
[
'pattern' => 'posts',
'route' => 'content/index',
'suffix' => '.xml',
],
]
这些路由可以用来配置您的应用程序以以不同格式响应某些类型的请求。
小贴士
默认情况下,以这种方式创建的规则将作为yii\web\UrlRule的一个实例创建,但可以通过定义class参数来更改。
专门 HTTP 方法 URL 规则
有时,您可能会发现将不同类型的 HTTP 方法路由到同一路由但以不同的方式处理它们是有益的。在 Yii2 中,可以通过在路由键之前添加方法类型来实现,如下例所示:
[
'PUT,POST users/<id:\d+>' => 'users/create',
'DELETE users/<id:\d+>' => 'users/delete',
'GET users/<id:\d+>' => 'users/view',
]
从 API 的角度来看,所有请求最终都将路由到users/<id>,但根据 HTTP 方法的不同,将执行不同的操作。
小贴士
具有指定 HTTP 方法的 URL 规则将仅用于路由目的,并且它们不会用于创建 URL,例如在使用yii\helper\Url时。
自定义 URL 规则类
虽然yii\web\Url非常灵活,并且应该覆盖您需要的所有 URL 规则用例,但往往会有需要自定义 URL 的情况。例如,出版商可能希望支持一种表示作者和书籍的格式,如/Author/Book,其中Author和Book都是从数据库中检索的数据。在 Yii2 中,可以通过扩展yii\base\Object并实现yii\web\UrlRuleInterface来创建自定义 URL 规则以解决这个问题,如下面的示例所示:
<?php
namespace app\components;
use yii\web\UrlRuleInterface;
use yii\base\Object;
class BookUrlRule extends Object implements UrlRuleInterface
{
public function createUrl($manager, $route, $params)
{
if ($route === 'book/index')
{
if (isset($params['author'], $params['book']))
return $params['author'] . '/' . $params['book'];
else if (isset($params['author']))
return $params['author'];
}
return false;
}
public function parseRequest($manager, $request)
{
$pathInfo = $request->getPathInfo();
if (preg_match('%^(\w+)(/(\w+))?$%', $pathInfo, $matches))
{
// If the parameterized identified in $matches[] matches a database value
// Set $params['author'] and $params['book'] to those attributes, then pass
// those arguments to your route
// return ['author/index', $params]
}
return false;
}
}
我们可以在yii\web\UrlManager::$rules部分实现我们的自定义规则,通过声明我们希望使用该类:
[
// [...],
[
// Reuslts in URL's like https://www.example.com/charlesportwodii/mastering-yii
'class' => 'app\components\BookUrlRule'
],
// [...],
]
动态规则生成
规则可以通过多种不同的方式以编程和动态的方式添加到您的应用程序中。动态规则生成可以采用自定义 URL 规则类,如前文所述,或者自定义 URL 管理器。然而,向 URL 管理器添加新 URL 规则的最简单方法还是使用addRules()方法。为了使规则生效,它们需要在应用程序引导过程中较早出现。为了使模块能够动态添加新规则,它们应该实现yii\base\BootstrapInterface并在bootstrap()方法中添加自定义 URL 规则,如下面的示例所示:
public function bootstrap($app)
{
$app->getUrlManager()->addRules([
// Add new rules here
], false);
}
小贴士
在复杂的 Web 应用程序中,监控您拥有的 URL 规则数量非常重要。添加许多不同的规则可能会严重降低应用程序的性能,因为 Yii2 需要遍历每个规则直到找到第一个匹配的规则。参数化路由和减少 URL 规则的数量可以显著提高应用程序的性能。
请求
在处理完我们希望请求去往的地方之后,我们通常需要编写特定的逻辑来处理 HTTP 请求的细节。为了帮助简化这个过程,Yii2 通过yii\web\Request对象表示 HTTP 请求,该对象可以提供有关 HTTP 请求的各种信息,例如请求体、GET和POST参数以及头部信息。在 Yii2 中,每个请求都可以通过请求应用程序组件轻松访问,在我们的代码中由Yii::$app->request表示。
检索请求参数和数据
当我们与请求对象一起工作时,最常见的工作任务是从GET和POST参数中检索数据,这些参数分别通过yii\web\Request::get()和yii\web\Request::post()方法实现。这些方法使我们能够一致且安全地访问应用程序的$_GET和$_POST参数:
$request = \Yii::$app->request;
// Retrieve all of the $_GET parameters
// similar to $get = $_GET
$get = $request->get();
// Retrieve all of the $_POST parameters
$post = $request->post();
然而,与原生的$_GET和$_POSTPHP 全局变量不同,Yii2 的请求对象允许我们安全地访问命名参数,如下一个示例所示:
// Retrieves the name $_GET parameter, $_GET['id']
// https://www.example.com/controller/action/id/5
// https://www.example.com/controller/action?id=5
$id = $request->get('id');
// Retrieves the named $_POST parameter
$name = $request->post('name');
如果参数未定义,Yii2 默认将返回NULL。这种行为可以通过设置yii\web\Request::get()和yii\web\Request::post()的第二个参数来修改:
// Default to 1 if ID is not set
$id = $request->get('id', 1);
// Default to 'Guest' if name is not set
$name = $request->post('name', 'Guest');
小贴士
除了提供对 $_GET 和 $_POST 数据的安全访问之外,请求对象在运行测试时也可以轻松地进行模拟。我们将在第十章 Testing with Codeception 中介绍如何与测试和模拟数据一起工作。使用 Codeception 进行测试。
作为额外的便利,Yii2 提供了确定我们正在处理的请求类型的能力,例如 GET、POST 或 PUT 请求。确定请求类型的最简单方法是查询 \Yii::$app->request->method,它将返回 HTTP 方法类型(例如 GET、PUT、POST、DELETE 等)。或者,我们可以通过查询许多布尔选项之一的条件检查请求,如下表所示:
| 属性 | 说明 |
|---|---|
yii\web\Request::$isAjax |
如果请求是 AJAX (XMLHTTPRequest) 请求 |
yii\web\Request::$isConsoleRequest |
如果请求是从控制台发出的 |
yii\web\Request::$isDelete |
如果请求是 HTTP DELETE 请求 |
yii\web\Request::$isFlash |
如果请求来自 Adobe Flex 或 Adobe Flash。 |
yii\web\Request::$isGet |
如果请求是 HTTP GET 请求 |
yii\web\Request::$isHead |
如果请求是 HTTP HEAD 请求 |
yii\web\Request::$isOptions |
如果请求是 HTTP OPTIONS 请求 |
yii\web\Request::$isPatch |
如果请求是 HTTP PATCH 请求 |
yii\web\Request::$isPjax |
如果请求是 HTTP PJAX 请求 |
yii\web\Request::$isPost |
如果请求是 HTTP POST 请求 |
yii\web\Request::$isPut |
如果请求是 HTTP PUT 请求 |
yii\web\Request::$isSecureConnection |
如果请求是通过安全(HTTPS)连接发出的 |
与作为表单发送的 GET 和 POST 请求不同,许多这些请求直接在请求体中提交数据。要访问这些数据,我们可以使用 yii\web\Request::getBodyParam() 和 yii\web\Request::getBodyParams(),如下所示:
$request = Yii::$app->request;
$allParamns = $request->bodyParams;
$name = $request->getBodyParam('name');
$manyParams = $request->getBodyParams(['name', 'age', 'gender']);
请求头部和 cookie
除了请求体之外,Yii2 的请求对象还可以检索随请求发送的头部和 cookie 信息。与我们请求一起发送的头部最终由 yii\web\HeaderCollection 表示,它提供了用于处理头部的一些方法,即 yii\web\HeaderCollection::get() 和 yii\web\HeaderCollection::has()。在下面的示例中,我们正在检查 X-Auth-Token 头部是否已设置,如果已设置,则将其分配给 $authToken 变量:
// $headers is an object of yii\web\HeaderCollection
$headers = Yii::$app->request->headers;
// If the header has 'X-Auth-Token', retrieve it.
if ($headers->has('X-Auth-Token'))
$authToken = $headers->get('X-Auth-Token');
提示
如果没有为 yii\web\HeaderCollection::get() 方法提供参数,将返回所有头部的数组。有关 yii\web\HeaderCollection 的更多详细信息,请参阅 www.yiiframework.com/doc-2.0/yii-web-headercollection.html。
请求对象有多个内置默认值,用于访问一些常用查询头,即:
-
yii\web\Request::$userAgent获取浏览器发送的用户代理 -
可以使用
yii\web\Request::$contentType来确定适当的响应类型 -
yii\web\Request::$acceptableContentTypes返回客户端将接受的全部可接受的内容类型 -
如果我们的应用程序配置为支持多种语言,则可以使用
yii\web\Request::$acceptableLanguages。
小贴士
请求对象也可以通过 yii\web\Request::getPreferedLanguage() 告诉我们客户端的首选语言。我们将在第十一章 Internationalization and Localization 中更深入地使用这个变量和通用翻译与本地化。
除了我们的头部信息外,我们还可以通过查询 Yii::$app->request->cookies 来检索与我们的请求一起发送的 cookie 数据,这将返回一个 yii\web\CookieCollection 实例,正如你可能怀疑的那样,它包含许多与 yii\web\HeaderCollection 提供的相同类型的函数,例如 get() 和 has()。
小贴士
Yii2 API 文档在 www.yiiframework.com/doc-2.0/yii-web-cookiecollection.html 提供了 yii\web\CookieCollection 的完整方法集。
获取客户端和 URL 信息
除了请求信息外,Yii2 请求对象还可以用来检索客户端和我们的应用程序状态信息。例如,可以使用 yii\web\Request::$userHost 和 yii\web\Request::$userIP 访问客户端信息,如主机名或 IP 地址。
小贴士
如果你的请求是通过代理或负载均衡器转发的,那么用户的 IP 地址可能不准确。请确保你的 Web 服务器已正确配置,以便传递原始数据。
可以通过引用各种方法来检查应用程序状态的数据,这些方法比查询 $_SERVER 全局变量更方便。以下表格显示了其中一些最常用的属性:
| 属性 | 说明 |
|---|---|
yii\web\Request::$absoluteUrl |
这是包含主机名和所有 GET 参数的绝对 URL(例如,https://www.example.com/controller/action/?name=foo) |
yii\web\Request::$baseUrl |
这是入口脚本之前使用的基 URL。 |
yii\web\Request::$hostInfo |
这是主机详情(例如,https://www.example.com) |
yii\web\Request::$pathInfo |
这是入口脚本之后的完整路径(例如,/controller/action) |
yii\web\Request::$queryString |
这是 GET 查询字符串(例如,name=foo) |
yii\web\Request::$scriptUrl |
这是没有路径和查询字符串的 URL(例如,/index.php) |
yii\web\Request::$serverName |
这是服务器名称(例如,example.com) |
yii\web\Request::$serverPort |
这是服务器正在运行的端口(通常为 TLS 连接的 80 或 443) |
yii\web\Request::$url |
这是完整的 URL,但不包含主机和方案信息(例如,controller/action/?name=foo)。 |
提示
请求对象能够表示 HTTP 请求的几乎所有方面以及可能存储在$_SERVER全局变量中的数据。有关请求对象的更多信息,请参阅 Yii2 API 文档www.yiiframework.com/doc-2.0/yii-web-request.html。
响应
在完成请求对象的处理之后,Yii2 随后生成一个响应对象,并将其发送回客户端。响应包含大量信息,例如 HTTP 状态码、响应体和头部。在 Yii2 中,响应对象由yii\web\Response实现,它由response应用程序组件表示。在本节中,我们将探讨如何与响应对象一起工作。
设置状态码
在大多数情况下,Yii2 能够完全胜任将适当的状态码设置回最终用户;然而,可能存在需要我们明确为我们的应用程序定义 HTTP 响应码的情况。要修改我们应用程序中的 HTTP 状态码,我们只需将yii\web\Response::$statusCode设置为有效的 HTTP 状态码:
\Yii::$app->response->statusCode = 200;
网络异常
默认情况下,Yii2 将为任何成功的请求返回 HTTP 200 状态码。如果我们想在不中断逻辑流程的情况下调整状态码,我们可以简单地为yii\web\Response::$statusCode定义一个新的状态码。在其他情况下,可能更好的做法是抛出异常,以在应用程序流程中引起短路,防止执行额外的逻辑。
通常,可以通过调用带有有效 HTTP 状态码的yii\web\HttpException来抛出网络异常:
throw new \yii\web\HttpException(409);
为了方便起见,Yii2 为几种不同类型的请求提供了几个特定的方法,如下表所示:
| 异常 | 状态码 | HTTP 错误 |
|---|---|---|
yii\web\BadRequestHttpException |
400 | 错误请求 |
yii\web\UnauthorizedHtpException |
401 | 未授权 |
yii\web\ForbiddenHttpException |
403 | 禁止访问 |
yii\web\NotFoundHttpException |
404 | 未找到 |
yii\web\MethodNotAllowedException |
405 | 不允许的方法 |
yii\web\NotAcceptableHttpException |
406 | 不可接受 |
yii\web\ConflictHttpException |
409 | 冲突 |
yii\web\GoneHttpException |
410 | 已消失 |
yii\web\UnsupportedMediaTypeHttpException |
415 | 不支持的媒体类型 |
yii\web\TooManyRequestsHttpException |
429 | 请求过多 |
yii\web\ServerErrorHttpException |
500 | 服务器错误 |
提示
作为抛出一个带有给定状态码的空 yii\web\HttpException 的替代方案,你也可以扩展 yii\web\HttpException 来实现你自己的 HttpException 异常。
设置响应头
与 yii\web\Request 对象一样,我们可以使用 yii\web\HeaderCollection 的 add() 和 remove() 方法来操作我们响应的 HTTP 头部,如下面的示例所示:
yii\web\HeaderCollection
$headers = Yii::$app->response->headers;
// Add two headers
$headers->add('X-Auth-Token', 'SADFLJKBQ43O7AGB28948QT');
$headers->add('Pragma', 'No-Cache');
// Remove a header
$headers->remove('Pragma');
提示
添加新头将覆盖具有相同名称的任何先前设置的头部。此外,通过 yii\web\HeaderCollection 设置的所有头部都是不区分大小写的。删除一个头部将删除任何具有当前发送名称的头部。
可以在任何时候操作头部,直到调用 yii\web\Response::send(),默认情况下,这是在响应体发送出去之前调用的。
响应体
通常,响应体将由 yii\web\View 的一个实例表示,这通常通过在控制器操作中返回渲染的视图来显示给最终用户,如下所示。默认情况下,Yii2 将以 MIME 类型 text/HTML 返回响应,并使用 yii\web\HtmlResponseFormatter 格式化响应:
public function actionIndex()
{
return $this->render('index');
}
然而,可能存在需要不同响应类型的情况,例如在显示 JSON 或 XML 数据时。在我们的控制器操作中,我们可以通过设置 yii\web\Response::$format 属性并返回表示我们想要格式化的数据的数组或字符串来更改输出格式,如下面的示例所示:
public function actionIndex()
{
\Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
return [
'message' => 'Index Action',
'code' => 200,
];
}
之前的示例将输出以下 JSON 数据给客户端:
{
"message": "Index Action",
"code": 200
}
除了 JSON 格式化外,yii\web\Response::$format 还可以设置为 JSONP、HTML、RAW 和 XML,具体细节如下表所示:
| 类型 | 格式化器类 | 格式值 |
|---|---|---|
| HTML | yii\web\HtmlResponseFormat |
FORMAT_HTML |
| RAW | FORMAT_RAW |
|
| XML | yii\web\XmlResponseFormatter |
FORMAT_XML |
| JSON | yii\web\JsonResponseFormatter |
FORMAT_JSON |
| JSONp | yii\web\JsonResponseFormatter |
FORMAT_JSON |
提示
原始数据将按原样提交给客户端,不对其应用任何额外的格式。
除了处理默认响应对象外,在 Yii2 中,你还可以创建新的响应对象以发送给最终用户,如下面的示例所示:
public function actionIndex()
{
return \Yii::createObject([
'class' => 'yii\web\Response',
'format' => \yii\web\Response::FORMAT_JSON,
'data' => [
'message' => 'Index Action',
'code' => 100,
],
]);
}
提示
为响应应用程序组件设置的任何自定义配置都不会应用于你实例化的任何自定义响应对象。
虽然控制器动作是您最可能编辑响应体的地方,但您可以通过直接操作\Yii::$app->response在任何地方修改 Yii2 中的响应体。任何已经格式化的数据都可以通过设置yii\web\Response::$content属性直接分配给响应对象。此外,如果您想在数据发送给用户之前通过响应格式化数据,可以设置yii\web\Response::$content,然后设置yii\web\Response::$data为要格式化的数据:
$response = \Yii::$app->response;
$response->format = yii\web\Response::FORMAT_JSON;
$response->data = [
'message' => 'Index Action',
'code' => 100
];
重定向
为了将浏览器重定向到新页面,响应必须设置特殊的头部位置。Yii2 通过yii\web\Response::redirect()方法提供了对此的特殊支持,该方法可以在控制器动作内部调用,如下所示:
public function actionIndex()
{
return $this->redirect('https://www.example.com/index2');
}
小贴士
默认情况下,Yii2 将返回 302 状态码,表示重定向应该是临时的。为了通知浏览器永久重定向请求,可以将yii\web\Response::redirect()的第二个参数设置为 301,这是永久重定向的 HTTP 状态码。
在控制器动作外部,可以通过调用redirect()方法然后立即发送响应来调用重定向,如下例所示:
\Yii::$app->response->redirect('https://www.example.com/index2', 301)->send();
文件输出
与浏览器重定向类似,输出文件到客户端需要设置几个自定义头部。为了便于将文件传输到浏览器,Yii2 提供了三种不同的方法来输出文件:
-
在发送位于磁盘上的现有文件时,应使用
yii\web\Response::sendFile()。 -
yii\web\Response::sendContentAsFile()将数据字符串作为文件发送(例如 CSV 文件) -
对于大文件(通常,文件大小超过 100 MB),应使用
yii\web\Response::sendStreamAsFile(),并且它应该以流的形式发送到浏览器,因为它更节省内存。在控制器中,可以直接调用这些方法来发送文件:public function actionReport() { return \Yii::$app->response->sendFile('path/to/report.csv'); }
与重定向浏览器类似,这些方法可以通过直接操作响应对象在控制器动作外部调用:
\Yii::$app->response->sendFile('path/to/report.csv')->send();
小贴士
关于响应对象的更多信息可以在 Yii2 文档中找到,请参阅www.yiiframework.com/doc-2.0/yii-web-response.html。
事件
在处理复杂的代码库时,我们可能会实现钩子和处理器,以便我们的应用程序可以在主应用程序流程之外调用自定义代码。在 Yii2 中,这些处理器被称为事件,当触发给定事件时可以自动执行。例如,在一个博客平台上,我们可能会创建一个事件来指示帖子已发布,这将触发一些自定义代码向特定邮件列表的用户发送电子邮件。在本节中,我们将介绍如何创建事件处理器、触发事件以及编写我们自己的自定义事件。
事件处理器
Yii2 中的事件是在yii\base\Component基类中实现的,这个基类几乎被 Yii2 中的每个类扩展。通过扩展这个类,我们可以在代码库的几乎任何地方绑定一个事件。要开始使用事件,我们首先需要创建一个事件处理器。
Yii2 中的事件处理器可以通过调用yii\base\Component::on()方法来绑定,并指定当事件被触发时应执行的回调。这些回调可以采取几种不同的形式,从作为字符串指定的全局 PHP 函数到在事件内联编写的匿名函数。例如,如果我们想调用一个全局 PHP 函数(如我们定义的或内置函数如trim),我们可以绑定我们的事件,如下所示:
$thing = new app\Thing;
$thing->on(Thing::EVENT_NAME, 'php_function_name');
可以在任意 PHP 对象上调用事件处理器:要么是我们已经有实例变量的对象,要么是我们应用程序中的命名空间类:
$thing->on(Thing::EVENT_NAME, [$object, 'method']);
$thing->on(Thing::EVENT_NAME, ['app\components\Thing ', 'doThing']);
此外,事件处理器可以编写为匿名函数:
$thing->on(Thing::EVENT_NAME, function($event) {
// Handle the event
});
可以通过将任何数据作为yii\base\Component::on()方法的第三个参数传递给事件处理器来传递额外的数据:
$thing->on(Thing::EVENT_NAME, function($event) {
echo $event->data['foo']; // bar
}, ['foo' => 'bar']);
此外,可以将多个事件处理器绑定到单个事件。当给定事件被触发时,每个事件将按照绑定到事件的顺序执行。如果事件处理器需要停止后续事件的执行,它可以将$event对象的yii\base\Event::$handled属性设置为true,这将防止所有绑定到该事件的处理器执行:
$thing->on(Thing::EVENT_NAME, function($event) {
// Handle the event
$event->handled = true;
}, $data);
默认情况下,Yii2 中的事件处理器按照调用的顺序绑定,这意味着绑定到给定事件的最后一个事件处理器将最后被调用。要将在事件处理器队列的开头添加一个事件处理器,可以将yii\base\Component::on()方法的$append参数设置为false,这将覆盖默认行为,并在事件被触发时首先触发事件处理器:
$thing->on(Thing::EVENT_NAME, function($event) {
// Handle the event
}, [], false);
事件处理器也可以通过使用与附加事件监听器到事件相同的语法调用yii\base\Component::off()来从它们监听的事件中解绑。或者,可以通过调用yii\base\Component::off()而不带任何额外参数来从事件中解绑所有事件处理器,如下面的示例所示:
$thing->off(Thing::EVENT_NAME);
触发事件
在 Yii2 中,通过调用yii\base\Component::trigger()方法来触发事件,该方法将事件名称作为第二个参数,并可选地传递yii\base\Event的实例作为第二个参数。例如,我们可以在代码中调用Thing::EVENT_NAME,如下所示:
$this->trigger(Thing::EVENT_NAME);
此事件之前绑定如下:
$thing->on(Thing::EVENT_NAME, ['app\components\Thing, 'doThing']);
现在,app\components\Thing::doThing()方法将被触发。这段代码对于我们的虚构组件可能如下所示:
<?php
namespace app\components;
use yii\base\Component;
use yii\base\Event;
class Thing extends Component
{
const EVENT_NAME = 'name';
public function doThing()
{
// This is the event handler
}
}
注意
Yii2 认为将事件名称存储为类中的常量是一种最佳实践。
可以通过扩展yii\base\Event并将其作为触发调用第二个参数传递,向我们的事件处理器发送附加信息,如下面的示例所示:
<?php
namespace app\components;
use yii\base\Component;
use yii\base\Event;
class LogEvent extends Event
{
public $message;
}
class Logger extends Component
{
const EVENT_LOG = 'log_event';
/**
* Log with $message
* @param string $message
*/
public function log($message)
{
$event = new LogEvent;
$event->message = $message;
$this->trigger(self::EVENT_LOG, $event);
}
}
由于 PHP 的单线程特性,Yii2 的事件将同步发生而不是异步发生,这将会阻塞所有其他应用程序流程,直到事件处理队列中的所有事件都完成。因此,在使用多个事件时应该小心,因为它们可能会对应用程序性能产生不利影响。
小贴士
使用实现异步事件(例如从 CMS 发送电子邮件通讯)的一种方法是将事件处理程序传递给第三方消息队列(如 Gearman、Sidekiq 或 Resque),并立即返回事件。然后,事件可以在单独的处理线程中处理,这可以是一个配置为从消息队列读取事件并单独处理它们的 Yii2 控制台命令。
类级事件
之前描述的事件是在实例级别绑定的。在 Yii2 中,事件可以绑定到类的每个实例,而不是特定实例,它们也可以绑定到 Yii2 的全局事件处理器。
类级事件可以通过直接通过yii\base\Event::on()附加事件处理器来绑定。例如,Active Record 在从数据库中删除记录时将触发EVENT_AFTER_DELETE事件。我们可以为每个 Active Record 实例记录此信息,如下面的示例所示:
<?php
use Yii;
use yii\base\Event;
use yii\db\ActiveRecord;
Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_DELETE, function ($event) {
Yii::trace(get_class($event->sender) . ' deleted a record.');
});
每当触发器发生时,它将首先调用实例级事件处理器,然后调用类级事件处理器,最后调用全局事件处理器。可以通过直接调用yii\base\Event::trigger()显式调用类级事件。此外,可以通过yii\base\Event::off()移除类级事件处理器。
全局事件
在 Yii2 中,通过将事件处理器绑定到应用程序单例实例本身来支持全局事件,如下面的示例所示:
<?php
use Yii;
use yii\base\Event;
use app\components\Foo;
Yii::$app->on('thing', function ($event) {
echo get_class($event->sender);
});
Yii::$app->trigger('thing', new Event(['sender' => new Thing]));
小贴士
当使用全局事件时,要小心不要覆盖 Yii2 的内置全局事件。你使用的任何全局事件都应该包含某种前缀,以避免与 Yii2 的内置事件冲突。
摘要
本章我们介绍了 Yii2 中请求和响应处理的基本知识。我们首先探讨了 Yii2 如何处理 URL 路由的分配,并学习了如何操作和创建我们自己的自定义 URL 规则。然后,我们探讨了yii\web\Request和yii\web\Response对象,并更好地理解了如何使用这些对象来操作进入和离开我们应用程序的请求和响应。最后,我们学习了 Yii2 中事件的工作方式,并学习了如何创建我们自己的事件。
在下一章中,我们将通过探索如何实现 RESTful API,将本章中获得的知识提升到新的水平。
第九章。RESTful API
表征状态转移(REST)是客户端-服务器通信的现代方法,它将客户端(如 Bowers 和移动应用程序)与应用程序的服务器组件解耦。RESTful 实现使后端实现能够使用一种通用语言(通常是 XML 或 JSON)进行通信,同时充分利用 HTTP 动词,如GET、POST、PUT和DELETE。RESTful 应用程序使我们能够构建无状态、可扩展和统一的应用程序,我们可以将其分发给我们的客户端。使用 Yii2,我们可以快速实现 RESTful API,作为我们应用程序的一部分或全部。
ActiveController
在 Yii2 中创建 RESTful API 的最简单方法是利用yii\rest\ActiveController。像yii\web\Controller一样,yii\rest\ActiveController提供了一个控制器接口,我们可以在我们的./controllers目录中实现。与yii\web\Controller不同,使用yii\rest\ActiveController和yii\db\ActiveRecord模型实现将立即为该模型创建一个完整的 REST API,而无需编写大量代码。使用yii\rest\ActiveController实现的模型还提供了以下附加功能:
-
XML 和 JSON 响应格式
-
速率限制
-
数据和 HTTP 缓存
-
认证
-
完全支持 HTTP 动词(
GET、POST、PATCH、HEAD和OPTIONS) -
数据验证
-
分页
-
支持 HATEOAS
例如,让我们公开我们在第四章中创建的用户模型,活动记录、模型和表单。要开始使用yii\rest\ActiveController,我们首先需要在controllers/目录中创建一个名为UserController.php的控制器,它引用我们之前创建的用户模型:
<?php
namespace app\controllers;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public $modelClass = 'app\models\User';
}
接下来,我们需要对我们的config/web.php配置文件进行一些配置更改,以便 Yii2 可以将正确的路由路由到我们新创建的控制器,并确保我们的应用程序可以接受 JSON 输入。因为我们已经启用了美观的 URL 并禁用了在urlManager组件中显示脚本名称,所以我们只需要为我们的用户类添加一个自定义 URL 规则。这个 URL 规则是yii\rest\UrlRule的一个实例,它将处理我们控制器所需的所有路由:
return [
// [...],
'components' => [
// [...],
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
['class' => 'yii\rest\UrlRule', 'controller' => 'user'],
],
],
]
];
接下来,我们需要修改我们的应用程序使用的基请求对象,以便它可以解析 JSON 输入:
return [
// [...],
'components' => [
// [...],
'request' => [
'parsers' => [
'application/json' => 'yii\web\JsonParser',
]
]
]
];
小贴士
对请求解析器的更改是为了方便,因为 JSON 是一种易于处理的输入类型。如果没有这个更改,我们的应用程序将只能解析application/x-www-form-urlencoded和multipart/form-data请求格式。
通过简单地添加几行代码,我们现在已经为我们的Users模型实现了一个完整的 REST API。以下表格展示了yii\rest\ActiveController为我们提供的完整方法列表:
| HTTP 方法 | 端点 | 结果 |
|---|---|---|
GET |
/users |
这是一个所有用户的列表 |
GET |
/users/<id> |
这包含具有给定 <id> 标签的用户的详细信息 |
POST |
/users |
这使用请求体中的数据创建一个新用户 |
PATCH |
/users/<id> |
这使用请求体中的数据修改具有给定 <id> 标签的用户 |
DELETE |
/users/<id> |
这将删除具有给定 <id> 标签的用户 |
HEAD |
/users |
这检索头部信息 |
HEAD |
/users/<idl> |
这检索给定 <id> 标签用户的头部信息 |
OPTIONS |
/users |
这检索类似 Ajax 的请求的 HTTP 选项 |
OPTIONS |
/users/<id> |
这检索具有给定 <id> 标签的用户的类似 Ajax 的请求的 HTTP 选项 |
查询我们新创建的 REST API 的简单方法是使用名为 CURL 的命令行工具。例如,为了检索 /users 端点的头部,我们可以运行以下命令:
$ curl -i -X HEAD https://www.example.com/users
您将得到类似以下输出的结果:
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
X-Pagination-Total-Count: 4
X-Pagination-Page-Count: 1
X-Pagination-Current-Page: 1
X-Pagination-Per-Page: 20
Link: <https://www.example.com/users?page=1>; rel=self
Access-Control-Allow-Origin: *
如前所述,yii\rest\ActiveRecord 立即为我们提供了一组有用的信息,例如 CORS 头和分页详情。
使用 CURL,我们还可以查询原始数据本身。我们的 API 可以根据我们随请求一起提交的 Accept 头部以 JSON 或 XML 格式响应。下一个示例演示了一个将以 JSON 格式响应的请求:
$ curl –I -H "Accept:application/json" https://www.example.com/users | jq .
[
{
"updated_at": 1442602004,
"created_at": 1442602004,
"role_id": 1,
"last_name": "Joe",
"first_name": "Jane",
"password": "$2y$13$pc0TEJged1BwmqpGL7dywupNzG6bCBWRjBbDMzBXhv7FewvUR/qqm",
"email": "jane.doe@example.com",
"id": 1
},
{...}
]
小贴士
默认情况下,CURL 将数据作为单行返回。如前一个命令所示,我们将 cURL 请求的 JSON 响应通过管道传输到名为 jq(stedolan.github.io/jq/) 的工具,该工具用于以易于阅读的格式格式化数据。或者,您可以选择安装一个基于图形的工具来提交和以易于阅读的格式显示响应。
此外,我们还可以通过在请求中传递它们作为 GET 参数来过滤特定的字段。例如,如果我们只想检索数据库中 ID 为 1 的用户的姓氏和名字,我们可以执行以下命令:
$ curl -H "Accept:application/json" /
https://www.example.com/users/1?fields=id,first_name,last_name | jq .
这将返回以下响应:
{
"last_name": "Joe",
"first_name": "Jane",
"id": 1
}
配置 ActiveController 显示字段
如您可能已注意到,yii\rest\ActiveController 只返回从数据库中填充的字段,并且不会返回您可能已创建的额外字段(例如,将姓氏和名字连接起来的 full_name 字段)或关系。此外,它还公开数据库中的每个字段,包括敏感数据,如加密的密码散列。绕过这种限制的一种方法是我们修改模型中的 fields() 方法。
例如,为了防止意外在我们的 API 中公开散列密码,我们可以在 User 模型中实现自定义的 fields 方法,如下所示。我们还可以更改某些字段的显示名称:
class User extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface
{
/**
* API safe fields
*/
public function fields()
{
return [
'id',
'email_address' => 'email',
'first_name',
'last_name',
'full_name' => function($model) {
return $model->getFullName();
},
'updated_at',
'created_at'
];
}
}
现在,让我们按照以下方式查询我们的 API:
$ curl -H "Accept:application/json" https://www.example.com/users/1 /
| jq .
我们检索以下响应:
{
"created_at": 1442602004,
"updated_at": 1442602004,
"full_name": "Jane Joe",
"last_name": "Joe",
"first_name": "Jane",
"email_address": "jane.doe@example.com",
"id": 1
}
此外,我们可以通过实现模型的extraFields()方法来公开关系数据,如下一个示例所示:
public function extraFields()
{
// Expose the 'role' relation
return ['role'];
}
小贴士
extraFields()方法将在我们的响应中公开整个模型关系,如果我们的关系包含敏感信息,则可能存在安全风险。请确保您在相关属性中使用fields()方法来限制将返回的数据。
我们可以通过在GET参数中添加expand=role来扩展这些数据,如下面的示例所示:
$ curl -H "Accept:application/json" /
https://www.example.com/users/1?expand=role | jq .
{
"role": {
"name": "User",
"id": 1
},
"created_at": 1442602004,
"updated_at": 1442602004,
"full_name": "Jane Joe",
"last_name": "Joe",
"first_name": "Jane",
"email_address": "jane.doe@example.com",
"id": 1
}
响应中的数据序列化
除了修改我们响应中显示的字段外,我们还可以修改我们的响应以包含有用的信息,例如在头信息中发送的信息(例如分页信息和链接),并将我们的响应包装在一个易于在响应中识别的容器中。我们可以通过在yi\rest\ActiveController中添加并指定序列化器来完成此操作,如下面的示例所示:
<?php
namespace app\controllers;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public $modelClass = 'app\models\User';
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'users',
];
}
现在,当我们查询我们的/users端点时,我们将得到以下响应:
$ curl -H "Accept:application/json" https://www.example.com /users /
| jq .
{
"_meta": {
"perPage": 20,
"currentPage": 1,
"pageCount": 2,
"totalCount": 21
},
"_links": {
"self": {
"href": "https://www.example.com/users?page=1"
},
"next": {
"href": "https://www.example.com/users?page=2"
}
},
"users": [
{
"created_at": 1442602004,
"updated_at": 1442602004,
"full_name": "Jane Joe",
"last_name": "Joe",
"first_name": "Jane",
"email_address": "jane.doe@example.com",
"id": 1
},
{...},
{...}
]
}
禁用 ActiveController 动作
虽然yii\rest\ActiveController提供了许多有用的动作,但可能存在您不希望公开默认暴露的每个方法的情况。以下动作由yii\rest\ActiveController自动暴露:
| 动作名称 | 结果 |
|---|---|
index |
这将列出模型提供的所有资源,并支持分页 |
view |
这将返回特定模型的详细信息 |
create |
这将创建一个新的模型实例 |
update |
这将更新现有的模型实例 |
delete |
这将删除模型 |
options |
这将返回可用的方法 |
除了重写动作方法外,还有几种不同的方法可以禁用动作。可以通过从控制器中的actions()方法内的动作列表中移除它们来禁用动作。例如,要禁用delete和create,我们可以按照以下方式移除它们:
<?php
namespace app\controllers;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public $modelClass = 'app\models\User';
public function actions()
{
$actions = parent::actions();
// disable the "delete" and "create" actions
unset($actions['delete'], $actions['create']);
return $actions;
}
}
或者,可以通过从我们的网络配置中的yii\rest\UrlRule移除路由来禁用动作,通过设置规则的only或except参数。在以下示例中,我们的路由器中已禁用了delete、create和update动作:
[
'class' => 'yii\rest\UrlRule',
'controller' => 'user',
// 'only' => [ 'index' ], // Only allow index
'except' => ['delete', 'create', 'update'], // Disabled
]
自定义 ActiveController 动作
有几种方法可以修改yii\rest\ActiveController提供的每个动作返回的数据。除了直接重载特定方法外,还可以修改每个动作的数据提供者。例如,要更改索引动作的数据提供者,我们可以编写类似于以下代码块的代码:
<?php
namespace app\controllers;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public function actions()
{
$actions = parent::actions();
// Customize the data provider preparation with the "prepareDataProvider()" method
$actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider'];
return $actions;
}
private function prepareDataProvider()
{
// Prepare a new data provider
}
}
认证过滤器
在第七章,认证和授权用户中,我们介绍了用户访问控制过滤器的基础知识,以控制哪些用户可以访问我们的控制器。与依赖于会话数据以在每次请求之间持久化用户数据的面向状态的程序不同,RESTful API 本质上是无状态的,这意味着每个请求都必须提供所需的信息以对每个用户进行身份验证。为了帮助我们通过 API 进行用户认证,Yii2 提供了三个内置方法来控制对 API 的访问:
-
HTTP 基本认证
-
查询参数认证
-
OAuth2 认证
此外,我们还可以定义我们自己的自定义认证方法。
要开始在我们的 API 中认证用户,我们需要对我们的应用程序进行以下更改:
-
通过以下方式配置我们的配置中的用户组件:
-
通过将
enableSession设置为false来禁用会话 -
将
loginUrl属性设置为 null 以防止重定向到登录页面
-
-
在我们的控制器
behaviors()方法中指定认证方法 -
在我们的用户身份类中实现
yii\web\IdentityInterface::findIdentityByAccessToken()
小贴士
如果您将 REST API 与您的正常 Yii2 应用程序混合使用,可能会遇到问题。因此,强烈建议您将 API 作为与 Yii2 应用程序分开的独立应用程序运行。
HTTP 基本认证
处理认证的最基本方法是实现 HTTP 基本认证。HTTP 基本认证由the yii\filters\auth\HttpBasicAuth类提供,可以按以下方式实现:
<?php
namespace app\controllers;
use yii\filters\auth\HttpBasicAuth;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpBasicAuth::className(),
];
return $behaviors;
}
}
现在,如果我们尝试在没有足够凭据的情况下查询我们的 API,我们将收到以下响应:
{
"name": "Unauthorized",
"message": "You are requesting with an invalid credential.",
"code": 0,
"status": 401,
"type": "yii\\web\\UnauthorizedHttpException"
}
如果我们尝试导航到应用程序中的任何端点,我们将收到以下弹出窗口,要求我们进行身份验证:

默认情况下,Yii2 将仅将用户名作为令牌传递给yii\web\IdentityInterface::findIdentityByAccessToken()。通常,用户名不足以对用户进行身份验证。可以通过指定yii\filters\auth\HttpBasicAuth的auth属性来覆盖此行为,这将允许我们将用户名和密码传递到我们选择的功能中:
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'auth' => ['app\models\User', 'httpBasicAuth' ],
'class' => HttpBasicAuth::className(),
];
return $behaviors;
}
在我们的用户模型中,我们可以定义httpBasicAuth()方法如下:
/**
* Handle HTTP basic auth
* @param string $email
* @param string $password
* @return static self
*/
public function httpBasicAuth($email, $password)
{
$model = static::findOne(['email' => $email]);
if ($model == NULL)
return NULL;
if (password_verify($password, $model->password))
return $model;
return NULL;
}
小贴士
在所示示例中,我们正在验证用户名和密码与我们创建在第四章的用户,活动记录、模型和表单。在这种情况下,我们正在将密码与先前创建的 bcrypt 散列进行验证。确保您参考该章节中列出的凭据以获取示例。
现在,如果我们查询我们的 API,如果我们有有效的凭据,我们将收到有效的响应;如果我们提供错误的凭据,我们将收到错误。
查询参数认证
作为查询参数认证的替代方案,我们可以通过指定查询参数来授权访问我们的 API。这可以是一个全局查询参数,我们将其视为密钥,或者它可以是我们在登录请求上签发的每个用户的令牌。查询参数认证可以通过实现 yii\filters\auth\QueryParamAuth 来实现。在以下示例中,我们正在寻找一个名为 token 的 GET 参数,其中包含我们的令牌:
<?php
namespace app\controllers;
use yii\filters\auth\QueryParamAuth;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'tokenParam' => 'token',
'class' => QueryParamAuth::className(),
];
return $behaviors;
}
}
认证可以通过在我们的模型中实现 yii\web\IdentityInterface::findIdentityByAccessToken() 来执行。最简单的例子是创建一个新的迁移,为我们的用户表添加一个名为 access_token 的新列,该列在我们认证请求时被填充。然后我们可以通过向我们的 User 模型添加以下代码来对其进行验证:
/**
* @inheritdoc
*/
public static function findIdentityByAccessToken($token, $type=null)
{
return static::findOne(['access_token' => $token]);
}
OAuth2 认证
在 Yii2 中,最复杂的认证方法是 OAuth2 认证,这是通过 yii\auth\filters\HttpBearerAuth 实现的。与 yii\auth\filters\QueryParamAuth 类似,yii\auth\filters\HttpBearerAuth 可以通过在 behaviors() 方法中设置适当的行为,然后实现 yii\web\IdentityInterface::findIdentityByAccessToken() 来实现。
<?php
namespace app\controllers;
use yii\filters\auth\HttpBearerAuth;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpBearerAuth::className(),
];
return $behaviors;
}
}
如果您不熟悉 OAuth2 的工作流程,可以通过设置带有特定 Bearer 令牌的 Authorization 标头来模拟登录请求,如下所示:
Headers:
Authorization: Bearer <token>
标头中的 <token> 部分最终将被传递给 yii\web\IdentityInterface::findIdentityByAccessToken()。
复合认证
为了提高我们 API 的安全性,我们可以通过实现 yii\filters\auth\CompositeAuth 将几个不同的认证过滤器捆绑在一起。为了认证我们的 API,我们需要满足 behaviors() 方法中列出的所有认证要求。复合认证可以在我们的控制器中按如下方式配置:
<?php
namespace app\controllers;
use yii\filters\auth\CompositeAuth;
use yii\filters\auth\HttpBasicAuth;
use yii\filters\auth\QueryParamAuth;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => CompositeAuth::className(),
'authMethods' => [
HttpBasicAuth::className(),
QueryParamAuth::className(),
],
];
return $behaviors;
}
}
自定义认证过滤器
作为内置认证提供程序的替代方案,我们还可以定义我们自己的认证过滤器。例如,作为我们登录请求的一部分,我们可能为用户生成一个唯一的令牌,以便对所有附加请求进行操作。而不是要求我们的 API 客户端存储用户的原始密码或将凭据作为可能出现在我们服务器日志文件中的 GET 参数传递,我们可以让我们的用户提交他们的认证令牌作为唯一的标头。以下是如何实现此功能的示例类:
<?php
namespace app\filters\auth;
use yii\filters\auth\AuthMethod;
use yii\web\UnauthorizedHttpException;
/**
* HeaderParamAuth is an action filter that supports the authentication based on the access token passed through a query parameter.
*/
class HeaderParamAuth extends AuthMethod
{
/**
* @var string the parameter name for passing the access token
*/
public $tokenParam = 'x-auth-token';
/**
* @inheritdoc
*/
public function authenticate($user, $request, $response)
{
$accessToken = $request->getHeaders()[$this->tokenParam];
if (is_string($accessToken))
{
$identity = $user->loginByAccessToken($accessToken, get_class($this));
if ($identity !== null)
return $identity;
}
if ($accessToken !== null)
$this->handleFailure($response);
return null;
}
/**
* @inheritdoc
*/
public function handleFailure($response)
{
throw new UnauthorizedHttpException('The token you are using has is either invalid, or has expired. Please re-authenticate to continue your session.');
}
}
然后,我们可以在我们的控制器中实现我们的自定义认证方法,如下所示:
<?php
namespace app\controllers;
use app\filters\auth\HeaderParamAuth;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HeaderParamAuth::className(),
];
return $behaviors;
}
}
如您所预期,x-auth-token 参数将是传递给 yii\web\IdentityInterface::findIdentityByAccessToken() 的令牌。
动作特定认证
使用only和except关键字作为验证器行为的一部分,可以将身份验证限制为某些操作。例如,使用我们之前创建的HeaderParamAuth类,我们只能要求对delete、create和update操作进行身份验证,同时允许未经身份验证的用户访问主索引操作:
<?php
namespace app\controllers;
use app\filters\auth\HeaderParamAuth;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HeaderParamAuth::className(),
'only' => [ 'delete', 'update', 'create' ]
];
return $behaviors;
}
}
检查访问
当公开 API 端点时,您通常需要捆绑身份验证和授权。使用yii\rest\Controller,这可以通过覆盖yii\rest\Controller::checkAccess()方法来处理:
/**
* Checks the privilege of the current user.
*
* This method should be overridden to check whether the current user has the privilege
* to run the specified action against the specified data model.
* If the user does not have access, a [[ForbiddenHttpException]] should be thrown.
*
* @param string $action the ID of the action to be executed
* @param \yii\base\Model $model the model to be accessed. If null, it means no specific model is being accessed.
* @param array $params additional parameters
* @throws ForbiddenHttpException if the user does not have access
*/
public function checkAccess($action, $model = null, $params = [])
{
// check if the user can access $action or $model
// throw ForbiddenHttpException if access should be denied
}
或者,您可以使用访问控制过滤器,如第七章,验证和授权用户中所示。
注意
授权确定哪些操作需要经过身份验证的访问。当与 API 一起工作时,您需要正确实现身份验证以确定哪些用户或用户组可以访问特定的命令。有关如何在您的应用程序中验证用户的详细信息,请参阅第七章,验证和授权用户。
动词过滤器
在创建自定义 API 端点时,您可能只想允许某些 HTTP 动词针对这些操作发出。例如,向删除用户的端点发出 PUT 请求没有太多意义。控制可以对我们的操作执行哪些 HTTP 动词的一种方法是通过使用yii\filters\VerbFilter。在使用yii\filters\VerbFilter时,我们只需指定每个公共操作将接受哪些 HTTP 动词。以下示例显示了yii\rest\ActiveController使用的默认动词过滤器:
public function behaviors()
{
return [
'verbs' => [
'class' => \yii\filters\VerbFilter::className(),
'actions' => [
'index' => ['get'],
'view' => ['get'],
'create' => ['get', 'post'],
'update' => ['get', 'put', 'post'],
'delete' => ['post', 'delete'],
],
],
];
}
跨源资源头部
当与对您的 API 发出 AJAX 请求的 JavaScript 应用程序一起工作时,您可能希望使用跨源资源共享(CORS)头部以确保只有您指定的域可以运行。可以通过将yii\filters\Cors添加到您的behaviors()方法来实现 CORS 头部,如下例所示:
public function behaviors()
{
return [
'corsFilter' => [
'class' => \yii\filters\Cors::className(),
],
];
}
通过设置特定的 CORS 头部,您可以扩展此行为,这些头部是您希望为您的控制器指定的:
public function behaviors()
{
return [
'corsFilter' => [
'class' => \yii\filters\Cors::className(),
'cors' => [
// Only allow https://www.example.com to execute against your domain in AJAX
'Origin' => ['https://www.example.com'],
// Only allow POST and DELETE methods from the domain
'Access-Control-Request-Method' => ['POST', 'DELETE'],
// Set cache control headers
'Access-Control-Max-Age' => 3600,
// Allow the X-Pagination-Current-Page header to be exposed to the browser.
'Access-Control-Expose-Headers' => ['X-Pagination-Current-Page'],
],
],
];
}
注意
当涉及到防止来自浏览器和其他域的 AJAX 请求访问您的域内容时,CORS(跨源资源共享)头部具有一个非常具体的目的,并且它们旨在作为对您的最终用户的安全预防措施,而不是您的 API。CORS 头部不会阻止 CURL 等工具或不符合规范的浏览器访问您的 API。在实施 CORS 之前,请确保您对它们有具体的理解,了解它们保护什么,以及使用哪些头部。有关 CORS 的更多信息,请参阅 W3C 参考指南www.w3.org/TR/cors/。
速率限制
在创建 API 时,您可能希望在 API 中实现速率限制,以防止对 API 进行过多的请求并耗尽服务器资源。如果您的 API 依赖于已经设置了速率限制的另一个 API,这一点尤为重要。在 Yii2 中,速率限制是通过 yii\filters\RateLimiter 和 yii\filters\RateLimitInterface 实现的。
要开始使用速率限制,我们首先需要将 yii\filters\Ratelimiter 添加到我们的控制器行为中。yii\filters\RateLimiter 类与我们的用户身份类耦合。因此,速率限制只会应用于受认证保护的操作。任何未受认证过滤器保护的操作都不会应用速率限制。以下示例说明了在控制器中实现 yii\filters\RateLimiter 所需的代码块:
<?php
namespace app\controllers;
use yii\filters\auth\HttpBasicAuth;
use yii\filters\RateLimiter;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpBasicAuth::className(),
'auth' => [ 'app\models\User', 'httpBasicAuth'],
'only' => [ 'delete', 'update', 'create', 'index']
];
$behaviors['rateLimiter'] = [
'class' => RateLimiter::className(),
'enableRateLimitHeaders' => true,
];
return $behaviors;
}
}
接下来,我们需要在我们的用户身份类中实现 yii\filters\RateLimitInterface 接口所需的方法。第一个方法 yii\filters\RateLimitInterface::getRateLimit() 定义了我们在单位时间内可以发出的请求数量。对于全局速率限制,我们可以简单地返回 [100, 600],这将允许在 600 秒内发出 100 个请求。然而,由于完整的请求和操作都传递给了 yii\filters\RateLimitInterface::getRateLimit() 方法,因此我们可以进一步细化每个控制器和操作配对的速率限制:
/**
* Returns the rate limit
* @param yii\web\Request $request
* @param yii\base\Action $action
* @return array
*/
public function getRateLimit($request, $action)
{
return [100, 600];
}
小贴士
yii\filters\RateLimiter 与您的用户身份耦合。如果您想为未认证的用户实现速率限制,您需要实现一个自定义过滤器。
接下来,我们需要实现两种方法来加载可用的速率限制,并在每次请求后更新可用的速率限制。这两种方法是 yii\filters\RateLimitInterface::loadAllowance() 和 yii\filters\RateLimitInterface::saveAllowance()。由于速率限制数据并不被视为敏感信息,并且如果数据意外删除,对我们的应用程序也不会有重大影响,因此这些数据可以存储在我们的缓存组件中,或者存储在 NoSQL 解决方案中,例如 MongoDB 或 Redis。方法签名定义如下:
/**
* Returns the rate limit allowance
* @param yii\web\Request $request
* @param yii\base\Action $action
* @return array
*/
public function loadAllowance($request, $action)
{
$allowance = 100; // Fetch the allowance from a datasource
return [$allowance, time()];
}
/**
* Saves the rate limit allowance
* @param yii\web\Request $request
* @param yii\base\Action $action
* @param integer $allowance
* @param Integer $timestamp
* @return array
*/
public function saveAllowance($request, $action, $allowance, $timestamp)
{
// Update a NoSQL solution or a cache
return true;
}
结合起来,我们的扩展类将如下所示:
<?php
namespace app\models;
use Yii;
class User extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface, \yii\filters\RateLimitInterface
{
public function getRateLimit( $request, $action )
{
return [100, 600];
}
public function loadAllowance( $request, $action )
{
return [100, time();
}
public function saveAllowance( $request, $action, $allowance, $timestamp )
{
return true;
}
}
现在,当您查询受保护的 API 端点时,响应将返回以下附加头信息:
x-rate-limit-remaining: <remaining_rate_limts>
x-rate-limit-limit: <rate_limit_upper_bound>
x-rate-limit-reset: <seconds_until_rate_limit_reset>
小贴士
现在您已经安装了 MinGW 和 MSYS,您不再需要羡慕那些拥有 Linux 安装的用户了,因为它们在您的系统中实现了 Linux 开发环境的最重要部分。
错误处理
通过扩展 yii\rest\Controller(或 yii\rest\ActiveController),我们可以在配置中定义适当的错误处理器,从而轻松地在我们的应用程序中实现错误处理,如前几章所示:
<?php return [
// [...],
'components' => [
// [...],
'errorHandler' => [
'errorAction' => 'user/error',
],
// [...]
],
];
与基于视图的响应不同,我们不需要在我们的控制器actions()方法中包含我们想要使用的错误处理程序的定义。相反,我们可以简单地返回发生的错误,或者我们可以覆盖错误以显示更通用的响应:
<?php
namespace app\controllers;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public function actionError()
{
$exception = Yii::$app->errorHandler->exception;
if ($exception !== null)
return ['exception' => $exception];
}
}
自定义 API 控制器
虽然方便,但yii\rest\ActiveController并不能解决创建 API 时遇到的每一个问题。当不使用yii\rest\ActiveController时,您可能希望从yii\rest\Controller扩展您的控制器类,以充分利用yii\rest\Controller实现的内置 REST API 默认值。以下部分说明了创建自定义 API 控制器的一些附加信息。
返回数据
在 Yii2 中,我们可以以几种方式考虑自定义 API 控制器。考虑将数据传递给客户端的最简单方法是通过绕过 MVC 模型中的视图部分,直接从我们的控制器返回数据。例如,如果我们要在我们的控制器命名空间内创建一个名为SiteController的新控制器,我们可以直接从我们新创建的控制器返回数据,如下所示:
<?php
namespace app\controllers;
use Yii;
class SiteController extends \yii\rest\Controller
{
public function actionIndex()
{
return [ 'foo' => 'bar'];
}
}
小贴士
记住,一旦我们开始修改我们的默认 URL 管理器规则,我们就需要添加将数据路由到其他控制器的所需规则。此规则将确保site/<action> maps回我们的site控制器:['class' => 'yii\web\UrlRule', 'pattern' => 'site/<action>', 'route' => 'site/index']。
对我们 API 的site/index端点进行 Curl 请求将返回以下内容:
$ curl –I -H "Accept:application/json" /
https://www.example.com/site/index | jq .
{
"foo": "bar"
}
响应格式化
Yii2 有一个非常具体的响应结构,它将使用默认的yii\rest\Controller返回。当创建 API 时,您可能已经有一个特定的响应结构您可能想要使用(例如,如果您正在重构一个现有的但过时的 API,并使用 Yii2 API)。您可能还希望您的 API 响应具有统一的格式,因为yii\rest\Controller和yii\rest\ActiveController提供的响应不匹配(如前几节所示)。
在这些情况下,您需要修改响应结构。为此,我们只需覆盖我们应用程序的response组件,并在beforeSend事件中修改$response->data变量,以包含我们想要的实际响应。在这个例子中,我们将有以下响应结构:
{
"status": <http_status_code>,
"message": <exceptions_or_messages>,
"response": <response_data_from_controllers>
}
实现此更改所需的代码如下所示:
<?php return [
// [...],
'components' => [
// [...],
'response' => [
'format' => yii\web\Response::FORMAT_JSON,
'charset' => 'UTF-8',
'on beforeSend' => function ($event) {
$response = $event->sender;
if ($response->data !== null)
{
$return = ($response->statusCode == 200 ? $response->data : $response->data['message']);
$response->data = [
'success' => ($response->statusCode === 200),
'status' => $response->statusCode,
'response' => $return
];
}
}
],
// [...],
]
];
现在,如果我们查询我们的 API,我们将为我们的SiteController和UserController都收到一个统一响应结构。这是针对SiteController的:
$ curl –I -H "Accept:application/json" /
https://www.example.com/site/index | jq .
{
"success": true,
"status": 200,
"response": {
"foo": "bar"
}
}
本查询涉及UserController:
$ curl –I -H "Accept:application/json" /
https://www.example.com/users | jq .
{
"success": true,
"status": 200,
"response": {
"users": [
{
"id": 1,
"email_address": "jane.doe@example.com",
"first_name": "Jane",
"last_name": "Joe",
"full_name": "Jane Joe",
"updated_at": 1442602004,
"created_at": 1442602004
},
{...},
],
"_links": {
"self": {
"href": "https://www.example.com/users?page=1"
}
},
"_meta": {
"totalCount": 4,
"pageCount": 1,
"currentPage": 1,
"perPage": 20
}
}
}
摘要
在本章中,我们扩展了我们迄今为止所学的所有知识,并学习了如何在 Yii2 中创建 RESTful JSON 和 XML API。首先,我们介绍了yii\rest\ActiveController的用法,这使得我们能够快速基于我们的模型类创建 CRUD API。然后,我们介绍了 Yii2 的内置身份验证过滤器,并介绍了我们如何通过要求身份验证来保护我们的资源。我们还介绍了创建自己的身份验证过滤器以支持不同的身份验证方案。接着,我们介绍了几个其他有用的 API 类,包括yii\filters\VerbFilter、yii\filters\Cors,并学习了如何在我们的 API 中处理错误。此外,我们还详细介绍了通过扩展yii\rest\Controller来创建自己的 API 端点的一些重要信息。
在涵盖了构建 Yii2 应用程序所需的所有信息之后,我们将花费本书剩余的章节来探讨如何增强我们的应用程序。在下一章中,我们将具体介绍构建应用程序最重要的一个方面:测试。我们将介绍如何使用强大的 Codeception 工具在我们的应用程序中设置测试,并详细说明如何设置和创建功能测试、单元测试和验收测试,以及如何创建用于测试的数据固定值。
第十章。使用 Codeception 进行测试
软件开发的一个重要但常常被忽视的方面是测试我们的应用程序以确保它按预期运行。我们可以以三种基本方式测试我们的应用程序:
-
单元测试
-
功能测试
-
接受测试
单元测试使我们能够在将代码与我们的应用程序耦合之前测试代码的各个部分。功能测试允许我们在模拟浏览器中测试代码的功能方面,而接受测试允许我们在真实浏览器中测试我们的应用程序并验证它是否做了我们构建它要做的事情。使用 Yii2,我们可以使用一个名为 Codeception 的工具来创建和执行单元、功能和接受测试。在本章中,我们将介绍如何在 Yii2 中创建和运行单元、功能和接受测试。除了这三种类型的测试之外,我们还可以使用固定数据(fixtures)来模拟数据,这可以帮助我们在测试之前将应用程序置于一个固定的状态。
小贴士
在我们完成本章的过程中,我们将使用前几章中的大量代码。为了您的方便,本章的源代码已提供在 GitHub 上github.com/masteringyii/chapter10,并分为三个不同的分支。我们将使用单元测试部分的unit分支,功能测试和接受测试部分的functional_and_acceptance分支,以及固定部分(fixtures)的fixture分支。
测试的原因
大多数软件开发者都会承认测试是一件好事,但许多开发者由于各种原因(如下所述)没有为他们的应用程序编写测试:
-
测试的恐惧
-
不知如何编写测试
-
认为他们的应用程序太小而不适合测试
-
时间不足
-
预算原因
虽然许多这些原因都是合理的,但测试可以对你的应用程序产生深远的影响,并极大地提高你代码的质量。以下列表提供了几个为什么应该在代码库中添加测试的原因:
-
在添加新功能时,测试可以减少错误
-
测试验证了你的代码确实做了你认为它做的事情
-
测试验证了你的代码确实做了你的客户想要的事情
-
测试可以限制功能
-
测试迫使我们放慢速度,并将我们的应用程序分解成具有约束特征的小型、可管理的组件
-
通过确保对单个功能的更改不会破坏另一个功能,测试可以降低更改的成本
-
测试提供了我们代码预期行为的文档
-
测试减少了更改会破坏我们应用程序中某些内容的恐惧
如何进行测试
现代开发中涉及许多因素,成本和开发时间是最主要的。我们可以采取几种现实的方法来测试,以克服这些限制。
手动测试
我们可以采取的最基本的测试方法是边编写代码边手动测试。无论你是否意识到,每次你进行代码更改并重新加载浏览器时,你都在测试你的代码。粗略一看,手动测试让我们能够验证新功能和已修复的 bug,但它要求我们在每次更改后手动验证应用程序的状态。此外,手动测试要求我们记住我们创建的每一个测试用例。使用 Codeception 等工具进行自动化测试可以减少这种认知负担,并释放我们的时间来执行其他任务。
测试几个核心组件
一种更好的测试方法是自动化测试我们应用程序的核心组件。采用这种方法,我们只为应用程序中的关键路径添加测试,这使我们能够在其他地方减少测试的情况下验证应用程序的重要部分。在时间和预算受限但你想自动化验证重要流程和路径的情况下,这种方法是完全没有测试的现实的替代方案。
测试驱动开发
测试驱动开发(TDD)是一种哲学,即我们在构建应用程序的同时为它创建测试。TDD 背后的主要思想是,我们可以通过事先编写一个测试来验证我们的代码是否正常工作。使用 TDD,我们通常先编写一个测试(它将失败),然后编写代码使其通过测试,然后我们在测试和代码之间不断迭代,直到我们的功能完成且测试通过。TDD 还强迫我们在将代码检查到版本控制系统之前通过测试来确保我们编写的代码是可用的,这鼓励我们编写好的测试。
使用 TDD(测试驱动开发),我们的目标是每个特性和组件都有一个测试,并且有许多测试能够彻底覆盖我们的应用程序。在理想的世界里,TDD 是在进行测试工作时采取的最佳方法,尽管这需要更多的时间和预算来实现。
配置 Codeception 与 Yii2
在我们能够使用 Codeception 测试我们的代码之前,我们首先需要配置 Codeception 以与 Yii2 一起工作:
-
在 Yii2 中设置 Codeception 的首选方式是使用 Composer 安装
yii2-codeception包和 Codeception 基础包:$ composer require --dev codeception/codeception $ composer require --dev yiisoft/yii2-codeception $ composer require --dev yiisoft/yii2-faker小贴士
我们 Composer 命令中的
--dev标志确保开发包不会安装到我们的生产环境中。使用–dev安装的包将被添加到composer.json文件的require-dev部分。存储 Codeception 和其他测试代码减少了生产环境中所需的依赖,并使我们的代码更加安全。第一个包包含我们将用于生成和执行测试的 Codeception 二进制文件,而第二个包包含 Codeception 将用于紧密集成到 Yii2 中的 Yii2 特定辅助器和绑定。
注意
这个过程可能需要很长时间,因为 Codeception 依赖于许多不同的包,包括 PHPUnit。
-
安装 Codeception 后,我们可以通过运行以下命令来执行命令:
$ ./vendor/bin/codecept单独的
codecept将输出 Codeception 提供的所有可用命令。![使用 Yii2 配置 Codeception]()
-
在验证 Codeception 已安装后,我们需要通过运行以下命令来引导 Codeception:
$ ./vendor/bin/codecept bootstrap![使用 Yii2 配置 Codeception]()
-
引导过程将创建几个文件。第一个文件被称为
codeception.yml,位于我们应用程序的根目录中。其余的文件存在于tests文件夹中,我们将添加我们的tests到该目录。 -
接下来,我们需要在
codeception.yml文件中配置 Codeception 以与我们的 Yii2 Codeception 模块一起工作。以下代码块中突出显示了所需添加的内容:actor: Tester paths: tests: tests log: tests/_output data: tests/_data support: tests/_support envs: tests/_envs settings: bootstrap: _bootstrap.php colors: true memory_limit: 1024M extensions: enabled: - Codeception\Extension\RunFailed -
此外,我们需要告诉 Codeception 自动加载我们的 composer 依赖和 Yii2。我们可以通过更新
tests/_bootstrap.php文件来实现。为了确保我们以类似于web/index.php加载数据的方式测试我们的应用程序,我们应该添加以下内容:<?php // Define our application_env variable as provided by nginx/apache if (!defined('APPLICATION_ENV')) { if (getenv('APPLICATION_ENV') != false) define('APPLICATION_ENV', getenv('APPLICATION_ENV')); else define('APPLICATION_ENV', 'prod'); } $env = require(__DIR__ . '/../config/env.php'); // comment out the following two lines when deployed to production defined('YII_DEBUG') or define('YII_DEBUG', $env['debug']); defined('YII_ENV') or define('YII_ENV', APPLICATION_ENV); require(__DIR__ . '/../vendor/autoload.php'); require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php'); $config = require(__DIR__ . '/../config/web.php'); (new yii\web\Application($config)); -
现在 Codeception 已经配置好了,我们可以通过运行以下命令来运行所有的测试:
$ ./vendor/bin/codecept run运行命令将输出以下内容:
![使用 Yii2 配置 Codeception]()
单元测试
我们可以创建的最基本的测试类型被称为单元测试。正如其名所示,单元测试旨在测试一个工作单元(无论是单个方法、函数还是更大的工作单元),然后检查对该工作单元的单个假设。一个好的单元测试将由以下组件组成:
-
完全自动化:一个好的单元测试是可以完全自动化的测试,无需人工干预。
-
全面性:全面的单元测试提供了对它们所测试的代码块的良好的覆盖率。
-
独立性:好的单元测试可以按任何顺序运行,并且它们的输出不应影响或对其他测试产生副作用。此外,每个单元测试应仅测试单个逻辑代码单元。失败的测试应精确指出失败的代码部分。
-
一致性和可重复性:单元测试应该始终产生相同的结果,并且应该依赖于静态数据,而不是生成或随机数据。
-
快速:单元测试需要快速执行。长时间运行的测试意味着在给定时间内运行的测试较少,执行时间过长的测试会鼓励开发者要么编写较少的测试,要么完全跳过编写测试。由于单元测试旨在测试小的单个代码单元,长时间运行的测试也可以是糟糕或不完整的测试的指标。
-
可读性:好的单元测试应该是可读的,并且应该是自我解释的,或者如果需要额外的解释,应该有详细的文档。
-
可维护性:最后,好的单元测试应该是可维护的。我们不维护的测试就是我们不使用或不与之工作的测试。
小贴士
在本节中,我们将使用位于 github.com/masteringyii/chapter10 的 unit 分支中的源代码。
生成单元测试
如果你熟悉 PHPUnit,使用 Codeception 编写测试应该会感觉非常熟悉。对于单元测试,Codeception 可以生成类似 PHPUnit 的测试,但它也可以生成一个 Codeception 特定的单元测试,该测试不需要 PHPUnit 来执行。
要生成 PHPUnit 特定的测试,我们可以运行以下命令,这将生成一个名为 Example 的 PHPUnit 单元测试:
$ ./vendor/bin/codecept generate:phpunit unit Example
或者,我们可以通过运行以下命令来生成一个名为 Example 的 Codeception 特定测试:
$ ./vendor/bin/codecept generate:test unit Example
小贴士
除非你有特定的需要 PHPUnit 类似的测试,否则应该优先考虑 Codeception 单元测试。
在生成我们的 Codeception 测试后,将生成一个名为 tests/unit/ExampleTest.php 的文件,它将包含以下代码。在我们开始为我们的应用程序编写单元测试之前,让我们探索一下 Codeception 单元测试类的结构:
<?php
class ExampleTest extends \Codeception\TestCase\Test
{
/**
* @var \UnitTester
*/
protected $tester;
protected function _before()
{
}
protected function _after()
{
}
// tests
public function testMe()
{
}
}
默认情况下,我们的 Codeception 单元测试将扩展 \Codeception\TestCase\Test,并实现两个受保护的方法(_before() 和 _after())和一个名为 $tester 的受保护属性,供 Codeception 内部使用。_before() 和 _after() 方法旨在设置和清理在类中每个预定义测试之前和之后立即执行的任务。
在 _before() 和 _after() 方法之后,我们有我们想要运行的所有的测试。一般来说,我们想要运行的任何测试都应该在一个以 test 为前缀的公共方法中。任何具有此签名的公共方法都将作为测试执行。作为一个简短的例子,让我们修改我们的 testMe() 方法以进行一个简单的断言(一个关于给定谓词(一个函数、方法或变量)评估为布尔值的陈述,我们可以验证它是否为真):
public function testMe()
{
// Assert that the boolean value "true" is indeed true
$this->assertTrue(true);
}
以这个简单的断言为例,我们可以通过运行以下命令来验证我们的测试是否运行:
$ ./vendor/bin/codecept run
或者,我们只需运行以下命令来运行我们的单元测试:
$ ./vendor/bin/codecept run unit

如前一个截图所示,我们的单元测试案例已成功执行。我们可以通过定义额外的测试方法来添加额外的单元测试,如下例所示:
public function testMeToo()
{
$this->assertFalse(false);
}
运行第二个测试将在我们的 Codeception 输出中显示。

单元测试示例
现在我们已经了解了使用 Codeception 进行单元测试的基础知识,让我们探索一些我们可以在之前构建的应用程序中测试的例子。从我们在 第九章 中开发的源代码开始,RESTful APIs,让我们为我们的模型编写一些单元测试。
测试用户模型方法
-
我们的
User模型是我们应用程序的重要组成部分。由于我们添加了自定义代码和自定义验证器,我们可以编写单元测试来验证我们的验证器是否准确,以及我们的自定义代码是否按预期工作。为了开始,让我们为我们的User模型创建一个新的单元测试:$ ./vendor/bin/codecept generate:test unit User -
由于我们正在测试我们的用户模型,我们需要明确指定我们想要在测试中使用该模型:
<?php namespace app\tests\unit\UserTest; use Codeception\TestCase\Test; use app\models\User; use Yii; class UserTest extends Test {} -
接下来,让我们定义一个方法来测试我们的
app\models\User::setFullName()方法是否正常工作:public function testSetFullName() { $user = new User; $user->setFullName('John Doe'); // Asser the setFullName method works $this->assertTrue($user->first_name == "John"); $this->assertTrue($user->last_name == "Doe"); $this->assertFalse($user->first_name == "Jane"); unset($user); $user = new User; $user->fullName = 'John Doe'; // Asser the full_name setter method works $this->assertTrue($user->first_name == "John"); $this->assertTrue($user->last_name == "Doe"); $this->assertFalse($user->first_name == "Jane"); unset($user); } -
执行我们的测试后,我们可以通过查看输出来验证我们新通过的测试用例。
![测试用户模型方法]()
小贴士
这里展示的测试用例非常基础。尝试扩展这个测试用例以确保此方法的完整代码覆盖率。
让我们为app\models\User::validatePassword()方法编写另一个测试用例,以正确验证现有用户的密码:
-
对于这个测试用例,我们将依赖于我们的迁移提供的数据。在创建测试之前,请确保您已迁移数据库:
$ ./yii migrate/up --interactive=0 -
接下来,我们将添加一个测试用例,该测试用例将加载我们的四个默认用户并验证它们的密码是否匹配,正如我们所期望的那样:
public function testValidatePassword() { $user = User::find()->where(['id' => 1])->one(); $this->assertTrue($user->validatePassword('password1')); $this->assertFalse($user->validatePassword('password2')); $user = User::find()->where(['id' => 2])->one(); $this->assertTrue($user->validatePassword('password2')); $this->assertFalse($user->validatePassword('password1')); $user = User::find()->where(['id' => 3])->one(); $this->assertTrue($user->validatePassword('password3')); $this->assertFalse($user->validatePassword('password4')); $user = User::find()->where(['id' => 4])->one(); $this->assertTrue($user->validatePassword('admin')); $this->assertFalse($user->validatePassword('notadmin')); }小贴士
在这种情况下,我们的用户信息是从我们的迁移中加载的,在某些情况下,如果我们想为最终用户提供合理的默认值,这是有意义的。在本章的后面部分,我们将探讨如何使用固定值来创建和填充测试的默认值,这将消除在迁移文件中包含这些默认值的需求。
-
运行我们的单元测试后,我们应该看到以下输出:
![测试用户模型方法]()
小贴士
注意在先前的测试用例中,我们测试了预期的结果以及我们预期会失败的几个结果。单元测试的一个重要部分是验证预期的通过用例都通过了,并且无效或错误输入不被接受。这确保了我们的应用程序按预期工作,同时不允许恶意或不良输入被接受。
你已经掌握了单元测试了吗?在继续进行功能测试之前,让我们编写一个测试用例来验证我们的验证器是否正常工作:
-
由于我们的
User模型验证了多个不同的属性,我们将检查我们的app\models\User::validate()方法是否返回预期的true或false结果,以及是否调用了适当的验证器。 -
为了使我们的测试输出更易于阅读,我们可以在项目中包含
codeception/specifycomposer 模块,这将允许我们在测试输出中指定每个测试部分的预期结果,以防测试失败。可以通过运行以下命令来安装此包:$ composer require codeception/specify要使用
specify,我们需要在UserTest类内部使用它,如下例所示:<?php namespace app\tests\unit\UserTest; use Codeception\TestCase\Test; use app\models\User; use yii\codeception\TestCase; use Yii; class UserTest extends Test { use \Codeception\Specify; } -
然后
specify可以按以下方式使用:public function testValidate() { $this->specify('false is false', function() { $this->assertFalse(false); }); }现在如果我们运行我们的测试用例,我们应该看到以下输出,具体表明我们的“false is false”测试用例失败了:
Test validate (app\tests\unit\UserTest\UserTest::/ testValidate) Ok -
现在我们知道了如何使用指定模块,让我们为我们的验证器编写几个测试用例:
public function testValidate() { $this->specify('email and password are required', function() { $user = new User; // Verify our validation fails as we didn't provide any attributes $this->assertFalse($user->validate()); // Verify that the email and password properties are required $this->assertTrue($user->hasErrors('email')); $this->assertTrue($user->hasErrors('password')); $user->email = 'user@example.com'; $user->password = password_hash('example', PASSWORD_BCRYPT, ['cost' => 13]); $this->assertTrue($user->validate()); }); $this->specify('email is unique', function() { $user = new User; // Verify email is unique $user->email = 'jane.doe@example.com'; $user->password = password_hash('example', PASSWORD_BCRYPT, ['cost' => 13]); $this->assertFalse($user->validate()); $this->assertTrue($user->hasErrors('email')); }); $this->specify('first and last name are strings', function() { $user = new User; $user->email = 'user@example.com'; $user->password = password_hash('example', PASSWORD_BCRYPT, ['cost' => 13]); // Verify first and last name has to be strings $user->first_name = (int)7; $user->last_name = (int)5; $this->assertFalse($user->validate()); $this->assertTrue($user->hasErrors('first_name')); $this->assertTrue($user->hasErrors('last_name')); // Verify that strings work $user->setFullName('Example User'); $this->assertTrue($user->validate()); }); } -
在运行我们的单元测试后,我们应该看到以下输出,表明我们的测试已通过。如果我们的测试在任何时候失败,指定的模块将输出第一个参数,指示测试的哪个具体部分失败了。随着我们的测试通过,我们将看到以下输出:
![测试用户模型方法]()
小贴士
Yii2 已经有一个针对
yii\db\ActiveRecord::validate()的测试用例。添加我们自己的测试用例并不是为了验证这个方法是否工作,而是为了验证我们是否放置了正确的验证器。关于指定模块的更多信息,请参阅
github.com/Codeception/Specify。
功能测试
我们可以生成的下一类测试被称为功能测试。功能测试允许我们在不通过 Web 服务器运行应用程序的情况下模拟我们的应用程序。这为我们提供了一种快速测试应用程序输出的方法,而不会引入 Web 服务器的开销。
这种模拟过程是通过在执行我们的应用程序之前直接操作$_REQUEST、$_POST和$_GET参数来实现的。然而,这种行为的副作用是,某些变量,如$_SESSION和$_COOKIE,以及头部信息,可能会导致抛出junk错误,这在真实环境中不一定会发生。此外,使用 Codeception,我们的功能测试将在单个内存容器中执行,这可能导致当作为一组测试运行时测试失败,而不是单独运行一个测试。此外,与验收测试不同,功能测试无法模拟 JavaScript 和 Ajax 请求。
总体而言,功能测试为我们提供了一种快速且简单的方法来证明我们的代码输出既实现了我们编程时想要的功能,也符合我们的最终用户和客户的期望。尽管功能测试可能会提出一些小问题,但在高层次上,它提供的报告可以让我们对我们的代码按预期工作以及代码库未来的更改不会显著改变我们的应用程序充满信心。在本节中,我们将介绍如何在我们的应用程序中生成和运行功能测试。
小贴士
在本节中,我们将使用位于github.com/masteringyii/chapter10的functional_and_acceptance分支中的源代码。
设置功能测试
由于功能测试和 API 实际上并不搭配,我们将使用我们在第六章中编写的代码来编写功能测试,资产管理,因为该章节中的代码包含了一些我们可以测试的良好组件。功能测试的行为与单元测试大不相同,因此在我们开始编写测试代码之前,我们需要对我们的测试配置做一些更改:
-
要开始进行功能测试,我们首先需要确保 Codeception 已经初始安装并配置。这个过程与我们之前章节中执行的过程相同:
-
安装所需的 composer 依赖项:
$ composer require --dev codeception/codeception $ composer require --dev yiisoft/yii2-codeception $ composer require --dev yiisoft/yii2-faker $ composer require --dev codeception/specify -
安装 Bootstrap Codeception:
$ ./vendor/bin/codecept bootstrap -
将所需的配置添加到
tests/_bootstrap.php。请注意,因为我们正在模拟完整的请求流程,我们需要预先填充几个变量,例如$_SERVER['SCRIPT_FILENAME']和$_SERVER['SCRIPT_NAME']。相关的部分如下所示:<?php define('DS', DIRECTORY_SEPARATOR); defined('YII_TEST_ENTRY_URL') or define('YII_TEST_ENTRY_URL', parse_url(\Codeception\Configuration::config()['config']['test_entry_url'], PHP_URL_PATH)); defined('YII_TEST_ENTRY_FILE') or define('YII_TEST_ENTRY_FILE', dirname(__DIR__) . '/web/index-test.php'); // Define our application_env variable as provided by nginx/apache if (!defined('APPLICATION_ENV')) { if (getenv('APPLICATION_ENV') != false) define('APPLICATION_ENV', getenv('APPLICATION_ENV')); else define('APPLICATION_ENV', 'prod'); } $env = require(__DIR__ . '/../config/env.php'); // comment out the following two lines when deployed to production defined('YII_DEBUG') or define('YII_DEBUG', $env['debug']); defined('YII_ENV') or define('YII_ENV', APPLICATION_ENV); require(__DIR__ . '/../vendor/autoload.php'); require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php'); $config = require(__DIR__ . '/../config/web.php'); $_SERVER['SCRIPT_FILENAME'] = YII_TEST_ENTRY_FILE; $_SERVER['SCRIPT_NAME'] = YII_TEST_ENTRY_URL; $_SERVER['SERVER_NAME'] = parse_url(\Codeception\Configuration::config()['config']['test_entry_url'], PHP_URL_HOST); $_SERVER['SERVER_PORT'] = parse_url(\Codeception\Configuration::config()['config']['test_entry_url'], PHP_URL_PORT) ?: '80'; Yii::setAlias('@tests', dirname(__DIR__)); (new yii\web\Application($config)); -
验证 Codeception 是否正在运行:
$ ./vendor/bin/codecept bootstrap
-
-
我们需要做的第一个更改是包含 Yii2 Codeception 模块。此模块将使我们能够在测试中利用 Yii2 特定的绑定,这将帮助我们更好地测试我们的 Yii2 应用程序。我们不需要为所有测试类型启用我们的模块,我们只需通过在
tests/functional.suite.yml中添加以下内容来为我们的功能测试启用它:# Codeception Test Suite Configuration # # Suite for functional (integration) tests # Emulate web requests and make application process them # Include one of framework modules (Symfony2, Yii2, Laravel5) to use it class_name: FunctionalTester modules: enabled: - Filesystem - Yii2 config: Yii2: configFile: 'tests/config/functional.php' -
接下来,我们需要在我们的表单上禁用跨站请求伪造(CSRF)验证。而不是对我们的
config/web.php文件进行全局配置更改,我们可以在tests/config/functional.php中创建一个自定义配置,该配置包括我们的config/web.php文件并在该文件中禁用 CSRF 验证:<?php $_SERVER['SCRIPT_FILENAME'] = YII_TEST_ENTRY_FILE; $_SERVER['SCRIPT_NAME'] = YII_TEST_ENTRY_URL; /** * Application configuration for functional tests */ return yii\helpers\ArrayHelper::merge( require(__DIR__ . '/../../config/web.php'), [ 'components' => [ 'request' => [ 'enableCsrfValidation' => false, ], ], ] ); -
接下来,我们需要启用我们的 Yii2 模块。要在 Codeception 中启用一个新模块,我们只需运行以下命令:
$ ./vendor/bin/codecept build![设置功能测试]()
-
最后,我们可以执行 Codeception 的运行命令来验证我们的更改是否已发生:
$ ./vendor/bin/codecept run -
如果成功,我们应该看到类似于以下图示的输出:
![设置功能测试]()
生成功能测试
与单元测试不同,功能测试不是在生成的类中执行的。相反,它们在 tests/functional 文件夹中的一个普通 PHP 文件中运行。要开始生成功能测试,我们需要再次使用 generate:cept 命令:
$ ./vendor/bin/codecept generate:cept functional Page
我们的功能测试将在 tests/functional/PageCept.php 中生成,并将包含以下内容:
<?php
$I = new FunctionalTester($scenario);
$I->wantTo('perform actions and see result');
现在,如果我们再次执行测试,我们应该看到以下内容:

功能测试的示例
现在我们已经知道了如何生成功能测试,让我们探索一些功能测试的示例。如果你还记得第六章中的内容,资产管理,我们的主页看起来如下:

让我们快速编写一个功能测试来验证我们的主页是否加载并且包含我们在屏幕上看到的元素:
-
使用 Yii2 的 Codeception 绑定,我们有几种方法可以导航到页面并加载数据。在我们的
tests/functional/PageCept.php测试中,我们可以编写以下内容来验证主页是否加载。我们可以使用FunctionalTester::amOnPage()方法来完成此操作,该方法验证FunctionalTester是否能够访问给定的页面:<?php $I = new FunctionalTester($scenario); $I->wantTo('Verify that homepage loads'); $I->amOnPage('/'); $I->amOnPage('site/index'); $I->amOnPage(['/site/index']); -
如您所见,我们可以通过查询根 URI 或
site/index操作来加载主页,无论是作为字符串还是数组。如果我们使用数组语法,我们可以将额外的参数作为 GET 参数传递给我们的页面。 -
现在我们已经验证了我们位于主页上,让我们验证
'Now you're thinking with widgets!'字符串是否显示。使用 Codeception,以下代码行可以非常容易地做到这一点:$I->see('Now you\'re thinking with widgets!'); -
我们还可以通过使用
FunctionalTester::see()方法来验证'Home'、'Register'和'Login'链接文本是否显示,该方法会扫描请求的文档以检查提供的文本是否存在:$I->see('Home'); $I->see('Register'); $I->see('Login'); -
现在我们来运行我们的功能测试。作为每次运行时同时运行我们的单元测试和验收测试的替代方案,我们可以通过指定我们想要运行的测试类型来仅运行我们的功能测试,如下面的示例所示:
$ ./vendor/bin/codecept run functional![功能测试示例]()
小贴士
尽管我们正在运行多个不同的测试,但 Codeception 只为整个测试文件报告通过或失败的结果。为了更深入地了解 Codeception 正在做什么,我们可以通过将“-v”标志传递给我们的命令来告诉 Codeception 更加详细。可以通过添加更多的“v”标志来添加额外的详细程度(例如,“-vv”或甚至“-vvv”):
$ ./vendor/bin/codecept run functional –vv![功能测试示例]()
-
由于我们正在创建一个新的测试,我们应该首先为我们创建一个新的功能测试文件来工作。运行以下命令将在
tests/functional/LoginCept.php生成一个测试文件:$ ./vendor/bin/codecept generate:cept functional Login -
首先,让我们编写一个测试来验证我们是否可以点击主页上的“登录”链接并导航到登录页面:
<?php $I = new FunctionalTester($scenario); $I->wantTo('Verify login page'); // Verify homepage link works $I->amOnPage('/'); $I->click('Login'); $I->amOnPage(['site/login']); -
在验证我们的测试通过之后,我们可以使用
seeElement()和dontSeeElement()方法来验证表单是否存在以及没有错误:// Verify form is present $I->seeElement('input', ['id' => 'userform-email']); $I->seeElement('input', ['id' => 'userform-password']); -
然后,在验证我们可以在页面上看到表单元素之后,让我们通过首先提交无效的用户名和密码,然后提交有效的用户名和密码组合来测试我们的表单是否工作:
// Verify bad user/pass fails $I->fillField(['id' => 'userform-email'], 'foo'); $I->fillField(['id' => 'userform-password'], 'bar'); $I->click("Submit"); $I->SeeCurrentUrlEquals('/site/login'); // Verify bad user/pass fails $I->fillField(['id' => 'userform-email'], 'admin@example.com'); $I->fillField(['id' => 'userform-password'], 'admin'); $I->click("Submit"); $I->SeeCurrentUrlEquals('/site/index'); -
现在我们来运行我们的登录测试。我们可以通过直接调用它来独立运行此测试,如下面的示例所示:
$ ./vendor/bin/codecept functional LoginCept –vv -
如以下截图所示,我们的功能测试遍历所有测试并验证登录表单流程是否按预期工作:
![功能测试示例]()
小贴士
Yii2 模块提供了一些方法,可以在运行功能测试和单元测试时使用。要获取 Yii2 Codeception 模块提供的所有方法的完整列表,请确保您参考了 Yii2 Codeception 模块页面codeception.com/docs/modules/Yii2。
验收测试
我们可以使用 Codeception 自动化的最后一种测试类型称为验收测试。它与功能测试非常相似,区别在于你的应用程序是使用真实浏览器而不是模拟浏览器进行测试。这提供了完全模拟最终用户行为的优势。验收测试不像功能测试那样有许多限制,例如内存限制、$_COOKIE、$_SESSION和头部限制。此外,验收测试可以由团队中的任何人执行,因为使用验收测试测试的内容与手动测试所做的工作是相同的。实际上,运行验收测试的唯一缺点是由于整个浏览器流程,对于特定的测试,验收测试可能会非常慢。在本节中,我们将介绍如何使用 Codeception 设置和运行验收测试。
小贴士
在本节中,我们将使用位于github.com/masteringyii/chapter10的functional_and_acceptance分支上的源代码。
设置验收测试
与功能测试一样,验收测试需要一些设置才能与 Yii2 一起工作:
-
要开始,我们首先需要指定我们想要使用的浏览器。在我们的例子中,我们将使用 PHP 和浏览器的组合。为此,我们首先需要将以下内容添加到我们的
tests/acceptance.suite.yml文件中。此外,由于我们想利用 Yii2 特定的插件,我们还将启用 Yii2 模块:--- class_name: AcceptanceTester modules: enabled: - PhpBrowser: url: "http://localhost:8082" - \Helper\Acceptance - Yii2 config: Yii2: configFile: 'tests/config/acceptance.php'小贴士
如果我们想使用真实浏览器,可以通过在
modules:enabled部分添加以下内容来启用 WebDriver 模块:- WebDriver: url: http://localhost browser: firefox restart: true -
接下来,我们需要重新构建我们的测试以包含添加的模块:
$ ./vendor/bin/codecept build -
然后,我们需要配置我们的
tests/acceptance/_bootstrap.php文件,以便我们可以在测试中加载我们的 Yii2 应用程序。幸运的是,这基本上与我们的功能测试引导文件相同:<?php define('DS', DIRECTORY_SEPARATOR); defined('YII_TEST_ENTRY_URL') or define('YII_TEST_ENTRY_URL', parse_url(\Codeception\Configuration::config()['config']['test_entry_url'], PHP_URL_PATH)); defined('YII_TEST_ENTRY_FILE') or define('YII_TEST_ENTRY_FILE', dirname(dirname(__DIR__)) . '/web/index-test.php'); // Define our application_env variable as provided by nginx/apache if (!defined('APPLICATION_ENV')) { if (getenv('APPLICATION_ENV') != false) define('APPLICATION_ENV', getenv('APPLICATION_ENV')); else define('APPLICATION_ENV', 'prod'); } $env = require(__DIR__ . '/../../config/env.php'); // comment out the following two lines when deployed to production defined('YII_DEBUG') or define('YII_DEBUG', $env['debug']); defined('YII_ENV') or define('YII_ENV', APPLICATION_ENV); require(__DIR__ . '/../../vendor/autoload.php'); require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php'); $config = require(__DIR__ . '/../../config/web.php'); Yii::setAlias('@tests', dirname(__DIR__)); (new yii\web\Application($config)); -
接下来,我们需要按照以下方式定义我们的
tests/config/acceptance.php文件:<?php $_SERVER['SCRIPT_FILENAME'] = YII_TEST_ENTRY_FILE; $_SERVER['SCRIPT_NAME'] = YII_TEST_ENTRY_URL; /** * Application configuration for functional tests */ return require(__DIR__ . '/../../config/web.php'); -
然后,我们需要使用
generate:cept命令生成我们的第一个验收测试:$ ./vendor/bin/codecept generate:cept acceptance Page -
最后,我们可以通过运行 run 命令来运行我们新创建的测试:
$ ./vendor/bin/codecept run acceptance![设置验收测试]()
小贴士
注意,我们正在传递必要的
APPLICATION_ENV变量到我们的内置服务器。此外,我们正在使用一个8082的高端口,如之前在tests/acceptance.suite.yml文件中定义的那样。使用高端口是为了避免需要以 root 权限运行 PHP 的内置服务器,这对于低于1024的端口是必需的。
验收测试的示例
由于接受测试与功能测试极其相似,我们可以重用之前章节中使用的大多数相同工具和方法。例如,我们可以编写一个接受测试来检查主页上的链接和文本,这些链接和文本我们在先前的功能测试中已经查找过,如下所示:
<?php
$I = new AcceptanceTester($scenario);
$I->wantTo('Verify that homepage loads');
$I->amOnPage('/');
$I->amOnPage('site/index');
$I->see('Now you\'re thinking with widgets!');
$I->see('Home');
$I->see('Login');
$I->see('Register');
注意,这里唯一的实质性区别是使用AcceptanceTester而不是FunctionalTester。现在以详细模式运行我们的测试将揭示以下内容:
$ ./vendor/bin/codecept run acceptance –vv

测试数据
在本章中,我们将讨论的最后一个是测试数据。测试数据是测试的重要组成部分,因为它们使我们能够在运行单元测试之前将应用程序状态设置到一个已知且精确的状态。然而,与这些其他测试类型不同,测试数据是由 Yii2 直接提供的,并通过 Yii2 Codeception 模块集成到 Codeception 中。
小贴士
在本节中,我们将使用位于github.com/masteringyii/chapter10的fixtures分支上的源代码。
创建测试数据
要开始使用测试数据,我们首先需要安装所需的 composer 依赖项,然后向我们的config/console.php文件中添加一些配置:
-
首先,我们需要确保已经安装了
yii2-faker composer包:$ composer require --dev yii2-faker -
或者,可以通过将以下内容添加到
composer.json的require-dev部分,然后执行composer update来安装yii2-faker扩展:"yiisoft/yii2-faker": "*", -
然后,我们需要向我们的控制台配置文件中添加相关部分:
<?php Yii::setAlias('@tests', dirname(__DIR__) . '/tests'); return [ // [...], 'controllerMap' => [ 'fixture' => [ 'class' => 'yii\faker\FixtureController', ], ], 'controllerNamespace' => 'app\commands', // [...] ]; -
最后,我们需要为我们的单元测试定义一个配置文件,以便从
tests/config/unit.php加载。为了保持简单,我们将只加载我们的 Web 配置文件:<?php /** * Application configuration for functional tests */ return require(__DIR__ . '/../../config/web.php');
在安装了所需的扩展后,我们可以通过./yii命令行工具创建和加载测试数据到我们的应用程序中:
./yii help fixture

定义测试数据
要定义新的测试数据,我们可以通过扩展yii\test\Fixture(用于通用测试数据)或yii\test\ActiveFixture(用于 ActiveRecord 条目)并将我们新创建的类放置在我们的应用程序的tests/fixtures文件夹中。定义了新的测试数据后,我们接下来将想要声明我们将用于测试数据的模型类。例如,让我们创建一个app\models\User类的测试数据,如下面的示例所示:
<?php
namespace app\tests\fixtures;
use yii\test\ActiveFixture;
class UserFixture extends ActiveFixture
{
public $modelClass = 'app\models\User';
}
现在我们已经定义了我们的测试数据类,我们需要创建我们的测试数据将填充的数据。当使用yii\test\ActiveFixture时,我们希望将我们的数据文件放置在@tests/fixtures/data/<database_table_name>.php或@tests/fixtures/data/user.php中,在我们的例子中。在这个测试数据文件中,我们将提供所有我们想要测试的模拟数据:
<?php return [
'user1' => [
'id' => 1,
'email' => 'jane.doe@example.com',
'password' => '$2y$13$iqINH3RvfW29zPupoz2Zeu9cTXUPosjn1V
.yhihP0iZEWFkEPSl6.',
'first_name' => 'Jane',
'last_name' => 'Doe',
'role_id' => 1,
'created_at' => 1448926013,
'updated_at' => 1448926013
],
'admin' => [
'id' => 4,
'email' => 'admin@example.com',
'password' => '$2y$13$uHCvsJWJr.M0vRcDlhWhVO9tTPLh8qD9.ngnhwThzzwGNC62.Ugl6',
'first_name' => 'Site',
'last_name' => 'Administrator',
'role_id' => 2,
'created_at' => 1448926013,
'updated_at' => 1448926013
]
];
我们可以通过调用带有适当命名空间的fixture命令来加载我们的测试数据:
$ ./yii fixture/load User --namespace=app/tests/fixtures
可以使用fixture/unload命令来卸载测试数据:
$ ./yii fixture/unload User --namespace=app/tests/fixtures
默认情况下,Yii2 将尝试从@app/tests/unit/fixtures文件夹加载我们的固定装置。在之前的示例中,我们通过提供--namespace参数覆盖了这种行为。为了避免每次都写这个,我们可以修改我们的控制台配置文件如下:
<?php
Yii::setAlias('@tests', dirname(__DIR__) . '/tests');
return [
// [...],
'controllerMap' => [
'fixture' => [
'class' => 'yii\faker\FixtureController',
'namespace' => 'tests\fixtures',
],
],
// [...]
];
通过这个更改,我们可以加载和卸载固定装置,而无需指定命名空间。
在单元测试中使用固定装置
现在我们知道了如何创建和加载固定装置,让我们来看看如何将它们作为测试的一部分加载。要开始使用固定装置进行测试,我们首先需要为我们的固定装置创建一个新的单元测试来运行。作为提醒,Codeception 中的新单元测试可以按照以下方式生成:
$ ./vendor/bin/codecept generate:test un it UserFixture
在创建我们的单元测试后,我们需要修改我们在tests/unit/UnitFixtureTest.php中创建的UserFixtureTest,进行几个更改:
-
首先,我们需要正确命名空间我们的测试并包含我们的
UserFixture类:<?php namespace app\tests\unit\UserFixtureTest; use app\tests\fixtures\UserFixture; use app\models\User; use Yii; -
接下来,我们需要让我们的
UserFixtureTest扩展\yii\codeception\DbTestCase并包含我们之前创建的单元测试配置文件:class UserFixtureTest extends \yii\codeception\DbTestCase { /** * @var \UnitTester */ protected $tester; public $appConfig = "@app/tests/config/unit.php"; } -
最后,我们需要告诉我们的测试用例加载我们的
UserFixture类:public function fixtures() { return [ 'users' => UserFixture::className(), ]; }
现在,在我们在新创建的UserFixtureTest类中创建的每个测试之后,我们之前创建的固定装置将在每个测试之前加载到我们的数据库中,然后在测试完成后删除。例如,我们可以创建以下测试来验证密码验证是否工作:
public function testValidatePassword()
{
$user = User::find()->where(['id' => 1])->one();
$this->assertTrue($user->validatePassword('password1'));
$this->assertFalse($user->validatePassword('password2'));
unset($user);
$user = User::find()->where(['id' => 4])->one();
$this->assertTrue($user->validatePassword('admin'));
$this->assertFalse($user->validatePassword('notadmin'));
unset($user);
}
我们新创建的测试和固定装置可以通过以下命令运行:
$ ./vendor/bin/codecept run unit

在这个测试的开始,我们的固定装置将被加载,然后我们的测试将运行。在测试运行后,我们的固定装置将被卸载,然后我们可以运行另一个测试。在这种情况下使用固定装置可以防止一个测试的结果影响另一个测试的结果。
自动更改测试
测试的一个重要方面是确保你的测试定期和经常运行。如果你遵循测试驱动开发哲学,你应该在编写代码之前编写测试,并在添加新代码时调整测试。虽然这会给你一个很好的想法,知道什么在起作用,什么不起作用,但它可能非常耗时,并且它不涵盖团队成员进行更改但既没有编写测试也没有在提交和推送到你的 DCVS 仓库之前运行测试的情况。
确保每次更改后都运行测试的最佳方式是使用第三方服务,例如 Travis CI。像 Travis CI 这样的工具将为你的仓库添加一个 webhook,每次提交后,它可以配置为运行所有测试,并在测试开始失败时通知你。
小贴士
通常,你应该在将代码提交到你的仓库之前,始终验证你的代码是否运行良好,并且测试是否通过。
以 Travis CI 为例,让我们将我们的仓库添加到 Travis CI 并启用自动构建:
-
要开始使用 Travis CI,我们首先需要用我们的 GitHub 账户登录到
travis-ci.org,然后导航到我们的个人资料travis-ci.org/profile。小贴士
Travis CI 与 GitHub 紧密耦合,不与其他服务(如 GitLab 或 Bitbucket)一起工作。该服务仅对公共仓库免费。然而,还有许多其他服务可以执行与 Travis CI 相同的服务,例如 Atlassian Bamboo、drone.io、circleci.com、GitLab CI 等。在使用持续集成工具之前,请确保你进行了研究,以确定最适合你团队的工具。对于你不在乎公开的项目,Travis CI 提供了一个很好的免费选项。
-
在导航到你的个人资料后,你需要为你的仓库启用 Travis CI。由于 Travis CI 与 GitHub 的紧密耦合,这只需要切换一个开关。
![自动变更测试]()
-
在建立与 Travis CI 的连接后,你需要在你的仓库中创建一个
.travis.yml文件。这个文件包含了如何构建和测试你的项目的说明。虽然 Travis CI 可以与许多不同的配置和矩阵一起工作,但我们将使用一个相对简单的配置,如下一节所示:sudo: false env: - "APPLICATION_ENV=dev" language: php cache: directories: - vendor php: - 5.6 - 7 install: - composer selfupdate - composer global require "fxp/composer-asset-plugin:~1.0" - composer install -o -n before_script: - ./yii migrate/up --interactive=0 script: - ./vendor/bin/codecept run -
我们的
.travis.yml文件包含几个部分:-
language部分定义了当 Travis CI 运行我们的构建时想要使用的语言。 -
cache选项的存在只是为了加快我们的构建和测试过程。在每次构建成功结束时,Travis CI 将缓存我们vendor/folder的内容,这将减少 Composer 安装所有必需依赖项所需的时间。 -
php部分列出了我们想要测试的所有 PHP 版本。通常,我们希望测试当前和下一个版本的 PHP,以便在那个版本发布时,我们可以开始使用它。测试未来的 PHP 版本使我们能够快速适应代码,以利用新 PHP 版本的新性能增强。 -
install部分允许我们定义在构建运行之前需要安装的软件。在这个部分中,我们定义了诸如 composer-asset-plugin 和我们的 composer 依赖项等。 -
before_script部分定义了在build/test脚本执行之前应该发生的事情。 -
最后,
script部分定义了我们想要构建或测试的内容。
-
-
在定义了我们的
.travis.yml文件之后,我们只需简单地将文件提交到我们的仓库。由于我们已将 Travis CI 连接到我们的 GitHub 项目,推送我们的项目将自动触发一个构建,我们可以在 Travis CI 上查看。在 Travis CI 上,我们可以查看我们项目发生的所有构建的历史记录。如果有人向我们的仓库推送的代码破坏了我们的构建,我们将收到通知,并可以通知破坏测试的人修复他们的代码后再尝试。此外,我们还可以查看每个提交的完整构建输出,这让我们能够深入了解每个构建中发生的情况。小贴士
例如,本章的仓库已链接到 Travis CI。您可以通过导航到
travis-ci.org/masteringyii来查看构建的实际操作。
摘要
在本章中,我们学到了很多关于测试的知识!我们首先介绍了如何在项目中设置和配置 Codeception 以运行。然后,我们介绍了如何设置单元测试、功能测试和验收测试,以确保我们对代码库有足够的测试覆盖率。接下来,我们介绍了如何创建和使用 fixtures 来模拟数据,以便我们的测试在一致的测试基础上运行。最后,我们介绍了如何使用 Travis CI,一个第三方持续集成服务来自动化我们的代码测试。
在下一章中,我们将介绍如何使用 Yii2 的国际化本地化功能,使我们的应用程序能够在多种语言中运行。
第十一章。国际化与本地化
在开发现代网络应用程序时,我们经常发现需要确保我们的语言对说和读写与我们不同的用户来说是可读的。为了帮助实现这一点,Yii2 提供了对国际化(i18n)和本地化(l10n)的内置支持。国际化是指规划和实施消息和视图的过程,以便它们可以轻松地适应其他语言。另一方面,本地化是指将我们的应用程序适应特定的语言或文化,包括我们的应用程序的外观和感觉,以符合特定语言或地区或市场的信息接受展示。在本章中,我们将了解如何使用 Yii2 的内置功能将我们的应用程序翻译和本地化为多种语言。
小贴士
i18n 和 l10n 是数字缩写词,而不是首字母缩略词。国际化简写为 i18n,因为它以字母 "I" 开头,后面跟着 18 个字符,并以字母 "N" 结尾。同样,本地化简写为 l10n,因为它以字母 "L" 开头,有 10 个额外的字母,然后以字母 "N" 结尾。这些缩写词仅仅是为了缩短单词,没有其他含义。在本章中,我们将使用全称和缩写形式来指代这两个词。
配置 Yii2 和 PHP
在我们开始使用 Yii2 的本地化功能之前,我们首先需要确保 intl PHP 扩展已安装。此扩展用于为 Yii2 提供大多数 i18n 功能,包括 Yii2 的消息和日期格式化器。虽然 Yii2 在此扩展未安装的情况下有一些内置的回退机制,但强烈建议您事先安装它。
Intl 扩展
许多默认的 PHP 安装都包含在 PHP 包中构建的 intl 扩展,但许多没有。幸运的是,有几种方法可以检查 intl 扩展是否已安装。对于那些更喜欢在网页浏览器中查看这些信息的人来说,只需在您的 webroot 中创建一个包含以下内容的空白 PHP 文件,并扫描输出以检查 intl 扩展是否存在并已启用:
<?php phpinfo();
如果你更喜欢使用命令行,你可以运行以下命令来检查你的 PHP 实例是否已安装 intl 扩展:
php –m | grep intl
如果 intl 扩展在任何输出中都没有出现,您可以通过您的系统包管理器(根据您的操作系统使用 apt 或 yum)安装它,或者您可以手动安装它。一般来说,可以通过 pecl 命令手动编译和安装扩展:
sudo pecl install intl
小贴士
如果你从源代码安装intl扩展,你需要确保你已经安装了intl库,最好是 49 或更高版本。如果你的系统有一个过时的intl库版本,你可以从site.icu-project.org/download下载并编译一个新版本。此外,随intl库提供的时区数据可能已过时。确保你参考intl文档以获取有关如何更新你的intl时区数据的详细信息,链接为userguide.icu-project.org/datetime/timezone#TOC-Updating-the-Time-Zone-Data。
编译完成后,你可以在你的php.ini配置文件中添加以下内容:
extension=intl.so
在重启你的 Web 服务器和 PHP 进程后,你应该会看到intl扩展通过之前列出的其中一个命令出现。
提示
关于如何安装intl扩展的更多信息可以在 PHP 手册页面上找到,链接为secure.php.net/manual/en/intl.installation.php。
应用程序语言
在我们开始使用 Yii2 的翻译功能之前,我们需要定义应用程序所使用的语言。Yii2 中的应用程序语言由一个唯一的 ID 定义,该 ID 由 ISO-639 格式定义的语言 ID 和一个由 ISO-3166 格式定义的区域 ID 组成。例如,en-US代表在美国使用的英语。
提示
关于 ISO-639 的详细信息可以在www.loc.gov/standards/iso639-2/找到,关于 ISO-3166 的详细信息可以在www.iso.org/obp/ui/#search找到。
Yii2 在我们的配置文件中定义了两个语言属性,我们可以进行定义。第一个sourceLanguage属性代表我们的应用程序所使用的语言或区域,通常在应用程序请求生命周期内不会改变。第二个language属性代表用户正在使用的语言或区域,用户可以在任何时间点更改它(通常通过在页面上某个位置放置一个language选择小部件)。这两个配置选项结合使用,使我们能够通知 Yii2 如何处理我们希望被翻译的消息。在我们的config/web.php或config/console.php配置文件中,这两个选项可以设置如下:
return [
'language' => 'ru-RU',
'sourceLanguage' => 'en-US',
];
提示
默认情况下,Yii2 会将sourceLanguage属性设置为en-US。
以编程方式设置应用程序语言
如果你正在开发一个多语言网站,而不是指定一个默认语言,你可能希望允许用户从下拉列表中选择他们的语言,并通过编程方式更改语言。为此,只需在你的代码中定义Yii::$app->language属性,并使用你选择的语言代码即可。
当以编程方式设置语言属性时,你通常会希望将用户的语言设置与其用户信息一起存储,或者作为一个会话变量。此外,你还需要确保在 Yii2 开始处理你的消息之前应用语言设置。设置此设置的好地方是在你的控制器流程的早期,例如在我们的控制器的init()方法中。
动态设置应用程序语言
除了在控制器中手动设置应用程序语言之外,我们还可以使用内容协商器过滤器(yii\filters\ContentNegotiator)从用户浏览器发送的Accept-Language头中确定用户的语言。要使用内容协商器过滤器,我们只需将yii\filters\ContentNegotiator添加到config/web.php配置文件的bootstrap部分,并指定我们想要自动支持的语言:
return [
// [...],
'bootstrap' => [
[
'class' => 'yii\filters\ContentNegotiator',
'languages' => [
'en',
'de',
],
],
],
];
小贴士
语言属性指定了当它们出现在Accept-Language头中时,Yii2 将自动将Yii::$app->language设置为哪些语言。在先前的例子中,我们只设置了语言为en或de。如果我们的Accept-Language头中出现的是我们应用程序配置中未列出的语言,我们将默认使用sourceLanguage属性中指定的语言。
除了全局设置之外,我们还可以在我们的控制器的behaviors()方法中设置内容协商器,并指定我们想要在该控制器中支持的语言。当你有一个可能支持比你的基础应用程序更多或不同语言的模块时,这很有益。在我们的控制器中,我们可以按照以下方式配置yii\filters\ContentNegotiator:
public function behaviors()
{
return [
[
'class' => 'yii\filters\ContentNegotiator',
'languages' => [
'en',
'de',
],
],
];
}
小贴士
这个yii\filters\ContentNegotiator路径可以提供比仅设置应用程序语言更多的功能。有关内容协商过滤器的更多信息,请确保查看 Yii2 文档在www.yiiframework.com/doc-2.0/yii-filters-contentnegotiator.html。
消息翻译
Yii2 的消息翻译服务通过在消息源文件中查找要翻译的消息,将给定的文本消息从源语言翻译到另一种语言。如果在目标语言的源中找到了消息,则返回该字符串而不是原始消息。如果找不到翻译文本,Yii2 将返回原始消息。
使用 Yii2 的消息翻译服务非常简单。在 Yii2 中翻译消息的第一步是将任何想要翻译的消息用Yii::t()静态方法包装,该方法的调用方式如下:
Yii::t('app', 'My message to be translated');
第一个参数表示我们想要存储消息的类别,第二个参数表示我们想要翻译的消息。
消息源
然而,在 Yii2 可以翻译我们的消息之前,我们首先需要定义一个消息源,该消息源将存储我们的基础消息和翻译后的消息文件。Yii2 提供了三种不同的消息源选项:
-
yii\i18n\PhpMessageSource以键值数组格式存储消息文件 -
yii\i18n\DbMessageSource将消息存储在数据库表中 -
yii\i18n\GettextMessageSource使用 GNU Gettext MO 或 PO 文件来存储翻译后的消息
我们希望使用的消息源可以在应用程序配置文件中的组件部分声明,如下所示:
<?php return [
// [...],
'components' => [
// [...],
'i18n' => [
'translations' => [
'app*' => [
'class' => 'yii\i18n\PhpMessageSource',
],
],
],
]
];
在前面的代码块中,消息源由yii\i18n\PhpMessageSource提供。app*模式表示所有以app开头的消息都应该由指定的消息源处理。默认情况下,Yii2 将在@app/messages文件夹中存储消息,并将源语言默认设置为en-US;但是,可以通过在类别块中指定basePath和sourceLanguage属性来更改此行为,如下所示:
<?php return [
// [...],
'components' => [
// [...],
'i18n' => [
'translations' => [
'app*' => [
'class' => 'yii\i18n\PhpMessageSource',
//'basePath' => '@app/messages',
//'sourceLanguage' => 'en-US',
],
],
],
]
];
此外,Yii2 将为类别创建与类别同名的消息文件。此行为可以通过在类别配置中指定fileMap属性来更改。除非使用fileMap属性另行指定,否则所有消息都将存储在@app/messages/<language>/<category>.php中。
默认翻译
Yii2 还允许我们为不匹配其他翻译的类别创建回退消息。这可以通过在配置文件中声明一个*类别来实现,如下面的示例所示:
<?php return [
// [...],
'components' => [
// [...],
'i18n' => [
'translations' => [
'*' => [
'class' => 'yii\i18n\PhpMessageSource'
],
],
],
]
];
框架消息
除了指定默认消息外,我们还可以修改 Yii2 原生提供的内置消息。默认情况下,Yii2 自带了诸如验证错误和其他基本字符串的几种翻译,所有这些都存储在yii类别中。由于默认的 Yii 消息可能不合适或不准确,您可以通过在配置文件中设置yii类别来重新定义默认消息:
<?php return [
// [...],
'components' => [
// [...],
'i18n' => [
'translations' => [
'yii' => [
'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => 'en-US',
'basePath' => '@app/messages'
],
],
],
]
];
处理缺失的翻译
如果源文件中缺少消息翻译,Yii2 将默认显示原始消息内容。虽然确保我们的网站至少显示一些内容是方便的,但这种行为可能会在调试和识别时造成麻烦。此外,我们可能希望在缺少翻译的情况下执行额外的处理。幸运的是,我们可以通过为yii\i18n\MessageSource触发的missingTranslation事件创建事件处理器来实现这一点,如下面的示例所示:
<?php return [
// [...],
'components' => [
// [...],
'i18n' => [
'translations' => [
'app*' => [
'class' => 'yii\i18n\PhpMessageSource',
'on missingTranslation' => ['app\components\TranslationEventHandler', 'handleMissingTranslation']
],
],
],
]
];
例如,我们可以编写一个事件处理器来输出一些值得注意的内容:
<?php
namespace app\components;
use yii\i18n\MissingTranslationEvent;
class TranslationEventHandler
{
public static function handleMissingTranslation(MissingTranslationEvent $event)
{
$event->translatedMessage = "@{$event->category}.{$event->message}-{$event->language}@";
}
}
小贴士
事件处理器仅处理该类别中的消息。如果您希望处理多个类别的相同事件,必须将事件处理器分配给每个类别,或者,作为替代,将其分配给*类别。
生成消息文件
在配置我们的消息源之后,我们需要生成我们的消息文件。为此,我们将使用 message 命令:
-
生成我们的消息文件的第一步是创建一个配置文件,该文件将定义我们想要支持的语言以及消息应存储的特定路径。这可以通过运行以下命令来完成:
./yii message/config path/to/messagesconfig.php -
根据我们在 Web 或控制台配置文件中先前指定的语言,这将生成类似以下的内容:
<?php return [ // string, required, root directory of all source files 'sourcePath' => __DIR__ . DIRECTORY_SEPARATOR . '..', // array, required, list of language codes that the extracted messages // should be translated to. For example, ['zh-CN', 'de']. 'languages' => ['de'], // string, the name of the function for translating messages. // Defaults to 'Yii::t'. This is used as a mark to find the messages to be // translated. You may use a string for single function name or an array for // multiple function names. 'translator' => 'Yii::t', // boolean, whether to sort messages by keys when merging new messages // with the existing ones. Defaults to false, which means the new (untranslated) // messages will be separated from the old (translated) ones. 'sort' => false, // boolean, whether to remove messages that no longer appear in the source code. // Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks. 'removeUnused' => false, // array, list of patterns that specify which files/directories should NOT be processed. // If empty or not set, all files/directories will be processed. // A path matches a pattern if it contains the pattern string at its end. For example, // '/a/b' will match all files and directories ending with '/a/b'; // the '*.svn' will match all files and directories whose name ends with '.svn'. // and the '.svn' will match all files and directories named exactly '.svn'. // Note, the '/' characters in a pattern matches both '/' and '\'. // See helpers/FileHelper::findFiles() description for more details on pattern matching rules. 'only' => ['*.php'], // array, list of patterns that specify which files (not directories) should be processed. // If empty or not set, all files will be processed. // Please refer to "except" for details about the patterns. // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed. 'except' => [ '.svn', '.git', '.gitignore', '.gitkeep', '.hgignore', '.hgkeep', '/messages', '/vendor, ], // 'php' output format is for saving messages to php files. 'format' => 'php', // Root directory containing message translations. 'messagePath' => __DIR__, // boolean, whether the message file should be overwritten with the merged messages 'overwrite' => true ]; -
在大多数情况下,Yii2 在此文件中提供的默认值应该足够。您应该考虑更改的唯一值是
languages选项和format选项。在继续之前,请确保您已适当地设置了这些值。 -
在对
messagesconfig.php文件进行必要的更改后,我们可以通过直接运行消息命令来生成消息文件,如下例所示:./yii message path/to/messagesconfig.php -
message命令是一个非常强大的工具,它允许我们快速生成可以交给翻译者的消息文件。配置文件中有几个选项可以使消息翻译更容易。例如,可以将removedUnused参数设置为true,以自动从我们的消息文件中删除不再列在我们的源代码中的字符串。此外,通过将overwrite参数设置为true,我们可以多次运行message命令来重新生成我们的翻译文件。小贴士
注意,
message命令不支持所有路径别名。当处理消息文件时,建议您使用绝对路径。此外,建议您将messagesconfig.php文件存储在应用程序的messages/目录中。
消息格式化
在翻译消息时,您可能希望将变量或数据从您的模型注入到消息中。为此,我们只需在我们的消息中嵌入一个 placeholder,然后在 Yii::t() 方法的第三个属性中定义 placeholder 的参数。例如,如果我们想使用用户的名字来问候用户,我们可以这样做:
<?php
// $model = User::find(1)->one();
echo Yii::t('app', 'Good Morning {name}', [
'name' => $model->first_name
]);
作为命名参数的替代,我们还可以使用位置参数,如下例所示:
$price = 500;
$count = 2;
$subtotal = 1000;
echo \Yii::t('app', 'Price: ${0}, Count: {1}, Subtotal: ${2}', [
$price,
$count,
$subtotal
]);
小贴士
Yii2 还支持数字、货币、日期、时间、原始和复数数据的参数格式化。更多信息可以在 Yii2 API 的 www.yiiframework.com/doc-2.0/yii-i18n-formatter.html 以及 Yii2 指南的参数格式化部分 www.yiiframework.com/doc-2.0/guide-tutorial-i18n.html#parameter-formatting 中找到。
查看文件翻译
作为翻译单个消息的替代方案,我们还可以通过在views文件夹的子目录中保存翻译的视图文件来翻译整个视图文件。例如,假设我们有一个位于views/site/login.php的视图脚本,我们可以通过在views/site/es-MX/login.php中放置一个翻译的消息文件来创建一个针对es-MX的西班牙语视图文件。假设我们的目标语言和源语言设置适当,当目标语言设置为es-MX时,Yii2 将自动渲染翻译的文件而不是基本文件。
小贴士
注意,如果源语言和目标语言相同,则无论是否存在翻译视图文件,都将渲染原始视图。
此外,视图文件翻译的使用并不遵循我们在整本书中强调的 DRY 模式。此外,将包含 PHP 代码的完整 HTML 文件交给翻译者可能会使这些文件的翻译变得困难,因为翻译行业基于字符串翻译,而不是代码中的字符串翻译。为了保持您的应用程序 DRY 并避免在翻译过程中可能出现的任何问题,强烈建议您使用之前提到的消息翻译方法,而不是视图文件翻译。
模块翻译
作为独立的实体,模块应包含它们自己的消息文件,而不是您的应用程序消息文件。在模块中使用消息的推荐方式如下:
-
在模块的
init()方法中,为模块定义一个新的翻译部分:parent::init();Yii::$app->i18n->translations['modules/mymodule*'] = [ 'class' => 'yii\i18n\PhpMessageSource', 'sourceLanguage' => 'en-US', 'basePath' => '@app/modules/mymodule/messages' ]; -
为
Yii::t()创建一个静态方法包装器:public static function t($category, $message, $params = [], $language = null) { return Yii::t('modules/mymodule/' . $category, $message, $params, $language); } -
最后,在模块的
messages/目录中创建一个单独的消息配置文件,指定翻译器为<ModuleName>::t:<?php return [ 'sourcePath' => __DIR__ . DIRECTORY_SEPARATOR . '..', 'languages' => ['de'], 'translator' => 'MyModule::t', 'sort' => false, 'removeUnused' => false, 'only' => ['*.php'], 'except' => [ '.svn', '.git', '.gitignore', '.gitkeep', '.hgignore', '.hgkeep', '/messages', '/vendor' ], 'format' => 'php', 'messagePath' => __DIR__, 'overwrite' => true ];
我们模块中的消息可以通过调用MyModule::t()进行翻译。此外,可以通过运行以下命令生成翻译的消息文件:
./yii message modules/mymodule/messages/messages.php
小部件翻译
类似地,小部件也可以通过遵循为模块概述的过程来拥有它们自己的消息翻译文件。使用我们在第五章中创建的GreetingWidget类,模块、小部件和助手将如下所示:
<?php
namespace app\components;
use yii\base\Widget;
use yii\helpers\Html;
use Yii;
class GreetingWidget extends Widget
{
public $name = null;
public $greeting;
public function init()
{
parent::init();
Yii::$app->i18n->translations['widgets/GreetingWidget*'] = [
'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => 'en-US',
'basePath' => '@app/components/widgets/GreetingWidget'
];
$hour = date('G');
if ( $hour >= 5 && $hour <= 11 )
$this->greeting = GreetingWidget::t("Good Morning");
else if ( $hour >= 12 && $hour <= 18 )
$this->greeting = GreetingWidget::t("Good Afternoon");
else if ( $hour >= 19 || $hours <= 4 )
$this->greeting = GreetingWidget::t("Good Evening");
}
public function run()
{
if ($this->name === null)
return HTML::encode($this->greeting);
else
return HTML::encode($this->greeting . ', ' . $this->name);
}
public static function t($category, $message, $params = [], $language = null)
{
return Yii::t('widgets/GreetingWidget/' . $category, $message, $params, $language);
}
}
因此,调用GreetingWidget::t()将渲染针对我们小部件的特定翻译消息。此外,由于小部件支持视图渲染,它们还可以通过遵循之前概述的过程来支持完全翻译的视图文件。
摘要
Yii2 为我们应用提供了强大的工具来支持国际化与本地化。在本章中,我们介绍了如何生成和存储消息源文件,如何生成消息和视图翻译,以及如何在模块和小部件中支持翻译。在下一章中,我们将介绍 Yii2 的性能特性,以及探索 Yii2 提供的几个内置安全特性。
第十二章。性能和安全
默认情况下,Yii2 既是高性能的也是高效的 PHP 框架。它被设计得尽可能快,同时仍然提供功能丰富的工具箱以供使用。有许多因素会影响我们应用程序的性能,这些因素可能会对我们的应用程序性能产生负面影响,例如长时间运行的查询和数据生成。在本章中,我们将介绍几种优化和微调 Yii2 的方法,以确保我们的应用程序保持高性能。我们还将介绍保护我们代码的几个重要方面。
缓存
提高我们应用程序性能的最简单方法之一是实现缓存。通过在我们的应用程序中实现缓存,我们可以减少生成和向最终用户交付数据所需的时间。使用 Yii2,我们可以缓存从生成数据、数据库查询到整个页面和页面片段的一切。我们还可以指示我们的浏览器为我们缓存页面。在本节中,我们将介绍几种不同的缓存技术,我们可以在 Yii2 中实现这些技术,以提高我们应用程序的性能。
缓存数据
数据缓存主要是关于存储常见生成数据,以便我们可以为给定时间段生成一次,而不是每次请求都生成,在 Yii2 中,它是通过应用程序的缓存组件实现的。Yii2 提供了多种不同的类,我们可以使用这些类来缓存数据,所有这些类都遵循并使用一致的 API,通过实现 yii\caching\Cache 抽象类。
这个一致的 API 允许我们无需修改应用程序内部的代码,就可以用以下表格中列出的任何缓存替换我们的缓存组件:
| 缓存名称 | 描述 | 类引用 |
|---|---|---|
yii\caching\ApcCache |
使用 APC PHP 扩展的缓存。在单个服务器配置中,APC 缓存性能非常好,但如果启用了 PHP Opcache,则存在兼容性问题。 | www.yiiframework.com/doc-2.0/yii-caching-apccache.html |
yii\caching\DbCache |
使用数据库表来存储信息的缓存。 | www.yiiframework.com/doc-2.0/yii-caching-dbcache.html |
yii\caching\DummyCache |
一个占位符缓存,不执行任何缓存操作,但在开发期间用作真实缓存的代表,以确保我们的应用程序能够与真实缓存一起工作。 | www.yiiframework.com/doc-2.0/yii-caching-dummycache.html |
yii\caching\FileCache |
一种将数据存储在文件存储中的缓存,适用于存储页面或页面片段。 | www.yiiframework.com/doc-2.0/yii-caching-filecache.html |
yii\caching\MemCache |
使用 PHP memcache 或 memcached 扩展存储数据的内存缓存。 | www.yiiframework.com/doc-2.0/yii-caching-memcache.html |
yii\caching\WinCache |
使用 WinCache PHP 扩展的缓存。 | www.yiiframework.com/doc-2.0/yii-caching-wincache.html |
yii\redis\Cache |
实现 Redis 键值存储的缓存。 | www.yiiframework.com/doc-2.0/yii-redis-cache.html |
yii\caching\XCache |
使用 XCache PHP 扩展的缓存。 | www.yiiframework.com/doc-2.0/yii-caching-xcache.html |
小贴士
虽然列出的每个缓存都实现了 yii\caching\Cache API,但一些缓存,例如 yii\redis\Cache 和 yii\caching\MemCache,需要一些额外的配置。请确保您参考您在应用程序中决定使用的缓存类的参考文档。
以 yii\caching\FileCache 为例,我们可以在应用程序配置文件中添加以下内容来实现应用程序内的缓存:
<?php return [
// [...],
'components' => [
// [...]
'cache' => [
'class' => 'yii\caching\FileCache',
]
]
];
在实现特定的缓存系统之后,我们可以在应用程序代码中通过引用 Yii::$app->cache 来使用我们的缓存。
如前所述,每个缓存都实现了由 yii\caching\Cache 抽象类定义的一致 API。因此,每个缓存都提供了以下方法,我们可以使用这些方法来操作缓存中的数据。
| 方法 | 说明 |
|---|---|
yii\caching\Cache::add() |
如果缓存中不存在,则使用给定键存储值。如果缓存的项已存在,则不会执行任何操作。 |
yii\caching\Cache::get() |
从缓存中检索具有给定键的项。 |
yii\caching\Cache::set() |
将具有给定键的项设置到缓存中,并可选择指定过期日期。设置了过期日期的缓存项将自动由底层缓存机制或 Yii2 本身清除。 |
yii\caching\Cache::madd() |
将多个项作为键值数组存储到缓存中。如果给定的缓存键已存在,则不会发生任何操作。在 Yii 2.1 中,此方法将被标记为已弃用,并由 yii\caching\Cache::multiAdd() 取代。 |
yii\caching\Cache::mget() |
同时从缓存中检索多个数据键。在 Yii 2.1 中,此方法将被标记为已弃用,并由 yii\caching\Cache::multiGet() 取代。 |
yii\caching\Cache::mset() |
将多个缓存项(以键值对的形式)同时设置到缓存中。带有过期时间的缓存项将被底层缓存机制或 Yii2 自动清除。在 Yii 2.1 中,此方法将被标记为弃用,并由yii\caching\Cache::multiSet()方法取代。 |
yii\caching\Cache::exists() |
返回一个布尔值,表示给定的缓存键是否存在于缓存中。 |
yii\caching\Cache::delete() |
从缓存中删除给定的缓存键。 |
yii\caching\Cache::flush() |
清除缓存中的所有数据。 |
小贴士
关于每个方法和其使用的更多信息,请参阅yii\caching\Cache抽象类中描述的非继承的公共方法,链接为www.yiiframework.com/doc-2.0/yii-caching-cache.html。
通常,我们可以通过调用Yii::$app->cache组件的任何这些方法来使用我们的缓存,如下面的示例所示:
$cache = Yii::$app->cache;
if ($cache->exists('example'))
$data = $cache->get('example');
else
{
// Generate data here...
$data = [];
// Cache the $data for 100 seconds
$cache->set('example', $data, 100);
}
return $data;
缓存依赖项
除了设置具有给定过期时间的缓存外,我们还可以使用某些依赖项(如文件的最后修改时间或某种表达式的表达式)来缓存数据,如果依赖项发生变化,我们的数据将自动过期。Yii2 提供了几个我们可以使用的依赖项。
| 方法 | 说明 | 类引用 |
|---|---|---|
yii\caching\ChainedDependency |
允许我们将多个依赖项链接在一起,如果任何依赖项失败,则使缓存项过期。 | www.yiiframework.com/doc-2.0/yii-caching-chaineddependency.html |
yii\caching\DbDependency |
基于给定 SQL 查询的一个依赖项。如果查询的结果发生变化,缓存将被失效。 | www.yiiframework.com/doc-2.0/yii-caching-dbdependency.html |
yii\caching\FileDependency |
基于文件的最后修改时间的一个依赖项。 | www.yiiframework.com/doc-2.0/yii-caching-filedependency.html |
yii\caching\ExpressionDependency |
由布尔表达式表示的依赖项。 | www.yiiframework.com/doc-2.0/yii-caching-expressiondependency.html |
yii\caching\TagDependency |
基于可管理的标签数组的一个依赖项。 | www.yiiframework.com/doc-2.0/yii-caching-tagdependency.html |
小贴士
查看每个依赖项的类引用,以获取有关其可用属性和方法的信息。
在我们之前的示例基础上,我们可以添加一个缓存依赖,如下面的示例所示。在下面的代码中,我们创建了一个对名为data.csv的文件的依赖,该文件可以包含报告或其他我们希望生成或导入到我们的应用程序中的数据:
$cache = Yii::$app->cache;
if ($cache->exists('example'))
$data = $cache->get('example');
else
{
// Generate data here...
$data = [];
$dependency = new \yii\caching\FileDependency(['fileName' => 'data.csv']);
// Cache $data for 100 seconds using the key "example" with a FileDependency
$cache->set('example', $data, 100, $dependency);
}
return $data;
数据库查询缓存
使用 Yii2,我们还可以缓存数据库查询的结果。要启用查询缓存,我们需要在我们的数据库组件中设置三个属性:$enableQueryCache,用于切换查询缓存的开启和关闭;$queryCacheDuration,用于设置查询应该缓存的持续时间;以及$queryCache,用于指定应使用的缓存组件。
以下连接示例说明了如何启用查询缓存:
<?php return [
// [...],
'components' => [
// [...]
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host='127.0.0.1;dbname=masteringyii',
'username' => '<username>,
'password' => '<password>',
'charset' => 'utf8',
'queryCacheEnabled' => true,
// 0 = Never expires
'queryCacheDuration' => 0,
'queryCache' => 'cache'
],
'cache' => [
'class' => 'yii\caching\FileCache',
]
]
];
在配置数据库查询缓存后,我们可以通过添加或链接缓存方法到我们的查询上来缓存单个 DAO 查询的结果,如下面的示例所示:
$duration = 100; // 100 seconds
$results = $db->createCommand('SELECT * FROM users WHERE id=1;')->cache($duration)->queryOne();
或者,如果我们想缓存多个查询,我们可以直接调用yii\db\Connection::cache()函数:
$result = $db->cache(function ($db) {
$result = $db->createCommand('SELECT * FROM users WHERE id=1;')->queryOne();
return $result;
}, $duration, $dependency);
ActiveRecord也可以通过从ActiveRecord模型获取数据库组件来利用查询缓存,如下面的示例所示:
$result = User::getDb()->cache(function ($db) {
return User::find()->where(['id' => 5])->one();
}, $duration, $dependency);
此外,在查询缓存中,我们可以通过将noCache()方法链接到我们的查询上来排除某些查询的缓存,如下面的示例所示:
$result = $db->cache(function ($db) {
// Cache queries in this block
$db->noCache(function ($db) {
// Do not cache queries in this block
});
// Don't cache this query either
$customer = $db->createCommand('SELECT * FROM users WHERE id=1')->noCache()->queryOne();
return $result;
});
提示
一些数据库,如 MySQL,在其软件层中实现了自己的内置缓存。同时实现 MySQL 的本地查询缓存和 Yii2 的查询缓存可能会导致确保正确数据呈现的问题。此外,任何作为资源处理器返回的数据都不能由 Yii2 缓存。此外,一些缓存,如Memcache,限制了可以与特定键关联的数据量。在使用查询缓存时,请注意这些限制。
片段缓存
片段缓存建立在数据缓存之上。Yii2 中的片段缓存允许我们缓存页面的一部分,并在每次请求时呈现该缓存片段,而不是重新生成页面的全部内容。通常,我们可以通过以下代码块使用片段缓存:
// $id = ...a unique key...
// $this = ...instance of yii\web\View...;
// Begin our cache and check to see if the data is already cached.
// If content is found, beginCache will output data, otherwise
// the conditional will execute.
if ($this->beginCache($id))
{
// Our cached content goes here
$this->endCache();
}
与数据缓存一样,片段缓存也支持多个条件,如持续时间、依赖项、变化和切换片段缓存的开启和关闭。这些条件可以作为键值数组添加到beginCache()方法的第二个参数中,如下面的示例所示:
if ($this->beginCache($id, [
// Time we want the fragment cache valid for
'duration' => 100,
// Any dependencies we want to add
'dependency' => [
'class' => 'yii\caching\DbDependency',
'sql' => 'SELECT MAX(updated_at) FROM user',
],
// Conditionally enable the cache for any boolean value
'enabled' => Yii::$app->request->isGet,
// Have a variation of this page for every language
'variations' => Yii::$app->language
]))
{
// Our cached content goes here
$this->endCache();
}
页面缓存
作为仅缓存网页片段的替代方案,使用 Yii2,我们还可以缓存整个页面,并在每次页面加载时提供缓存的副本而不是生成页面。这对于我们有一个读密集型应用程序,如博客来说非常有用。在 Yii2 中,通过将yii\filters\PageCache过滤器添加到我们的控制器中的behaviors()方法来实现页面缓存,如下面的示例所示。与片段缓存一样,我们可以为我们的页面指定变体,指定内容应该无效化的依赖项,以及缓存的时间长度。与其他过滤器一样,我们还可以使用only和except参数指定我们想要我们的缓存应用到的操作。以下示例说明了页面缓存的使用:
public function behaviors()
{
return [
[
'class' => 'yii\filters\PageCache',
'only' => ['article'],
'duration' => 60,
'variations' => [
Yii::$app->language,
Yii::$app->user->isGuest
],
'dependency' => [
'class' => 'yii\caching\DbDependency',
'sql' => 'SELECT MAX(updated_at) FROM articles',
],
],
];
}
HTTP 缓存
数据、片段和页面缓存都是我们可以用来优化我们应用程序服务器端性能的策略。为了进一步提高我们应用程序的性能,我们还可以将带有我们应用程序的标题发送出去,以指示我们希望客户端的浏览器缓存我们页面的输出。这三个标题是Last-Modified、ETag和Cache-Control。通过将这三个标题与我们的应用程序一起发送,我们可以显著减少客户端对我们应用程序发送的 HTTP 请求的数量,对于不经常更改的页面。在 Yii2 中,HTTP 缓存是通过yii\filtersHttpCache过滤器实现的:
-
第一个标题
Last-Modified通知客户端页面最后更改的时间。如果客户端向服务器发送 HEAD 请求并看到Last-Modified标题与它目前拥有的不同,它将重新请求页面并缓存它。否则,它将从客户端的缓存中加载页面。 -
ETag标题用于表示标签的哈希。与Last-Modified标题一样,如果ETag哈希发生变化,浏览器就知道它必须重新下载页面。 -
最后,
Cache-Control标题指示页面应该存储在哪种类型的缓存中,以及存储多长时间。默认情况下,Yii2 将为该标题发送public; max-age: 3600,这将指示客户端应将内容缓存 3600 秒或 1 小时。小贴士
更多关于 Cache-Control 头部的信息可以在 w3c 规范参考指南中找到,链接为
www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9。
下面是一个说明如何结合使用这三个标题的示例:
public function behaviors()
{
return [
[
'class' => 'yii\filters\HttpCache',
'only' => ['index'],
'lastModified' => function ($action, $params) {
$q = new \yii\db\Query();
return $q->from('articles')->max('updated_at');
},
'etag' => function($action, $params) {
$article = Article::find()->where(['id' => \Yii::$app->request->get('id')])->one();
return serialize([$article->title, $article->content]);
},
'cacheControlHeader' => 'public; max-age:3600'
],
];
}
小贴士
注意,对于 HTTP 缓存,您只需要指定您想要发送的标题。指定多个标题可以为您提供更精细的控制,以确定缓存何时过期。
缓存数据库模式
为了使ActiveRecord模型自动工作,Yii2 将在每次查询的开始自动查询数据库以确定应用程序的模式。虽然这在开发环境中很有用,但在我们的模式很少改变的生产环境中,这个操作是不必要的。我们可以通过启用数据库组件的三个属性来告诉 Yii2 缓存我们的数据库模式,以改善数据库操作的性能:$schemeCache,它代表我们想要使用的缓存组件;$schemaCacheDuration,它定义了 Yii2 缓存我们的模式的时间长度;以及$enableSchemaCache,它启用或禁用模式缓存。
以下 MySQL 数据库组件说明了模式缓存属性的使用:
<?php return [
// [...],
'components' => [
// [...]
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host='127.0.0.1;dbname=masteringyii',
'username' => '<username>,
'password' => '<password>',
'charset' => 'utf8',
'enableSchemaCache' => true,
'schemaCacheDuration' => 0,
'schemaCache' => 'cache'
],
'cache' => [
'class' => 'yii\caching\FileCache',
]
]
];
小贴士
当启用模式缓存时,在应用新的迁移之后运行cache/flush命令,以便 Yii2 能够获取你的新数据库结构。
通用性能提升
为了获得显著的性能提升,你可以对你的应用程序以及你的 Web 服务器环境进行一些更改,这些更改可以显著提高应用程序的性能。
启用 OPCache
与 C 和 C++等编译型语言不同,PHP 是一种解释型脚本语言。因此,每次我们的 Web 服务器请求一个新页面,或者每次我们从命令行运行一个命令时,PHP 都需要将我们的代码解释成服务器可以实际运行的机器码。即使我们的源代码没有改变,PHP 也会在每次请求时自动执行这一步骤。在我们的开发环境中,这允许我们简单地更改源代码,保存文件,然后在页面上重新加载以查看我们的更改。然而,在生产环境中,这一步骤是不必要的,因为我们的代码只有在进行部署时才会改变。
从 PHP 5.5 开始,Zend Framework Technologies Ltd 发布了一个名为 OPCache 的新工具,并将其集成到 PHP 核心中。一旦启用,OPCache 将缓存由我们的 PHP 代码生成的编译和优化后的操作码,并将其存储在共享内存存储中。如果我们的代码再次运行,OPCache 将查找那个共享内存存储中的代码并执行它,而不是重新解释我们的原始源代码文件。根据我们应用程序的大小,启用 OPCache 可以对我们的应用程序性能产生重大影响。此外,由于 OPCache 现在已集成到 PHP 中,启用它相对简单。
小贴士
注意,Zend OPCache 和 APCCache 都可以配置为缓存 PHP 的操作码。强烈建议你不要同时运行 Zend OPCache 和 APCCache,因为这可能会在 PHP 中引起不稳定。由于 Zend OPCache 由 PHP 维护者维护,建议你使用它而不是 APC。
根据你的包管理器,OPCache 可能已经内置到你的 PHP 实例中,或者作为外部扩展提供。一个简单的方法是检查 OPCache 是否已安装,可以通过从你的命令行运行以下命令来完成:
$ php –m
如果 OPCache 已安装,你应该在输出中看到Zend OPcache。如果你没有看到这个输出,你需要从你的包管理器安装 OPCache。一旦安装了 OPCache,你可以通过将以下内容添加到你的php.ini文件或 PHP INI 包含文件夹中的文件,并重新启动你的 Web 服务器来启用它:
zend_extension=opcache.so
opcache.enable = true
opcache.enable_cli = true
opcache.save_comments = false
opcache.enable_file_override = true
小贴士
当你执行部署时,你需要清除 OPCache 以使新代码生效。通常,这是通过重新启动你的 Web 服务器或 PHP 进程来完成的。或者,你可以使用像cachetool(可在github.com/gordalina/cachetool)这样的工具来清除缓存。使用像cachetool这样的工具是有益的,因为它允许你在不重新启动 Web 服务器并面临潜在的中断的情况下清除 OPCache。
优化 Composer 依赖项
作为部署的一部分,你可以进行另一种性能改进,即从你的生产部署中排除开发依赖项:
$ composer install --no-dev
由于我们的开发依赖项在开发中使用,将此代码加载并注册到我们的应用程序中只会给我们的应用程序增加额外的开销。
此外,我们可以在安装 composer 依赖项时通过运行以下命令来指示 Composer 优化它生成的自动加载器:
$ composer install –o
或者,我们可以在安装依赖项后通过运行以下命令生成一个优化的自动加载文件:
$ composer dumpautoload -o
通过优化 Composer 的自动加载文件,我们可以减少在源代码中加载类时所需的文件和磁盘查找次数,这反过来会使我们的应用程序运行更快。
升级到 PHP 7
在发布时,PHP 7 已经发布,它包含了一个重构的 PHP 引擎,能够以显著较少的指令解释、编译和执行相同的 PHP 代码。通过减少 CPU 指令和内存使用,PHP 7 比 PHP 5.6 快得多。为了获得显著的性能提升,考虑将你的 PHP 实例从 5.6 升级到 7。
切换到 Facebook 的 HHVM
作为升级到 PHP 7 的替代方案,你可以考虑保留 PHP 引擎并切换到 HHVM,这是 Facebook 创建的针对 PHP 的重构引擎。与 PHP 7 一样,HHVM 比 PHP 5.6 快得多,对于高流量应用程序,它可以显著降低托管高流量应用程序的相关成本。然而,与 PHP 7 不同的是,HHVM 不支持你可能习惯的所有 PHP 模块。此外,虽然 Yii2 完全兼容 HHVM,但第三方 Composer 包可能不兼容,如果不进行彻底测试,可能会出现问题。有关 HHVM 的更多信息,请参阅docs.hhvm.com/manual/en/index.php的 HHVM 文档。
安全考虑
当使用 Yii2 时,重要的是要记住遵循安全最佳实践,以确保应用程序、运行它们的服务器、我们收集的数据以及将此信息托付于我们的最终用户的安全。在之前的章节中,我们探讨了如何使用yii\base\Security类安全地加密和散列数据,以及如何使用 Bcrypt 等哈希算法来保护密码。在本节中,我们将介绍一些在构建我们的应用程序时可以应用的其他安全最佳实践。
证书
在几乎每个 Yii2 将提供后端支持的应用程序中,我们的客户端(浏览器或本地客户端)将通过 HTTP(超文本传输协议)与我们的应用程序通信。确保我们的客户端从客户端提交的信息以相同的状态到达我们的服务器的一个简单方法是通过 TLS(传输层安全性)协议传输由受信任的证书颁发机构签名的证书来加密客户端和服务器之间的流量。
备注
TLS(传输层安全性)是 SSL(安全套接字层)的后继者,两者通常统称为 SSL 证书。截至 2014 年,所有版本的 SSL(1.0、2.0 和 3.0)由于 SSL 协议本身存在的已知安全问题而被弃用。其继任者,TLS 1.1 和 1.2 版本不受影响,并且在通过 HTTP 在客户端和服务器之间加密数据时是推荐的协议。
将已签名和受信任的证书添加到我们的服务器具有几个主要优点:
-
在传输过程中加密数据可以防止第三方查看和操纵数据。健康信息、信用卡信息、用户名和密码都可以通过在传输过程中加密数据来得到保护。
-
客户端可以锁定我们发布的证书,这样他们就知道只有当我们的证书与他们锁定的证书匹配时才与我们通信。这防止了中间人攻击(MITM)并阻止他人了解我们的数据。此外,当使用锁定证书时,我们的客户端将知道不要与冒充我们的服务器通信。再次,这保护了我们和我们的用户。
-
搜索引擎如 Google 和 Bing 会给使用 TLS 的网站更高的排名
-
在我们的网络服务器中实施 TLS 是一个简单的任务,在现代计算机上,它几乎不产生开销
当实施 TLS 时,您可以使用多种资源来确定最安全的加密套件,并验证您的配置是否安全。例如,cipherli.st网站提供了一系列现代加密套件,适用于各种网络服务器和配置。Qualys 的 SSL Labs 网站(www.ssllabs.com/ssltest/)也可以为您提供完整的 TLS 配置报告,并验证您的网络服务器配置。结合这些工具,可以帮助更好地保护您的应用程序和基础设施。
Cookie
当使用yii\web\Request和yii\web\Response从 cookie 中检索数据时,Yii2 会自动使用您的 cookie 验证密钥对您的 cookie 信息进行加密:
return [
// [...],
'components' => [
// [...]
'request' => [
'cookieValidationKey' => '<your secret key here>',
],
],
];
当处理 cookie 和会话 cookie 时,我们可以通过向我们的 cookie 添加额外的属性来采取额外的保护措施,例如yii\web\Cookie::$secure和yii\web\Cookie::$httpOnly。通过将我们的 cookie 标记为secure,我们可以确保我们的 cookie 只通过安全连接发送,如前所述。此外,通过将我们的 cookie 设置为httpOnly,我们可以确保 JavaScript 和其他网络脚本语言无法读取我们的 cookie。通过配置这两个标志,我们可以显著提高我们应用程序的安全性。
防止跨站脚本攻击
作为网络开发的一般规则,每次我们展示由最终用户提交的信息时,我们都应该对其进行编码,以便保护我们的网站和用户免受跨站脚本(XSS)攻击。XSS 发生在用户提交的数据,当显示在我们的页面上时,可以被我们的浏览器解释。这可能是一些无害的操作,例如在我们的标记中添加<em>或<b>标签,或者可能是更危险的操作,例如注入一个<script>标签来跟踪用户信息或将他们重定向到另一个网站。幸运的是,Yii2 提供了两种处理最终用户提交数据并显示的方法。
我们可以用来保护我们的网站免受 XSS 攻击的第一种方法是通过使用yii\helpers\Html::encode()方法对最终用户数据进行编码,如下面的示例所示:
<?php echo \yii\helpers\Html::encode($data); ?>
当使用这种方法对数据进行编码时,Yii2 会将<和>等标签转换为现代浏览器知道如何显示和解释的 HTML 编码实体。
在我们确实希望将最终用户数据以 HTML 形式显示的情况下,我们可以使用yii\web\HtmlPurifier::purify()来正确解析我们想要的数据,同时不允许注入 JavaScript 代码:
<?php \yii\helpers\HtmlPurifier::process($longData);
注意
即使配置得当,HtmlPurifier 也可能非常慢。在部署代码之前,请确保您正确理解和配置 HtmlPurifier,因为它可能会显著影响您应用程序的性能。有关如何在 Yii2 中配置 HtmlPurifier 的更多信息,请参阅www.yiiframework.com/doc-2.0/yii-helpers-htmlpurifier.html,HtmlPurifier 的完整文档可以在htmlpurifier.org/找到。
启用跨站请求伪造保护
CSRF(跨站请求伪造)是许多网站处理的一种常见漏洞,Yii2 可以帮助我们保护自己免受其侵害。在处理客户端请求时,我们通常假设请求来自用户本人。然而,使用 JavaScript,我们可以在用户不知情的情况下在后台发送虚假请求。这些请求可能很简单,比如在用户不知情的情况下将其从特定服务中注销,或者抓取有关用户信息的特定页面并将其传输到恶意服务器。Yii2 自动保护我们免受 CSRF 攻击。您唯一可以执行的其他保护措施是遵循 HTTP 规范(例如,不允许在 GET 请求上更改状态)。
小贴士
注意,可能会有很多次出于各种原因需要禁用 CSRF。在我们的控制器中,我们可以通过在动作中添加以下代码来为特定动作禁用 CSRF:将Yii::$app->controller->enableCsrfValidation设置为false。
摘要
在本章中,我们介绍了多种提高和探索我们应用程序性能的方法,并学习了如何提高我们应用程序的安全性。我们探讨了如何使用数据、页面、片段、HTTP、数据库和模式缓存来提高我们应用程序的性能。我们还发现了我们可以对 Yii2 和 PHP 进行的一般改进,以使我们的应用程序运行得更快。最后,我们发现了通过使用证书、启用某些 cookie 属性以及保护我们的网站免受 XSS 和 CSRF 攻击的几种方法来提高我们应用程序安全性的几种方法。
在我们的最后一章中,我们将介绍如何使用 Yii2 加快我们已有的快速开发时间,学习如何通过日志记录探索我们的应用程序,并发现快速且安全的方法来部署我们的应用程序,几乎不会出现停机或服务中断。
第十三章:调试和部署
在处理现代 Web 应用程序时,最重要的任务之一是确定在应用程序的开发和运行过程中出了什么问题。如果不了解出了什么问题,就无法确定纠正问题的正确步骤。Yii2 提供了几个工具和组件,使我们的应用程序调试变得轻松简单。在本章中,我们将探讨几种不同的调试应用程序的方法。我们还将概述完成开发后部署 Yii2 应用程序的一些最佳实践。
调试
调试是一个重要的过程,通过这个过程我们可以发现我们的应用程序出了什么问题。无论我们是解决本地的问题还是试图在生产环境中识别问题,我们的应用程序都需要配置为提供所需的信息,以便快速有效地识别和解决出现的问题。在本节中,我们将介绍如何在我们的应用程序中启用日志记录,如何基准测试代码的某些部分和处理错误,以及一般的调试工具和指南。
日志记录
为了帮助我们调试应用程序,Yii2 内置了几个不同的日志组件和日志方法,我们可以在应用程序中实现。要开始使用 Yii2 的日志记录,我们首先需要在应用程序中实现一个日志组件。Yii2 提供了几个不同的组件,我们可以将它们一起或独立地实现。
| 日志类 | 描述 | 类参考 |
|---|---|---|
yii\log\DbTarget |
将信息记录到数据库表 | www.yiiframework.com/doc-2.0/yii-log-dbtarget.html |
yii\log\EmailTarget |
在记录事件时,向指定的电子邮件地址发送电子邮件 | www.yiiframework.com/doc-2.0/yii-log-emailtarget.html |
yii\log\FileTarget |
将事件记录到文件 | www.yiiframework.com/doc-2.0/yii-log-filetarget.html |
yii\log\SyslogTarget |
使用 PHP 的syslog()函数记录事件 |
www.yiiframework.com/doc-2.0/yii-log-syslogtarget.html |
小贴士
列出的每个日志记录器在其配置上都有细微的差异。有关如何具体配置每个日志目标的更多信息,请参阅该日志记录器类的类参考。
要在应用程序中启用日志目标,我们首先需要引导日志组件,然后在应用程序配置的组件部分指定我们想要使用的日志记录器目标,如下例所示:
return [
// [...],
'bootstrap' => ['log'],
// [...],
'components' => [
// [...],
'log' => [
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
]
],
],
],
];
小贴士
在前面的示例中,我们通过自身启用了yii\log\FileTarget以处理任何错误和警告日志事件。请注意,可以通过在targets数组中指定额外的记录器来同时启用多个记录器。
每个日志目标都可以配置为监听某些事件。Yii2 提供了五个不同的事件,我们可以将它们记录到,以及几个可以添加到我们代码中的日志方法:
-
错误:当发生常规错误或致命错误时,由
Yii:error()触发。这些类型的事件应立即处理,因为它们表明应用程序中存在失败。 -
警告:这是由
Yii::warning()触发的。这些事件表明应用程序中出现了问题。 -
信息:这是由
Yii::info()触发的。通常,这些事件用于记录发生的有用或有趣的事情。 -
跟踪:这是由
Yii::trace()触发的,通常在开发期间用于跟踪特定的代码片段。 -
性能分析:这是由
Yii::beginProfile()和Yii::endProfile()触发的。
注意
大多数这些方法只是Yii::log()的包装器。
每个日志目标都可以通过指定该日志目标对象的level属性来配置它监听特定的事件集,默认情况下,如果未指定level属性,Yii2 将处理任何严重性的消息。
每个日志方法具有类似的方法签名:
function($message, $category='application')
Yii2 的日志方法将通过yii\helpers\VarDumper::export()方法允许字符串和复杂的数据对象或数组。在记录信息时,指定一个类别非常重要,因为这个类别可以在我们的日志中进行搜索和过滤。正如方法签名所示,Yii2 默认将信息记录到application类别。在指定类别时,通常最好以分层的方式指定,例如以类似斜杠的格式:
app\components\MyEvent
另一种有效的格式是使用 PHP 魔法方法__METHOD__,它将返回调用日志记录器的命名空间和方法:
app\components\MyEvent::myMethod
在我们的记录器组件中,我们可以通过指定categories参数来指定我们希望记录器处理的类别。类别参数可以配置为监听特定的类别,如yii\db\Connection,但它也可以使用通配符进行配置。例如,如果我们想在yii\db中的任何类别被调用时发送电子邮件,我们可以配置以下记录器目标:
return [
// [...],
'bootstrap' => ['log'],
// [...],
'components' => [
// [...],
'log' => [
'targets' => [
[
'class' => 'yii\log\EmailTarget',
'categories' => ['yii\db\*'],
'message' => [
'from' => ['systems@example.com'],
'to' => ['administrator@example.com'],
'subject' => 'Database errors for example.com',
],
]
],
],
],
];
小贴士
如果你决定使用电子邮件日志记录,你可能会迅速收到多个消息,甚至可能被你的电子邮件提供商限制速率。强烈建议你只为电子邮件日志记录指定最关键的类别。
在我们需要记录多个类别的情况下,例如yii\web\HttpException,我们还可以通过指定except属性来排除某些类别不被记录。例如,如果我们想记录所有非 HTTP 404 异常,我们可以按照以下方式配置我们的记录器以实现这一点:
return [
// [...],
'bootstrap' => ['log'],
// [...],
'components' => [
// [...],
'log' => [
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning', 'info'],
'categories' => ['yii\web\HttpException:*'],
'except' => [
'yii\web\HttpException:404',
],
]
],
],
],
];
最后,在我们的应用程序中,可以通过设置日志目标对象的 enabled 属性来开启或关闭记录器。要程序化地禁用日志目标,我们首先需要指定我们的日志目标键:
return [
// [...],
'bootstrap' => ['log'],
// [...],
'components' => [
// [...],
'log' => [
'targets' => [
'file' => [
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning', 'info'],
'categories' => ['yii\web\HttpException:*'],
'except' => [
'yii\web\HttpException:404',
],
]
],
],
],
];
然后,在我们的代码中,我们可以使用以下代码临时禁用之前示例中指定的 file 目标:
Yii::$app->log->targets['file']->enabled = false;
基准测试
我们还可以使用分析器工具来调试我们的应用程序。分析器工具允许我们了解特定代码片段执行所需的时间。要使用分析器,我们只需将以下代码块包裹我们要检查的代码即可:
Yii::beginProfile('myProfile');
// Code inside this will be profiled
Yii::endProfile('myProfile');
小贴士
beginProfile() 和 endProfile() 方法可以嵌套在其他分析器部分中。这些方法内的代码将被输出到您的日志目标以进行分析。在生产环境中,您应该禁用分析。
错误处理
默认情况下,Yii2 具有一个相当全面的错误处理程序,它将自动捕获并显示所有非致命 PHP 错误。在开发过程中,错误处理程序可以是一个极其强大的工具,因为它可以在出现故障时提供完整的堆栈跟踪。

小贴士
默认情况下,错误处理程序作为我们应用程序的一部分自动启用,但可以通过在引导文件中将 YII_ENABLE_ERROR_HANDLER 常量设置为 false 来禁用。
错误处理程序配置在我们的主要应用程序配置文件中,并支持多个不同的配置选项,如下例所示:
return [
// [...],
'components' => [
// [...],
'errorHandler' => [
'maxSourceLines' => 20,
'errorAction' => 'site/error',
'maxTraceSourceLines' => 13,
// [...]
],
],
];
小贴士
关于错误处理及其属性的更多信息可以在 Yii2 类参考页面上找到,网址为 www.yiiframework.com/doc-2.0/yii-web-errorhandler.html。
默认情况下,错误处理程序将使用两个视图来显示错误:
-
@yii/views/errorHandler/error.php:此视图将用于显示不带调用堆栈的错误,并且是当YII_DEBUG设置为false时的默认视图。 -
@yii/views/errorHandler/exception.php:当错误显示完整的调用堆栈时将使用此视图。
我们可以通过指定错误处理程序的 errorView 和 exceptionView 属性来自定义错误视图文件。
作为默认错误页面的替代方案,如前一个屏幕截图所示,可以通过指定错误处理程序的 errorAction 属性将错误重定向到不同的操作。然后,我们可以通过向 actions() 方法添加错误操作并在指定的控制器中定义 actionError() 操作来从我们的应用程序中单独处理错误:
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
class SiteController extends Controller
{
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
];
}
public function actionError()
{
$exception = Yii::$app->errorHandler->exception;
if ($exception !== null) {
return $this->render('error', ['exception' => $exception]);
}
}
}
然后,我们可以在 views/site/error.php 文件中创建我们自定义的错误处理页面。
处理非 HTML 响应中的错误
当处理非 HTML 响应,如 JSON 或 XML 时,Yii2 将显示简化的错误响应,如下例所示:
{
"name": "Not Found Exception",
"message": "The requested resource was not found.",
"code": 0,
"status": 404
}
如果你想在非生产环境中显示更多调试信息,你可以通过覆盖响应组件的 on beforeSend 事件来创建一个自定义响应处理程序。我们的响应处理程序可以重写如下以实现此目的:
<?php
return [
// [...],
'components' => [
// [...],
'response' => [
'format' => yii\web\Response::FORMAT_JSON,
'charset' => 'UTF-8',
'on beforeSend' => ['app\components\ResponseEvent', 'beforeSend']
],
// [...]
]
];
我们位于 @app/components/ResponseEvent.php 的响应处理类可以编写如下,以更改当 YII_DEBUG 设置为 true 时的错误行为:
<?php
namespace app\components;
use Yii;
/**
* Event handler for response object
*/
class ResponseEvent extends yii\base\Event
{
/**
* Before Send event handler
* @param yii\base\Event $event
*/
public function beforeSend($event)
{
$response = $event->sender;
if (\Yii::$app->request->getIsOptions())
{
$response->statusCode = 200;
$response->data = null;
}
if ($response->data !== null)
{
$return = ($response->statusCode == 200 ? $response->data : $response->data['message']);
$response->data = [
'data' => $return
];
// Handle and display errors in the API for easy debugging
$exception = \Yii::$app->errorHandler->exception;
if ($exception && get_class($exception) !== "yii\web\HttpException" && !is_subclass_of($exception, 'yii\web\HttpException') && YII_DEBUG)
{
$response->data['success'] = false;
$response->data['exception'] = [
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString()
];
}
}
}
}
现在当发生错误时,将显示类似以下内容的输出,节省了我们切换浏览器和应用程序日志所需的时间:
{
"data": "<message>",
"success": false,
"exception": {
"message": "Invalid",
"file": "/path/to/SiteController.php",
"line": 48,
"trace": "#0 [internal function]: app\\controllers\\SiteController->actionIndex()\n# ... {main}"
}
}
使用 Yii2 调试扩展进行调试
我们可以用来调试应用程序的另一个强大工具是 yii2-debug 扩展。启用后,调试扩展提供了对请求每个方面的深入了解,从日志、配置、性能分析、请求、资产包,甚至我们的应用程序发送的电子邮件。使用此工具,我们可以确切地了解在特定请求期间发生了什么。
要开始使用 yii2-debug 扩展,我们首先需要将其作为我们的 composer 依赖项安装:
composer require --dev --prefer-dist yiisoft/yii2-debug
安装包并运行 composer update 后,我们可以通过向我们的 config/web.php 配置文件添加以下内容来配置调试扩展:
if (YII_DEBUG)
{
$config['bootstrap'][] = 'debug';
$config['modules']['debug'] = [
'class' => 'yii\debug\Module',
'allowedIPs' => ['*']
];
}
启用扩展后,我们将在应用程序的每个视图中底部看到它。

默认情况下,扩展将显示我们应用程序的一些基本信息;然而,如果我们点击它,我们可以深入了解针对特定请求的应用程序各个方面。

或者,我们可以导航到应用程序的 /debug 端点来查看扩展捕获的所有调试请求。

部署
与任何 Yii2 应用程序一起工作的最后一步是将它移至生产环境并创建一个部署策略。我们可以使用许多不同的工具来部署我们的代码,从 Bamboo、TravisCI、Jenkins、Capistrano,甚至手动 SSH 部署——仅举几例。
然而,一般来说,在部署我们的代码时,我们应该牢记几个关键概念:
-
部署应该是自动化的,无需人工干预。为了保持一致性,你的部署应该由一个工具或服务运行,该工具或服务可以每次运行相同的任务。这消除了部署过程中可能出现的任何人为错误,并确保了一致性。
-
部署应该是快速的,为你提供快速推出新功能和修复错误的能力。
-
您应用程序的实际构建(如合并和压缩 JavaScript、CSS 和其他配置)应该在构建服务器上完成,然后以预构建的方式推送到您的生产服务器。这确保了您的生产服务器上没有额外的工具,这些工具可能包含安全漏洞,同时也确保了每次构建项目时都使用相同的工具。
-
部署应该是可逆的。如果我们部署了代码并且我们的应用程序崩溃了,我们应该能够轻松地回滚到之前的版本。
-
在部署时,我们应该删除任何开发工具、脚本以及我们的 DCVS 仓库信息。这确保了如果我们的代码或我们的 Web 服务器中存在错误或安全漏洞,这些信息不会被泄露。
-
包含我们其他信息日志的目录(如
runtime)应该存储在持久目录中,然后将其符号链接回我们的项目。这确保了我们的日志和其他数据可以在多个部署之间持续存在。 -
我们的部署应该以这种方式结构化,以确保服务中断。通常,这是通过将我们的部署存储在特定的文件夹中,然后重命名或创建符号链接到我们的 Web 服务器指向的目录来实现的。这确保了当我们进行更改时,我们的网站不会出现中断。
-
在部署新代码时,我们应该清除任何应用程序特定的缓存,例如我们的模式缓存、配置缓存和 PHP OPCache,以确保我们的新代码更改生效。
-
配置文件绝不应该提交到我们的 DCVS 中,因为它们包含数据库用户名、密码和其他机密信息。考虑将这些数据存储在服务器上的环境变量中,或者以只有生产服务器才能解密和使用数据的方式加密它们。
通过遵循这些通用指南,我们可以确保我们的 Yii2 应用程序能够无缝且容易地部署。
摘要
在本章中,我们介绍了调试和部署我们应用程序的基础知识。我们介绍了如何设置日志和基准测试,以及如何使用yii2-debug扩展来调试我们的应用程序,同时详细介绍了通用的指南和一些我们可以用来将我们的应用程序部署到生产的工具。
如您所预期的那样,Yii2 的内容远不止本书所涵盖的。在开发 Yii2 应用程序时,请记住,位于www.yiiframework.com/doc-2.0/的 Yii2 API 文档提供了优秀的类参考文档,以及关于如何使用许多类的卓越文档。随着本书的结束,您应该对自己的 Yii2 知识和掌握感到自信,并且应该准备好用 Yii2 承担任何项目。





















浙公网安备 33010602011771号