Yii2-应用开发秘籍第三版-全-

Yii2 应用开发秘籍第三版(全)

原文:zh.annas-archive.org/md5/b352917fa64176429796f99b3e7702b7

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Yii 是一个免费的开源网络应用程序开发框架,用 PHP5 编写,它促进了干净的 DRY 设计,并鼓励快速开发。它旨在简化您的应用程序开发时间,并帮助确保最终产品极其高效、可扩展和易于维护。由于性能优化极高,Yii 是任何规模项目的完美选择。然而,它是以复杂的企业应用程序为设计理念的。您可以从头到尾(从表示到持久化)完全控制配置,以符合您的企业开发指南。它附带了一些工具,可以帮助测试和调试您的应用程序,并且具有清晰和全面的文档。

本书是 Yii2 菜谱的集合。每个菜谱都是一个完整且独立的条目,展示了来自真实网络应用程序的解决方案。因此,您可以在自己的环境中轻松复制它们,快速且无泪地学习 Yii2。所有菜谱都配有逐步的代码示例和清晰的截图。Yii2 像一件从货架上看起来很棒的西装,同时也非常容易根据您的需求定制。几乎框架的每个组件都是可扩展的。本书将展示如何使用官方扩展,扩展任何组件,或编写一个新的组件。

本书将帮助您快速创建现代网络应用程序,并确保它们通过真实生活中的示例和业务逻辑表现良好。您将处理 Yii 命令行、迁移和资产。您将了解基于角色的访问、安全和部署。我们将向您展示如何轻松开始,配置您的环境,并准备好高效快速地编写网络应用程序。

本书涵盖的内容

第一章, 基础,涵盖了如何安装 Yii 框架以及不同的安装方式。我们将向您介绍应用模板:基本和高级,以及它们之间的区别。然后您将了解依赖注入容器。本章包含有关模型事件的信息,这些事件在执行一些简单操作(如模型保存和更新等)后触发。我们将学习如何使用外部代码,包括在示例中使用 ZendFramework、Laravel 和 Sympony。我们还将学习如何逐步将基于 yii-1.x.x 的应用程序更新到 yii2。更多食谱可在www.packtpub.com/sites/default/files/downloads/4270OS_Chapter1.pdf找到。

第二章, 路由、控制器和视图,介绍了关于 Yii URL 路由器、控制器和视图的一些实用技巧。您将能够使您的控制器和视图更加灵活。

第三章,ActiveRecord、模型和数据库,讨论了在 Yii 中与数据库交互的三个主要方法:Active Record、查询构建器和通过 DAO 的直接 SQL 查询。这三个方法在语法、功能和性能方面都不同。在本章中,我们将学习如何高效地与数据库交互,何时使用模型以及何时不使用,如何处理多个数据库,如何自动预处理 Active Record 字段,以及如何使用强大的数据库条件。

第四章,表单,介绍了 Yii 如何使处理表单变得轻松,并且关于它的文档几乎已经完成。尽管如此,还有一些需要澄清和示例的区域。

第五章,安全,讨论了根据通用 Web 应用安全原则“过滤输入,转义输出”如何保持您的应用程序安全。我们将涵盖创建自己的控制器过滤器、防止 XSS、CSRF 和 SQL 注入、转义输出以及使用基于角色的访问控制等主题。

第六章,RESTful Web 服务,介绍了如何使用 Yii2 和内置功能编写 RESTful Web 服务。

第七章,官方扩展,解释了如何在项目中安装和使用官方扩展。您将学习如何编写自己的扩展并与其他开发者共享。

第八章,扩展 Yii,不仅涵盖了如何实现自己的 Yii 扩展,还涵盖了如何使您的扩展可重用并对社区有用。此外,我们将关注许多您应该做的事情,以便使您的扩展尽可能高效。

第九章,性能调优,教授了一些开发出能够平稳运行直到非常高的负载的应用程序的最佳实践。Yii 是其中最快的框架之一。然而,在开发和部署应用程序时,拥有一些额外的免费性能以及遵循应用程序本身的最佳实践是很好的。在本章中,我们将了解如何配置 Yii 以获得额外的性能。此外,我们还将学习一些开发出能够平稳运行直到非常高的负载的应用程序的最佳实践。

第十章,部署,涵盖了各种技巧,这些技巧在应用部署时特别有用,当你在团队中开发应用,或者只是想要使你的开发环境更加舒适时。

第十一章,测试,教我们如何使用最佳测试技术,如 Codeception、PhpUnit、Atoum 和 Behat。你将了解如何编写简单的测试以及如何避免在应用程序中产生回归错误。

第十二章,调试、日志记录和错误处理,讨论了审查日志、分析异常堆栈跟踪以及实现我们自己的错误处理器。如果应用相对复杂,那么创建一个无错误的程序是不可能的,因此开发者必须尽快检测错误并处理它们。Yii 提供了一套很好的实用功能来处理日志记录和错误处理。此外,在调试模式下,如果出现错误,Yii 会提供堆栈跟踪。使用它,你可以更快地修复错误。

本书所需

为了运行本书中的示例,需要以下软件:

  • 网络服务器

  • 数据库服务器

  • PHP

  • Yii2

本书面向对象

本书是为具有良好 PHP5 知识和 MVC 框架知识,并尝试使用 Yii 1.x.x 版本开发应用程序的开发者而编写的。对于那些想要尝试 Yii2,或者害怕从 Yii 1.x.x 迁移到 Yii2 的开发者来说,本书将非常有用。如果你还没有尝试过 Yii2,这本书绝对适合你!

部分

在本书中,你会发现一些频繁出现的标题(准备、如何操作...、工作原理...、更多内容...和相关信息)。

为了清楚地说明如何完成一个食谱,我们使用以下部分如下:

准备工作

本节告诉你在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。

如何操作…

本节包含遵循食谱所需的步骤。

工作原理…

本节通常包含对上一节发生情况的详细解释。

更多内容…

本节包含有关食谱的附加信息,以便使读者对食谱有更深入的了解。

相关内容

本节提供了对其他有用信息的链接,这些信息对食谱很有帮助。

术语约定

在本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“我们正在定义一个别名参数,该参数应在/page/之后在 URL 中指定。”

代码块设置如下:

'urlManager' => array(
    'enablePrettyUrl' => true,
    'showScriptName' => false,
),

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

'urlManager' => array(
 'enablePrettyUrl' => true,
 'showScriptName' => false,
),

任何命令行输入或输出都如下所示:

./yii migrate up

新术语重要单词以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中如下所示:“使用 Gii 生成启用了生成 ActiveQuery选项的Post模型,这将生成PostQuery类。”

注意

警告或重要注意事项以如下方式显示在框中。

小贴士

小贴士和技巧看起来像这样。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要向我们发送一般反馈,请简单地发送电子邮件至<feedback@packtpub.com>,并在邮件的主题中提及书籍的标题。

如果您在某个主题上有所专长,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,网址为www.packtpub.com/authors

客户支持

现在您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您的账户中下载本书的示例代码文件,网址为www.packtpub.com。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的支持标签上。

  3. 点击代码下载与勘误

  4. 搜索框中输入书籍的名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买这本书的地方。

  7. 点击代码下载

下载文件后,请确保您使用最新版本的软件解压缩或提取文件夹:

  • Windows 上的 WinRAR / 7-Zip

  • Mac 上的 Zipeg / iZip / UnRarX

  • Linux 上的 7-Zip / PeaZip

书籍的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Yii2-Application-Development-Cookbook-Third-Edition。我们还有其他来自我们丰富图书和视频目录的代码包可供选择,网址为github.com/PacktPublishing/。查看它们吧!

下载本书彩色图像

我们还为您提供了一个包含本书中使用的截图/图表的彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从 www.packtpub.com/sites/default/files/downloads/Yii2ApplicationDevelopmentCookbookThirdEdition_ColorImages.pdf 下载此文件。

错误清单

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这个问题,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您的错误详情来报告它们。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站或添加到该标题的错误清单部分。

要查看之前提交的错误清单,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将在错误清单部分显示。

盗版

在互联网上盗版受版权保护的材料是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上遇到任何形式的我们作品的非法副本,请立即向我们提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供疑似盗版材料的链接。

我们感谢您的帮助,以保护我们的作者和我们为您提供有价值内容的能力。

问题

如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。

第一章. 基础知识

在本章中,我们将涵盖以下主题:

  • 安装框架

  • 应用模板

  • 依赖注入容器

  • 服务定位器

  • 代码生成

  • 配置组件

  • 处理事件

  • 使用外部代码

简介

在本章中,我们将介绍如何安装 Yii 框架以及可能的安装技术。我们将向您介绍应用模板:基本和高级模板及其之间的区别。然后您将了解依赖注入容器。本章包含有关模型事件的信息,这些事件在执行某些操作(如模型保存、更新等)后触发。我们将学习如何使用外部代码,包括 ZendFramework、Laravel 或 Symfony。我们还将学习如何逐步将基于 yii-1.x.x 的应用程序更新到 yii2

安装框架

Yii2 是一个现代的 PHP 框架,以 Composer 包的形式提供。在本教程中,我们将通过 Composer 包管理器安装该框架,并配置应用程序的数据库连接。

准备工作

首先,在您的系统上安装 Composer 包管理器。

注意

注意:如果您在 Windows 上使用 OpenServer 应用程序,那么 composer 命令已经在 OpenServer 终端中存在。

在 Mac 或 Linux 系统中,从 getcomposer.org/download/ 下载安装程序,并使用以下命令全局安装:

sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer

在 Windows 系统中,从 getcomposer.org/doc/00-intro.md 页面下载并运行 Composer-Setup.exe

如果您没有系统管理员权限,那么作为替代方案,您可以只下载 getcomposer.org/composer.phar 原始文件,并使用 php composer.phar 调用代替单个的 composer 命令。

安装完成后,在您的终端中运行:

composer

或者(如果您只是下载了存档),其替代方案:

php composer.phar

当安装成功时,您将看到以下响应:

   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ '__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 1.2.0 2016-07-18 11:27:19

目前您可以从 packagist.org 仓库安装任何包。

如何操作…

您可以安装基本或高级应用模板。为了了解模板之间的区别,请参阅 应用模板 部分的说明。

注意

注意,在安装过程中,Composer 包管理器从 GitHub 网站获取大量信息。GitHub 可能会限制匿名用户的请求。在这种情况下,Composer 会要求您输入您的访问令牌。您只需注册 github.com 网站,并通过 github.com/blog/1509-personal-api-tokens 指南生成新的令牌即可。

安装基本项目模板

执行以下步骤以安装基本项目模板:

  1. 作为第一步,打开您的终端并安装 Bower-to-Composer 适配器:

    composer global require "fxp/composer-asset-plugin:¹.2.0"
    
    

    它提供了一种简单的方法,可以从 Bower 仓库加载相关的非 PHP 包(JavaScript 和 CSS)。

  2. 在新的 basic 目录中创建一个新的应用程序:

    composer create-project --prefer-dist yiisoft/yii2-app-basic basic
    
    
  3. 检查您的 PHP 是否包含所需的扩展:

    cd basic
    php requirements.php
    
    

    注意

    注意:命令行模式下的 PHP 和网页界面模式下的 PHP 可以使用不同的 php.ini 文件,具有不同的配置和不同的扩展。

  4. 创建一个新的数据库(如果您的项目需要),并在 config/db.php 文件中进行配置。

  5. 尝试通过以下控制台命令运行应用程序:

    php yii serve
    
    
  6. 通过 http://localhost:8080 地址检查浏览器中应用程序是否工作:安装基本项目模板

为了永久工作,在您的服务器(Apache、Nginx 等)上创建一个新的主机,并将 web 目录设置为该主机的文档根。

安装高级项目模板

执行以下步骤以安装高级项目模板:

  1. 作为第一步,在终端中安装 Bower-to-Composer 适配器:

    composer global require "fxp/composer-asset-plugin:¹.2.0"
    
    

    它提供了一种简单的方法,可以从 Bower 仓库加载相关的非 PHP 包(JavaScript 和 CSS)。

  2. 在新的 basic 目录中创建一个新的应用程序:

    composer create-project --prefer-dist yiisoft/yii2-app-advanced advanced
    
    
  3. 新应用程序尚未包含本地配置文件和 index.php 入口脚本。要生成这些文件,只需 init 一个工作环境:

    cd advanced
    php init
    
    

    在初始化过程中选择 开发 环境。

  4. 检查您的 PHP 是否包含所需的扩展:

    php requirements.php
    
    

    注意

    注意:命令行模式下的 PHP 和网页界面模式下的 PHP 可以使用不同的 php.ini 文件,具有不同的配置和不同的扩展。

  5. 创建一个新的数据库,并在生成的 common/config/main-local.php 文件中进行配置。

  6. 应用应用程序迁移:

    php yii migrate
    
    

    此命令将在您的数据库中自动创建一个 user 表。

  7. 尝试通过以下控制台命令运行前端应用程序:

    php yii serve --docroot=@frontend/web --port=8080
    
    

    然后,在另一个终端窗口中运行后端:

    php yii serve --docroot=@backend/web --port=8090
    
    
  8. 在浏览器中检查应用程序是否通过 http://localhost:8080http://localhost:8090 地址工作:安装高级项目模板

在您的服务器(Apache、Nginx 等)上创建两个新的主机用于后端和前端应用程序,并将 backend/webfrontend/web 目录设置为这些主机的文档根。

它是如何工作的…

首先,我们安装了 Composer 包管理器和 Bower 资产插件。

通过 composer create-project 命令安装应用程序后,该命令会自动创建一个新的空目录,克隆应用程序模板的源代码,并将所有内部依赖(框架和其他组件)加载到 vendor 子目录中。

如果需要,我们将初始化应用程序配置并设置一个新的数据库。

我们可以通过在控制台或浏览器模式下运行 requirements.php 脚本来检查系统要求。

在代码克隆之后,我们可以配置自己的 PHP 服务器,使其与 web 目录作为服务器的文档根一起工作。

参见

应用程序模板

Yii2 为开发提供了两个应用程序模板:基本和高级。基本和高级模板之间有什么区别?

名称令人困惑。有些人最终选择基本,因为高级可能听起来令人反感。在本章中,我们将探讨这些区别。

如何做到这一点...

请参考安装框架菜谱的如何做...部分,以了解和学习如何安装不同的模板。

它是如何工作的...

高级模板有一个自定义的配置系统。它是为了使团队能够共同工作在一个项目上,但每个开发者都可以为开发、测试和其他环境定制自己的配置。

配置环境可能很复杂,通常在单独开发时不会使用。

高级模板为 Web 应用程序的前端和后端部分分别提供了前端和后端文件夹。因此,你可以为每个文件夹配置一个单独的主机,从而隔离前端和后端部分。

这是一种将文件组织到目录中并配置 Web 服务器的简单方法。你可以在基本模板中轻松完成相同的事情。

前端/后端分离或用户管理本身并不是选择高级模板的好理由。更好的做法是将这些功能适应到你的应用程序中——你会学到更多,而且不会遇到困难的配置问题。

如果你将与团队一起在项目上工作,并且可能需要配置灵活性,请使用不同的环境进行开发,在这种情况下,更好的选择是使用高级应用程序模板。如果你将单独工作,并且你的项目很简单,你应该选择基本应用程序模板。

依赖注入容器

依赖倒置原则DIP)建议我们通过提取清晰的抽象子系统来创建模块化低耦合代码。

例如,如果你想简化一个大类,你可以将其拆分为许多常规代码块,并将每个块提取到一个新的简单分离的类中。

原则指出,你的低级块应该实现一个全面且清晰的抽象,而高级代码应该只与此抽象一起工作,而不是与低级实现一起工作。

当我们将一个大型的多任务类分割成小的专业类时,我们面临创建依赖对象并将它们注入到彼此中的问题。

如果我们之前能创建一个实例:

$service = new MyGiantSuperService();

在分割之后,我们将创建或获取所有依赖项并构建我们的服务:

$service = new MyService(
    new Repository(new PDO('dsn', 'username', 'password')),
    new Session(),
    new Mailer(new SmtpMailerTransport('username', 'password', host')),
    new Cache(new FileSystem('/tmp/cache')),
);

依赖注入容器是一个工厂,它允许我们不必关心构建我们的对象。在 Yii2 中,我们可以配置容器一次,并像这样使用它来检索我们的服务:

$service = Yii::$container->get('app\services\MyService')

我们也可以使用这个:

$service = Yii::createObject('app\services\MyService')

或者我们要求容器将其作为依赖项注入到其他服务的构造函数中:

use app\services\MyService;
class OtherService
{
    public function __construct(MyService $myService) { … }
}

当我们将获取OtherService实例时:

$otherService = Yii::createObject('app\services\OtherService')

在所有情况下,容器将解决所有依赖项,并在彼此之间注入依赖对象。

在配方中,我们创建带有存储子系统的购物车,并自动将购物车注入到控制器中。

准备工作

使用官方指南中描述的 Composer 包管理器创建新应用程序,官方指南链接为www.yiiframework.com/doc-2.0/guide-startinstallation.html

如何操作…

执行以下步骤:

  1. 创建购物车类:

    <?php
    namespace app\cart;
    
    use app\cart\storage\StorageInterface;
    
    class ShoppingCart
    {
        private $storage;
    
        private $_items = [];
    
        public function __construct(StorageInterface $storage)
        {
            $this->storage = $storage;
        }
    
        public function add($id, $amount)
        {
            $this->loadItems();
            if (array_key_exists($id, $this->_items)) {
                $this->_items[$id]['amount'] += $amount;
            } else {
                $this->_items[$id] = [
                    'id' => $id,
                    'amount' => $amount,
                ];
            }
            $this->saveItems();
        }
    
        public function remove($id)
        {
            $this->loadItems();
            $this->_items = array_diff_key($this->_items, [$id => []]);
            $this->saveItems();
        }
    
        public function clear()
        {
            $this->_items = [];
            $this->saveItems();
        }
    
        public function getItems()
        {
            $this->loadItems();
            return $this->_items;
        }
    
        private function loadItems()
        {
            $this->_items = $this->storage->load();
        }
    
        private function saveItems()
        {
            $this->storage->save($this->_items);
        }
    }
    
  2. 它只适用于自己的项目。它不会将内置存储项目到会话中,而是将这项责任委托给任何实现StorageInterface接口的外部存储类。

  3. 购物车类在其自己的构造函数中获取存储对象,将其实例保存到私有$storage字段中,并调用其load()save()方法。

  4. 定义一个具有所需方法的通用购物车存储接口:

    <?php
    namespace app\cart\storage;
    
    interface StorageInterface
    {
        /**
        * @return array of cart items
        */
        public function load();
    
        /**
        * @param array $items from cart
        */
        public function save(array $items);
    }
    
  5. 创建一个简单的存储实现。它将在服务器会话中存储所选项目:

    <?php
    namespace app\cart\storage;
    
    use yii\web\Session;
    
    class SessionStorage implements StorageInterface
    {
        private $session;
        private $key;
    
        public function __construct(Session $session, $key)
        {
            $this->key = $key;
            $this->session = $session;
        }
    
        public function load()
        {
            return $this->session->get($this->key, []);
        }
    
        public function save(array $items)
        {
            $this->session->set($this->key, $items);
        }
    }
    
  6. 存储在构造函数中获取任何框架会话实例,并在稍后用于检索和存储项目。

  7. config/web.php文件中配置ShoppingCart类及其依赖项:

    <?php
    use app\cart\storage\SessionStorage;
    
    Yii::$container->setSingleton('app\cart\ShoppingCart');
    
    Yii::$container->set('app\cart\storage\StorageInterface', function() {
        return new SessionStorage(Yii::$app->session, 'primary-cart');
    });
    
    $params = require(__DIR__ . '/params.php');
    
    //…
    
  8. 使用扩展构造函数创建购物车控制器:

    <?php
    namespace app\controllers;
    
    use app\cart\ShoppingCart;
    use app\models\CartAddForm;
    use Yii;
    use yii\data\ArrayDataProvider;
    use yii\filters\VerbFilter;
    use yii\web\Controller;
    
    class CartController extends Controller
    {
        private $cart;
    
        public function __construct($id, $module, ShoppingCart $cart, $config = [])
        {
            $this->cart = $cart;
            parent::__construct($id, $module, $config);
        }
    
        public function behaviors()
        {
            return [
                'verbs' => [
                    'class' => VerbFilter::className(),
                    'actions' => [
                        'delete' => ['post'],
                    ],
                ],
            ];
        }
    
        public function actionIndex()
        {
            $dataProvider = new ArrayDataProvider([
                'allModels' => $this->cart->getItems(),
            ]);
    
            return $this->render('index', [
                'dataProvider' => $dataProvider,
            ]);
        }
    
        public function actionAdd()
        {
            $form = new CartAddForm();
    
            if ($form->load(Yii::$app->request->post()) && $form->validate()) {
                $this->cart->add($form->productId, $form->amount);
                return $this->redirect(['index']);
            }
    
            return $this->render('add', [
                'model' => $form,
            ]);
        }
    
        public function actionDelete($id)
        {
            $this->cart->remove($id);
    
            return $this->redirect(['index']);
        }
    }
    
  9. 创建一个表单:

    <?php
    namespace app\models;
    
    use yii\base\Model;
    
    class CartAddForm extends Model
    {
        public $productId;
        public $amount;
    
        public function rules()
        {
            return [
                [['productId', 'amount'], 'required'],
                [['amount'], 'integer', 'min' => 1],
            ];
        }
    }
    
  10. 创建views/cart/index.php视图:

    <?php
    use yii\grid\ActionColumn;
    use yii\grid\GridView;
    use yii\grid\SerialColumn;
    use yii\helpers\Html;
    
    /* @var $this yii\web\View */
    /* @var $dataProvider yii\data\ArrayDataProvider */
    
    $this->title = 'Cart';
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div class="cart-index">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <p><?= Html::a('Add Item', ['add'], ['class' => 'btn btn-success']) ?></p>
    
        <?= GridView::widget([
            'dataProvider' => $dataProvider,
            'columns' => [
                ['class' => SerialColumn::className()],
    
                'id:text:Product ID',
                'amount:text:Amount',
    
                [
                    'class' => ActionColumn::className(),
                    'template' => '{delete}',
                ]
            ],
        ]) ?>
    </div>
    
  11. 创建views/cart/add.php视图:

    <?php
    use yii\helpers\Html;
    use yii\bootstrap\ActiveForm;
    
    /* @var $this yii\web\View */
    /* @var $form yii\bootstrap\ActiveForm */
    /* @var $model app\models\CartAddForm */
    
    $this->title = 'Add item';
    $this->params['breadcrumbs'][] = ['label' => 'Cart', 'url' => ['index']];
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div class="cart-add">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <?php $form = ActiveForm::begin(['id' => 'contact-form']); ?>
            <?= $form->field($model, 'productId') ?>
            <?= $form->field($model, 'amount') ?>
            <div class="form-group">
                <?= Html::submitButton('Add', ['class' => 'btn btn-primary']) ?>
            </div>
        <?php ActiveForm::end(); ?>
    </div>
    
  12. 将链接项目添加到主菜单中:

    ['label' => 'Home', 'url' => ['/site/index']],
    ['label' => 'Cart', 'url' => ['/cart/index']],
    ['label' => 'About', 'url' => ['/site/about']],
    // …
    
  13. 打开购物车页面并尝试添加行:如何操作…

它是如何工作的…

在这种情况下,我们有一个主要的ShoppingCart类,它具有低级依赖项,由抽象接口定义:

class ShoppingCart
{
    public function __construct(StorageInterface $storage) { … }
}

interface StorageInterface
{
   public function load();
   public function save(array $items);
}

并且我们有一些抽象的实现:

class SessionStorage implements StorageInterface
{
    public function __construct(Session $session, $key) { … }
}

目前,我们可以像这样手动创建购物车实例:

$storage = new SessionStorage(Yii::$app->session, 'primary-cart');
$cart = new ShoppingCart($storage)

它允许我们创建许多不同的实现,例如SessionStorageCookieStorageDbStorage。我们可以在不同的项目和不同的框架中使用框架无关的ShoppingCart类和StorageInterface接口。我们只需为所需的框架实现具有接口方法的存储类即可。

但是,我们不必手动创建具有所有依赖项的实例,我们可以使用依赖注入容器。

默认情况下,容器解析所有类的构造函数,并递归地创建所有所需的实例。例如,如果我们有四个类:

class A {
     public function __construct(B $b, C $c) { … }
}

class B {
    ...
}

class C {
    public function __construct(D $d) { … }
}

class D {
    ...
}

我们可以通过两种方式检索类A的实例:

$a = Yii::$container->get('app\services\A')
// or
$a = Yii::createObject('app\services\A')

容器自动创建BDCA类的实例,并将它们相互注入。

在我们的案例中,我们将购物车实例标记为单例:

Yii::$container->setSingleton('app\cart\ShoppingCart');

这意味着容器将为每个重复的调用返回单个实例,而不是反复创建购物车。

此外,我们的ShoppingCart在其构造函数中具有StorageInterface类型,容器知道必须为该类型实例化哪个类。我们必须像这样手动将类绑定到接口:

Yii::$container->set('app\cart\storage\StorageInterface', 'app\cart\storage\CustomStorage',);

但我们的SessionStorage类具有非标准构造函数:

class SessionStorage implements StorageInterface
{
    public function __construct(Session $session, $key) { … }
}

因此我们使用匿名函数手动创建实例:

Yii::$container->set('app\cart\storage\StorageInterface', function() {
    return new SessionStorage(Yii::$app->session, 'primary-cart');
});

在所有这些之后,我们可以在自己的控制器、小部件和其他地方手动从容器中检索购物车对象:

$cart = Yii::createObject('app\cart\ShoppingCart')

但每个控制器和其他对象将通过框架内部的createObject方法创建。并且我们可以通过控制器构造函数注入购物车:

class CartController extends Controller
{
    private $cart;

    public function __construct($id, $module, ShoppingCart $cart, $config = [])
    {
        $this->cart = $cart;
        parent::__construct($id, $module, $config);
    }

    // ...
}

使用这个注入的购物车对象:

public function actionDelete($id)
{
    $this->cart->remove($id);
    return $this->redirect(['index']);
}

参见

服务定位器

我们不是手动创建不同共享服务(应用程序组件)的实例,而是可以从一个特殊的全局对象中获取它们,该对象包含所有组件的配置和实例。

服务定位器是一个全局对象,其中包含一个组件或定义的列表,每个组件或定义都有一个唯一的 ID,并允许我们通过其 ID 检索任何所需的实例。定位器在第一次调用时即时创建组件的单个实例,并在后续调用中返回之前创建的实例。

在这个菜谱中,我们将创建一个购物车组件,并将编写一个用于处理它的购物车控制器。

准备工作

使用 Composer 包管理器创建一个新的应用程序,如官方指南中所述www.yiiframework.com/doc-2.0/guide-start-installation.html

如何操作…

执行以下步骤以创建购物车组件:

  1. 创建一个购物车组件。它将在用户会话中存储所选项目:

    <?php
    namespace app\components;
    
    use Yii;
    use yii\base\Component;
    
    class ShoppingCart extends Component
    {
        public $sessionKey = 'cart';
    
        private $_items = [];
    
        public function add($id, $amount)
        {
            $this->loadItems();
            if (array_key_exists($id, $this->_items)) {
                $this->_items[$id]['amount'] += $amount;
            } else {
                $this->_items[$id] = [
                    'id' => $id,
                    'amount' => $amount,
                ];
            }
           $this->saveItems();
        }
    
        public function remove($id)
        {
            $this->loadItems();
            $this->_items = array_diff_key($this->_items, [$id => []]);
            $this->saveItems();
        }
    
        public function clear()
        {
            $this->_items = [];
            $this->saveItems();
        }
    
        public function getItems()
        {
            $this->loadItems();
            return $this->_items;
        }
    
        private function loadItems()
        {
            $this->_items = Yii::$app->session->get($this->sessionKey, []);
        }
    
        private function saveItems()
        {
            Yii::$app->session->set($this->sessionKey, $this->_items);
        }
    }
    
  2. config/web.php文件中将ShoppingCart注册到服务定位器中,作为应用程序组件:

    'components' => [
        …
        'cart => [
            'class' => 'app\components\ShoppingCart',
            'sessionKey' => 'primary-cart',
        ],
    ]
    
  3. 创建购物车控制器:

    <?php
    namespace app\controllers;
    
    use app\models\CartAddForm;
    use Yii;
    use yii\data\ArrayDataProvider;
    use yii\filters\VerbFilter;
    use yii\web\Controller;
    
    class CartController extends Controller
    {
        public function behaviors()
        {
            return [
                'verbs' => [
                    'class' => VerbFilter::className(),
                    'actions' => [
                        'delete' => ['post'],
                    ],
                ],
            ];
        }
    
        public function actionIndex()
        {
            $dataProvider = new ArrayDataProvider([
                'allModels' => Yii::$app->cart->getItems(),
            ]);
    
            return $this->render('index', [
                'dataProvider' => $dataProvider,
            ]);
        }
    
        public function actionAdd()
        {
            $form = new CartAddForm();
    
            if ($form->load(Yii::$app->request->post()) && $form->validate()) {
                Yii::$app->cart->add($form->productId, $form->amount);
                return $this->redirect(['index']);
            }
    
            return $this->render('add', [
                'model' => $form,
            ]);
        }
    
        public function actionDelete($id)
        {
            Yii::$app->cart->remove($id);
    
            return $this->redirect(['index']);
        }
    }
    
  4. 创建一个表单:

    <?php
    namespace app\models;
    
    use yii\base\Model;
    
    class CartAddForm extends Model
    {
        public $productId;
        public $amount;
    
        public function rules()
        {
            return [
                [['productId', 'amount'], 'required'],
                [['amount'], 'integer', 'min' => 1],
            ];
        }
    }
    
  5. 创建views/cart/index.php视图:

    <?php
    use yii\grid\ActionColumn;
    use yii\grid\GridView;
    use yii\grid\SerialColumn;
    use yii\helpers\Html;
    
    /* @var $this yii\web\View */
    /* @var $dataProvider yii\data\ArrayDataProvider */
    
    $this->title = 'Cart';
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div class="site-contact">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <p><?= Html::a('Add Item', ['add'], ['class' => 'btn btn-success']) ?></p>
    
        <?= GridView::widget([
            'dataProvider' => $dataProvider,
            'columns' => [
                ['class' => SerialColumn::className()],
    
                'id:text:Product ID',
                'amount:text:Amount',
    
                [
                   'class' => ActionColumn::className(),
                   'template' => '{delete}',
                ]
            ],
        ]) ?>
    </div>
    
  6. 创建views/cart/add.php视图:

    <?php
    use yii\helpers\Html;
    use yii\bootstrap\ActiveForm;
    
    /* @var $this yii\web\View */
    /* @var $form yii\bootstrap\ActiveForm */
    /* @var $model app\models\CartAddForm */
    
    $this->title = 'Add item';
    $this->params['breadcrumbs'][] = ['label' => 'Cart', 'url' => ['index']];
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div class="site-contact">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <?php $form = ActiveForm::begin(['id' => 'contact-form']); ?>
            <?= $form->field($model, 'productId') ?>
            <?= $form->field($model, 'amount') ?>
            <div class="form-group">
                <?= Html::submitButton('Add', ['class' => 'btn btn-primary']) ?>
            </div>
        <?php ActiveForm::end(); ?>
    </div>
    
  7. 在主菜单中添加一个链接项:

    ['label' => 'Home', 'url' => ['/site/index']],
    ['label' => 'Cart', 'url' => ['/cart/index']],
    ['label' => 'About', 'url' => ['/site/about']],
    // …
    
  8. 打开购物车页面并尝试添加行:如何操作…

它是如何工作的…

首先,我们创建了自己的类,并公开了 sessionKey 选项:

<?php
namespace app\components;
use yii\base\Component;

class ShoppingCart extends Component
{
    public $sessionKey = 'cart';

    // …
}

其次,我们将组件定义添加到配置文件的 components 部分:

'components' => [
    …
    'cart => [
        'class' => 'app\components\ShoppingCart',
        'sessionKey' => 'primary-cart',
    ],
]

目前我们可以通过两种方式检索组件实例:

$cart = Yii::$app->cart;
$cart = Yii::$app->get('cart');

我们可以使用这个对象在我们的控制器、小部件和其他地方。

当我们调用任何组件,如 cart

Yii::$app->cart

我们在 Yii::$app 静态变量中调用 Application 类实例的虚拟属性。但是,yii\base\Application 类扩展了 yii\base\Module 类,该类通过 __get 魔法方法扩展了 yii\di\ServiceLocator 类。这个魔法方法只是调用 yii\di\ServiceLocator 类的 get() 方法:

namespace yii\di;

class ServiceLocator extends Component
{
    private $_components = [];
    private $_definitions = [];

    public function __get($name)
    {
        if ($this->has($name)) {
            return $this->get($name);
        } else {
            return parent::__get($name);
        }
    }
    // …
}

因此,它是直接通过 get 方法调用服务的一个替代方案:

Yii::$app->get('cart);

当我们从服务定位器的 get 方法获取组件时,定位器在其 _definitions 列表中查找所需定义,如果成功,它将根据定义动态创建一个新对象,将其注册在其自己的完整实例列表 _components 中,并返回该对象。

如果我们获取某个组件,通过定位器乘以定位器将始终返回之前保存的实例:

$cart1 = Yii::$app->cart;
$cart2 = Yii::$app->cart;
var_dump($cart1 === $cart2); // bool(true)

它允许我们使用共享的单个购物车实例 Yii::$app->cart 或单个数据库连接 Yii::$app->db,而不是每次都从头开始创建一个大型集合。

参见

代码生成

Yii2 提供了强大的模块 Gii,用于生成模型、控制器和视图,你可以轻松地进行修改和定制。这是一个真正有助于快速开发的工具。

在本节中,我们将探讨如何使用 Gii 生成代码。例如,你有一个名为 film 的数据库表,你希望为这个表创建一个具有 CRUD 操作的应用程序。这很简单。

准备工作

  1. 按照官方指南使用 composer 创建一个新的应用程序 www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. dev.mysql.com/doc/index-other.html 下载 Sakila 数据库。

  3. 执行下载的 SQL 文件:首先执行模式,然后执行数据。

  4. config/main.php 中配置数据库连接以使用 Sakila 数据库。

  5. 通过 ./yii serve 运行你的 web 服务器。

如何做…

  1. 访问 http://localhost:8080/index.php?r=gii 并选择 模型生成器

  2. 表名 填写为 actor,将 模型类 填写为 Actor,然后在页面底部点击 生成 按钮。如何做…

  3. 通过点击页眉上的 yii 代码生成器 标志返回主 Gii 菜单,并选择 CRUD 生成器

  4. 模型类 字段中填写 app\models\Actor,在 控制器类 中填写 app\controllers\ActorController。![如何操作…]

  5. 在页面底部点击 预览 按钮,然后点击绿色按钮 生成

  6. 通过 http://localhost:8080/index.php?actor/create 检查结果。![如何操作…]

如何工作…

如果你检查你的项目结构,你会看到自动生成的代码:

如何工作…

首先,我们创建了一个 Actor 模型。Gii 会自动创建所有模型规则,这些规则依赖于 mysql 字段类型。例如,如果你的 MySQL actor 表的字段 first_namelast_nameIS NOT NULL 标志,那么 Yii 会自动创建一个 required 规则,并设置最大长度为 45 个符号,因为在我们数据库中,这个字段的长度设置为 45

public function rules()
{
    return [
        [['first_name', 'last_name'], 'required'],
        [['last_update'], 'safe'],
        [['first_name', 'last_name'], 'string', 'max' => 45],
    ];
}

此外,Yii 会根据你添加到数据库的外键自动创建模型之间的关系。在我们的例子中,创建了两个自动关系。

public function getFilmActors()
{
    return $this->hasMany(FilmActor::className(), ['actor_id' => 'actor_id']);
}

public function getFilms()
{
    return $this->hasMany(Film::className(), ['film_id' => 'film_id'])->viaTable('film_actor', ['actor_id' => 'actor_id']);
}

由于我们的数据库中有两个外键,因此创建了这种关系。film_actor 表有外键 fk_film_actor_actor,它指向 actor 表的字段 actor_id,以及 fk_film_actor_film,它指向 film 表的字段 film_id

注意,你还没有生成 FilmActor 模型。所以如果你要开发全功能应用而不是演示,你必须生成 FilmFilmActor 模型。对于其他部分,请参阅 www.yiiframework.com/doc-2.0/guide-start-gii.html

配置组件

Yii 是一个非常可定制的框架。此外,正如所有可定制代码一样,应该有一个方便的方式来设置不同的应用程序部分。在 Yii 中,这是通过位于 config 的配置文件来提供的。

准备工作

通过官方指南中描述的 Composer 包管理器创建一个新的应用程序,请参阅 www.yiiframework.com/doc-2.0/guide-startinstallation.html

如何操作…

如果你之前使用过 Yii,那么你可能已经配置了一个数据库连接:

return [
    …
    'components' => [
        'db' => [
            'class' => 'system.db.CDbConnection',
            'dsn' => 'mysql:host=localhost;dbname=database_name',
            'username' => 'root',
            'password' => '',
            'charset' => 'utf8',
        ],
        …
    ],
    …
];

当你想要在应用程序的所有部分使用一个组件时,会使用这种方式来配置组件。使用前面的配置,你可以通过其名称访问一个组件,例如 Yii::$app->db

如何工作…

当你第一次直接使用或通过 Active Record 模型使用 Yii::$app->db 组件时,Yii 会创建一个组件,并使用应用程序配置文件 components 部分下 db 数组中提供的对应值初始化其公共属性。在上面的代码中,dsn 值将被分配给 yii\db\Connection::dsnusername 将被分配给 Connection::username,依此类推。

如果您想了解charset的含义或想知道您可以在db组件中配置什么,那么您需要知道它的类。在db组件的情况下,类是yii\db\Connection。您可以打开这个类并查找其公共属性,您可以从配置中设置这些属性。

在前面的代码中,class属性有点特殊,因为它用于指定组件类名。它不存在于yii\db\Connection类中。因此,它可以用来覆盖类,如下所示:

return [
    …
    'components' => [
        'db' => [
            'class' => app\components\MyConnection',
            …
        ],
        …
    ],
     …
);

这样,您可以覆盖每个应用程序组件;当标准组件不适合您的应用程序时,这非常有用。

内置组件

现在,让我们找出您可以配置哪些标准的 Yii 应用程序组件。Yii 捆绑了两种应用程序类型:

  • Web 应用程序(yii\webApplication

  • 控制台应用程序(yii\console\Application

两者都扩展自yii\base\Application,因此控制台和 Web 应用程序共享其组件。

您可以从coreComponents()应用程序方法的源代码中获取组件名称。

您可以通过添加新的配置项并将它们的类属性指向您的自定义类来简单地添加您自己的应用程序组件(从yii\base\Component扩展的类)。

参见

与事件一起工作

Yii 的事件提供了一个简单的实现,它允许您监听和订阅在您的 Web 应用程序中发生的各种事件。例如,您可能希望在发布新材料时向每位读者发送有关新文章的通知。

准备工作

  1. 按照官方指南在www.yiiframework.com/doc-2.0/guide-start-installation.html中使用 Composer 包管理器创建一个新的应用程序。

  2. 在您的服务器上执行以下 SQL 代码以创建article表:

    CREATE TABLE 'article' (
        'id' int(11) NOT NULL AUTO_INCREMENT,
        'name' varchar(255) DEFAULT NULL,
        'description' text,
        PRIMARY KEY ('id')
    ) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8;
    
  3. 使用 Gii 生成Article模型。

  4. 使用./yii serve命令运行您的 Web 服务器。

如何做…

  1. \controllers\SiteController中添加一个测试操作:

    public function actionTest()
    {
        $article = new Article();
        $article->name = 'Valentine\'s Day\'s coming? Aw crap! I forgot to get a girlfriend again!';
        $article->description = 'Bender is angry at Fry for dating a robot. Stay away from our women.
        You've got metal fever, boy. Metal fever';
    
        // $event is an object of yii\base\Event or a child class
        $article->on(ActiveRecord::EVENT_AFTER_INSERT, function($event) {
            $followers = ['john2@teleworm.us', 'shivawhite@cuvox.de', 'kate@dayrep.com' ];
            foreach($followers as $follower) {
                Yii::$app->mailer->compose()
                    ->setFrom('techblog@teleworm.us')
                    ->setTo($follower)
                    ->setSubject($event->sender->name)
                    ->setTextBody($event->sender->description)
                    ->send();
            }
            echo 'Emails has been sent';
        });
    
        if (!$article->save()) {
            echo VarDumper::dumpAsString($article->getErrors());
        };
    }
    
  2. 使用以下代码更新config/web.php组件mailer

    'mailer' => [
        'class' => 'yii\swiftmailer\Mailer',
        'useFileTransport' => false,
    ],
    
  3. 在您的浏览器中运行此 URL:http://localhost:8080/index.php?r=site/test

  4. 也请检查http://www.fakemailgenerator.com/inbox/teleworm.u``s/john2/如何做…

它是如何工作的…

我们已经创建了一个Article模型,并为Article模型添加了一个处理ActiveRecord::EVENT_AFTER_INSERT事件的处理器。这意味着每次我们保存一篇新文章时,都会触发一个事件,并且我们的附加处理器将被调用。

在现实世界中,我们希望在每次发布新文章时通知我们的博客关注者。在一个真实的应用程序中,我们会有一个followeruser表,以及不同的博客部分,而不仅仅是单个博客。在这个例子中,在保存我们的模型后,我们通知我们的关注者john2@teleworm.usshivawhite@cuvox.dekate@dayrep.com。在最后一步中,我们只是证明用户已经收到了我们的通知,特别是john2。你可以用任何名字创建自己的事件。在这个例子中,我们使用了一个内置的事件,称为ActiveRecord::EVENT_AFTER_INSERT,它在每次向数据库插入后都会被调用。

例如,我们可以创建自己的事件。只需添加一个新的actionTestNew,代码如下:

public function actionTestNew()
{
    $article = new Article();
    $article->name = 'Valentine\'s Day\'s coming? Aw crap! I forgot to get a girlfriend again!';
    $article->description = 'Bender is angry at Fry for dating a robot. Stay away from our women.
    You've got metal fever, boy. Metal fever';

    // $event is an object of yii\base\Event or a child class
    $article->on(Article::EVENT_OUR_CUSTOM_EVENT, function($event) {
        $followers = ['john2@teleworm.us', 'shivawhite@cuvox.de', 'kate@dayrep.com' ];
        foreach($followers as $follower) {
            Yii::$app->mailer->compose()
                ->setFrom('techblog@teleworm.us')
                ->setTo($follower)
                ->setSubject($event->sender->name)
                ->setTextBody($event->sender->description)
                ->send();
        }
        echo 'Emails have been sent';
    });

    if ($article->save()) {
        $article->trigger(Article::EVENT_OUR_CUSTOM_EVENT);
    }
}

还要将EVENT_OUR_CUSTOM_EVENT常量添加到models/Article中,如下所示:

class Article extends \yii\db\ActiveRecord
{
    CONST EVENT_OUR_CUSTOM_EVENT = 'eventOurCustomEvent';
…
}

运行http://localhost:8080/index.php?r=site/test-new

你应该看到相同的结果,并且所有通知都会再次发送给关注者。主要区别是我们使用了自定义的事件名称。

保存后,我们已经触发了我们的事件。事件可以通过调用yii\base\Component::trigger()方法来触发。该方法需要一个事件名称,以及可选的事件对象,该对象描述了要传递给事件处理器的参数。

参见

更多关于事件的信息,请参阅www.yiiframework.com/doc-2.0/guide-concept-events.html

使用外部代码

软件包仓库、PSR 标准和社会编码为我们提供了大量高质量的可重用库和其他免费许可的组件。我们只需在项目中安装任何外部组件,而不是从头开始重新设计它们。这提高了开发性能,并使代码质量更高。

准备工作

按照官方指南使用 Composer 包管理器创建一个新应用程序,如www.yiiframework.com/doc-2.0/guide-start-installation.html中所述。

如何做到这一点…

在这个菜谱中,我们将尝试手动和通过 Composer 安装一些库。

通过 Composer 安装库

当你使用没有自增主键的 NoSQL 或其他数据库时,你必须手动生成唯一的标识符。例如,你可以使用通用唯一标识符UUID)而不是数字标识符。让我们来做这件事:

  1. 通过 Composer 安装github.com/ramsey/uuid组件:

    composer require ramsey/uuid
    
    
  2. 创建一个演示控制台控制器:

    <?php
    namespace app\commands;
    
    use Ramsey\Uuid\Uuid;
    use yii\console\Controller;
    
    class UuidController extends Controller
    {
        public function actionGenerate()
        {
            $this->stdout(Uuid::uuid4()->toString() . PHP_EOL);
            $this->stdout(Uuid::uuid4()->toString() . PHP_EOL);
            $this->stdout(Uuid::uuid4()->toString() . PHP_EOL);
            $this->stdout(Uuid::uuid4()->toString() . PHP_EOL);
            $this->stdout(Uuid::uuid4()->toString() . PHP_EOL);
        }
    }
    
  3. 然后运行它:

    ./yii uuid/generate
    
  4. 如果成功,你将看到以下输出:

    25841e6c-6060-4a81-8368-4d99aa3617dd
    fcac910a-a9dc-4760-8528-491c17591a26
    4d745da3-0a6c-47df-aee7-993a42ed915c
    0f3e6da5-88f1-4385-9334-b47d1801ca0f
    21a28940-c749-430d-908e-1893c52f1fe0
    
  5. 就这样!现在你可以在你的项目中使用Ramsey\Uuid\Uuid类。

手动安装库

当一个库以 Composer 包的形式提供时,我们可以自动安装它。在其他情况下,我们必须手动安装。

例如,创建一些库示例:

  1. 使用以下代码创建awesome/namespaced/Library.php文件:

    <?php
    namespace awesome\namespaced;
    
    class Library
    {
        public function method()
        {
            return 'I am an awesome library with namespace.';
        }
    }
    
  2. 创建old/OldLibrary.php文件:

    <?php
    class OldLibrary
    {
        function method()
        {
            return 'I am an old library without namespace.';
        }
    }
    
  3. 将一组函数作为一个old/functions.php文件创建:

    <?php
    function simpleFunction()
    {
        return 'I am a simple function.';
    }
    

    现在在我们的应用程序中设置此文件:

  4. config/web.php文件中的aliases部分定义awesome库命名空间根的新别名:

    $config = [
        'id' => 'basic',
        'basePath' => dirname(__DIR__),
        'bootstrap' => ['log'],
        'aliases' => [
            '@awesome' => '@app/awesome',
        ],
        'components' => [
            // …
        ],
        'params' => // …
    ];
    

    或者通过setAlias方法:

    Yii::setAlias('@awesome', '@app/awesome');
    
  5. config/web.php文件的顶部定义一个简单的类文件路径:

    Yii::$classMap['OldLibrary'] = '@old/OldLibrary.php';
    
  6. composer.json中配置functions.php文件的自动加载:

    "require-dev": {
        ...
    },
    "autoload": {
        "files": ["old/functions.php"]
    },
    "config": {
        ...
    },
    

    并应用更改:

    composer update
    
    
  7. 现在创建一个示例控制器:

    <?php
    namespace app\controllers;
    
    use yii\base\Controller;
    
    class LibraryController extends Controller
    {
        public function actionIndex()
        {
            $awesome = new \awesome\namespaced\Library();
            echo '<pre>' . $awesome->method() . '</pre>';
    
            $old = new \OldLibrary();
            echo '<pre>' . $old->method() . '</pre>';
    
            echo '<pre>' . simpleFunction() . '</pre>';
        }
    }
    

    并打开页面:

    手动安装库

在其他框架中使用 Yii2 代码

如果你想在其他框架中使用 Yii2 框架的代码,只需在composer.json中添加 Yii2 特定的参数:

{
    ...
    "extra": {
        "asset-installer-paths": {
            "npm-asset-library": "vendor/npm",
            "bower-asset-library": "vendor/bower"
        }
    }
}

并安装框架:

composer require yiisoft/yii2

现在打开你应用程序的入口脚本(在 ZendFramework、Laravel、Symfony 等),引入 Yii2 自动加载器,并创建 Yii 应用程序实例:

require(__DIR__ . '/../vendor/autoload.php');
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
$config = require(__DIR__ . '/../config/yii/web.php');
new yii\web\Application($config);

就这样!现在你可以使用 Yii::$app 实例、模型、小部件和其他从 Yii2 来的组件。

如何工作…

在第一种情况下,我们只需在我们的项目中安装一个新的 Composer 包并使用它,因为它的composer.json文件定义了所有关于autoloading库文件的方面。

但在第二种情况下,我们没有 Composer 包,而是手动在自动加载机制中注册了文件。在 Yii2 中,我们可以使用别名和Yii::$classMap来注册 PSR-4 命名空间根和单个文件。

但作为替代,我们可以为所有情况使用 Composer 自动加载器。只需在composer.json文件中定义一个扩展的autoload部分,如下所示:

"autoload": {
    "psr-0": { "": "old/" },
    "psr-4": {"awesome\\": "awesome/"},
    "files": ["old/functions.php"]
}

使用以下命令应用更改:

composer update

目前你可以从你的配置文件中移除别名和$classMap定义,并确保示例页面仍然可以正确工作:

如何工作…

此示例完全使用 Composer 的自动加载器而不是框架的自动加载器。

参见

第二章:路由、控制器和视图

在本章中,我们将介绍以下主题:

  • 配置 URL 规则

  • 生成 URL

  • 在 URL 规则中使用正则表达式

  • 使用基本控制器

  • 使用独立动作

  • 创建自定义过滤器

  • 显示静态页面

  • 使用闪存消息

  • 在视图中使用控制器上下文

  • 使用部分重用视图

  • 使用块

  • 使用装饰器

  • 定义多个布局

  • 分页和排序数据

简介

本章将帮助你了解一些关于 Yii URL 路由器、控制器和视图的实用技巧。你将能够使你的控制器和视图更加灵活。

配置 URL 规则

在这个菜谱中,我们将学习如何配置 URL 规则。在我们开始之前,让我们设置一个应用程序。

准备工作

  1. 使用 Composer 包管理器创建一个新的应用程序,如官方指南www.yiiframework.com/doc-2.0/guide-start-installation.html中所述。

  2. 创建包含以下代码的@app/controllers/TestController.php控制器:

    <?php
    
    namespace app\controllers;
    
    use yii\helpers\Html;
    use yii\web\Controller;
    
    class TestController extends Controller
    {
        public function actionIndex()
        {
            return $this->renderContent(Html::tag('h2',
                'Index action'
            ));
        }
    
        public function actionPage($alias)
        {
            return $this->renderContent(Html::tag('h2',
                'Page is '. Html::encode($alias)
            ));
        }
    }
    

    这是我们要自定义 URL 的应用程序控制器。

  3. 配置你的应用程序服务器以使用干净的 URL。如果你使用 Apache 并开启了mod_rewriteAllowOverride,那么你应该在你的@web目录下的.htaccess文件中添加以下行:

    Options +FollowSymLinks
    IndexIgnore */*
    RewriteEngine on
    # if a directory or a file exists, use it directly
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    # otherwise forward it to index.php
    RewriteRule . index.php
    

如何做到这一点…

我们的网站应该在/home显示首页,在其他所有页面显示为/page/<alias_ here>。此外,/about应指向一个带有别名 about 的页面:

  1. @app/config/web.php中添加以下urlManager组件的配置:

    'components' => [
        // ..
        'urlManager' => [
            'enablePrettyUrl' => true,
            'rules' => [
                'home' => 'test/index',
                '<alias:about>' => 'test/page',
                'page/<alias>' => 'test/page',
            ]
        ],
        // ..
    ],
    

    保存你的更改后,你应该能够浏览以下 URL:

    ‰/home
    ‰/about
    ‰/page/about 
    /page/test
    
  2. 尝试运行/home URL,你将得到以下结果:如何做到这一点…

  3. 然后尝试运行/about页面:如何做到这一点…

它是如何工作的…

让我们回顾一下已经做了什么以及为什么它有效。我们将从第一条规则的最右边开始:

  'home' => 'test/index',

test/index究竟是什么?在 Yii 应用中,每个控制器及其动作都有对应的内部路由。内部路由的格式为moduleID/controllerID/actionID。例如,TestControlleractionPage方法对应于test/page路由。因此,为了获取控制器 ID,你应该去掉 Controller 后缀,并将首字母小写。为了获取动作 ID,你应该去掉动作方法名的前缀,同样,首字母也要小写。

现在,什么是首页?为了更好地理解它,我们需要至少表面地了解当我们使用不同的 URL 访问我们的应用程序时发生了什么。

当我们使用/home时,URL 路由器会从顶部开始逐个检查我们的规则,尝试将输入的 URL 与规则匹配。如果找到匹配项,则路由器从分配给该规则的内部分配中获取控制器及其动作并执行它。因此,/home是定义哪些 URL 将由其所属规则处理的 URL 模式。

还有更多...

您还可以使用特殊语法创建参数化规则。让我们回顾第三条规则:

'page/<alias>' => test/page',

在这里,我们定义了一个别名参数,该参数应在/page/之后指定在 URL 中。它可以几乎是任何东西,并且将作为$alias参数传递给以下:

TestController::actionPage($alias).

您可以为这样的参数定义一个模式。我们为第二个规则做了如下操作:

'<alias:about>' => test/page',

此处的别名应匹配about,否则规则将不会应用。

参见

参考以下链接以获取更多阅读材料:

生成 URL

Yii 不仅允许您将 URL 路由到不同的控制器动作,还可以通过指定适当的内部路由及其参数来生成 URL。这非常有用,因为在开发应用程序时,您可以专注于内部路由,而在上线前只需关注真实 URL。永远不要直接指定 URL,并确保您使用 Yii URL 工具集。它将允许您在不重写大量应用程序代码的情况下更改 URL。

准备工作

  1. 按照官方指南中的说明,使用 Composer 包管理器创建一个新的应用程序:www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 找到您的@app/config/web.php文件,并按以下方式替换规则数组:

    'urlManager' => array(
        'enablePrettyUrl' => true,
        'showScriptName' => false,
    ),
    
  3. 配置您的应用程序服务器以使用干净的 URL。如果您正在使用启用了mod_rewriteAllowOverride的 Apache,那么您应该在@app/web文件夹下的.htaccess文件中添加以下行:

    Options +FollowSymLinks
    IndexIgnore */*
    RewriteEngine on
    # if a directory or a file exists, use it directly
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    # otherwise forward it to index.php
    RewriteRule . index.php
    

如何做到这一点...

  1. 在您的@app/controllers目录下,创建BlogController,并在其中放置以下代码:

    <?php
    
    namespace app\controllers;
    use yii\web\Controller;
    
    class BlogController extends Controller
    {
    
        public function actionIndex()
        {
            return $this->render('index');
        }
    
        public function actionRssFeed($param)
        {
            return $this->renderContent('This is RSS feed for our blog and ' . $param);
        }
    
        public function actionArticle($alias)
        {
            return $this->renderContent('This is an article with alias ' . $alias);
        }
    
        public function actionList()
        {
            return $this->renderContent('Blog\'s articles here');
        }
    
        public function actionHiTech()
        {
            return $this->renderContent('Just a test of action which contains more than one words in the name') ;
        }
    }
    

    这是我们将要为它生成自定义 URL 的博客控制器。

  2. 在您的@app/controllers目录下,创建TestController,并在其中放置以下代码:

    <?php
    
    namespace app\controllers;
    use Yii;
    use yii\web\Controller;
    
    class TestController extends Controller
    {
    
        public function actionUrls()
        {
            return $this->render('urls');
        }
    
    }
    
  3. @app/views目录下,创建test目录和urls.php视图文件,并在其中放置以下代码:

    <?php
        use yii\helpers\Url;
        use yii\helpers\Html;
    ?>
    <h1>Generating URLs</h1>
    
    <h3>Generating a link with URL to <i>blog</i> controller and <i>article</i> action with alias as param</h3>
    <?= Html::a('Link Name', ['blog/article', 'alias' => 'someAlias']); ?>
    
    <h3>Current url</h3>
    <?=Url::to('')?>
    
    <h3>Current Controller, but you can specify an action</h3>
    <?=Url::toRoute(['view', 'id' => 'contact']);?>
    
    <h3>Current module, but you can specify controller and action</h3>
    <?= Url::toRoute('blog/article')?>
    
    <h3>An absolute route to blog/list </h3>
    <?= Url::toRoute('/blog/list')?>
    
    <h3> URL for <i>blog</i> controller and action <i>HiTech</i> </h3>
    <?= Url::toRoute('blog/hi-tech')?>
    
    <h3>Canonical URL for current page</h3>
    <?= Url::canonical()?>
    
    <h3>Getting a home URL</h3>
    <?= Url::home()?>
    
    <h3>Saving a URL of the current page and getting it for re-use</h3>
    <?php Url::remember()?>
    <?=Url::previous()?>
    
    <h3>Creating URL to <i>blog</i> controller and <i>rss-feed</i> action while URL helper isn't available</h3>
    <?=Yii::$app->urlManager->createUrl(['blog/rss-feed', 'param' => 'someParam'])?>
    
    <h3>Creating an absolute URL to <i>blog</i> controller and <i>rss-feed</i></h3>
    <p>It's very useful for emails and console applications</p>
    
    <?=Yii::$app->urlManager->createAbsoluteUrl(['blog/rss-feed', 'param' => 'someParam'])?>
    
  4. 前往 URL http://yii-book.app/test/urls,您将看到输出。(参考前面代码中的完整方法列表。)如何做到这一点…

它是如何工作的...

我们需要生成指向 BlogController 控制器动作(RssFeed、Article、List、HiTech)的 URL。

根据我们的需求,有不同的实现方式,但基本原理是相同的。让我们列出一些生成 URL 的方法。

内部路由是什么?每个控制器及其动作都有相应的路由。路由的格式是 moduleID/controllerID/actionID。例如,BlogControlleractionHiTech 方法对应于 blog/hi-tech 路由。

要获取控制器 ID,你应该去掉 Controller 后缀,并将首字母转换为小写。要获取动作 ID,你应该去掉动作前缀,并将每个单词的首字母转换为小写,并用连字符(-)分隔(例如,actionHiTech 将是 hi-tech)。

$_GET 变量是将传递给指定内部路由的动作的参数。例如,如果我们想创建一个指向 BlogController::action 文章的 URL,并将 $_GET['name'] 参数传递给它,可以这样做:

<?= Html::a('Link Name', ['blog/article', 'alias' => 'someAlias']); ?>

相对 URL 可以在你的应用程序中使用,而绝对 URL 应该用于指向网站外部的位置(例如其他网站)或用于链接到外部可访问的资源(如 RSS 源、电子邮件等)。

你可以使用 URL 管理器轻松实现。URL 管理器是一个名为 urlManager 的内置应用程序组件。你必须使用这个组件,它可以通过 Yii::$app->urlManager 从 Web 和控制台应用程序访问。

当你无法获取控制器实例时,例如,当你实现控制台应用程序时,你可以使用以下两种 urlManager 创建方法:

<?=Yii::$app->urlManager->createUrl(['blog/rss-feed', 'param' => 'someParam'])?>
<?=Yii::$app->urlManager->createAbsoluteUrl(['blog/rss-feed', 'param' => 'someParam'])?>

还有更多...

有关更多信息,请参阅以下 URL:

相关内容

  • 配置 URL 规则 菜谱

在 URL 规则中使用正则表达式

Yii URL 路由器的隐藏特性之一是你可以使用相当强大的正则表达式来处理字符串。

准备工作

  1. 使用 Composer 包管理器创建新应用程序,如官方指南中所述,请参阅 www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 在你的 @app/controllers 目录中,使用以下内容创建 PostController.php

    <?php
    
    namespace app\controllers;
    
    use yii\helpers\Html;
    use yii\web\Controller;
    
    class PostController extends Controller
    {
        public function actionView($alias)
        {
            return $this->renderContent(Html::tag('h2',
                'Showing post with alias ' . Html::encode($alias)
            ));
        }
    
        public function actionIndex($type = 'posts', $order = 'DESC')
       {
            return $this->renderContent(Html::tag('h2',
               'Showing ' . Html::encode($type) . ' ordered ' . Html::encode($order)
            ));
        }
    
        public function actionHello($name)
        {
            return $this->renderContent(Html::tag('h2',
                'Hello, ' . Html::encode($name) . '!'
            ));
        }
    }
    

    这是我们的应用程序控制器,我们将通过自定义 URL 来访问它。

  3. 配置您的应用程序服务器以使用干净的 URL。如果您使用 Apache 并开启了mod_rewriteAllowOverride,那么您应该在@web文件夹下的.htaccess文件中添加以下行:

    Options +FollowSymLinks
    IndexIgnore */*
    RewriteEngine on
    # if a directory or a file exists, use it directly
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    # otherwise forward it to index.php
    RewriteRule . index.php
    

如何操作…

我们希望我们的PostController操作能够根据某些指定的规则接受参数,并为所有不匹配的参数给出404 not found HTTP 响应。此外,post/index应该有一个别名 URL 为 archive。

将以下urlManager组件的配置添加到@app/config/web.php

'components' => [
    // ..
    'urlManager' => [
        'enablePrettyUrl' => true,
        'rules' => [
            'post/<alias:[-a-z]+>' => 'post/view',
            '<type:(archive|posts)>' => 'post/index',
            '<type:(archive|posts)>/<order:(DESC|ASC)>' => 'post/index',
            'sayhello/<name>' => 'post/hello',
        ]
    ],
    // ..
],

以下 URL 将成功:

  • http://yii-book.app/post/test

  • http://yii-book.app/posts

  • http://yii-book.app/archive

  • http://yii-book.app/posts/ASC

  • http://yii-book.app/sayhello

以下 URL 将失败:

  • http://yii-book.app/archive/test

  • http://yii-book.app/post/another_post

以下截图显示,URL http://yii-book.app/post/test 已成功运行:

如何操作…

以下截图显示,URL http://yii-book.app/archive 也已成功运行:

如何操作…

以下截图显示,URL http://yii-book.app/archive/test 没有成功运行并遇到了错误:

如何操作…

它是如何工作的…

您可以在参数定义和规则的其他部分使用正则表达式。让我们逐条阅读我们的规则:

'post/<alias:[-a-z]+>' => 'post/view',

别名参数应包含一个或多个英文字母或破折号。不允许使用其他符号。

'(posts|archive)' => 'post/index', 
'(posts|archive)/<order:(DESC|ASC)>' => 'post/index',

postsarchive都指向post/indexorder参数只能接受两个值——DESCASC

'sayhello/<name>' => 'post/hello',

您应该指定名称部分,但对允许使用的字符没有限制。请注意,无论使用哪种规则,开发者都不应假设输入数据是安全的。

它是如何工作的…

还有更多…

要了解更多关于正则表达式的信息,您可以使用以下资源:

参见

  • 配置 URL 规则配方

使用基控制器

在许多框架中,被其他控制器扩展的基控制器的概念在指南中就有描述。在 Yii 中,指南中没有提及,因为您可以通过许多其他方式实现灵活性。尽管如此,使用基控制器是可能的,并且可能是有用的。

假设我们想要添加一些只有当用户登录时才能访问的控制器。我们当然可以为每个控制器单独设置这个约束,但我们会以更好的方式来做。

准备工作

使用 Composer 包管理器创建一个新的应用程序,具体操作请参考官方指南中的www.yiiframework.com/doc-2.0/guide-startinstallation.html

如何操作…

  1. 首先,我们需要一个基础控制器,我们的用户控制器将使用它。让我们创建@app/components/BaseController.php,代码如下:

    <?php
    
    namespace app\components;
    
    use Yii;
    use yii\web\Controller;
    use yii\filters\AccessControl;
    
    class BaseController extends Controller
    {
        public function actions()
        {
            return [
                'error' => ['class' => 'yii\web\ErrorAction'],
            ];
        }
    
        public function behaviors()
        {
            return [
                'access' => [
                    'class' => AccessControl::className(),
                    'rules' => [
                        [
                            'allow' => true,
                            'actions' => 'error'
                        ],
                        [
                            'allow' => true,
                            'roles' => ['@'],
                        ],
                    ],
                ]
            ];
        }
    }
    

    此控制器有一个包含错误操作的 action 映射。

  2. 现在通过 Gii 创建TestController,但将基类字段的值设置为app/components/BaseController如何操作…

    您将得到以下类似的结果:

    <?php
    namespace app\controllers;
    class TestController extends \app\components\BaseController
    {
        public function actionIndex()
        {
            return $this->render('index');
        }
    }
    
  3. 现在,您的TestController只有在用户登录时才能访问,尽管我们没有在TestController类中明确声明。您可以通过在注销状态下访问http://yii-book.app/index.php?r=test/index来检查它。

它是如何工作的…

这个技巧不过是一个基本的类继承。如果TestController中没有找到过滤器或访问控制规则,那么它们将从SecureController中调用。

还有更多…

如果您需要扩展基础控制器的方法,请记住它不能被覆盖。例如,我们需要向控制器动作映射中添加一个页面操作:

<?php

namespace app\controllers;

use yii\helpers\ArrayHelper;
use app\components\BaseController;

class TestController extends BaseController
{
    public function actions()
    {
        return ArrayHelper::merge(parent::actions(), [
            'page' => [
                'class' => 'yii\web\ViewAction',
            ],
        ]);
    }

    public function behaviors()
    {
        $behaviors = parent::behaviors();

        $rules = $behaviors['access']['rules'];

        $rules = ArrayHelper::merge(
            $rules,
            [
                [
                    'allow' => true,
                    'actions' => ['page']
                ]
            ]
        );

        $behaviors['access']['rules'] = $rules;

        return $behaviors;
    }

    public function actionIndex()
    {
        return $this->render('index');
    }
}

如需更多信息,请参阅www.yiiframework.com/doc-2.0/yii-base-controller.html

使用独立操作

在 Yii 中,您可以定义控制器操作为单独的类,然后将它们连接到您的控制器。这将帮助您重用一些常用功能。

例如,您可以将自动完成字段的后端移动到操作中,并通过不必重复编写来节省时间。

另一个例子是,我们可以创建所有 CRUD 操作作为独立的独立操作。我们将编写、创建、查看和删除模型操作,以及查看模型列表操作。

准备工作

  1. 使用 Composer 包管理器创建一个新的应用程序,具体操作请参考官方指南中的www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 让我们创建post表。使用以下命令创建迁移:

    ./yii migrate/create create_post_table
    
  3. 按照以下方式更新刚刚创建的迁移的方法和导入的类列表:

    <?php
    
    use yii\db\Schema;
    use yii\db\Migration;
    
    class m150719_152435_create_post_table extends Migration
    {
        const TABLE_NAME = '{{%post}}';
    
        public function up()
        {
            $tableOptions = null;
            if ($this->db->driverName === 'mysql') {
                $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
            }
    
            $this->createTable(self::TABLE_NAME, [
                'id' => Schema::TYPE_PK,
                'title' => Schema::TYPE_STRING.'(255) NOT NULL',
                'content' => Schema::TYPE_TEXT.' NOT NULL',
            ], $tableOptions);
    
            for ($i = 1; $i < 7; $i++) {
                $this->insert(self::TABLE_NAME, [
                    'title' => 'Test article #'.$i,
                    'content' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '
                     .'Sed sit amet mauris est. Sed at dignissim dui. '
                     .'Phasellus arcu massa, facilisis a fringilla sit amet, '
                     .'rhoncus ut enim.',
                ]);
            }
        }
    
        public function down()
        {
            $this->dropTable(self::TABLE_NAME);
        }
    }
    
  4. 使用以下命令安装所有迁移:

    ./yii migrate up
    
  5. 使用 Gii 创建Post模型。

如何操作…

  1. 创建独立的操作@app/actions/CreateAction.php,代码如下:

    <?php
    
    namespace app\actions;
    
    use Yii;
    use yii\base\Action;
    
    class CreateAction extends Action
    {
        public $modelClass;
    
        public function run()
        {
            $model = new $this->modelClass();
    
            if ($model->load(Yii::$app->request->post()) && $model->save()) {
                $this->controller->redirect(['view', 'id' => $model->getPrimaryKey()]);
            } else {
                return $this->controller->render('//crud/create', [
                    'model' => $model
                ]);
            }
        }
    }
    
  2. 创建独立的操作@app/actions/DeleteAction.php,代码如下:

    <?php
    
    namespace app\actions;
    
    use yii\base\Action;
    use yii\web\NotFoundHttpException;
    
    class DeleteAction extends Action
    {
        public $modelClass;
    
        public function run($id)
        {
            $class = $this->modelClass;
    
            if (($model = $class::findOne($id)) === null) {
                throw new NotFoundHttpException('The requested page does not exist.');
            }
    
            $model->delete();
    
            return $this->controller->redirect(['index']);
        }
    }
    
  3. 创建独立的操作@app/actions/IndexAction.php,代码如下:

    <?php
    
    namespace app\actions;
    
    use yii\base\Action;
    use yii\data\Pagination;
    
    class IndexAction extends Action
    {
        public $modelClass;
        public $pageSize = 3;
    
        public function run()
        {
            $class = $this->modelClass;
            $query = $class::find();
            $countQuery = clone $query;
    
            $pages = new Pagination([
                'totalCount' => $countQuery->count(),
            ]);
            $pages->setPageSize($this->pageSize);
    
            $models = $query->offset($pages->offset)
                               ->limit($pages->limit)
                               ->all();
    
            return $this->controller->render('//crud/index', [
                'pages' => $pages,
                'models' => $models
            ]);
        }
    }
    
  4. 创建独立的操作@app/actions/ViewAction.php,代码如下:

    <?php
    
    namespace app\actions;
    
    use yii\base\Action;
    use yii\web\NotFoundHttpException;
    
    class ViewAction extends Action
    {
        public $modelClass;
    
        public function run($id)
        {
            $class = $this->modelClass;
    
            if (($model = $class::findOne($id)) === null) {
                 throw new NotFoundHttpException('The requested page does not exist.');
            }
    
             return $this->controller->render('//crud/view', [
                'model' => $model
             ]);
        }
    }
    
  5. 创建视图文件@app/views/crud/create.php,代码如下:

    <?php
    
    use yii\helpers\Html;
    use yii\widgets\ActiveForm;
    
    /*
    * @var yii\web\View $this
    */
    
    ?>
    <h1><?= Yii::t('app', 'Create post'); ?></h1>
    <?php $form = ActiveForm::begin();?>
    <?php $form->errorSummary($model); ?>
    
    <?= $form->field($model, 'title')->textInput() ?>
    <?= $form->field($model, 'content')->textarea() ?>
    
    <?= Html::submitButton(Yii::t('app', 'Create'), ['class' => 'btn btn-primary']) ?>
    
    <?php ActiveForm::end(); ?>
    
  6. 创建视图文件@app/views/crud/index.php,代码如下:

    <?php
    
    use yii\widgets\LinkPager;
    use yii\helpers\Html;
    use yii\helpers\Url;
    
    /*
    * @var yii\web\View $this
    * @var yii\data\Pagination $pages
    * @var array $models
    */
    
    ?>
    <h1>Posts</h1>
    <?= Html::a('+ Create a post', Url::toRoute('post/create')); ?>
    
    <?php foreach ($models as $model):?>
        <h3><?= Html::encode($model->title);?></h3>
        <p><?= Html::encode($model->content);?></p>
    
        <p>
            <?= Html::a('view', Url::toRoute(['post/view', 'id' => $model->id]));?> |
            <?= Html::a('delete', Url::toRoute(['post/delete', 'id' => $model->id]));?>
        </p>
    <?php endforeach; ?>
    
    <?= LinkPager::widget([
        'pagination' => $pages,
    ]); ?>
    
  7. 创建视图文件@app/views/crud/view.php,代码如下:

    <?php
    
    use yii\helpers\Html;
    use yii\helpers\Url;
    
    /*
    * @var yii\web\View $this
    * @var app\models\Post $model
    */
    
    ?>
    <p><?= Html::a('< back to posts', Url::toRoute('post/index')); ?></p>
    
    <h2><?= Html::encode($model->title);?></h2>
    <p><?= Html::encode($model->content);?></p>
    

    要使用独立动作,我们在动作映射中通过重写actions方法来声明它。

  8. 运行post/index如何做…

它是如何工作的…

每个控制器都可以由独立动作构建,就像拼图一样。不同之处在于你可以使独立动作非常灵活,并在许多地方重用它们。

在我们的动作中,我们定义了modelClass公共属性,这有助于在PostControlleractions方法中设置特定的模型类。

参见

对于更多信息,请参阅www.yiiframework.com/doc-2.0/guide-structure-controllers.html#standalone-actions

创建自定义过滤器

过滤器是在控制器动作之前和/或之后运行的对象。例如,一个访问控制过滤器可能在动作之前运行,以确保特定的最终用户可以访问它们;一个内容压缩过滤器可能在动作之后运行,在将响应内容发送给最终用户之前压缩它们。

过滤器可能由一个预过滤器(在动作之前应用的过滤逻辑)和一个/或后过滤器(在动作之后应用的逻辑)组成。过滤器本质上是一种特殊的行为。因此,使用过滤器与使用行为相同。

假设我们有一个网络应用程序,它只为指定的小时提供用户界面,例如,从上午 10 点到下午 6 点。

准备工作

使用 Composer 包管理器创建一个新的应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guide-start-installation.html

如何做…

  1. 创建一个控制器,@app/controllers/TestController.php,如下所示:

    <?php
    
    namespace app\controllers;
    
    use app\components\CustomFilter;
    use yii\helpers\Html;
    use yii\web\Controller;
    
    class TestController extends Controller
    {
        public function behaviors()
        {
            return [
                'access' => [
                    'class' => CustomFilter::className(),
                ],
            ];
        }
    
        public function actionIndex()
        {
            return $this->renderContent(Html::tag('h1',
                'This is a test content'
            ));
        }
    }
    
  2. 创建一个新的过滤器,@app/components/CustomFilter.php,如下所示:

    <?php
    namespace app\components;
    
    use Yii;
    use yii\base\ActionFilter;
    use yii\web\HttpException;
    
    class CustomFilter extends ActionFilter
    {
        const WORK_TIME_BEGIN = 10;
        const WORK_TIME_END = 18;
    
        protected function canBeDisplayed()
        {
            $hours = date('G');
    
            return $hours >= self::WORK_TIME_BEGIN && $hours <= self::WORK_TIME_END;
        }
    
        public function beforeAction($action)
        {
            if (!$this->canBeDisplayed())
            {
                $error = 'This part of website works from '
                        . self::WORK_TIME_BEGIN . ' to '
                        . self::WORK_TIME_END . ' hours.';
    
                throw new HttpException(403, $error);
            }
    
            return parent::beforeAction($action);
        }
    
        public function afterAction($action, $result)
        {
            if (Yii::$app->request->url == '/test/index') {
                Yii::trace("This is the index action");
            }
    
            return parent::afterAction($action, $result);
        }
    }
    
  3. 如果你在这个指定的时间段外访问了这个页面,你会得到以下内容:如何做…

它是如何工作的…

首先,我们在控制器中添加了一段代码,实现了我们的自定义过滤器:

public function behaviors()
{
    return [
        'access' => [
            'class' => CustomFilter::className(),
           ],
        ];
}

默认情况下,过滤器应用于控制器的所有动作,但我们可以指定要应用过滤器的动作,甚至可以排除过滤器中的动作。

在其中有两个动作——beforeActionafterActions。第一个在控制器动作之前运行,下一个在之后运行。

在我们的简单示例中,我们定义了一个条件,如果时间早于上午 10 点,则不允许访问网站,在after方法中,如果当前路径是test/index,我们只是运行一个跟踪方法。

你可以在调试器的log部分看到结果:

它是如何工作的…

在实际应用中,过滤器更复杂,而且 Yii2 提供了许多内置过滤器,例如核心、认证、内容协商、HTTP 缓存端等。

参见

对于更多信息,请参阅www.yiiframework.com/doc-2.0/guidestructure-filters.html

显示静态页面

如果你只有几个静态页面,并且不太经常更改它们,那么查询数据库和为它们实现页面管理就没什么必要了。

准备工作

使用 Composer 包管理器创建一个新的应用程序,如官方指南中所述www.yiiframework.com/doc-2.0/guide-startinstallation.html

如何操作…

  1. 创建测试控制器文件,@app/controllers/TestController.php,如下所示:

    <?php
    
    namespace app\controllers;
    
    use yii\web\Controller;
    
    class TestController extends Controller
    {
        public function actions()
        {
            return [
                'page' => [
                    'class' => 'yii\web\ViewAction',
                ]
            ];
        }
    }
    
  2. 现在,将你的页面放入views/test/pages,并命名为index.phpcontact.phpindex.php的内容如下:

    <h1>Index</h1>
    content of index file
    
    Contact.php content is:
    
    <h2>Contacts</h2>
    <p>Our contact: contact@localhost</p>
    
  3. 现在,你可以通过输入 URL 来检查你的页面,

  4. http://yii-book.app/index.php?r=test/page&view=contact:如何操作…

  5. 或者,如果你已经配置了路径格式的干净 URL,你可以输入 URL http://yii-book.app/test/page/view/about

它是如何工作的…

我们连接名为\yii\web\ViewAction的外部操作,它只是尝试找到与提供的$_GET参数相同的视图名称。如果存在,则显示它。如果不存在,则将显示一个404 not found页面。如果未设置viewParam,则使用defaultView值。

还有更多…

关于 ViewAction

有一些有用的\yii\web\ViewAction参数我们可以使用。这些列在下表中:

参数名称 描述
defaultView 当用户未提供yii\web\ViewAction::$viewParam GET 参数时使用的默认视图名称。默认为'index'。这应该采用与GET参数中给出的类似的path/to/view格式。
layout 要应用于请求视图的布局名称。在渲染视图之前,这将分配给yii\base\Controller::$layout。默认为 null,表示将使用控制器的布局。如果为 false,则不应用布局。
viewParam 包含请求视图名称的GET参数的名称。
viewPrefix 一个字符串,用于添加到用户指定的视图名称之前,以形成一个完整的视图名称。例如,如果用户请求tutorial/chap1,相应的视图名称将是pages/tutorial/chap1,假设前缀是 pages。实际的视图文件由yii\base\View::findViewFile()确定。

配置 URL 规则

ViewAction操作提供了一个最小化你的控制器的方法,但 URL 看起来像http://yii-book.app/index.php?r=test/page&page=about。为了使 URL 更短、更易读,请向urlManager组件添加 URL 规则:

'<view:about>' => 'test/page'

如果urlManager组件配置正确,你将得到以下结果:

配置 URL 规则

要配置urlManager组件,请参考配置 URL 规则配方。

相关内容

更多信息,请参考以下 URL:

使用闪存消息

当你正在使用表单编辑模型、删除模型或执行任何其他操作时,告诉用户操作是否成功或出现错误是很好的。通常,在编辑表单等某种操作之后,将发生重定向,我们需要在想要跳转的页面上显示一条消息。然而,我们如何从当前页面传递到重定向目标并之后清理它?Flash 消息将帮助我们完成这项任务。

准备工作

使用 Composer 包管理器创建一个新的应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guide-start-installation.html

如何操作…

  1. 让我们创建一个 @app/controllers/TestController.php 控制器,如下所示:

    <?php
    
    namespace app\controllers;
    
    use Yii;
    use yii\web\Controller;
    use yii\filters\AccessControl;
    
    class TestController extends Controller
    {
        public function behaviors()
        {
            return [
                'access' => [
                    'class' => AccessControl::className(),
                    'rules' => [
                        [
                            'allow' => true,
                            'roles' => ['@'],
                            'actions' => ['user']
                        ],
                        [
                            'allow' => true,
                            'roles' => ['?'],
                            'actions' => ['index', 'success', 'error']
                        ],
                    ],
                    'denyCallback' => function ($rule, $action) {
                        Yii::$app->session->setFlash('error', 
                        'This section is only for registered users.');
                        $this->redirect(['index']);
                    },
                ],
            ];
        }
    
        public function actionUser()
        {
            return $this->renderContent('user');
        }
    
        public function actionSuccess()
        {
            Yii::$app->session->setFlash('success', 'Everything went fine!');
            $this->redirect(['index']);
        }
    
        public function actionError()
        {
            Yii::$app->session->setFlash('error', 'Everything went wrong!');
            $this->redirect(['index']);
        }
    
        public function actionIndex()
        {
            return $this->render('index');
        }
    }
    
  2. 此外,创建 @app/views/common/alert.php 视图,如下所示:

    <?php
        use yii\bootstrap\Alert;
    ?>
    <?php if (Yii::$app->session->hasFlash('success')):?>
        <?= Alert::widget([
            'options' => ['class' => 'alert-success'],
            'body' => Yii::$app->session->getFlash('success'),
        ]);?>
    <?php endif ?>
    
    <?php if (Yii::$app->session->hasFlash('error')) :?>
        <?= Alert::widget([
            'options' => ['class' => 'alert-danger'],
            'body' => Yii::$app->session->getFlash('error'),
        ]);?>
    <?php endif; ?>
    
  3. 创建 @app/views/test/index.php 文件视图,如下所示:

    <?php
    
    /* @var $this yii\web\View */
    
    ?>
    
    <?= $this->render('//common/alert') ?>
    
    <h2>Guest page</h2>
    <p>There's a content of guest page</p>
    
  4. 创建 @app/views/test/user.php 文件视图,如下所示:

    <?php
    
    /* @var $this yii\web\View */
    
    ?>
    
    <?= $this->render('//common/alert') ?>
    
    <h2>User page</h2>
    <p>There's a content of user page</p>
    
  5. 现在,如果你访问 http://yii-book.app/index.php?r=test/success,你将被重定向到 http://yii-book.app/index.php?r=test/index,并显示一条成功消息如下:如何操作…

  6. 此外,如果你访问 http://yii-book.app/index.php?r=test/error,你将被重定向到相同的页面,但会显示一个错误消息。刷新 index 页面将隐藏该消息:如何操作…

  7. 然后尝试运行 http://yii-book.app/index.php?r=test/user。你将被重定向到 http://yii-book.app/index.php?r=test/index,并在 denyCallback 函数中显示一个错误信息:如何操作…

工作原理…

我们使用 Yii::$app->session->('success', 'Everything went fine!') 设置闪存消息。内部,它将消息保存到会话状态中,因此在最底层,我们的消息被保存在 $_SESSION 中,直到调用 Yii::$app->session->getFlash('success') 并删除 $_SESSION 键。

闪存消息在请求访问后将被自动删除。

更多内容…

getAllFlashes() 方法

有时你需要处理所有闪存。你可以简单地这样做,如下所示:

$flashes = Yii::$app->session->getAllFlashes();

<?php foreach ($flashes as $key => $message): ?>
    <?= Alert::widget([
        'options' => ['class' => 'alert-info'],
        'body' => $message,
    ]);
    ?>
<?php endforeach; ?>

removeAllFlashes() 方法

当你需要清除所有闪存时,使用以下方法:

Yii::$app->session->removeAllFlashes();

removeFlash() 方法

当你需要使用指定键移除 flash 方法时,使用以下方法:

Yii::$app->session->removeFlash('success');

在这个例子中,我们添加了一个非常有用的回调函数,它设置了一个错误消息并将重定向到 test/ind ex 页面。

相关内容

更多信息,请参考:

在视图中使用控制器上下文

Yii 视图非常强大,具有许多功能。其中之一是您可以在视图中使用控制器上下文。所以,让我们试试。

准备工作

使用 Composer 包管理器创建一个新的应用程序,如官方指南中所述 www.yiiframework.com/doc-2.0/guide-start-installation.html

如何操作...

  1. 创建一个 controllers/ViewController.php 文件,如下所示:

    <?php
    
    namespace app\controllers;
    
    use yii\web\Controller;
    
    class ViewController extends Controller
    {
        public $pageTitle;
    
        public function actionIndex()
        {
            $this->pageTitle = 'Controller context test';
    
            return $this->render('index');
        }
    
        public function hello()
        {
            if (!empty($_GET['name'])) {
                echo 'Hello, '  . $_GET['name'] . '!';
            }
        }
    }
    
  2. 现在,我们将创建一个 views/view.php 来展示我们可以做什么:

    <h1><?= $this->context->pageTitle ?></h1>
    <p>Hello call. <?php $this->context->hello() ?></p>
    
  3. 为了测试它,你可以按照 /index.php?r=view/index&name=Alex:如何操作...

它是如何工作的...

我们在视图中使用 $this 来引用当前正在运行的控制器。当这样做时,我们可以调用控制器方法并访问其属性。最有用的属性是 pageTitle,它指向当前页面的标题。在视图中有许多非常有用的内置方法,例如 renderPartials 和小部件。

还有更多...

www.yiiframework.com/doc-2.0/guide-structure-views.html#accessing-data-in-views URL 包含 CController 的 API 文档,您可以在其中找到您可以在视图中使用的良好方法列表。

使用部分重用视图

Yii 支持部分,所以如果您有一个逻辑不多的块想要重用或者想要实现电子邮件模板,部分是处理这个问题的正确方式。

假设我们有两个 Twitter 账户,一个用于我们的博客,另一个用于公司活动,我们的目标是输出指定页面上的 Twitter 时间线。

准备工作

  1. 使用 Composer 包管理器创建一个新的应用程序,如官方指南中所述 www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. twitter.com/settings/widgets/php_netyiiframework 用户创建 Twitter 小部件,并为每个创建的小部件找到一个 data-widget-id 值。

如何操作...

  1. 创建一个控制器,@app/controllers/BlogController.php,如下所示:

    <?php
    
    namespace app\controllers;
    
    use yii\web\Controller;
    
    class BlogController extends Controller
    {
        public function actionIndex()
        {
            $posts = [
                [
                    'title' => 'First post',
                    'content' => 'There\'s an example of reusing views with partials.',
                ],
                [
                    'title' => 'Second post',
                    'content' => 'We use twitter widget.'
                ],
            ];
    
            return $this->render('index', [
                'posts' => $posts
            ]);
        }
    }
    
  2. 创建一个名为 @app/views/common/twitter.php 的视图文件,并粘贴来自 Twitter 的嵌入代码。你将得到以下类似的内容:

    <?php
    
    /* @var $this \yii\web\View */
    /* @var $widget_id integer */
    /* @var $screen_name string */
    
    ?>
    <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
    
    <?php if ($widget_id && $screen_name): ?>
    <a class="twitter-timeline"
        data-widget-id="<?= $widget_id?>"
        href="https://twitter.com/<?= $screen_name?>"
        height="300">
        Tweets by @<?= $screen_name?>
    </a>
    <?php endif;?>
    
  3. 创建一个视图文件 @app/views/blog/index.php,如下所示:

    <?php
    
    /* @var $category string */
    /* @var $posts array */
    /* @var $this \yii\web\View */
    
    ?>
    
    <div class="row">
        <div class="col-xs-7">
            <h1>Posts</h1>
            <hr>
            <?php foreach ($posts as $post): ?>
                <h3><?= $post['title']?></h3>
                <p><?= $post['content']?></p>
            <?php endforeach;?>
        </div>
        <div class="col-xs-5">
            <?= $this->render('//common/twitter', [
                'widget_id' => '620531418213576704',
                'screen_name' => 'php_net',
            ]);?>
        </div>
    </div>
    
  4. @app/views/site/about.php 文件的内容替换为以下内容:

    <?php
    
    use yii\helpers\Html;
    /* @var $this yii\web\View */
    $this->title = 'About';
    ?>
    
    <div class="col-xs-7">
        <h1><?= Html::encode($this->title) ?></h1>
        <p>
            This is the About page. You may modify this page.
        </p>
    </div>
    <div class="col-xs-5">
        <?= $this->render('//common/twitter', [
            'widget_id' => '620526086343012352',
            'screen_name' => 'yiiframework'
        ]);?>
    </div>
    
  5. 尝试运行 index.php?r=blog/index:如何操作...

  6. 尝试运行 index.php?r=site/about:如何操作...

它是如何工作的...

在当前示例中,两个视图使用额外的参数渲染@app/views/common/twitter.php,以在自身内部形成 Twitter 小部件。请注意,视图可以在控制器、小部件或任何其他地方通过调用视图渲染方法进行渲染。例如,\yii\base\Controller::render\yii\base\View::render执行相同的模板处理,但前者不使用布局。

在每个视图文件中,我们可以使用$this访问两个 View 类的实例,因此任何视图文件都可以通过调用render方法在其他视图中渲染。

还有更多…

如需更多信息,请参阅www.yiiframework.com/doc-2.0/guidestructure-views.html#rendering-views

使用块

你可以在视图中使用的一个 Yii 特性是块。基本思想是你可以记录一些输出,然后稍后在视图中重用它。一个很好的例子是为你的布局定义额外的内容区域,并在其他地方填充它们。

在之前的版本中,Yii 1.1,块被称为剪辑。

准备工作

使用 Composer 包管理器创建一个新的应用程序,如官方指南中所述www.yiiframework.com/doc-2.0/guide-start-installation.html

如何做到这一点…

  1. 对于我们的示例,我们需要在布局中定义两个区域——beforeContentfooter

  2. 打开@app/views/layouts/main.php并在内容输出之前插入以下代码行:

    <?php if(!empty($this->blocks['beforeContent'])) echo $this->blocks['beforeContent']; ?>
    
  3. 然后,将页脚代码替换为以下代码:

    <footer class="footer">
        <div class="container">
            <?php if (!empty($this->blocks['footer'])):
                echo $this->blocks['footer'] ?>
            <?php else: ?>
               <p class="pull-left">&copy; My Company <?= date('Y') ?></p>
               <p class="pull-right"><?= Yii::powered() ?></p>
            <?php endif; ?>
        </div>
    </footer>
    
  4. 就这样!然后,向controllers/SiteController.php中添加一个新的操作,命名为blocks

    public function actionBlocks()
    {
        return $this->render('blocks');
    }
    
  5. 现在,创建一个视图文件,views/site/blocks.php,内容如下:

    <?php
    
    use \yii\Helpers\Html;
    
    /* @var $this \yii\web\View */
    ?>
    
    <?php $this->beginBlock('beforeContent');
        echo Html::tag('pre', 'Your IP is ' . Yii::$app->request->userIP);
    $this->endBlock(); ?>
    
    <?php $this->beginBlock('footer');
        echo Html::tag('h3', 'My custom footer block');
    $this->endBlock(); ?>
    
    <h1>Blocks usage example</h1>
    
  6. 现在,当你打开/index.php?r=site/blocks页面时,你应该在页面内容之前看到你的 IP 地址,以及在页脚的构建注释:如何做到这一点…

它是如何工作的…

我们使用代码标记区域,该代码仅检查特定块的存在,如果块存在,则输出它。然后,我们使用名为beginBlockendBlock的特殊控制器方法记录我们定义的块的内容。

从控制器中,你可以轻松通过$this->view->blocks['blockID']访问我们的块变量。

还有更多…

使用装饰器

在 Yii 中,我们可以将内容包围在装饰器中。装饰器的常见用法是布局。当你使用控制器中的render方法渲染视图时,Yii 会自动使用主布局装饰它。让我们创建一个简单的装饰器,它可以正确地格式化引号。

准备工作

使用官方指南中描述的 Composer 包管理器创建一个新的应用程序,指南链接为 www.yiiframework.com/doc-2.0/guide-start-installation.html

如何做…

  1. 首先,我们将创建一个装饰器文件,@app/views/decorators/quote.php:

    <div class="quote">
        <h2>&ldquo;<?= $content?>&rdquo;, <?= $author?></h2>
    </div>
    
  2. 现在,将 @app/views/site/index.php 的内容替换为以下代码:

    <?php
    
    use yii\widgets\ContentDecorator;
    
    /* @var */
    ?>
    
    <?php ContentDecorator::begin([
            'viewFile' => '@app/views/decorators/quote.php',
            'view' => $this,
            'params' => ['author' => 'S. Freud']
        ]
    );?>
    Time spent with cats is never wasted.
    <?php ContentDecorator::end();?>
    
  3. 现在,你的 主页 应该看起来像以下这样:如何做…

它是如何工作的…

装饰器相当简单。在 ContentDecorator::begin()ContentDecorator::end() 之间的所有内容都会渲染到 $content 变量中,并传递到装饰器模板中。然后,装饰器模板被渲染并插入到调用 ContentDecorator::end() 的位置。

我们可以使用 ContentDecorator::begin() 的第二个参数将额外的变量传递到装饰器模板中,例如我们为作者所做的。

注意,我们已经使用了 @app/views/decorators/quot e.php 作为视图路径。

参见

定义多个布局

大多数应用程序使用单个布局来展示所有视图。然而,在某些情况下,需要多个布局。例如,一个应用程序可以在不同的页面上使用不同的布局:博客使用两个额外的列,文章使用一个额外的列,而投资组合则没有额外的列。

准备工作

使用官方指南中描述的 Composer 包管理器创建一个新的应用程序,指南链接为 www.yiiframework.com/doc-2.0/guide-startinstallation.html

如何做…

  1. 在 views/layouts 中创建两个布局:blogarticles。博客将包含以下代码:

    <?php $this->beginContent('//layouts/main')?>
        <div>
            <?= $content ?>
        </div>
        <div class="sidebar tags">
            <ul>
                <li><a href="#php">PHP</a></li>
                <li><a href="#yii">Yii</a></li>
            </ul>
        </div>
        <div class="sidebar links">
            <ul>
                <li><a href="http://yiiframework.com/">
                    Yiiframework</a></li>
                <li><a href="http://php.net/">PHP</a></li>
            </ul>
        </div>
    <?php $this->endContent()?>
    
  2. 文章将包含以下代码:

    <?php
    
        /* @var $this yii\web\View */
    ?>
    
    <?php $this->beginContent('@app/views/layouts/main.php'); ?>
        <div class="container">
            <div class="col-xs-8">
                <?= $content ?>
            </div>
            <div class="col-xs-4">
                <h4>Table of contents</h4>
                <ol>
                    <li><a href="#intro">Introduction</a></li>
                    <li><a href="#quick-start">Quick start</a></li>
                    <li>..</li>
                </ol>
            </div>
        </div>
    <?php $this->endContent() ?>
    
  3. 创建一个视图文件,views/site/content.php,内容如下:

    <h1>Title</h1>
    <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
    
  4. 创建三个控制器,分别命名为 BlogControllerArticleControllerPortfolioController,所有三个控制器都包含索引操作。controllers/BlogController.php 文件的内容如下:

    <?php
    
    namespace app\controllers;
    
    use yii\web\Controller;
    
    class BlogController extends Controller
    {
        public $layout = 'blog';
    
        public function actionIndex()
        {
            return $this->render('//site/content');
        }
    }
    
  5. controllers/ArticleController.php 文件的内容如下:

    <?php
    
    namespace app\controllers;
    
    use yii\web\Controller;
    
    class ArticleController extends Controller
    {
        public $layout = 'articles';
    
        public function actionIndex()
        {
            return $this->render('//site/content');
        }
    }
    
  6. controllers/PortfolioController.php 文件的内容如下:

    <?php
    
    namespace app\controllers;
    
    use yii\web\Controller;
    
    class PortfolioController extends Controller
    {
        public function actionIndex()
        {
            return $this->render('//site/content');
        }
    }
    
  7. 现在尝试运行 http://yii-book.app/?r=blog/index:如何做…

  8. 然后尝试运行 http://yii-book.app/?r=article/index:如何做…

  9. 最后,尝试运行 http://yii-book.app/?r=portfolio/index:如何做…

它是如何工作的…

我们为博客和文章定义了两个额外的布局。因为我们不想从主布局中复制和粘贴公共部分,所以我们使用 $this->beginContent$this->endContent 应用额外的布局装饰器。

因此,我们使用在文章布局内部渲染的视图作为主布局的$content

参见

分页和排序数据

在最新的 Yii 版本中,焦点从直接使用 Active Record 转移到了网格、列表和数据提供者。尽管如此,有时直接使用 Active Record 会更好。让我们看看如何列出带有排序能力的分页 AR 记录。在本节中,我们希望创建一个电影列表,并按数据库中的某些属性对其进行排序。在我们的例子中,我们将按电影标题和租金属性对电影进行排序。

准备工作

  1. 使用官方指南中描述的 Composer 包管理器创建一个新的应用程序www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. dev.mysql.com/doc/index-other.html下载 Sakila 数据库。

  3. 执行下载的 SQL 文件;首先执行模式,然后执行数据。

  4. config/main.php中配置数据库连接以使用 Sakila 数据库。

  5. 使用 Gii 创建Film模型。

如何做到这一点…

  1. 首先,你需要创建@app/controllers/FilmController.php

    <?php
    
    namespace app\controllers;
    
    use app\models\Film;
    use yii\web\Controller;
    use yii\data\Pagination;
    use yii\data\Sort;
    
    class FilmController extends Controller
    {
        public function actionIndex()
        {
            $query = Film::find();
            $countQuery = clone $query;
            $pages = new Pagination(['totalCount' => $countQuery->count()]);
            $pages->pageSize = 5;
    
            $sort = new Sort([
                'attributes' => [
                    'title',
                    'rental_rate'
                ]
            ]);
    
            $models = $query->offset($pages->offset)
                ->limit($pages->limit)
                ->orderBy($sort->orders)
                ->all();
    
            return $this->render('index', [
                'models' => $models,
                'sort' => $sort,
                'pages' => $pages
            ]);
        }
    }
    
  2. 现在,让我们实现@app/views/film/index.php,如下所示:

    <?php
    
    use yii\widgets\LinkPager;
    
    /**
     * @var \app\models\Film $models
     * @var \yii\web\View $this
     * @var \yii\data\Pagination $pages
     * @var \yii\data\Sort $sort
     */
    
    ?>
    
    <h1>Films List</h1>
    
    <p><?=$sort->link('title')?> | <?=$sort->link('rental_rate')?></p>
    
    <?php foreach ($models as $model): ?>
        <div class="list-group">
            <h4 class="list-group-item-heading"> <?=$model->title ?>
                <label class="label label-default"> <?=$model->rental_rate ?>
                </label>
            </h4>
            <p class="list-group-item-text"><?=$model->description ?></p>
        </div>
    <?php endforeach ?>
    
    <?=LinkPager::widget([
        'pagination' => $pages
    ]); ?>
    
  3. 尝试加载http://yii-book.app/index.php?r=film/index。你应该得到一个带有允许按电影标题或租金排序的列表的分页和链接:如何做到这一点…

它是如何工作的…

首先,我们获取了总模型数,并通过将totalCount变量传递给我们的Pagination实例来初始化新的分页组件实例。然后,我们使用$pages->pageSize字段设置分页的页面大小。之后,我们为模型创建了一个排序器实例,指定了想要排序的模型属性,并通过调用orderBy并传递$sort->orders作为参数来应用查询的排序条件。然后,我们调用all()从数据库中获取记录。

到目前为止,我们有了模型列表、页面和用于链接分页的数据,以及我们用来生成排序链接的排序器。

在视图中,我们使用我们收集到的数据。首先,我们使用Sort::link方法生成链接。然后,我们列出模型。最后,使用LinkPager小部件,我们渲染分页控件。

参见

访问以下链接以获取有关分页和排序的更多信息:

第三章:ActiveRecord、模型和数据库

在本章中,我们将涵盖以下主题:

  • 从数据库获取数据

  • 定义和使用多个 DB 连接

  • 自定义 ActiveQuery 类

  • 使用 AR 事件方法处理模型字段

  • 自动化时间戳

  • 自动设置作者

  • 自动设置 slug

  • 事务

  • 复制和读写分离

  • 实现单表继承

简介

在本章中,您将学习如何高效地与数据库工作,何时使用模型,何时不使用模型,如何与多个数据库工作,如何自动预处理 Active Record 字段,如何使用事务,等等。

从数据库获取数据

今天,大多数应用程序都使用数据库。无论是小型网站还是社交网络,至少有一部分是由数据库驱动的。

Yii 介绍了三种方法来允许您与数据库工作。具体如下:

  • Active Record

  • 查询构建器

  • 通过 DAO 使用 SQL

我们将使用所有这些方法从filmfilm_actoractor表中获取数据,并在列表中显示。此外,我们还将比较执行时间和内存使用情况,以确定在哪些情况下应该使用这些方法。

准备工作

  1. 使用官方指南中描述的 Composer 包管理器创建一个新的应用程序,官方指南链接为www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. dev.mysql.com/doc/index-other.html下载 Sakila 数据库。

  3. 执行下载的 SQL 文件;首先执行模式,然后执行数据。

  4. config/main.php中配置 DB 连接以使用 Sakila 数据库。

  5. 使用 Gii 为 actor 和 film 表创建模型。

如何做…

  1. 创建app/controllers/DbController.php如下:

    <?php
    
    namespace app\controllers;
    
    use app\models\Actor;
    use Yii;
    use yii\db\Query;
    use yii\helpers\ArrayHelper;
    use yii\helpers\Html;
    use yii\web\Controller;
    
    /**
    * Class DbController
    * @package app\controllers
    */
    class DbController extends Controller
    {
        /**
        * Example of Active Record usage.
        *
        * @return string
        */
        public function actionAr()
        {
            $records = Actor::find()
                            ->joinWith('films')
                            ->orderBy('actor.first_name, actor.last_name, film.title')
                            ->all();
    
            return $this->renderRecords($records);
        }
    
        /**
        * Example of Query class usage.
        *
        * @return string
        */
        public function actionQuery()
        {
            $rows = (new Query())
                ->from('actor')
                ->innerJoin('film_actor', 'actor.actor_id=film_actor.actor_id')
                ->leftJoin('film', 'film.film_id=film_actor.film_id')
                ->orderBy('actor.first_name, actor.last_name, actor.actor_id, film.title')
                ->all();
    
            return $this->renderRows($rows);
        }
    
        /**
        * Example of SQL execution usage.
        *
        * @return string
        */
        public function actionSql()
        {
            $sql = 'SELECT *
                FROM actor a
                JOIN film_actor fa ON fa.actor_id = a.actor_id
                JOIN film f ON fa.film_id = f.film_id
                ORDER BY a.first_name, a.last_name, a.actor_id, f.title';
    
            $rows = Yii::$app->db->createCommand($sql)->queryAll();
    
            return $this->renderRows($rows);
        }
    
        /**
        * Render records for Active Record array.
        *
        * @param array $records
        *
        * @return string
        */
        protected function renderRecords(array $records = [])
        {
            if (!$records) {
                return $this->renderContent('Actor list is empty.');
            }
    
            $items = [];
    
            foreach ($records as $record) {
                $actorFilms = $record->films
                    ? Html::ol(ArrayHelper::getColumn($record->films, 'title')): null;
                $actorName = $record->first_name.' '.$record->last_name;
                    $items[] = $actorName.$actorFilms;
           }
    
            return $this->renderContent(Html::ol($items, [
                'encode' => false,
            ]));
        }
    
        /**
        * Render rows for result of query.
        *
        * @param array $rows
        *
        * @return string
        */
        protected function renderRows(array $rows = [])
        {
            if (!$rows) {
                return $this->renderContent('Actor list is empty.');
            }
    
            $items = [];
            $films = [];
    
            $actorId = null;
            $actorName = null;
            $actorFilms = null;
    
            $lastActorId = $rows[0]['actor_id'];
    
            foreach ($rows as $row) {
                $actorId = $row['actor_id'];
                $films[] = $row['title'];
    
                if ($actorId != $lastActorId) {
                    $actorName = $row['first_name'].' '.$row['last_name'];
                    $actorFilms = $films ? Html::ol($films) : null;
    
                    $items[] = $actorName.$actorFilms;
                    $films = [];
                    $lastActorId = $actorId;
                }
            }
    
            if ($actorId == $lastActorId) {
                $actorFilms = $films ? Html::ol($films) : null;
                $items[] = $actorName.$actorFilms;
            }
    
            return $this->renderContent(Html::ol($items, [
                'encode' => false,
            ]));
        }
    }
    
  2. 在这里,我们有三个动作对应于从数据库获取数据的三个不同方法。

  3. 在运行前面的db/ardb/querydb/sql动作之后,您应该得到一个显示 200 名演员和他们在其中出演的 1,000 部电影的树状图,如下截图所示:如何做…

  4. 在底部,有一些统计信息提供了有关内存使用和执行时间的详细信息。如果您运行此代码,绝对数字可能会有所不同,但使用的方法之间的差异应该大致相同:

    方法 内存使用(兆字节) 执行时间(秒)
    Active Record 21.4 2.398
    查询构建器 28.3 0.477
    SQL(DAO) 27.6 0.481

它是如何工作的…

actionAr动作方法使用 Active Record 方法获取模型实例。

我们从 Gii 生成的 Actor 模型开始,以获取所有演员,并指定 joinWith => 'films' 以使用单个查询或通过关系预加载获取相应的电影,这些关系由 Gii 从 InnoDB 表的外键为我们构建。然后,我们简单地遍历所有演员,并对每个演员及其每部电影进行遍历。然后,对于每个项目,我们打印其名称。

actionQuery 函数使用查询构建器。首先,我们使用 \yii\db\Query 为当前数据库连接创建查询。然后,我们使用 fromjoinInnerleftJoin 逐个添加查询部分。这些方法会自动转义值、表和字段名称。\yii\db\Queryall() 函数返回一个原始数据库行数组。每一行也是一个数组,使用结果字段名称作为索引。我们将结果传递给 renderRows,它将其渲染。

使用 actionSql,我们做同样的事情,只是我们直接传递 SQL 而不是逐个添加其部分。值得注意的是,在使用查询字符串之前,我们应该使用 Yii::app()->db->quoteValue 手动转义参数值:

renderRows 方法渲染查询构建器。

renderRecords 方法渲染活动记录。

方法 Active Record 查询构建器 SQL (DAO)
语法 这将为您生成 SQL。Gii 将为您生成模型和关系。与模型一起工作,完全面向对象风格,API 非常干净。结果生成正确嵌套的模型数组。 清洁的 API,适合实时构建查询。结果生成原始数据数组。 适合复杂 SQL。手动值和关键字引用。不适合实时构建查询。结果生成原始数据数组。
性能 与 SQL 和查询构建器相比,内存使用量和执行时间更高。 正常。 正常。
额外功能 自动引用值和名称。行为。前后钩子。验证。原型选择。 自动引用值和名称。 无。
适用于 单个模型(模型与表单结合使用时提供巨大优势)的更新、删除和创建操作。 处理大量数据并在实时构建查询。 使用纯 SQL 完成复杂查询,并具有最大可能的性能。

更多内容...

为了了解如何在 Yii 中与数据库一起工作,请参考以下资源:

定义和使用多个数据库连接

在新的独立 Web 应用程序中,多数据库连接并不常用。然而,当你为现有系统构建附加应用程序时,你很可能需要另一个数据库连接。

从这个菜谱中,你将学习如何定义多个数据库连接,并使用它们与 DAO、查询构建器和活动记录模型。

准备工作

  1. 使用 Composer 包管理器创建一个新的应用程序,如官方指南www.yiiframework.com/doc-2.0/guide-start-installation.html中所述。

  2. 创建两个名为db1db2的 MySQL 数据库。

  3. db1中创建一个名为post的表,如下所示:

    DROP TABLE IF EXISTS 'post';
    CREATE TABLE IF NOT EXISTS 'post' (
        'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
        'title' VARCHAR(255) NOT NULL,
        'text' TEXT NOT NULL,
         PRIMARY KEY  ('id')
    );
    
  4. db2中创建一个名为comment的表,如下所示:

    DROP TABLE IF EXISTS 'comment';
    CREATE TABLE IF NOT EXISTS 'comment' (
        'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
        'text' TEXT NOT NULL,
        'post_id' INT(10) UNSIGNED NOT NULL,
         PRIMARY KEY  ('id')
    );
    

如何操作...

  1. 我们将从配置数据库连接开始。打开config/main.php,并按照官方指南定义一个主连接:

    'db' => [
        'connectionString' =>'mysql:host=localhost;dbname=db1',
        'username' => 'root',
        'password' => '',
        'charset' => 'utf8',
    ],
    
  2. 复制它,将db组件重命名为db2,并相应地更改连接字符串。此外,你还需要添加类名如下:

    'db2' => [
        'class'=>'yii\db\Connection',
        'connectionString' => 'mysql:host=localhost;dbname=db2',
        'username' => 'root',
        'password' => '',
        'charset' => 'utf8',
    ],
    
  3. 就这样。现在你有了两个数据库连接,你可以使用它们与 DAO 和查询构建器,如下所示:

    $rows1 = Yii::$app->db->createCommand($sql)->queryAll();
    $rows2 = Yii::$app->db2->createCommand($sql)->queryAll();
    
  4. 现在,如果我们需要使用活动记录模型,我们首先需要使用 Gii 创建 Post 和 Comment 模型。你可以为每个模型选择一个合适的连接。在创建 Comment 模型时,将数据库连接 ID 设置为db2,如下所示:如何操作...

  5. 现在,你可以像往常一样使用Comment模型,并创建controllers/DbController.php,如下所示:

    <?php
    
    namespace app\controllers;
    
    use app\models\Post;
    use app\models\Comment;
    use yii\helpers\ArrayHelper;
    use yii\helpers\Html;
    use yii\web\Controller;
    
    /**
    * Class DbController.
    * @package app\controllers
    */
    class DbController extends Controller
    {
        public function actionIndex()
        {
            $post = new Post();
            $post->title = 'Post #'.rand(1, 1000);
            $post->text = 'text';
            $post->save();
    
            $posts = Post::find()->all();
    
            echo Html::tag('h1', 'Posts');
            echo Html::ul(ArrayHelper::getColumn($posts, 'title'));
    
            $comment = new Comment();
            $comment->post_id = $post->id;
            $comment->text = 'comment #'.rand(1, 1000);
            $comment->save();
    
            $comments = Comment::find()->all();
    
            echo Html::tag('h1', 'Comments');
            echo Html::ul(ArrayHelper::getColumn($comments, 'text'));
        }
    }
    
  6. 多次运行db/index,你应该会看到记录被添加到两个数据库中,如下所示:如何操作...

它是如何工作的...

在 Yii 中,你可以通过配置文件添加和配置自己的组件。对于非标准组件,如db2,你必须指定组件类。同样,你可以添加db3db4或任何其他组件,例如facebookApi。剩余的数组键/值对分别分配给组件的公共属性。

还有更多...

根据使用的 RDBMS,我们可以做更多的事情来简化使用多个数据库的过程。

跨数据库关系

如果你使用的是 MySQL,你可以为你的模型创建跨数据库关系。为了做到这一点,你应该在Comment模型表名前加上数据库名,如下所示:

class Comment extends \yii\db\ActiveRecord
{
    //...
    public function tableName()
    {
        return 'db2.comment';
    }
    //... 
}

现在,如果你在Post模型的关系方法中定义了评论关系,你可以使用以下代码:

$posts = Post::find()->joinWith('comments')->all();

参见

对于更多信息,请参阅www.yiiframework.com/doc-2.0/guide-db-dao.html#creating-db-connections

自定义 ActiveQuery 类

默认情况下,所有 Active Record 查询都由yii\db\ActiveQuery支持。要在 Active Record 类中使用自定义查询类,您应该重写yii\db\ActiveRecord::find()方法并返回您自定义查询类的实例。

准备工作

  1. 使用官方指南中描述的 Composer 包管理器创建一个新的应用程序,官方指南链接为www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 按照以下步骤设置数据库连接并创建一个名为post的表:

    DROP TABLE IF EXISTS 'post';
    CREATE TABLE IF NOT EXISTS 'post' (
        'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
        'lang' VARCHAR(5) NOT NULL DEFAULT 'en',
        'title' VARCHAR(255) NOT NULL,
        'text' TEXT NOT NULL,
         PRIMARY KEY ('id')
    );
    INSERT INTO 'post'('id','lang','title','text')
    VALUES (1,'en_us','Yii news','Text in English'),
    (2,'de','Yii Nachrichten','Text in Deutsch');
    
  3. 使用 Gii 生成带有已启用生成 ActiveQuery选项的Post模型,这将生成PostQuery类。

如何操作...

  1. 将以下方法添加到models/PostQuery.php中:

    <?php
    
    namespace app\models;
    
    /**
    * This is the ActiveQuery class for [[Post]].
    *
    * @see Post
    */
    class PostQuery extends \yii\db\ActiveQuery
    {
        /**
        * @param $lang
        *
        * @return $this
        */
        public function lang($lang)
        {
            return $this->where([ 'lang' => $lang ]);
        }
    }
    
  2. 就这些了。现在,我们可以使用我们的模型。按照以下步骤创建controllers/DbController.php

    <?php
    
    namespace app\controllers;
    
    use app\models\Post;
    use yii\helpers\Html;
    use yii\web\Controller;
    
    /**
    * Class DbController.
    * @package app\controllers
    */
    class DbController extends Controller
    {
        public function actionIndex()
        {
            // Get posts written in default application language
            $posts = Post::find()->all();
    
            echo Html::tag('h1', 'Default language');
            foreach ($posts as $post) {
                echo Html::tag('h2', $post->title);
                echo $post->text;
            }
    
           // Get posts written in German
           $posts = Post::find()->lang('de')->all();
    
            echo Html::tag('h1', 'German');
            foreach ($posts as $post) {
                echo Html::tag('h2', $post->title);
                echo $post->text;
            }
        }
    }
    
  3. 现在,运行db/index,您应该得到一个类似于以下截图的输出:如何操作...

如何工作...

我们在Post模型中重写了find方法并扩展了 ActiveQuery 类。lang方法返回具有指定语言值的 ActiveQuery。为了支持链式调用,lang方法返回模型实例本身。

还有更多...

根据 Yii2 指南,在 Yii 1.1 中有一个名为“范围”的概念。在 Yii 2.0 中,范围不再直接支持,您应该使用自定义查询类和查询方法来实现相同的目标。

相关内容

对于更多信息,请参考以下 URL:

使用 AR 事件方法处理模型字段

Yii 中的 Active Record 实现非常强大,具有许多功能。其中之一是事件方法,您可以使用它来在将模型字段放入数据库或从数据库获取之前预处理模型字段,以及删除与模型相关的数据等。

在这个菜谱中,我们将链接帖子文本中的所有 URL 并列出所有现有的 Active Record 事件方法。

准备工作

  1. 使用 Composer 包管理器创建一个新的应用程序,如官方指南中所述,官方指南链接为www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 按照以下步骤设置数据库连接并创建一个名为post的表:

    DROP TABLE IF EXISTS 'post';
    CREATE TABLE IF NOT EXISTS 'post' (
        'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
        'title' VARCHAR(255) NOT NULL,
        'text' TEXT NOT NULL,
         PRIMARY KEY ('id')
    );
    
  3. 使用 Gii 生成post模型。

如何操作...

  1. 将以下方法添加到models/Post.php中:

    /**
    * @param bool $insert
    *
    * @return bool
    */
    public function beforeSave($insert)
    {
        $this->text = preg_replace('~((?:https?|ftps?)://.*?)( |$)~iu',
        '<a href="\1">\1</a>\2', $this->text);
    
        return parent::beforeSave($insert);
    }
    
  2. 就这些了。现在,尝试保存一个包含链接的帖子。按照以下步骤创建controllers/TestController.php

    <?php
    
    namespace app\controllers;
    
    use app\models\Post;
    use yii\helpers\Html;
    use yii\helpers\VarDumper;
    use yii\web\Controller;
    
    /**
    * Class TestController.
    * @package app\controllers
    */
    class TestController extends Controller
    {
        public function actionIndex()
        {
            $post = new Post();
            $post->title = 'links test';
            $post->text = 'before http://www.yiiframework.com/ after';
            $post->save();
    
            return $this->renderContent(Html::tag('pre', VarDumper::dumpAsString(
                $post->attributes
            )));
        }
    }
    
  3. 就这些了。现在,运行 test/index。你应该得到以下结果:如何操作...

它是如何工作的...

beforeSave 方法在 ActiveRecord 类中实现,并在保存模型之前执行。使用正则表达式,我们替换所有看起来像 URL 的内容,并用使用此 URL 的链接替换,然后调用父实现,以确保正确地引发真实事件。为了防止保存,你可以返回 false。

参见

自动化时间戳

例如,我们有一个简单的博客应用程序。就像任何博客一样,它有帖子、评论等。我们希望在创建/更新帖子事件期间填充时间戳。让我们假设我们的帖子模型名为 BlogPost 模型。

准备工作

  1. 使用官方指南中描述的 Composer 包管理器创建一个新的应用程序,请参阅www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 设置数据库连接并创建一个名为 blog_post 的表,如下所示:

    DROP TABLE IF EXISTS 'blog_post';
    CREATE TABLE IF NOT EXISTS 'blog_post' (
        'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
        'title' VARCHAR(255) NOT NULL,
        'text' TEXT NOT NULL,
        'created_date' INTEGER,
        'modified_date'INTEGER,
         PRIMARY KEY  ('id')
    );
    
  3. 使用 Gii 为 blog_post 表创建一个模型。

如何操作...

  1. 将以下方法添加到 models/BlogPost.php

    /**
    * @return array
    */
    public function behaviors()
    {
        return [
            'timestamp'=> [
                'class' => 'yii\behaviors\TimestampBehavior',
                'createdAtAttribute' => 'creation_date',
                'updatedAtAttribute' => 'modified_date'
            ]
        ];
    }
    
  2. 创建 controllers/TestController.php,如下所示:

    <?php
    
    namespace app\controllers;
    
    use app\models\BlogPost;
    use yii\helpers\Html;
    use yii\helpers\VarDumper;
    use yii\web\Controller;
    
    /**
    * Class TestController.
    * @package app\controllers
    */
    class TestController extends Controller
    {
        public function actionIndex()
        {
            $blogPost = new BlogPost();
            $blogPost->title = 'Gotcha!';
            $blogPost->text = 'We need some laughter to ease the tension of holiday shopping.';
            $blogPost->save();
    
            return $this->renderContent(Html::tag('pre',
            VarDumper::dumpAsString($blogPost->attributes)
            ));
        }
    }
    
  3. 就这些了。现在,运行 test/index。你应该得到以下结果:如何操作...

它是如何工作的...

默认情况下,时间戳行为会填充 created_at(指向模型创建时间的戳)和 updated_at(模型更新的时间)。命名这些字段是标准做法,但如果我们要进行更改,我们可以指定要更新的字段和模型事件。

还有更多...

例如,我们的字段命名为 creation_datemodified_date

让我们根据这些字段配置我们的模型。此外,我们还应该将我们的行为代码添加到我们的 Post 模型中:

<?php

namespace app\models;

use Yii;
use yii\db\BaseActiveRecord;

class Post extends \yii\db\ActiveRecord
{
    // ..
    public function behaviors()
    {
        return [
            [
                'class' => 'yii\behaviors\TimestampBehavior',
                'attributes' => [
                    BaseActiveRecord::EVENT_BEFORE_INSERT => 'creation_date',
                    BaseActiveRecord::EVENT_BEFORE_UPDATE => 'modified_date',
               ]
           ]
       ];
    }
    // ..
}

在这个例子中,我们通过使用特殊的 ActiveRecord 事件:EVENT_BEFORE_INSERTEVENT_BEFORE_UPDATE,在创建和更新模型之前相应地指向 creation_datemodified_date 属性。

此外...

你可能想为自定义场景保存时间戳。比如说,你想更新 last_login 字段,例如,对于特定的控制器操作。在这种情况下,你可以使用以下方式触发特定属性的时戳更新:

$model->touch('last_login');

注意,touch() 不能用于新模型。在这种情况下,您将得到 InvalidCallException

$model = new Post();
$model->touch('creation_date');

touch() 方法在其内部调用模型保存,因此您在调用它之后不需要保存模型。

参见

对于更多信息,请参阅www.yiiframework.com/doc-2.0/guide-concept-behaviors.html#using-timestampbehavior

自动设置作者

Blameable 行为允许您自动更新一个或多个作者的字段。这主要用于将数据填充到 created_byupdated_by 字段中。类似于 Timestamp 行为,您可以轻松指定一些特殊参数和基本事件来使用此行为。

让我们回到上一节的例子。在我们的博客应用程序中,我们也有帖子。例如,假设我们的博客模型被命名为 BlogPost。该模型有 author_id 字段,它指向创建此帖子的用户,还有 updater_id 字段,它指向更新它的用户。我们希望在创建/更新模型事件期间自动填充这些属性。现在您可以学习如何做到这一点。

准备工作

  1. 使用官方指南中描述的 Composer 包管理器创建一个新的应用程序,请参阅www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 设置数据库连接并创建一个名为 blog_post 的表,如下所示:

    DROP TABLE IF EXISTS 'blog_post';
    CREATE TABLE IF NOT EXISTS 'blog_post' (
        'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
        'author_id' INT(10) UNSIGNED DEFAULT NULL,
        'updater_id' INT(10) UNSIGNED DEFAULT NULL,
        'title' VARCHAR(255) NOT NULL,
        'text' TEXT NOT NULL,
        PRIMARY KEY  ('id')
    );
    
  3. 使用 Gii 为 blost_post 表创建 BlogPost 模型。

如何做到这一点…

  1. 将以下 behaviors 方法添加到 models/BlogPost.php

    <?php
    
    namespace app\models;
    
    use Yii;
    use yii\db\BaseActiveRecord;
    
    /**
    * This is the model class for table "blog_post".
    *
    * @property integer $id
    * @property integer $author_id
    * @property integer $updater_id
    * @property string $title
    * @property string $text
    */
    class BlogPost extends \yii\db\ActiveRecord
    {
        /**
        * @return array
        */
        public function behaviors()
        {
            return [
                [
                    'class' => 'yii\behaviors\BlameableBehavior',
                    'attributes' => [
                        BaseActiveRecord::EVENT_BEFORE_INSERT => 'author_id',
                        BaseActiveRecord::EVENT_BEFORE_UPDATE => 'updater_id'
                    ]
                ]
            ];
        }
    }
    
  2. 按照以下方式创建 controllers/TestController.php

    <?php
    
    namespace app\controllers;
    
    use app\models\BlogPost;
    use app\models\User;
    use Yii;
    use yii\helpers\Html;
    use yii\helpers\VarDumper;
    use yii\web\Controller;
    
    /**
    * Class TestController.
    * @package app\controllers
    */
    class TestController extends Controller
    {
        public function actionIndex()
        {
            $users = new User();
            $identity = $users->findIdentity(100);
    
            Yii::$app->user->setIdentity($identity);
    
            $blogPost = new BlogPost();
            $blogPost->title = 'Very pretty title';
            $blogPost->text = 'Success is not final, failure is not fatal...';
            $blogPost->save();
    
            return $this->renderContent(Html::tag('pre', VarDumper::dumpAsString(
                $blogPost->attributes
            )));
        }
    }
    
  3. 就这样。现在,运行 test/index。您将得到以下结果:如何做到这一点…

它是如何工作的...

默认情况下,Blameable 行为填充 created_byupdated_by 属性,但我们将进行更改,并根据自己的字段设置我们的行为。

我们还在模型中指定了模型事件和字段,因此,在模型创建期间,author_id 将被填充。同样,在模型更新期间,我们将填充 updater_id

Blameable 的作用是在创建/更新模型事件期间将当前用户 ID 值插入到 created_byupdated_by 字段中。这是一种非常方便的做法。每次模型被创建或更新时,我们都会自动填写这些基本字段。

这对于小型项目来说效果很好,例如对于大型系统,其中多个用户是管理员,你需要跟踪谁在做什么。你也可以用于前端实现,例如,如果你有一个blog_comment表,你想要使用这种方法来跟踪评论的作者。此外,你可以在控制器中设置作者的字段,但行为可以帮助你避免编写不必要的额外代码。这是一个非常有效且简单的方法来实现这一点。

还有更多…

有时我们需要用除当前用户 ID 之外的其他 ID 填写author_idupdater_id。在这种情况下,我们可能需要按照以下方式移除我们的行为:

$model->detachBehavior('blammable');

我们可以用这种方式移除任何我们喜欢的行为。

参见

如需更多信息,请参阅www.yiiframework.com/doc-2.0/yii-behaviors-blameablebehavior.html

自动设置缩略名

在网络上,缩略名是用于 URL 中标识和描述资源的简短文本。缩略名是 URL 的一部分,它使用人类可读的关键字来标识一个页面。可缩略行为是 Yii2 模型行为,允许我们生成唯一的缩略名。

在本节中,我们将指导你修改 Yii 的默认视图 URL 路由,使其对模型对象更加用户友好和搜索引擎友好。Yii 通过其可缩略行为提供了内置支持。

准备工作

  1. 使用 Composer 包管理器创建一个新的应用程序,如官方指南中所述www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 设置数据库连接并创建一个名为blog_post的表,如下所示:

    DROP TABLE IF EXISTS 'blog_post';
    CREATE TABLE IF NOT EXISTS 'blog_post' (
        'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
        'title' VARCHAR(255) NOT NULL,
        'slug' VARCHAR(255) NOT NULL,
        'text' TEXT NOT NULL,
        PRIMARY KEY ('id')
    );
    
  3. 使用 Gii 为帖子表创建模型。

如何做…

  1. 将以下behaviors方法添加到models/BlogPost.php中:

    <?php
    
    namespace app\models;
    
    use Yii;
    use yii\db\BaseActiveRecord;
    
    class BlogPost extends \yii\db\ActiveRecord
    {
        // ..
        public function behaviors()
        {
            return [
                [
                    'class' => 'yii\behaviors\SluggableBehavior',
                    'attribute' => 'title',
                    'slugAttribute' => 'slug',
                    'immutable'=> false,
                    'ensureUnique' => true
                ]
            ];
        }
        // ..
    }
    
  2. 按照以下方式创建controllers/TestController.php

    <?php
    
    namespace app\controllers;
    
    use app\models\BlogPost;
    use Yii;
    use yii\helpers\Html;
    use yii\helpers\VarDumper;
    use yii\web\Controller;
    
    /**
    * Class TestController
    * @package app\controllers
    */
    class TestController extends Controller
    {
        public function actionIndex()
        {
            $blogPostA        = new BlogPost();
            $blogPostA->title = 'Super Quote title 1';
            $blogPostA->text  = 'The price of success is hard work, dedication to the job at hand';
            $blogPostA->save();
    
            $blogPostB        = new BlogPost();
            $blogPostB->title = 'Super Quote title 2';
            $blogPostB->text  = 'Happiness lies in the joy of achievement...';
            $blogPostB->save();
    
            return $this->renderContent(
                '<pre>' .
                VarDumper::dumpAsString(
                  $blogPostA->attributes
              ).
              VarDumper::dumpAsString(
                  $blogPostB->attributes
              ) .
              '</pre>'
          );
      }
    }
    
  3. 结果将如下所示:如何做…

它是如何工作的…

  • Yii 为SluggableBehavior提供了一些有用的增强功能。

  • 例如,一旦搜索引擎记录了一个缩略名,你可能不希望页面 URL 发生变化。

  • 不可变属性告诉 Yii 在首次创建后保持缩略名不变——即使标题将被更新。

  • 如果用户输入的内容重叠的消息,ensureUnique属性将自动为重复项添加一个唯一的后缀。这确保了每个消息都有一个唯一的 URL,即使消息是相同的。

  • 如果你继续创建一个与标题完全相同的另一篇帖子,你会发现它的缩略名已增加为 hot-update-for-ios-devices-2。

注意

注意:如果你遇到了与不可变属性相关的错误,可能需要运行 Composer 更新以获取 Yii 的最新版本。

还有更多…

  1. 使用 Gii 为模型类app\models\Post和控制器类app\controllers\BlogPostController生成 CRUD。

  2. 将以下操作添加到 controllers/BlogPostController.php 文件中:

    /**
    * @param $slug
    *
    * @return string
    * @throws NotFoundHttpException
    */
    public function actionSlug($slug)
    {
        $model = BlogPost::findOne(['slug'=>$slug]);
    
        if ($model === null) {
            throw new NotFoundHttpException('The requested page does not exist.');
        }
    
        return $this->render('view', [
            'model' => $model,
            ]);
    }
    
  3. 就这样。如果你使用 slug 值为 sluggablebehavior-testblogpost/slug 运行,你将得到以下结果:还有更多…

  4. 建议使用创建的 Post 模型实例成功完成之前的 slug 调配方。

  5. 为了美化 URL,请在 config\web.php 中添加以下 urlManager 组件:

    //..
    'urlManager' => [
        'enablePrettyUrl' => true,
        'rules' => [
            'blog-post' => 'blog-post/index',
            'blog-post/index' => 'blog-post/index',
            'blog-post/create' => 'blog-post/create',
            'blog-post/view/<id:\d+>' => 'blog-post/view',
            'blog-post/update/<id:\d+>' => 'blog-post/update',
            'blog-post/delete/<id:\d+>' => 'blog-post/delete',
            'blog-post/<slug>' => 'blog-post/slug',
            'defaultRoute' => '/site/index',
        ],
    ]
    //..
    
  6. 确保 'blog-post/<slug>' => 'blog-post/slug' 规则在帖子 URL 规则列表中是最后一个。

  7. 现在,如果你使用 slug URL 访问页面,例如 index.php/blog-post/super-quote-title-1/,你将得到类似于第 3 步的结果:还有更多…

参见

有关更多信息,请参阅:

事务

在现代数据库中,事务还会做一些其他事情,例如确保你不能访问其他人半途写入的数据。然而,基本思想是相同的——事务是为了确保无论发生什么情况,你处理的数据都将处于合理的状态。它们保证不会出现从某个账户提取了钱,但没有存入另一个账户的情况。

Yii2 支持一个强大的带有 savepoints 的事务机制。

一个经典的例子是将一笔钱从一个银行账户转到另一个账户。为此,你必须首先从源账户中提取金额,然后将其存入目标账户。操作必须完全成功。如果你半途而废,钱就会丢失,这非常糟糕。例如,我们有一个收款账户和一个付款账户。我们希望从付款账户向收款账户转账。假设我们有一个账户模型。

准备中...

我们的账户模型将非常简单,它只包含 idbalance 字段。

  1. 使用官方指南中描述的 Composer 包管理器创建一个新的应用程序,官方指南链接为 www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 创建一个迁移,使用以下命令添加账户表:

    ./yii migrate/create create_account_table
    
  3. 此外,使用以下代码更新刚刚创建的迁移:

    <?php
    
    use yii\db\Schema;
    use yii\db\Migration;
    
    class m150620_062034_create_account_table extends Migration
    {
        const TABLE_NAME = '{{%account}}';
    
        public function up()
        {
            $tableOptions = null;
            if ($this->db->driverName === 'mysql') {
                $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
           }
    
           $this->createTable(self::TABLE_NAME, [
               'id' => Schema::TYPE_PK,
               'balance' => ' NUMERIC(15,2) DEFAULT NULL',
           ], $tableOptions);
    
        }
    
        public function down()
        {
            $this->dropTable(self::TABLE_NAME);
        }
    }
    
  4. 然后,使用以下命令安装迁移:

    ./yii migrate up
    
  5. 使用 Gii 创建账户表的模型。

  6. 创建一个迁移,为我们的表添加一些带有余额的测试 Account 模型:

    ./yii migrate/create add_account_records
    
  7. 此外,使用以下代码更新刚刚创建的迁移:

    <?php
    
    use yii\db\Migration;
    use app\models\Account;
    
    class m150620_063252_add_account_records extends Migration
    {
        public function up()
        {
            $accountFirst = new Account();
            $accountFirst->balance = 1110;
            $accountFirst->save();
    
            $accountSecond = new Account();
            $accountSecond->balance = 779;
            $accountSecond->save();
    
            $accountThird = new Account();
            $accountThird->balance = 568;
            $accountThird->save();
            return true;
        }
    
        public function down()
        {
            $this->truncateTable('{{%account}}');
            return false;
        }
    }
    

如何做到这一点…

  1. 将以下规则添加到 rules 方法中,到 models/Account.php 文件中:

    public function rules()
    {
        return [
            //..
            [['balance'], 'number', 'min' => 0],
            //..
        ];
    }
    
  2. 假设我们的余额只能是正数,不能是负数。

  3. 创建具有成功和错误操作的 TestController

    <?php
    
    namespace app\controllers;
    
    use app\models\Account;
    use Yii;
    use yii\db\Exception;
    use yii\helpers\Html;
    use yii\helpers\VarDumper;
    use yii\web\Controller;
    
    class TestController extends Controller
    {
    
        public function actionSuccess()
        {
            $transaction = Yii::$app->db->beginTransaction();
    
            try {
                $recipient = Account::findOne(1);
                $sender    = Account::findOne(2);
    
                $transferAmount = 177;
                $recipient->balance += $transferAmount;
                $sender->balance -= $transferAmount;
    
                if ($sender->save() && $recipient->save()) {
                    $transaction->commit();
    
                    return $this->renderContent(
                        Html::tag('h1', 'Money transfer was successfully')
                    );
                } else {
                    $transaction->rollBack();
                    throw new Exception('Money transfer failed:' .
                    VarDumper::dumpAsString($sender->getErrors()) .
                    VarDumper::dumpAsString($recipient->getErrors())
                    );
                }
            } catch ( Exception $e ) {
                $transaction->rollBack();
                throw $e;
            }
        }
    
        public function actionError()
        {
            $transaction = Yii::$app->db->beginTransaction();
    
            try {
                $recipient = Account::findOne(1);
                $sender    = Account::findOne(3);
    
                $transferAmount = 1000;
                $recipient->balance += $transferAmount;
                $sender->balance -= $transferAmount;
    
                if ($sender->save() && $recipient->save()) {
                    $transaction->commit();
    
                    return $this->renderContent(
                        Html::tag('h1', 'Money transfer was successfully')
                    );
                } else {
                    $transaction->rollBack();
    
                    throw new Exception('Money transfer failed: ' .
                    VarDumper::dumpAsString($sender->getErrors()) .
                    VarDumper::dumpAsString($recipient->getErrors())
                   );
               }
    
            } catch ( Exception $e ) {
                $transaction->rollBack();
                throw $e;
            }
        }
    }
    
  4. 运行 test/success 并应得到以下截图所示的输出:如何操作…

  5. 在这种情况下,如果发生某些错误,事务机制将不会更新接收者和发送者的余额。

  6. 运行 test/error 并应得到以下截图所示的输出:如何操作…

如你所记,我们向 Account 模型添加了一条规则,因此我们的账户余额只能是正数。在这种情况下,事务将回滚,这防止了从发送者账户中取款但未将款项存入接收者账户的情况发生。

另请参阅

如需更多信息,请参阅:

复制和读写分离

在这个菜谱中,我们将探讨如何进行复制和读写分离。我们将看到从服务器和主服务器如何帮助我们完成这些操作。

准备工作

  1. 使用 Composer 包管理器创建一个新的应用程序,如官方指南www.yiiframework.com/doc-2.0/guide-start-installation.html中所述。

  2. 按照官方指南设置数据库连接并创建一个名为 post 的表,如下所示:

    DROP TABLE IF EXISTS 'blog_post';
    CREATE TABLE IF NOT EXISTS 'blog_post' (
        'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
        'title' VARCHAR(255) NOT NULL,
        'text' TEXT NOT NULL,
        'created_at' INTEGER,
        'modified_at'INTEGER,
         PRIMARY KEY  ('id')
    );
    
  3. blog_post 表生成 BlogPost 模型。

  4. 在你的数据库服务器之间配置主从复制,例如,如文章www.digitalocean.com/community/tutorials/how-to-set-up-master-slave-replication-in-mysql/中所述。

  5. config/main.php 中配置 db 组件;以下是一个配置示例:

    'components' =>
        // ..
        'db' => [
            'class' => 'yii\db\Connection',
    
            'dsn' => 'mysql:host=4.4.4.4;dbname=masterdb',
            'username' => 'master',
            'password' => 'pass',
            'charset' => 'utf8',
    
            'slaveConfig' => [
                'username' => 'slave',
                'password' => 'pass',
            ],
    
            // list of slave configurations
            'slaves' => [
                ['dsn' => 'mysql:host=5.5.5.5;dbname=slavedb']
            ]
        ],
        // ..
    ]
    

如何操作…

  1. 创建 TestController.php 如下所示:

    <?php
    
    namespace app\controllers;
    
    use app\models\BlogPost;
    use Yii;
    use yii\helpers\Html;
    use yii\helpers\VarDumper;
    use yii\web\Controller;
    
    /**
    * Class TestController
    * @package app\controllers
    */
    class TestController extends Controller
    {
        public function actionIndex(){
    
            $masterModel = new BlogPost();
            $masterModel->title = 'Awesome';
            $masterModel->text = 'Something is going on..';
            $masterModel->save();
    
            $postId = $masterModel->id;
    
            $replModel = BlogPost::findOne($postId);
    
            return $this->renderContent(
                Html::tag('h2', 'Master') .
                Html::tag('pre', VarDumper::dumpAsString(
                    $masterModel
                       ? $masterModel->attributes
                       : null
                )) .
                Html::tag('h2', 'Slave') .
                Html::tag('pre', VarDumper::dumpAsString(
                    $replModel
                        ? $replModel->attributes
                        : null
    
                ))
            );
        }
    
    }
    
  2. 运行 test/index 并应得到以下截图所示的输出:如何操作…

工作原理…

从服务器用于数据读取,而主服务器用于写入。在主服务器上保存 ActiveRecord 模型后,新记录将复制到从服务器,然后 $replModel 在其上查找记录。

还有更多…

\yii\db\Connection 组件支持在从库之间进行负载均衡和故障转移。当第一次执行读查询时,\yii\db\Connection 组件将随机选择一个从库并尝试连接到它。如果发现从库已死,它将尝试另一个。如果所有从库都不可用,它将连接到主库。通过配置服务器状态缓存,可以记住已死的服务器,这样在一段时间内就不会再次尝试连接。

另请参阅

如需更多信息,请参阅以下网址:

实现单表继承

关系型数据库不支持继承。如果我们需要在数据库中存储继承,我们应该通过某种方式通过代码来支持它。这段代码应该高效,因此应该尽可能少地生成 JOIN 操作。对此问题的一个常见解决方案是由 Martin Fowler 描述的,被称为单表继承

当我们使用这种模式时,我们将整个类树的数据存储在单个表中,并使用类型字段来确定每一行的模型。

例如,我们将为以下类树实现单表继承:

Car

|- SportCar

|- FamilyCar

准备工作

  1. 使用 Composer 包管理器创建一个新的应用程序,具体操作请参考官方指南中的www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 创建并设置数据库。添加以下表:

    DROP TABLE IF EXISTS 'car';
    CREATE TABLE 'car' (
        'id' int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
        'name' varchar(255) NOT NULL,
        'type' varchar(100) NOT NULL,
         PRIMARY KEY ('id')
    );
    
    INSERT INTO 'car' ('name', 'type')
    VALUES ('Ford Focus', 'family'),
    ('Opel Astra', 'family'),
    ('Kia Ceed', 'family'),
    ('Porsche Boxster', 'sport'),
    ('Ferrari 550', 'sport');
    
  3. 使用 Gii 为 car 表创建一个 Car 模型,并为 Car 模型生成 ActiveQuery。

如何做这件事...

  1. 将以下方法和属性添加到 models/CarQuery.php:

    /**
    * @var
    */
    public $type;
    
    /**
    * @param \yii\db\QueryBuilder $builder
    *
    * @return \yii\db\Query
    */
    public function prepare($builder)
        {
            if ($this->type !== null) {
                $this->andWhere(['type' => $this->type]);
            }
            return parent::prepare($builder);
        }
    
  2. 按照以下方式创建 models/SportCar.php:

    <?php
    
    namespace app\models;
    
    use Yii;
    
    /**
    * Class SportCar
    * @package app\models
    */
    class SportCar extends Car
    {
        const TYPE = 'sport';
    
        /**
        * @return CarQuery
        */
        public static function find()
        {
            return new CarQuery(get_called_class(), ['where' => ['type' => self::TYPE]]);
        }
    
        /**
        * @param bool $insert
        *
        * @return bool
        */
        public function beforeSave($insert)
        {
            $this->type = self::TYPE;
            return parent::beforeSave($insert);
        }
    }
    
  3. 按照以下方式创建 models/FamilyCar.php:

    <?php
    
    namespace app\models;
    
    use Yii;
    
    /**
    * Class FamilyCar
    * @package app\models
    */
    class FamilyCar extends Car
    {
        const TYPE = 'family';
    
        /**
        * @return CarQuery
        */
        public static function find()
        {
            return new CarQuery(get_called_class(), ['where' => ['type' => self::TYPE]]);
        }
    
        /**
        * @param bool $insert
        *
        * @return bool
        */
        public function beforeSave($insert)
        {
            $this->type = self::TYPE;
            return parent::beforeSave($insert);
        }
    }
    
  4. 将以下方法添加到 models/Car.php:

        /**
        * @param array $row
        *
        * @return Car|FamilyCar|SportCar
        */
        public static function instantiate($row)
        {
            switch ($row['type']) {
                case SportCar::TYPE:
                    return new SportCar();
                case FamilyCar::TYPE:
                    return new FamilyCar();
                default:
                    return new self;
            }
        }
    
  5. 添加以下代码的 TestController:

    <?php
    
    namespace app\controllers;
    
    use app\models\Car;
    use app\models\FamilyCar;
    use Yii;
    use yii\helpers\Html;
    use yii\web\Controller;
    
    /**
    * Class TestController
    * @package app\controllers
    */
    class TestController extends Controller
    {
        public function actionIndex()
        {
            echo Html::tag('h1', 'All cars');
    
            $cars = Car::find()->all();
            foreach ($cars as $car) {
                // Each car can be of class Car, SportCar or FamilyCar
                echo get_class($car).' '.$car->name."<br />";
            }
    
            echo Html::tag('h1', 'Family cars');
    
            $familyCars = FamilyCar::find()->all();
            foreach($familyCars as $car)
            {
                // Each car should be FamilyCar
                echo get_class($car).' '.$car->name."<br />";
            }
        }
    }
    
  6. 运行 test/index,你应该会得到以下截图所示的输出:如何做这件事…

它是如何工作的...

基础模型 Car 是一个典型的 Yii AR 模型,除了它有两个附加的方法。tableName 方法明确声明了用于模型的表名。对于 Car 模型本身来说,这没有意义,但对于子模型来说,它将返回相同的汽车表,这正是我们想要的——整个类树的单个表。instantiate 方法由 AR 内部使用,在调用 Car:::find()->all() 等方法时,从原始数据创建模型实例。我们使用 switch 语句根据类型属性创建不同的类,如果属性值未指定或指向不存在的类,则使用相同的类。

SportCarFamilyCar 模型仅简单地设置了默认的 AR 范围,因此当我们使用 SportCar:: model()-> 方法搜索模型时,我们只会得到 SportCar 模型。

参考信息

使用以下参考资料了解更多关于单表继承模式和 Yii Active Record 实现的信息:

第四章:表单

在本章中,我们将涵盖以下主题:

  • 编写自己的验证器

  • 上传文件

  • 添加和自定义 CaptchaWidget

  • 自定义 Captcha

  • 创建自定义输入小部件

  • 表格输入

  • 条件验证

  • 具有多个模型的复杂表单

  • AJAX 依赖的下拉列表

  • AJAX 验证

  • 创建自定义客户端验证

简介

Yii 使处理表单变得简单,其文档几乎已经完成。尽管如此,还有一些需要澄清和示例的区域。我们将在本章中描述它们。

编写自己的验证器

Yii 提供了一套良好的内置表单验证器,它们覆盖了大多数开发者的典型需求,并且可高度配置。然而,在某些情况下,开发者可能需要创建自定义验证器。

这个示例是一个创建独立验证器以检查单词数量的好例子。

准备工作

使用 Composer 包管理器创建新应用,如官方指南中所述,请参阅 www.yiiframework.com/doc-2.0/guide-start-installation.html

如何操作...

  1. @app/components/WordsValidator.php 中创建一个独立的验证器,如下所示:

    <?php
    namespace app\components;
    use yii\validators\Validator;
    class WordsValidator extends Validator
    {
        public $size = 50;
        public function validateValue($value){
            if (str_word_count($value) > $this->size) {
                return ['The number of words must be less than {size}', ['size' => $this->size]];
            }
            return false;
        }
    }
    
  2. @app/models/Article.php 中创建一个 Article 模型,如下所示:

    <?php
    namespace app\models;
    use app\components\WordsValidator;
    use yii\base\Model;
    class Article extends Model
    {
        public $title;
        public function rules()
        {
            return [
                ['title', 'string'],
                ['title', WordsValidator::className(), 'size' => 10],
            ];
        }
    }
    
  3. 按照以下方式创建 @app/controllers/ModelValidationController.php:

    <?php
    namespace app\controllers;
    use app\models\Article;
    use yii\helpers\Html;
    use yii\web\Controller;
    class ModelValidationController extends Controller
    {
        private function getLongTitle()
        {
            return 'There is a very long content for current article, '.'it should be less then ten words';
        }
        private function getShortTitle()
        {
            return 'There is a shot title';
        }
        private function renderContentByModel($title)
        {
            $model = new Article();
            $model->title = $title;
            if ($model->validate()) {
                $content = Html::tag('div', 'Model is valid.',[
                    'class' => 'alert alert-success',
                ]);
            } else {
                $content = Html::errorSummary($model, [
                    'class' => 'alert alert-danger',
                ]);
            }
            return $this->renderContent($content);
        }
        public function actionSuccess()
        {
            $title = $this->getShortTitle();
            return $this->renderContentByModel($title);
        }
        public function actionFailure()
        {
            $title = $this->getLongTitle();
            return $this->renderContentByModel($title);
        }
    }
    
  4. 通过打开 index.php?r=model-validation/success URL 运行 modelValidation 控制器的 success 动作,你将得到以下内容:如何操作...

  5. 通过打开 index.php?r=model-validation/failure URL 运行 modelValidation 控制器的 failure 动作,你将得到以下内容:如何操作...

  6. 按照以下方式创建 @app/controllers/AdhocValidationController.php:

    <?php
    namespace app\controllers;
    use app\components\WordsValidator;
    use app\models\Article;
    use yii\helpers\Html;
    use yii\web\Controller;
    class AdhocValidationController extends Controller
    {
        private function getLongTitle()
        {
            return 'There is a very long content for current article, '.'it should be less then ten words';
        }
        private function getShortTitle()
        {
            return 'There is a shot title';
        }
        private function renderContentByTitle($title)
        {
            $validator = new WordsValidator([
                'size' => 10,
            ]);
            if ($validator->validate($title, $error)) {
                $content = Html::tag('div', 'Value is valid.',[
                    'class' => 'alert alert-success',
                ]);
            } else {
                $content = Html::tag('div', $error, [
                    'class' => 'alert alert-danger',
                ]);
            }
            return $this->renderContent($content);
        }
        public function actionSuccess()
        {
            $title = $this->getShortTitle();
            return $this->renderContentByTitle($title);
        }
        public function actionFailure()
        {
            $title = $this->getLongTitle();
            return $this->renderContentByTitle($title);
        }
    }
    
  7. 通过打开 index.php?r=adhoc-validation/success URL 运行 AdhocValidationControllersuccess 动作,你将得到以下内容:如何操作...

  8. 通过打开 index.php?r=adhoc-validation/failure URL 运行 adhocValidation 控制器的 failure 动作,你将得到以下内容:如何操作...

它是如何工作的...

首先,我们创建了一个独立的验证器,该验证器通过使用标准的 str_word_count PHP 函数来检查单词数量,然后演示了两个验证器使用案例:

  • Article 模型中将验证器用作验证规则

  • 将验证器用作临时验证器

验证器有一个大小属性,它设置单词数量的最大值。

相关内容

如需更多信息,请参考以下网址:

上传文件

处理文件上传是 Web 应用的一个相当常见的任务。Yii 内置了一些有用的类来完成这个任务。让我们创建一个简单的表单,允许上传 ZIP 存档并将它们存储在/uploads

准备工作

  1. 使用官方指南中描述的 Composer 包管理器创建一个新的应用程序,官方指南在www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 创建@app/web/uploads目录。

如何操作...

  1. 我们将从模型开始,因此创建@app/models/Upload.php模型如下:

    <?php
    namespace app\models;
    use yii\base\Model;
    use yii\web\UploadedFile;
    class UploadForm extends Model
    {
        /**
        * @var UploadedFile
        */
        public $file;
        public function rules()
        {
            return [
                ['file', 'file', 'skipOnEmpty' => false, 'extensions' => 'zip'],
            ];
        }
        public function upload()
        {
            if ($this->validate()) {
                $this->file->saveAs('uploads/' . $this->file->baseName . '.' . $this->file->extension);
               return true;
            } else {
                return false;
            }
        }
    }
    
  2. 现在我们将转到控制器,因此创建@app/controllers/UploadController.php如下:

    <?php
    namespace app\controllers;
    use Yii;
    use yii\web\Controller;
    use app\models\UploadForm;
    use yii\web\UploadedFile;
    class UploadController extends Controller
    {
        public function actionUpload()
        {
            $model = new UploadForm();
            if (Yii::$app->request->isPost) {
                $model->file = UploadedFile::getInstance($model, 'file');
                if ($model->upload()) {
                    return $this->renderContent("File {$model->file->name} is uploaded successfully");
                }
            }
            return $this->render('index', ['model' => $model]);
        }
    }
    
  3. 最后,你可以按照以下方式查看@app/views/upload/index.php

    <?php
    use yii\widgets\ActiveForm;
    use yii\helpers\Html;
    ?>
    <?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data']]) ?>
        <?= $form->field($model, 'file')->fileInput() ?>
        <?= Html::submitButton('Upload', ['class' => 'btn-success'])?>
    <?php ActiveForm::end() ?>
    
  4. 就这些了。现在,运行上传控制器并尝试上传 ZIP 存档和其他文件,如下面的截图所示:如何操作...

它是如何工作的...

我们使用的模型相当简单。我们只定义了一个字段,名为$file,以及一个使用FileValidator文件验证器的验证规则,该验证器只读取 ZIP 文件。

如果表单被提交,我们将创建一个模型实例并用$_POST中的数据填充它:

$model->file = UploadedFile::getInstance($model, 'file');
if ($model->upload()) {
    return $this->renderContent("File {$model->file->name} is uploaded successfully");
}

我们然后使用UploadedFile::getInstance,这使我们能够访问使用UploadedFile实例。这个类是 PHP 在文件上传时填充的$_FILE数组的包装器。我们通过调用模型的validate方法来确保文件是 ZIP 存档,然后我们使用UploadedFile::saveAs来保存文件。

为了上传文件,HTML 表单必须满足以下两个重要要求:

  • 方法必须设置为POST

  • enctype属性必须设置为multipart/form-data

重要的是要记住,你需要将enctype选项添加到表单中,以便文件能够正确上传。

我们可以使用Html辅助器或设置htmlOptionsActiveForm来生成此 HTML。在这里,使用了 HTML:

<?= Html::beginForm('', 'post', ['enctype'=>'multipart/form-data'])?>

最后,我们显示一个错误和一个用于模型文件属性的输入字段,并渲染一个提交按钮。

还有更多...

要上传多个文件,Yii2 实现了两个特殊的方法。

例如,你在视图文件中定义了模型中的$imageFiles,在公共文件中所有这些都将相同,只有一点不同:

..
<?= $form->field($model, 'imageFiles[]')->fileInput(['multiple' => true, 'accept' => 'image/*']) ?>
..

要获取所有文件实例,你必须调用UploadedFile::getInstances()而不是UploadedFile::getInstance()

..
$model->imageFiles = UploadedFile::getInstances($model, 'imageFiles');
..

处理和保存多个文件可以通过一个简单的代码片段来完成:

foreach ($this->imageFiles as $file) {
    $file->saveAs('uploads/' . $file->baseName . '.' . $file->extension);
}

参见

更多信息,请参考:

添加和自定义 CaptchaWidget

现在,在互联网上,如果你不添加垃圾邮件保护就留下一个表单,你将在短时间内收到大量的垃圾数据。Yii 包括一个验证码组件,使得添加此类保护变得轻而易举。唯一的问题是,没有关于如何使用它的系统指南。

在以下示例中,我们将向一个简单的表单添加验证码保护。

准备工作

  1. 使用 Composer 包管理器创建一个新的应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 创建一个表单模型,@app/models/EmailForm.php,如下所示:

    <?php
    namespace app\models;
    use yii\base\Model;
    class EmailForm extends Model
    {
        public $email;
        public function rules()
        {
            return [
                ['email', 'email']
            ];
        }
    }
    
  3. 创建一个控制器,@app/controllers/EmailController.php,如下所示:

    <?php
    namespace app\controllers;
    use Yii;
    use yii\web\Controller;
    use app\models\EmailForm;
    class EmailController extends Controller
    {
        public function actionIndex(){
            $success = false;
            $model = new EmailForm();
            if ($model->load(Yii::$app->request->post()) && $model->validate()) {
                Yii::$app->session->setFlash('success', 'Success!');
            }
            return $this->render('index', [
                'model' => $model,
                'success' => $success,
            ]);
        }
    }
    
  4. 创建一个视图,@app/views/email/index.php,如下所示:

    <?php
    use yii\helpers\Html;
    use yii\captcha\Captcha;
    use yii\widgets\ActiveForm;
    ?>
    <?php if (Yii::$app->session->hasFlash('success')): ?>
        <div class="alert alert-success"><?=Yii::$app->session->getFlash('success')?></div>
    <?php else: ?>
        <?php $form = ActiveForm::begin()?>
            <div class="control-group">
                <div class="controls">
                    <?= $form->field($model, 'email')->textInput(['class' => 'form-control']); ?>
                    <?php echo Html::error($model, 'email', ['class' => 'help-block'])?>
                </div>
            </div>
            <?php if (Captcha::checkRequirements() && Yii::$app->user->isGuest): ?>
                <div class="control-group">
                    <?= $form->field($model, 'verifyCode')->widget(\yii\captcha\Captcha::classname(), [
                    'captchaAction' => 'email/captcha'
                    ]) ?>
                </div>
            <?php endif; ?>
            <div class="control-group">
                <label class="control-label" for=""></label>
                <div class="controls">
                    <?=Html::submitButton('Submit', ['class' => 'btn btn-success'])?>
                </div>
            </div>
        <?php ActiveForm::end()?>
    <?php endif;?>
    
  5. 现在,我们有一个电子邮件提交表单,如下面的截图所示,它验证电子邮件字段。让我们添加验证码:准备工作

如何操作...

  1. 首先,我们需要自定义表单模型。我们需要添加$verifyCode,它将保存输入的验证码,并为其添加一个验证规则:

    <?php
    namespace app\models;
    use yii\base\Model;
    use yii\captcha\Captcha;
    class EmailForm extends Model
    {
        public $email;
        public $verifyCode;
        public function rules()
        {
            return [
                ['email', 'email'],
                ['verifyCode', 'captcha', 'skipOnEmpty' => !Captcha::checkRequirements(), 'captchaAction' => 'email/captcha']
            ];
        }
    }
    
  2. 然后,我们需要向控制器添加一个外部操作。向其中添加以下代码:

    public function actions()
    {
        return [
            'captcha' => [
                'class' => 'yii\captcha\CaptchaAction',
            ],
        ];
    }
    
  3. 在视图中,我们需要显示一个额外的字段和验证码图片。以下代码将为我们完成这项工作:

    ...
    <?php if (Captcha::checkRequirements() && Yii::$app->user->isGuest): ?>
        <div class="control-group">
            <?=Captcha::widget([
                'model' => $model,
                'attribute' => 'verifyCode',
            ]);?>
            <?php echo Html::error($model, 'verifyCode')?>
        </div>
    <?php endif; ?>
    ...
    
  4. 同时,别忘了在视图的头部部分添加Captcha导入:

    <?php
        use yii\helpers\Html;
        use yii\captcha\Captcha;
    ?>
    ….
    
  5. 就这样。现在,你可以运行电子邮件控制器,并看到验证码的实际效果,如下面的截图所示:如何操作...

如果屏幕上没有错误,并且表单上没有Captcha字段,那么很可能是你没有安装和配置 GD PHP 或 Imagick 扩展。验证码需要 GD 或 Imagick,因为它生成图片。我们添加了几个Captcha::checkRequirements()检查,所以如果无法显示图片,应用程序将不会使用验证码,但它仍然可以工作。

它是如何工作的...

在视图中,我们调用验证码小部件,该小部件渲染带有指向我们添加到控制器中的验证码操作的src属性的img标签。在这个操作中,生成一个带有随机单词的图片。生成的单词是一个用户应该输入到表单中的代码。它存储在用户会话中,并向用户显示一个图片。当用户将电子邮件和验证码输入到表单中时,我们将这些值分配给表单模型,然后对其进行验证。对于验证码字段的验证,我们使用CaptchaValidator。它从用户会话中获取代码,并将其与输入的代码进行比较。如果它们不匹配,则模型数据被视为无效。

更多内容...

如果你通过使用accessRules控制器方法来限制对控制器操作的访问,别忘了授予每个人对这些操作的访问权限:

public function behaviors()
{
    return [
        'access' => [
            'class' => AccessControl::className(),
            'rules' => [
                [
                    'actions' => ['index', 'captcha'],
                    'allow' => true,
                ]
            ],
        ],
    ];
}

自定义验证码

标准的 Yii 验证码足以保护你免受垃圾邮件的侵扰,但有时你可能想对其进行自定义,例如以下情况:

  • 您面临一个能够读取图像文本的垃圾邮件机器人,您需要增加更多的安全性

  • 您希望使其更有趣或更容易输入 Captcha 文本

在我们的例子中,我们将修改 Yii 的 Captcha,使其要求用户解决一个真正的简单算术谜题,而不是仅仅重复图像中的文本。

准备工作

作为本例的起点,我们将采用 添加和自定义 CaptchaWidget 菜谱的结果。或者,您也可以采用任何使用 Captcha 的表单,因为我们并没有大量修改现有代码。

如何实现...

我们需要自定义 CaptchaAction,该动作生成代码并渲染其图像表示。代码应该是一个随机数,表示应该是一个给出相同结果的算术表达式:

  1. 创建一个 @app/components/MathCaptchaAction.php 动作,如下所示:

    <?php
    namespace app\components;
    use \Yii;
    use yii\captcha\CaptchaAction;
    class MathCaptchaAction extends CaptchaAction
    {
        protected function renderImage($code)
        {
            return parent::renderImage($this->getText($code));
        }
        protected function generateVerifyCode()
        {
            return mt_rand((int)$this->minLength,
            (int)$this->maxLength);
        }
        protected function getText($code)
        {
            $code = (int) $code;
            $rand = mt_rand(1, $code-1);
            $op = mt_rand(0, 1);
            if ($op) {
                return $code - $rand . " + "  . $rand;
            }
            else {
                return $code + $rand . " - " . " " . $rand;
            }
        }
    }
    
  2. 现在,在我们的控制器 actions 方法中,我们需要将 CaptchaAction 替换为我们的 Captcha 动作,如下所示:

    public function actions()
    {
        return [
            'captcha' => [
                'class' => 'app\components\MathCaptchaAction',
                'minLength' => 1,
                'maxLength' => 10,
            ],
        ];
    }
    
  3. 现在,运行您的表单并尝试新的 Captcha。它将显示从 1 到 10 的算术表达式,并要求输入答案,如下面的截图所示:如何实现...

我们重写了两个 CaptchaAction 方法。在 generateVerifyCode() 方法中,我们生成一个随机数而不是文本。然后,由于我们需要渲染一个表达式而不是仅仅显示文本,我们重写了 renderImage 方法。这个表达式本身是在我们的自定义 getText() 方法中生成的。

$minLength$maxLenght 属性已经在 CaptchaAction 中定义,所以我们不需要将它们添加到我们的 Math CaptchaAction 类中。

参见

如需更多信息,请参考以下内容:

创建一个自定义输入小部件

Yii 拥有一套非常好的表单小部件,但就像所有现有的框架一样,Yii 并没有全部拥有它们。在本菜谱中,我们将学习如何创建自己的输入小部件。在我们的例子中,我们将创建一个范围输入小部件。

准备工作

使用 Composer 软件包管理器创建一个新的应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guide-start-installation.html

如何实现...

  1. 创建一个小部件文件,@app/components/RangeInputWidget.php,如下所示:

    <?php
    namespace app\components;
    use yii\base\Exception;
    use yii\base\Model;
    use yii\base\Widget;
    use yii\helpers\Html;
    class RangeInputWidget extends Widget
    {
        public $model;
        public $attributeFrom;
        public $attributeTo;
        public $htmlOptions = [];
        protected function hasModel()
        {
            return $this->model instanceof Model&& $this->attributeFrom !== null&& $this->attributeTo !== null;}
        public function run()
        {
            if (!$this->hasModel()) {
                throw new Exception('Model must be set');
            }
            return Html::activeTextInput($this->model, $this->attributeFrom, $this->htmlOptions)
                .' &rarr; '
                .Html::activeTextInput($this->model, $this->attributeTo, $this->htmlOptions);
        }
    }
    
  2. 创建一个控制器文件,@app/controllers/RangeController.php,如下所示:

    <?php
    namespace app\controllers;
    use Yii;
    use yii\web\Controller;
    use app\models\RangeForm;
    class RangeController extends Controller
    {
        public function actionIndex()
        {
            $model = new RangeForm();
            if ($model->load(Yii::$app->request->post()) && $model->validate()) {
                Yii::$app->session->setFlash('rangeFormSubmitted',
                    'The form was successfully processed!'
                );
            }
            return $this->render('index', array(
                'model' => $model,
            ));
        }
    }
    
  3. 创建一个表单文件,@app/models/RangeForm.php,如下所示:

    <?php
    namespace app\models;
    use yii\base\Model;
    class RangeForm extends Model
    {
        public $from;
        public $to;
        public function rules()
        {
            return [
                [['from', 'to'], 'number', 'integerOnly' => true],
                ['from', 'compare', 'compareAttribute' => 'to', 'operator' => '<='],
            ];
        }
    }
    
  4. 创建一个视图文件,@app/views/range/index.php,如下所示:

    <?php
    use yii\helpers\Html;
    use yii\bootstrap\ActiveForm;
    use app\components\RangeInputWidget;
    ?>
    <h1>Range form</h1>
    <?php if (Yii::$app->session->hasFlash('rangeFormSubmitted')): ?>
        <div class="alert alert-success">
            <?= Yii::$app->session->getFlash('rangeFormSubmitted'); ?>
        </div>
    <?php endif?>
    <?= Html::errorSummary($model, ['class'=>'alert alert-danger'])?>
    <?php $form = ActiveForm::begin([
        'options' => [
            'class' => 'form-inline'
        ]
    ]); ?>
        <div class="form-group">
            <?= RangeInputWidget::widget([
                'model' => $model,
                'attributeFrom' => 'from',
                'attributeTo' => 'to',
                'htmlOptions' => [
                    'class' =>'form-control'
                ]
            ]) ?>
        </div>
        <?= Html::submitButton('Submit', ['class' => 'btn btn-primary', 'name' => 'contact-button']) ?>
    <?php ActiveForm::end(); ?>
    
  5. 通过打开index.php?r=range运行一个range控制器,你将得到以下内容:如何操作...

  6. 在第一个文本输入字段中输入200,在第二个输入字段中输入300,你将得到以下内容:如何操作...

  7. 如果第一个值大于第二个值,则小部件输出错误;就是这样。尝试为第一个和第二个输入分别输入正确的值100200如何操作...

它是如何工作的...

我们编写了范围输入小部件,它需要四个参数:

  • model:如果没有设置,将抛出异常

  • attributeFrom:用于设置最小范围值

  • attributeTo:用于设置最大范围值

  • htmlOptions:它传递给每个输入

此小部件用于表单验证,并设置为检查第一个值是否小于或等于第二个值。

更多...

Yii2 框架有一个官方的 Twitter Bootstrap 扩展,它为你提供了一组 Twitter Bootstrap 小部件的 PHP 包装器。在编写你自己的小部件之前,请检查www.yiiframework.com/doc-2.0/extbootstrap-index.html上是否存在 Bootstrap 小部件。

参见

为了了解更多关于小部件的信息,您可以使用以下资源:

表格输入

在本节中,我们将向您展示如何使用模型来保存和验证相关模型。有时您可能需要在单个表单中处理同一类型的多个模型。

例如,我们有比赛和比赛的奖品。任何比赛都可能包含无限数量的奖品。因此,我们需要创建带有奖品的比赛,验证它们,显示所有错误,并将主模型(比赛模型)以及所有相关模型(奖品模型)保存到数据库中。

准备工作

  1. 通过使用 Composer 包管理器创建新应用程序,如官方指南www.yiiframework.com/doc-2.0/guide-start-installation.html中所述。

  2. 使用以下命令为比赛和奖品表创建迁移:

    ./yii migrate/create create_table_contest_and_prize_table
    Update just created migration's methods up() and down() by following code
    public function up()
    {
        $tableOptions = null;
        if ($this->db->driverName === 'mysql') {
            $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
        }
        $this->createTable('{{%contest}}', [
            'id' => Schema::TYPE_PK,
            'name' => Schema::TYPE_STRING . ' NOT NULL',
        ], $tableOptions);
        $this->createTable('{{%prize}}', [
            'id' => Schema::TYPE_PK,
            'name' => Schema::TYPE_STRING,
            'amount' => Schema::TYPE_INTEGER,
        ], $tableOptions);
        $this->createTable('{{%contest_prize_assn}}', [
            'contest_id' => Schema::TYPE_INTEGER,
            'prize_id' => Schema::TYPE_INTEGER,
        ], $tableOptions);
        $this-addForeignKey('fk_contest_prize_assn_contest_id', '{{%contest_prize_assn}}', 'contest_id', {{%contest}}', 'id');
        $this->addForeignKey('fk_contest_prize_assn_prize_id', '{{%contest_prize_assn}}', 'prize_id', '{{%prize}}', 'id');
    }
    public function down()
    {
        $this-dropForeignKey('fk_contest_prize_assn_contest_id', '{{%contest_prize_assn}}');
        $this->dropForeignKey('fk_contest_prize_assn_prize_id', '{{%contest_prize_assn}}');
        $this->dropTable('{{%contest_prize_assn}}');
        $this->dropTable('{{%prize}}');
        $this->dropTable('{{%contest}}');
    }
    
  3. 然后,使用以下命令安装迁移:

    ./yii migrate/up
    
  4. 使用 Gii 生成比赛、奖品和ContestPrizeAssn模型。

如何操作...

  1. 让我们创建@app/controllers/ContestController.php,并使用以下代码:

    <?php
    namespace app\controllers;
    use app\models\Contest;
    use app\models\ContestPrizeAssn;
    use app\models\Prize;
    use Yii;
    use yii\base\Model;
    use yii\helpers\VarDumper;
    use yii\web\Controller;
    class ContestController extends Controller
    {
        public function actionCreate()
        {
            $contestName = 'Happy New Year';
            $firstPrize = new Prize();
            $firstPrize->name = 'Iphone 6s';
            $firstPrize->amount = 4;
            $secondPrize = new Prize();
            $secondPrize->name = 'Sony Playstation 4';
            $secondPrize->amount = 2;
            $contest = new Contest();
            $contest->name = $contestName;
            $prizes = [$firstPrize, $secondPrize];
            if ($contest->validate() && Model::validateMultiple($prizes)) {
                $contest->save(false);
                foreach ($prizes as $prize) {
                    $prize->save(false);
                    $contestPrizeAssn = new ContestPrizeAssn();
                    $contestPrizeAssn->prize_id = $prize->id;
                    $contestPrizeAssn->contest_id = $contest>id;
                    $contestPrizeAssn->save(false);
                }
                return $this->renderContent(
                    'All prizes have been successfully saved!'
                );
            } else {
                return $this->renderContent(
                    VarDumper::dumpAsString($contest->getErrors())
                );
            }
        }
        public function actionUpdate()
        {
            $prizes = Prize::find()->all();
            if (Model::loadMultiple($prizes, Yii::$app->request->post()) && Model::validateMultiple($prizes)) {
                foreach ($prizes as $prize) {
                    $prize->save(false);
                }
                return $this->renderContent(
                    'All prizes have been successfully saved!'
                );
            }
            return $this->render('update', ['prizes' => $prizes]);
        }
    }
    
  2. 创建@app/views/contest/update.php,并在其中放置以下代码:

    <?php
    use yii\helpers\Html;
    use yii\widgets\ActiveForm;
    $form = ActiveForm::begin();
    foreach ($prizes as $i => $prize) {
        echo $form->field($prize, "[$i]amount")->label($prize->name);
    }
    echo Html::submitButton('submit' , ['class' => 'btn btn-success']);
    ActiveForm::end();
    

它是如何工作的...

以下信息显示了如何在 Yii 中实现表格输入。

contest/update动作中,我们将能够显示所有奖品及其金额,并一次性编辑它们。我们使用了两个特殊的方法:

  • Model::loadMultiple():此方法使用来自最终用户的数据填充一组模型

  • Model::validateMultiple():此方法接受一组模型并将它们一次性全部验证

因为我们在使用validateMultiple()验证了我们的模型之后,所以我们在save()方法中传递false作为参数,以避免运行两次验证。

首先,访问/index.php?r=contest/create页面。访问后,你会看到将验证并创建带有两个奖项的'Happy New Year'页面,并将奖项传递给当前的比赛模型。你应该注意,我们只有在它们有效的情况下才会将比赛模型和奖项保存到数据库中:

如何操作...

它通过以下条件提供:

if ($contest->validate() && Model::validateMultiple($prizes)) { ...}

前往/index.php?r=contest/update页面,你会看到这个表单:

如何操作...

@app/views/contest/update.php中为每个奖项渲染一个名称和一个金额输入。我们必须为每个输入名称添加一个索引,以便Model::loadMultiple()能够识别用哪些值填充哪个模型。

总之,这种方法用于在处理来自视图表单的所有属性时收集表格输入数据,并从表单中填充父模型和相关模型。

参见

如需更多信息,请参阅以下 URL:

www.yiiframework.com/doc-2.0/guide-input-tabular-input.html#collecting-tabular-input

条件验证

有时候在模型中启用或禁用特定的验证规则是必要的。Yii2 提供了一个机制来实现这一点。

准备工作

通过使用官方指南中描述的 Composer 包管理器创建一个新的应用程序,该指南的网址为www.yiiframework.com/doc-2.0/guide-startinstallation.html

如何操作...

  1. 创建一个表单文件,@app/models/DeliveryForm.php,如下所示:

    <?php
    namespace app\models;
    use app\components\WordsValidator;
    use yii\base\Model;
    class DeliveryForm extends Model
    {
        const TYPE_PICKUP = 1;
        const TYPE_COURIER = 2;
        public $type;
        public $address;
        public function rules()
        {
            return [
                ['type', 'required'],
                ['type', 'in', 'range'=>[self::TYPE_PICKUP, self::TYPE_COURIER]],
                ['address', 'required', 'when' => function ($model) {
                    return $model->type == self::TYPE_COURIER;
                }, 'whenClient' => "function (attribute, value) {
                    return $('#deliveryform-type').val() == '".self::TYPE_COURIER."';
                }"]
            ];
        }
        public function typeList()
        {
            return [
                self::TYPE_PICKUP => 'Pickup',
                self::TYPE_COURIER => 'Courier delivery',
            ];
        }
    }
    
  2. 创建一个控制器文件,@app/controllers/ValidationController.php,如下所示:

    <?php
    namespace app\controllers;
    use Yii;
    use yii\web\Controller;
    use app\models\DeliveryForm;
    class ValidationController extends Controller
    {
        public function actionIndex()
        {
            $model = new DeliveryForm();
            if ($model->load(Yii::$app->request->post()) && $model->validate()) {
                Yii::$app->session->setFlash('success',
                    'The form was successfully processed!'
                );
            }
            return $this->render('index', array(
                'model' => $model,
            ));
        }
    }
    
  3. 创建一个视图文件,@app/views/validation/index.php,如下所示:

    <?php
    use yii\bootstrap\ActiveForm;
    use yii\helpers\Html;
    ?>
        <h1>Delivery form</h1>
        <?php if (Yii::$app->session->hasFlash('success')): ?>
        <div class="alert alert-success"><?= Yii::$app->session->getFlash('success'); ?></div>
        <?php endif; ?>
        <?php $form = ActiveForm::begin(); ?>
        <?= $form->field($model, 'type')->dropDownList($model->typeList(), [
            'prompt'=>'Select delivery type']
        ) ?>
        <?= $form->field($model, 'address') ?>
        <div class="form-group">
            <?= Html::submitButton('Submit', ['class' => 'btn btn-primary']) ?>
        </div>
    <?php ActiveForm::end(); ?>
    
  4. 通过打开index.php?r=validation URL 来运行validation控制器,并为类型输入选择courier delivery值;然后你会得到以下结果:如何操作...

如何操作...

type属性设置为DeliveryForm::TYPE_COURIER时,DeliveryForm address属性是必需的;否则,我们在type选择中选中Courier delivery选项。

此外,为了支持客户端条件验证,我们配置了whenClient属性,它接受一个表示 JavaScript 函数的字符串,其返回值决定是否应用该规则。

参见

如需更多信息,请参阅www.yiiframework.com/doc-2.0/guideinput-validation.html#conditional-validation

复杂的多模型表格

当处理一些复杂数据时,你可能需要使用多个不同的模型来收集用户输入。例如,你有一个包含用户信息(如名字、姓氏和电话号码)的订单表格;你还有一个送货地址和一些类型的产品。

你希望在一个表格中保存所有这些数据。使用 Yii 模型和支持表格,你可以轻松地做到这一点。假设用户信息将存储在用户表中,并在订单表格中,我们将保存产品信息和已订购产品的用户user_id。我们还有一个包含一些信息的产品表。

准备工作

  1. 按照官方指南中的描述,使用 Composer 包管理器创建一个新的应用程序,www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 使用以下命令为竞赛和奖品表创建迁移:

    ./yii migrate/create create_order_tables
    
  3. 使用以下代码更新新创建迁移的up()down()方法:

    <?php
    use yii\db\Schema;
    use yii\db\Migration;
    use app\models\Product;
    class m150813_161817_create_order_form_tables extends Migration
    {
        public function up()
        {
            $tableOptions = null;
            if ($this->db->driverName === 'mysql') {
                $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
            }
            $this->createTable('user', [
                'id' => Schema::TYPE_PK,
                'first_name' => Schema::TYPE_STRING . ' NOT NULL',
                'last_name' => Schema::TYPE_STRING . ' NOT NULL',
                'phone' => Schema::TYPE_STRING . ' NOT NULL',
            ], $tableOptions);
            $this->createTable('product', [
                'id' => Schema::TYPE_PK,
                'title' => Schema::TYPE_STRING . ' NOT NULL',
                'price' => Schema::TYPE_FLOAT . '(6,2) ',
            ], $tableOptions);
            $this->createTable('order', [
                'id' => Schema::TYPE_PK,
                'user_id' => Schema::TYPE_INTEGER . ' NULL',
                'address' => Schema::TYPE_STRING . ' NOT NULL',
                'product_id' => Schema::TYPE_INTEGER . ' NOT NULL',
            ], $tableOptions);
            $product1 = new Product();
            $product1->title = 'Iphone 6';
            $product1->price = 400.5;
            $product1->save();
            $product3 = new Product();
            $product3->title = 'Samsung Galaxy Note 5';
            $product3->price = 900;
            $product3->save();
            $this->addForeignKey('fk_order_product_id', 'order', 'product_id', 'product', 'id');
        }
        public function down()
        {
            $this->dropTable('order');
            $this->dropTable('user');
            $this->dropTable('product');
        }
    }
    
  4. 然后,使用以下命令安装迁移:

    ./yii migrate/up
    
  5. 使用 Gii 生成用户、订单和产品模型。

如何操作...

  1. 使用以下代码创建@app/controllers/TestController

    <?php
    namespace app\controllers;
    use app\models\Order;
    use app\models\User;
    use Yii;
    use yii\web\Controller;
    class TestController extends Controller
    {
        public function actionOrder()
        {
            $user = new User();
            $order = new Order();
            if ($user->load(Yii::$app->request->post()) && $order->load(Yii::$app->request->post())) {
            if ($user->validate() && $order->validate()) {
                $user->save(false);
                $order->user_id = $user->id;
                $order->save(false);
                $this->redirect(['/test/result', 'id' => $order->id]);
                }
            }
            return $this->render('order', ['user' => $user, 'order' => $order]);
        }
        public function actionResult($id)
        {
            $order = Order::find($id)->with('product', 'user')->one();
            return $this->renderContent(
                'Product: ' . $order->product->title . '</br>' .
                'Price: ' . $order->product->price . '</br>' .
                'Customer: ' . $order->user->first_name . ' ' . $order->user->last_name . '</br>' .
                'Address: ' . $order->address
            );
        }
    }
    
  2. 然后创建一个视图文件,@app/views/test/order.php,并添加以下代码:

    <?php
    use yii\helpers\Html;
    use yii\widgets\ActiveForm;
    use app\models\Product;
    use yii\helpers\ArrayHelper;
    /**
    * @var $user \app\models\User
    * @var $order \app\models\Order
    */
    $form = ActiveForm::begin([
        'id' => 'order-form',
        'options' => ['class' => 'form-horizontal'],
    ]) ?>
    <?= $form->field($user, 'first_name')->textInput(); ?>
    <?= $form->field($user, 'last_name')->textInput(); ?>
    <?= $form->field($user, 'phone')->textInput(); ?>
    <?= $form->field($order, 'product_id')->dropDownList(ArrayHelper::map(Product::find()->all(), 'id', 'title')); ?>
    <?= $form->field($order, 'address')->textInput(); ?>
    <?= Html::submitButton('Save', ['class' => 'btn btn-primary']) ?>
    <?php ActiveForm::end() ?>
    

如何工作...

你可以在http://yii-book.app/index.php?r=test/order看到这个表格。我们的表格从用户和订单模型收集信息。

让我们填写我们的表格:

如何工作...

保存后,你会看到以下结果:

如何工作...

在控制器中,我们验证并存储它。当然,这个例子非常简单。在实际项目中,你可能会有多个模型,并且你可以使用这种方法来处理它们。当你想在同一个表格中创建或更新多个实例时,这种方法非常有用。

相关内容

对于更多信息,请参阅www.yiiframework.com/doc-2.0/guide-input-multiple-models.html

AJAX 依赖的下拉列表

通常,你可能需要一个带有两个下拉菜单的表格,其中一个下拉菜单的值将依赖于另一个下拉菜单的值。使用 Yii 内置的 AJAX 功能,你可以创建这样的下拉菜单。

准备工作

  1. 按照官方指南中的描述,使用 composer 创建一个新的应用程序,www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 按照以下方式创建一个@app/model/Product.php模型:

    <?php
    namespace app\models;
    use yii\db\ActiveRecord;
    class Product extends ActiveRecord
    {
        public function rules()
        {
            return [
                ['title', 'string'],
                [['title', 'category_id', 'sub_category_id'], 'required'],
                ['category_id', 'exist', 'targetAttribute' => 'id', 'targetClass' => 'app\models\Category'],
                ['sub_category_id', 'exist', 'targetAttribute' => 'id', 'targetClass' => 'app\models\Category'],
            ];
        }
        public function attributeLabels()
        {
            return [
                'category_id' => 'Category',
                'sub_category_id' => 'Sub category',
            ];
   }
    }
    
  3. 按照以下方式创建一个@app/models/Category.php模型:

    <?php
    namespace app\models;
    use yii\db\ActiveRecord;
    class Category extends ActiveRecord
    {
        public function rules()
        {
            return [
                ['title', 'string'],
            ];
        }
        /**
        * @return array
        */
        public static function getSubCategories($categoryId)
        {
            $subCategories = [];
            if ($categoryId) {
                $subCategories = self::find()
                    ->where(['category_id' => $categoryId])
                    ->asArray()
                    ->all();
            }
            return $subCategories;
        }
    }
    
  4. 使用以下命令创建create_category_and_product_tables迁移:

    ./yii migrate/create create_category_and_product_tables
    
  5. 按照以下方式更新刚刚创建的迁移的方法和导入的类列表:

    <?php
    use yii\db\Schema;
    use yii\db\Migration;
    class m150813_005030_create_categories extends Migration
    {
        public function up()
        {
            $tableOptions = null;
            $this->createTable('{{%product}}', [
                'id' => Schema::TYPE_PK,
                'category_id' => Schema::TYPE_INTEGER . ' NOT NULL',
                'sub_category_id' => Schema::TYPE_INTEGER . ' NOT NULL',
                'title' => Schema::TYPE_STRING . ' NOT NULL',
            ], $tableOptions);
            $this->createTable('{{%category}}', [
                'id' => Schema::TYPE_PK,
                'category_id' => Schema::TYPE_INTEGER,
                'title' => Schema::TYPE_STRING . ' NOT NULL',
            ], $tableOptions);
            $this->addForeignKey('fk_product_category_id', '{{%product}}', 'category_id', '{{%category}}', 'id');
            $this->addForeignKey('fk_product_sub_category_id', '{{%product}}', 'category_id', '{{%category}}', 'id');
            $this->batchInsert('{{%category}}', ['id', 'title'], [
                [1, 'TV, Audio/Video'],
                [2, 'Photo'],
                [3, 'Video']
            ]);
            $this->batchInsert('{{%category}}', ['category_id', 'title'], [
                [1, 'TV'],
                [1, 'Acoustic System'],
                [2, 'Cameras'],
                [2, 'Flashes and Lenses '],
                [3, 'Video Cams'],
                [3, 'Action Cams'],
                [3, 'Accessories']
            ]);
        }
        public function down()
        {
            $this->dropTable('{{%product}}');
            $this->dropTable('{{%category}}');
        }
    }
    

如何操作...

  1. 按照以下方式创建控制器文件,@app/controllers/DropdownController.php

    <?php
    namespace app\controllers;
    use app\models\Product;
    use app\models\Category;
    use app\models\SubCategory;
    use Yii;
    use yii\helpers\ArrayHelper;
    use yii\helpers\Json;
    use yii\web\Controller;
    use yii\web\HttpException;
    class DropdownController extends Controller
    {
        public function actionGetSubCategories($id)
        {
            if (!Yii::$app->request->isAjax) {
                throw new HttpException(400, 'Only ajax request is allowed.');
            }
            return Json::encode(Category::getSubCategories($id));
        }
        public function actionIndex()
        {
            $model = new Product();
            if ($model->load(Yii::$app->request->post()) && $model->validate()) {
                Yii::$app->session->setFlash('success',
                'Model was successfully saved'
                );
            }
            return $this->render('index', [
                'model' => $model,
            ]);
        }
    }
    
  2. 创建一个视图文件,@app/views/dropdown/index.php,如下所示:

    <?php
    use yii\bootstrap\ActiveForm;
    use yii\helpers\Html;
    use yii\helpers\Url;
    use app\models\Category;
    use yii\helpers\ArrayHelper;
    use yii\web\View;
    $url = Url::toRoute(['dropdown/get-sub-categories']);
    $this->registerJs("
    (function(){
        var select = $('#product-sub_category_id');
        var buildOptions = function(options) {
            if (typeof options === 'object') {
                select.children('option').remove();
                $('<option />')
                    .appendTo(select)
                    .html('Select a sub category')
                $.each(options, function(index, option) {
                    $('<option />', {value:option.id})
                    .appendTo(select)
                    .html(option.title);
                });
            }
        };
        var categoryOnChange = function(category_id){
            $.ajax({
                dataType: 'json',
                url: '" . $url . "&id=' + category_id ,
                success: buildOptions
            });
        };
        window.buildOptions = buildOptions;
        window.categoryOnChange = categoryOnChange;
    })();
    ", View::POS_READY);
    ?>
    <h1>Product</h1>
    <?php if (Yii::$app->session->hasFlash('success')): ?>
        <div class="alert alert-success"><?= Yii::$app->session->getFlash('success'); ?></div>
    <?php endif; ?>
    <?php $form = ActiveForm::begin(); ?>
        <?= $form->field($model, 'title')->textInput() ?>
        <?= $form->field($model, 'category_id')->dropDownList(ArrayHelper::map(
            Category::find()->where('category_id IS NULL')->asArray()->all(),'id', 'title'), [
            'prompt' => 'Select a category',
            'onChange' => 'categoryOnChange($(this).val());',
        ]) ?>
        <?= $form->field($model, 'sub_category_id')->dropDownList(
            ArrayHelper::map(Category::getSubCategories($model->sub_category_id), 'id' ,'title'), [
            'prompt' => 'Select a sub category',
        ]) ?>
        <div class="form-group">
            <?= Html::submitButton('Submit', ['class' => 'btn btn-primary']) ?>
        </div>
    <?php ActiveForm::end(); ?>
    
  3. 通过打开 index.php?r=dropdown 来运行 dropdown 控制器,然后为标题字段添加一个新产品 Canon - EOS Rebel T6i DSLR 的值:如何操作...

  4. 如您所见,Category 输入有三个选项。让我们选择 照片 选项,然后第二个输入选择将有两个进一步选项:如何操作...

  5. 就这样。如果您选择另一个类别,您将得到该类别的子类别。

它是如何工作的...

在这个例子中,我们有两个依赖于类别和子类别的列表,以及一个模型 Category。主要思想很简单:我们只是将 JQuery 的 onChange 事件绑定到我们表单中的 category_id 字段。每次用户更改此字段时,我们的应用程序都会向 get-sub-categories 动作发送一个 AJAX 请求。此操作返回一个以 JSON 格式编写的子类别列表,然后,在客户端,我们为子类别列表构建一个选项列表。

AJAX 验证

一些验证只能在服务器端进行,因为只有服务器才有必要的信息。例如,为了验证公司名称或用户电子邮件是否唯一,我们必须检查服务器端的相应表。在这种情况下,您应该使用内置的 AJAX 验证。Yii2 支持 AJAX 表单验证,它本质上会将表单值发送到服务器,进行验证,并将验证错误以 JSON 格式发送回来。它会在您每次从(更改的)字段中移出标签时执行此操作。

准备工作

使用官方指南中描述的 Composer 软件包管理器创建一个新应用程序,www.yiiframework.com/doc-2.0/guide-startinstallation.html

如何操作...

  1. 在基本应用模板中,我们有一个简单的联系表单。您可以在 http://yii-book.app/index.php?r=site/contact 查看此页面。打开并修改相关的视图表单,@app/views/site/contact.php。要为整个表单启用 AJAX 验证,请在 form 配置中将 enableAjaxValidation 选项设置为 true

    $form = ActiveForm::begin([
        'id' => 'contact-form',
        'enableAjaxValidation' => true,
    ]);
    
  2. 此外,您应该在服务器端添加对 AJAX 验证的处理。此代码片段仅检查当前请求是否为 AJAX,并且是否为 POST 请求。如果是,我们将以 JSON 格式接收错误:

    if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) {
        Yii::$app->response->format = Response::FORMAT_JSON;
        return ActiveForm::validate($model);
    }
    
  3. 让我们用以下代码修改 SiteController 中的 actionContact()

    public function actionContact()
    {
        $model = new ContactForm();
        if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) {
            Yii::$app->response->format = Response::FORMAT_JSON;
            return ActiveForm::validate($model);
        }
        if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) {
            Yii::$app->session->setFlash('contactFormSubmitted');
            return $this->refresh();
        } else {
            return $this->render('contact', [
                'model' => $model,
            ]);
        }
    }
    

它是如何工作的...

之前的代码将检查当前请求是否为 AJAX。如果是,它将通过运行验证并以 JSON 格式返回错误来响应此请求。

您可以在浏览器的调试面板中检查来自服务器的响应。尝试提交一个空表单,您将看到响应。

例如,在 Google Chrome 浏览器中,按 F12 并在开发工具栏中选择 网络 选项卡。您将看到包含错误和消息的 JSON 数组:

如何工作...

参见

www.yiiframework.com/doc-2.0/guide-input-validation.html#ajaxvalidation

创建自定义客户端验证

编写自己的验证器 菜谱中,我们创建了一个独立的验证器。在这个菜谱中,我们将修改一个验证器以创建额外的客户端验证,该验证器也会检查单词数量。

准备工作

使用 Composer 包管理器创建一个新的应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guide-startinstallation.html

如何做到...

  1. 按照以下方式创建 @app/components/WordsValidator.php:

    <?php
    namespace app\components;
    use yii\validators\Validator;
    class WordsValidator extends Validator
    {
        public $size = 50;
        public $message = 'The number of words must be less than {size}';
        public function validateValue($value)
        {
            preg_match_all('/(\w+)/i', $value, $matches);
            if (count($matches[0]) > $this->size) {
                return [$this->message, ['size' => $this->size]];
            }
        }
        public function clientValidateAttribute($model, $attribute, $view)
        {
            $message = strtr($this->message, ['{size}' => $this->size]);
            return <<<JS
            if (value.split(/\w+/gi).length > $this->size ) {
                messages.push("$message");
            }
            JS;
        }
    }
    
  2. 按照以下方式创建 @app/models/Article.php:

    <?php
    namespace app\models;
    use app\components\WordsValidator;
    use yii\base\Model;
    class Article extends Model
    {
        public $title;
        public function rules()
        {
            return [
                ['title', 'string'],
                ['title', WordsValidator::className(), 'size' => 10],
            ];
        }
    }
    
  3. 按照以下方式创建 @app/controllers/ValidationController.php:

    <?php
    namespace app\controllers;
    use app\models\Article;
    use Yii;
    use yii\web\Controller;
    class ValidationController extends Controller
    {
        public function actionIndex()
        {
            $model = new Article();
            if ($model->load(Yii::$app->request->post()) && $model->validate()) {
                Yii::$app->session->setFlash('success', 'Model is valid');
            }
            return $this->render('index', [
                'model' => $model,
            ]);
        }
    }
    
  4. 按照以下方式创建 @app/views/validation/index.php:

    <?php
    use yii\bootstrap\ActiveForm;
    use yii\helpers\Html;
    ?>
    <h1>Article form</h1>
    <?php if (Yii::$app->session->hasFlash('success')): ?>
        <div class="alert alert-success"><?= Yii::$app->session->getFlash('success'); ?></div>
    <?php endif; ?>
    <?php $form = ActiveForm::begin(); ?>
        <?= $form->field($model, 'title') ?>
        <div class="form-group">
            <?= Html::submitButton('Submit', ['class' => 'btn btn-primary']) ?>
        </div>
    <?php ActiveForm::end(); ?>
    

如何工作...

通过打开 index.php?r=validation 来运行验证控制器。如果你输入超过十个单词,你会看到一个错误的值示例:

如何工作...

如果你输入的单词少于十个,客户端验证将成功:

如何工作...

首先,我们创建了 @app/components/WordsValidator.php,它扩展了 @yii\validators\Validator 类,并将新创建的验证器类添加到 Article 模型的标题属性中:

..
['title', WordsValidator::className(), 'size' => 10],
..

在我们的验证器内部,我们定义了两个特殊方法:validateValue()clientValidateAttribute()

我们的验证器类实现了 validateValue() 方法,以支持在数据模型上下文之外进行数据验证。第二个方法仅返回执行客户端验证所需的 JavaScript。

还有更多...

如果我们想隐藏验证器的实现,或者只想在服务器端控制所有验证过程,我们可以创建一个 Deferred 对象。

首先,修改 WordsValidator 验证器如下:

<?php
namespace app\components;
use yii\validators\Validator;
use yii\helpers\Url;
class WordsValidator extends Validator
{
    public $size = 50;
    public $message = 'The number of words must be less than {size}';
    public function validateValue($value)
    {
        if (str_word_count($value) > $this->size) {
            return ['The number of words must be less than {size}', ['size' => $this->size]];
        }
        return false;
    }
    public function clientValidateAttribute($model, $attribute, $view)
    {
        $url = Url::toRoute(['validation/check-words']);
        return <<<JS
        deferred.push($.get("$url", {words: value}).done(function(data) {
            if (!data.result) {
                messages.push(data.error);
            }
        }));
        JS;
    }
}

在前面的代码中,deferred 变量是由 Yii 提供的,它是一个包含 Deferred 对象的数组。$.get() jQuery 方法创建一个 Deferred 对象,并将其推送到 deferred 数组中。

第二步,将此 checkWords 动作添加到 validation 控制器中:

public function actionCheckWords()
{
    \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
    $value = Yii::$app->getRequest()->get('words');
    $validator = new WordsValidator([
    'size' => 10,
    ]);
    $result = $validator->validate($value, $error);
    return ['result' => $result,'error' => $error
    ];
}

参见

对于更多信息,请参考以下 URL:

第五章:安全

在本章中,我们将涵盖以下主题:

  • 认证

  • 使用控制器过滤器

  • 防止 XSS

  • 防止 SQL 注入

  • 防止 CSRF

  • 使用 RBAC

  • 加密/解密数据

简介

安全是任何 Web 应用程序的关键部分。

在本章中,您将学习如何根据通用 Web 应用程序安全原则“过滤输入,转义输出”来保持您的应用程序安全。我们将涵盖创建自己的控制器过滤器、防止 XSS、CSRF 和 SQL 注入、转义输出以及使用基于角色的访问控制等主题。要了解安全最佳实践,请参阅www.yiiframework.com/doc-2.0/guide-security-best-practices.html#avoiding-debug-info-and-tools-at-production

认证

大多数 Web 应用程序都为用户提供了一种登录或重置忘记的密码的方式。在 Yii2 中,默认情况下我们没有这个机会。对于 basic 应用程序模板,Yii 默认提供了两个测试用户,这些用户在 User 模型中静态描述。因此,我们必须实现特殊的代码才能从数据库中启用用户登录。

准备工作

  1. 按照官方指南使用 Composer 包管理器创建一个新应用程序,官方指南请参阅www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 在您的配置组件部分添加:

    'user' => [
        'identityClass' => 'app\models\User',
        'enableAutoLogin' => true,
    ],
    
  3. 创建一个 User 表。通过输入以下命令创建迁移:

    ./yii migrate/create create_user_table
    
    
  4. 使用以下代码更新刚刚创建的迁移:

    <?php
    
    use yii\db\Schema;
    use yii\db\Migration;
    
    class m150626_112049_create_user_table extends Migration
    {
      public function up()
      {
          $tableOptions = null;
          if ($this->db->driverName === 'mysql') {
              $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
          }
    
          $this->createTable('{{%user}}', [
              'id' => Schema::TYPE_PK,
              'username' => Schema::TYPE_STRING . ' NOT NULL',
              'auth_key' => Schema::TYPE_STRING . '(32) NOT NULL',
              'password_hash' => Schema::TYPE_STRING . ' NOT NULL',
              'password_reset_token' => Schema::TYPE_STRING,
          ], $tableOptions);
      }
    
      public function down()
      {
          $this->dropTable('{{%user}}');
      }
    }
    
  5. 使用以下代码更新现有的 models/User 模型:

    <?php
    
    namespace app\models;
    use yii\db\ActiveRecord;
    use yii\web\IdentityInterface;
    use yii\base\NotSupportedException;
    use Yii;
    
    class User extends ActiveRecord implements IdentityInterface
    {
      /**
       * @inheritdoc
       */
      public function rules()
      {
          return [
    
              ['username', 'required'],
              ['username', 'unique'],
              ['username', 'string', 'min' => 3],
              ['username', 'match', 'pattern' => '~^[A-Za-z][A-Za-z0-9]+$~', 'message' => 'Username can contain only alphanumeric characters.'],
    
              [['username', 'password_hash', 'password_reset_token'],
                  'string', 'max' => 255
              ],
              ['auth_key', 'string', 'max' => 32],
          ];
      }
    
      /**
       * @inheritdoc
       */
      public static function findIdentity($id)
      {
          return static::findOne($id);
      }
    
      public static function findIdentityByAccessToken($token, $type = null)
      {
          throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.');
      }
    
      /**
       * Finds user by username
       *
       * @param  string      $username
       * @return User
       */
      public static function findByUsername($username)
      {
          return static::findOne(['username' => $username]);
      }
      /**
       * @inheritdoc
       */
      public function getId()
      {
          return $this->getPrimaryKey();
      }
    
      /**
       * @inheritdoc
       */
      public function getAuthKey()
      {
          return $this->auth_key;
      }
    
      /**
       * @inheritdoc
       */
      public function validateAuthKey($authKey)
      {
          return $this->getAuthKey() === $authKey;
      }
    
      /**
       * Validates password
       *
       * @param  string  $password password to validate
       * @return boolean if password provided is valid for current user
       */
      public function validatePassword($password)
      {
          return Yii::$app->getSecurity()->validatePassword($password, $this->password_hash);
      }
    
      /**
       * Generates password hash from password and sets it to the model
       *
       * @param string $password
       */
      public function setPassword($password)
      {
          $this->password_hash = Yii::$app->getSecurity()->generatePasswordHash($password);
      }
    
      /**
       * Generates "remember me" authentication key
       */
      public function generateAuthKey()
      {
          $this->auth_key = Yii::$app->getSecurity()->generateRandomString();
      }
    
      /**
       * Generates new password reset token
       */
      public function generatePasswordResetToken()
      {
          $this->password_reset_token = Yii::$app->getSecurity()->generateRandomString() . '_' . time();
      }
    
      /**
       * Finds user by password reset token
       *
       * @param  string      $token password reset token
       * @return static|null
       */
    
      public static function findByPasswordResetToken($token)
      {
          $expire = Yii::$app->params['user.passwordResetTokenExpire'];
          $parts = explode('_', $token);
          $timestamp = (int) end($parts);
          if ($timestamp + $expire < time()) {
              return null;
          }
          return static::findOne([
              'password_reset_token' => $token
          ]);
      }
    }
    
  6. 创建一个迁移,该迁移将添加一个测试用户。使用以下命令:

    ./yii migrate/create create_test_user
    
    
  7. 使用以下代码更新刚刚创建的迁移:

    <?php
    
    use yii\db\Migration;
    use app\models\User;
    
    class m150626_120355_create_test_user extends Migration
    {
      public function up()
      {
          $testUser = new User();
          $testUser->username = 'admin';
          $testUser->setPassword('admin');
          $testUser->generateAuthKey();
          $testUser->save();
    
      }
    
      public function down()
      {
          User::findByUsername('turbulence')->delete();
          return false;
      }
    }
    
  8. 使用以下命令安装所有迁移:

    ./yii migrate up
    
    

如何操作...

  1. 现在,跟随 URL site/login 动作并使用 admin/admin 作为您的凭据:如何操作...

  2. 恭喜!如果您已完成这些步骤,您应该能够登录。

如何工作...

  1. 首先,我们为用户表创建了一个迁移。除了我们的 ID 和用户名外,我们的表还包含一些特殊字段,如 auth_key(此字段的主要用途是通过 cookie 验证用户),password_hash(出于安全原因,我们不会存储密码本身,而只存储密码哈希),以及 password_reset_token(在我们需要重置用户密码时使用)。

  2. 安装后和 create_test_user 迁移的结果应如下截图所示:它是如何工作的...

我们还向 User 模型添加了特殊方法,并将继承改为 class User extends ActiveRecord implements IdentityInterface,因为我们需要能够在数据库中找到用户。

您还可以从高级应用程序 github.com/yiisoft/yii2-app-advanced/blob/master/common/models/User.php 复制 User 模型。

参见

更多信息,请参阅 www.yiiframework.com/doc-2.0/guide-security-authentication.html

使用控制器过滤器

在许多情况下,我们需要根据数据过滤传入的数据或执行某些操作。例如,使用自定义过滤器,我们可以根据 IP 过滤访客,强制用户使用 HTTPS,或者在用户使用应用程序之前将其重定向到安装页面。

在 Yii2 中,过滤器本质上是一种特殊的行为,因此使用过滤器与使用行为相同。

Yii 有很多内置的可用过滤器,包括:

  • 核心

  • 自定义

  • 认证

  • 内容协商

  • HttpCache

  • 页面缓存

  • RateLimiter

  • 请求动词

  • 跨源资源共享(CORS)

在本配方中,我们将实现以下内容:

  • 仅允许授权用户访问控制器操作

  • 限制控制器操作对指定 IP 的访问

  • 限制对特定用户角色的访问

准备工作

  1. 使用 Composer 软件包管理器创建一个新应用程序,如官方指南 www.yiiframework.com/doc-2.0/guide-start-installation.html 中所述。

  2. 创建 app/components/AccessRule.php

    <?php
    
    namespace app\components;
    
    use app\models\User;
    class AccessRule extends \yii\filters\AccessRule {
    
      /**
       * @inheritdoc
       */
      protected function matchRole($user)
      {
          if (empty($this->roles)) {
              return true;
          }
          $isGuest = $user->getIsGuest();
          foreach ($this->roles as $role) {
              switch($role) {
                  case '?':
                      return ($isGuest) ? true : false;
                  case User::ROLE_USER:
                      return (!$isGuest) ? true : false;
                  case $user->identity->role: // Check if the user is logged in, and the roles match
    
                      return (!$isGuest) ? true : false;
                  default:
                      return false;
              }
          }
          return false;
      }
    }
    
  3. 按如下方式创建 app/controllers/AccessController.php

    <?php
    
    namespace app\controllers;
    use app\models\User;
    use Yii;
    use yii\filters\AccessControl;
    use app\components\AccessRule;
    use yii\web\Controller;
    
    class AccessController extends Controller
    {
      public function behaviors()
      {
          return [
              'access' => [
                  'class' => AccessControl::className(),
                  // We will override the default rule config with the new AccessRule class
                  'ruleConfig' => [
                      'class' => AccessRule::className(),
                  ],
                  'rules' => [
                      [
                          'allow' => true,
                          'actions' => ['auth-only'],
                          'roles' => [User::ROLE_USER]
                      ],
                      [
                          'allow' => true,
                          'actions' => ['ip'],
                          'ips' => ['127.0.0.1'],
                      ],
                      [
                          'allow' => true,
                          'actions' => ['user'],
                          'roles' => [ User::ROLE_ADMIN],
                      ],
                      [
                          'allow' => false,
                      ]
                  ],
              ]
          ];
      }
    
      public function actionAuthOnly()
      {
          echo "Looks like you are authorized to run me.";
      }
      public function actionIp()
      {
          echo "Your IP is in our list. Lucky you!";
      }
      public function actionUser()
      {
          echo "You're the right man. Welcome!";
      }
    }
    
  4. 按如下方式修改 User 类:

    <?php
    
    namespace app\models;
    
    class User extends \yii\base\Object implements \yii\web\IdentityInterface
    {
     // add roles contstants 
      CONST ROLE_USER  = 200;
      CONST ROLE_ADMIN  = 100; 
    
      public $id;
      public $username;
      public $password;
      public $authKey;
      public $accessToken;
      public $role;
    
      private static $users = [
          '100' => [
              'id' => '100',
              'username' => 'admin',
              'password' => 'admin',
              'authKey' => 'test100key',
              'accessToken' => '100-token',
              'role' => USER::ROLE_ADMIN // add admin role for admin user
          ],
          '101' => [
              'id' => '101',
              'username' => 'demo',
              'password' => 'demo',
              'authKey' => 'test101key',
              'accessToken' => '101-token',
              'role' => USER::ROLE_USER // add user role for admin user
          ],
      ];
    …
    }
    

如何操作...

  1. 要使用 AccessControl,在控制器类的 behaviors() 方法中声明它。我们这样做如下:

    public function behaviors()
    {
      return [
          'access' => [
              'class' => AccessControl::className(),
              'rules' => [
                  [
                      'allow' => true,
                      'actions' => ['auth-only'],
                      'roles' => ['@'],
                  ],
                  [
                      'allow' => true,
                      'actions' => ['ip'],
                      'ips' => ['127.0.0.1'],
                  ],
                  [
                      'allow' => true,
                      'actions' => ['user'],
                        'roles' => ['admin'],
                  ],
                  [
                    'allow' => true,
                    'actions' => ['user'],
                    'matchCallback' => function ($rule, $action) {
                      return preg_match('/MSIE 9/',$_SERVER['HTTP_USER_AGENT']) !== false;
                    }
                  ],
    
                  ['allow' => false]
              ],
          ]
      ];
    }
    
  2. 现在尝试使用 Internet Explorer 和其他浏览器通过使用 admindemo 用户名来运行控制器操作。

工作原理...

我们将从仅允许授权用户访问控制器操作开始。请参阅 rules 数组中的以下代码:

[
  'allow' => true,
  'actions' => ['auth-only'],
  'roles' => [User::ROLE_USER]
],

这里每个数组都是一个访问规则。您可以使用 allow=trueallow=false 为拒绝规则。对于每个规则,都有几个参数。

默认情况下,Yii 不拒绝所有内容,因此如果您需要最大安全性,请考虑在您的规则列表末尾添加 ['allow' => false]

在我们的规则中,我们使用两个参数。第一个是动作参数,它接受一个动作数组,规则将应用于这些动作。第二个是角色参数,它接受一个用户角色数组,以确定此规则应用于哪些用户。

Yii2 的内置访问控制默认只支持两个角色:访客(未登录),用 ? 表示,和认证用户,用 @ 表示。

通过简单的访问控制,我们可以根据登录状态限制对特定页面或控制器操作的访问。如果用户在访问这些页面时未登录,Yii 将将他们重定向到登录页面。

规则按顺序执行,从顶部开始,直到匹配为止。如果没有匹配项,则将操作视为允许。

下一个任务是限制对特定 IP 的访问。在这种情况下,涉及以下两个访问规则:

              [
                  'allow' => true,
                  'actions' => ['ip'],
                  'ips' => ['127.0.0.1'],
              ],

第一条规则允许来自指定 IP 列表的 IP 访问 IP 操作。在我们的例子中,我们使用的是回环地址,它始终指向我们自己的计算机。尝试将其更改为127.0.0.2,例如,以查看当地址不匹配时它是如何工作的。第二条规则拒绝所有内容,包括所有其他 IP。

接下来,我们限制对特定用户角色的访问,如下所示:

[
  'allow' => true,
  'actions' => ['user'],
  'roles' => [ User::ROLE_ADMIN],
],

前一条规则允许具有等于admin角色的用户运行用户操作。因此,如果你以admin身份登录,它将允许你进入,但如果你以demo身份登录,则不会。

如何工作...

我们已经覆盖了我们自己的标准AccessRule类,该类位于components/AccessRule.php文件中。在我们的AccessRule类中,我们覆盖了自己的matchRole方法,在那里我们获取并检查当前用户角色,并将其与我们的规则中的角色匹配。

最后,我们需要拒绝对特定浏览器的访问。对于这个配方,我们只拒绝 Internet Explorer 9。规则本身放在最上面,因此它首先执行,如下所示:

[
  'allow' => true,
  'actions' => ['user'],
  'matchCallback' => function ($rule, $action) {
      return preg_match('/MSIE 9/',$_SERVER['HTTP_USER_AGENT'])!== false;
  }
],

我们所使用的检测技术并不非常可靠,因为 MSIE 包含在许多其他用户代理字符串中。有关可能的用户代理字符串列表,您可以参考www.useragentstring.com/

在前面的代码中,我们使用了另一个过滤器规则属性,名为'matchCallback'。此属性仅在描述在此属性中的函数返回true时应用。

我们的功能检查用户代理字符串是否包含 MSIE 9.0 字符串。根据您的需求,您可以指定任何 PHP 代码。

参见

为了了解更多关于访问控制和过滤器的内容,请参考以下链接:

防止 XSS

XSS 代表跨站脚本,是一种允许在由其他用户查看的页面上注入客户端脚本(通常是 JavaScript)的漏洞。考虑到客户端脚本的力量,这可能导致非常严重的后果,如绕过安全检查、获取其他用户的凭证或数据泄露。

在这个菜谱中,我们将看到如何通过使用 \yii\helpers\Html\yii\helpers\HtmlPurifier 转义输出来防止 XSS。

准备中

  1. 按照官方指南使用 Composer 包管理器创建一个新应用,官方指南链接为 www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 创建 controllers/XssController.php

    <?php
    
    namespace app\controllers;
    
    use Yii;
    use yii\helpers\Html;
    use yii\web\Controller;
    
    /**
    * Class SiteController.
    * @package app\controllers
    */
    class XssController extends Controller
    {
       /**
        * @return string
        */
       public function actionIndex()
       {
           $username = Yii::$app->request->get('username', 'nobody');
    
           return $this->renderContent(Html::tag('h1',
               'Hello, ' . $username . '!'
           ));
       }
    }
    
  3. 通常,它将被用作 /xss/simple?username=Administrator。然而,由于没有考虑到主要的安全原则 过滤输入,转义输出,恶意用户将能够以下这种方式使用它:

    /xss/simple?username=<script>alert('XSS');</script>
    
  4. 之前的代码将导致脚本执行,如下截图所示:准备中

如何操作...

执行以下步骤:

  1. 为了防止之前截图中的 XSS 警告,我们需要在传递给浏览器之前转义数据。我们这样做如下:

    <?php
    
    namespace app\controllers;
    
    use Yii;
    use yii\helpers\Html;
    use yii\web\Controller;
    
    /**
    * Class SiteController.
    * @package app\controllers
    */
    class XssController extends Controller
    {
       /**
        * @return string
        */
       public function actionIndex()
       {
           $username = Yii::$app->request->get('username', 'nobody');
    
           return $this->renderContent(Html::tag('h1',
               Html::encode('Hello, ' . $username . '!')
           ));
       }
    }
    
  2. 现在,我们将得到正确转义的 HTML,如下截图所示:如何操作...

  3. 因此,基本规则是始终转义所有动态数据。例如,我们也应该对链接名称做同样的处理:

    use \yii\helpers\Html;
    
    echo Html::a(Html::encode($_GET['username']), array());
    

就这样。你有一个没有 XSS 的页面。现在,如果我们想允许一些 HTML 通过,我们不能再使用 \yii\helpers\Html::encode,因为它会将 HTML 渲染为代码,而我们需要实际的表示。幸运的是,Yii 中有一个工具可以用来过滤恶意 HTML。它被称为 HTML Purifier,可以使用以下方式:

<?php

namespace app\controllers;

use Yii;
use yii\helpers\Html;
use yii\helpers\HtmlPurifier;
use yii\web\Controller;

/**
* Class SiteController.
* @package app\controllers
*/
class XssController extends Controller
{
   /**
    * @return string
    */
   public function actionIndex()
   {
       $username = Yii::$app->request->get('username', 'nobody');

       $content = Html::tag('h1', 'Hello, ' . $username . '!');

       return $this->renderContent(
           HtmlPurifier::process($content)
       );
   }
}

现在如果我们通过类似 /xss/index?username=<i>username</i>!<script>alert('XSS')</script> 的 URL 访问 HTML 动作,HTML Purifier 将移除恶意部分,我们得到以下结果:

如何操作...

它是如何工作的...

  1. 在内部,\yii\helpers\Html::encode 看起来如下:

    public static function encode($content, $doubleEncode = true)
    {
       return htmlspecialchars($content, ENT_QUOTES | ENT_SUBSTITUTE, Yii::$app ? Yii::$app->charset : 'UTF-8', $doubleEncode);
    }
    
  2. 因此,基本上,我们使用 PHP 的内部 htmlspecialchars 函数,如果不会忘记在第三个参数中传递正确的字符集,它相当安全。

\yii\helpers\HtmlPurifier 使用 HTML Purifier 库,这是目前最先进的解决方案,用于防止 HTML 中的 XSS。我们使用了它的默认配置,这对于大多数用户输入的内容来说是足够的。

还有更多...

关于 XSS 和 HTML Purifier 有更多需要了解的内容;它们将在以下部分中讨论。

XSS 类型

XSS 注入主要有两种类型,具体如下:

  • 非持久性

  • 持久性

第一种类型是我们菜谱中使用的,也是最常见的一种 XSS 类型;它可以在大多数不安全的 Web 应用程序中找到。用户通过或通过 URL 传递的数据不会存储在任何地方,因此注入的脚本只会被执行一次,并且只针对输入它的用户。尽管如此,它并不像看起来那么安全。恶意用户可以在指向另一个网站的链接中包含 XSS,并且当另一个用户点击链接时,其核心将被执行。

第二种类型要严重得多,因为恶意用户输入的数据被存储在数据库中,并且被许多,如果不是所有网站用户看到。使用这种 XSS,恶意用户可以命令所有用户删除他们可以访问的所有数据,从而实际上破坏你的网站。

参见

为了了解更多关于 XSS 及其处理方法的信息,请参考以下资源:

防止 SQL 注入

SQL 注入是一种代码注入类型,它利用数据库级别的漏洞,允许你执行任意的 SQL 语句,使得恶意用户能够执行诸如删除数据或提升权限等操作。

在这个菜谱中,我们将看到易受攻击的代码示例并修复它们。

准备工作

  1. 使用 Composer 包管理器创建一个新的应用程序,如官方指南www.yiiframework.com/doc-2.0/guide-start-installation.html中所述。

  2. 执行以下 SQL:

    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user` (
       `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
       `username` varchar(100) NOT NULL,
       `password` varchar(32) NOT NULL,
       PRIMARY KEY (`id`)
    );
    
    INSERT INTO `user`(`id`,`username`,`password`) VALUES ( '1','Alex','202cb962ac59075b964b07152d234b70');
    
    INSERT INTO `user`(`id`,`username`,`password`) VALUES ( '2','Qiang','202cb962ac59075b964b07152d234b70');
    
  3. 使用 Gii 生成User模型。

如何操作...

  1. 首先,我们将实现一个简单的动作,检查来自 URL 的用户名和密码是否正确。创建app/controllers/SqlController.php

    <?php
    
    namespace app\controllers;
    
    use app\models\User;
    use Yii;
    use yii\base\Controller;
    use yii\base\Exception;
    use yii\helpers\ArrayHelper;
    use yii\helpers\Html;
    
    /**
    * Class SqlController.
    * @package app\controllers
    */
    class SqlController extends Controller
    {
       protected function renderContentByResult($result)
       {
           if ($result) {
               $content = "Success";
           } else {
               $content = "Failure";
           }
    
           return $this->renderContent($content);
       }
    
       public function actionSimple()
       {
           $userName = Yii::$app->request->get('username');
           $password = Yii::$app->request->get('password');
    
           $passwordHash = md5($password);
    
           $sql = "SELECT * FROM `user`"
                  ." WHERE `username` = '".$userName."'"
                  ." AND password = '".$passwordHash."' LIMIT |1";
    
           $result = Yii::$app->db->createCommand($sql)->queryOne();
    
           return $this->renderContentByResult($result);
       }
    
    }
    
  2. 让我们尝试使用/sql/simple?username=test&password=test URL 来访问它。由于我们不知道用户名和密码,它将像预期的那样打印失败

  3. 现在尝试访问/sql/simple?username=%27+or+%271%27%3D%271%27%3B+--&password=whatever。这次,它让我们进去了,尽管我们仍然不知道实际的凭证。usernamevalue解码部分看起来如下:

    ' or '1'='1'; --
    
  4. 关闭引号以保持语法正确。添加OR '1'='1',使条件始终为真。使用; --结束查询并注释其余部分。

  5. 由于没有进行转义,整个查询执行如下:

    SELECT * FROM user WHERE username = '' or '1'='1'; --' AND password = '008c5926ca861023c1d2a36653fd88e2' LIMIT 1;
    
    
  6. 解决这个问题最好的方法是使用预处理语句,如下所示:

    public function actionPrepared()
    {
       $userName = Yii::$app->request->get('username');
       $password = Yii::$app->request->get('password');
    
       $passwordHash = md5($password);
    
       $sql = "SELECT * FROM `user`"
              ." WHERE `username` = :username"
              ." AND password = :password LIMIT 1";
    
       $command = Yii::$app->db->createCommand($sql);
       $command->bindValue(':username', $userName);
       $command->bindValue(':password', $passwordHash);
       $result = $command->queryOne();
    
       return $this->renderContentByResult($result);
    }
    
  7. 现在用相同的恶意参数检查/sql/prepared。这次一切正常,我们收到了失败消息。同样的原则也适用于 ActiveRecord。这里唯一的区别是 AR 使用不同的语法:

    public function actionAr()
    {
       $userName = Yii::$app->request->get('username');
       $password = Yii::$app->request->get('password');
    
       $passwordHash = md5($password);
    
       $result = User::findOne([
           'username' => $userName,
           'password' => $passwordHash
       ]);
    
       return $this->renderContentByResult($result);
    }
    
  8. 在之前的代码中,我们像数组键一样使用了usernamepassword参数,并带有值样式。如果我们只使用第一个参数来编写之前的代码,它就会变得脆弱:

    public function actionWrongAr()
    {
       $userName = Yii::$app->request->get('username');
       $password = Yii::$app->request->get('password');
    
       $passwordHash = md5($password);
    
       $condition = "`username` = '".$userName." AND `password` = '".$passwordHash."'";
    
       $result = User::find()->where($condition)->one();
    
       return $this->renderContentByResult($result);
    }
    
  9. 如果使用得当,预处理语句可以让你免受所有类型的 SQL 注入。尽管如此,还有一些常见问题:

    • 你只能将一个值绑定到一个参数上,所以如果你想查询WHERE IN(1, 2, 3, 4),你将不得不创建并绑定四个参数。

    • 预处理语句不能用于表名、列名和其他关键字。

  10. 当使用ActiveRecord时,第一个问题可以通过添加where来解决,如下所示:

    public function actionIn()
    {
       $names  = ['Alex', 'Qiang'];
       $users = User::find()->where(['username' => $names])->all();
    
       return $this->renderContent(Html::ul(
           ArrayHelper::getColumn($users, 'username')
       ));
    }
    
  11. 第二个问题可以通过多种方式解决。第一种方式是依赖于活动记录和 PDO 引号:

    public function actionColumn()
    {
       $attr = Yii::$app->request->get('attr');
       $value = Yii::$app->request->get('value');
    
       $users = User::find()->where([$attr => $value])->all();
    
       return $this->renderContent(Html::ul(
           ArrayHelper::getColumn($users, 'username')
       ));
    }
    
  12. 但最安全的方式是使用白名单方法,如下所示:

    public function actionWhiteList()
    {
       $attr = Yii::$app->request->get('attr');
       $value = Yii::$app->request->get('value');
    
       $allowedAttr = ['username', 'id'];
    
       if (!in_array($attr, $allowedAttr)) {
           throw new Exception("Attribute specified is not allowed.");
       }
    
       $users = User::find()->where([$attr => $value])->all();
    
       return $this->renderContent(Html::ul(
           ArrayHelper::getColumn($users, 'username')
       ));
    }
    

它是如何工作的...

防止 SQL 注入的主要目标是正确地过滤输入。在除了表名之外的所有情况下,我们使用了预处理语句——这是大多数关系型数据库服务器支持的一个特性。它们允许你一次性构建语句,然后多次使用,并提供了一种绑定参数值的安全方式。

在 Yii 中,你可以为 Active Record 和 DAO 使用预处理语句。当使用 DAO 时,可以通过使用bindValuebindParam来实现。后者在我们想要执行多个相同类型的查询同时改变参数值时很有用:

public function actionBind()
{
   $userName = 'Alex';
   $passwordHash = md5('password1');

   $sql = "INSERT INTO `user` (`username`, `password`) VALUES (:username, :password);";

   // insert first user
   $command = Yii::$app->db->createCommand($sql);
   $command->bindParam('username', $userName);
   $command->bindParam('password', $passwordHash);
   $command->execute();

   // insert second user
   $userName = 'Qiang';
   $passwordHash = md5('password2');
   $command->execute();

   return $this->renderContent(Html::ul(
       ArrayHelper::getColumn(User::find()->all(), 'username')
   ));
}

大多数 Active Record 方法都接受参数。为了安全起见,你应该使用这些参数而不是直接传递原始数据。

至于引号表名、列名和其他关键字,你可以依赖于活动记录或使用白名单方法。

参见

为了了解更多关于 SQL 注入以及通过 Yii 与数据库交互的信息,请参考以下内容:

防止 CSRF

CSRF 是跨站请求伪造的缩写,恶意用户欺骗用户的浏览器在用户登录时静默地向网站执行 HTTP 请求。

这种攻击的一个例子是插入一个不可见的图像标签,其 src 指向 http://example.com/site/logout。即使该 image 标签插入在其他网站上,你也会立即从 example.com 登出。CSRF 的后果可能非常严重:破坏网站数据,阻止所有网站用户登录,泄露私人数据等等。

关于 CSRF 的几个事实:

  • 由于 CSRF 应该由受害者用户的浏览器执行,攻击者通常无法更改发送的 HTTP 头部。然而,存在浏览器和 Flash 插件漏洞,允许用户伪造头部,因此我们不应依赖这些。

  • 攻击者应该传递与用户通常相同的参数和值。

考虑到这些,处理 CSRF 的一个好方法是,在表单提交期间传递和检查一个唯一的令牌,并且,此外,根据 HTTP 规范使用 GET。

Yii 包含内置的令牌生成和令牌检查功能。此外,它还可以自动将令牌插入到 HTML 表单中。

为了避免 CSRF,你应该始终:

  • 遵循 HTTP 规范,即 GET 不应改变其应用程序状态

  • 保持 Yii CSRF 保护启用

在这个菜谱中,我们将看到如何确保我们的应用程序对 CSRF 具有抵抗力。

准备工作

通过使用官方指南中描述的 Composer 包管理器创建一个新的应用程序,官方指南的网址为 www.yiiframework.com/doc-2.0/guide-start-installation.html

如何操作...

  1. 为了启用反 CSRF 保护,我们应该按照以下方式添加 config/main.php

    'components' => [
        ..
       request => [
            ..
           'enableCsrfValidation => true,
            ..
       ],
        ..
    ],
    
  2. 选项 enableCsrfValidation 默认为 true。当 CSRF 验证启用时,提交给 Yii 网络应用程序的表单必须来自同一应用程序。如果不是,将引发 400 HTTP 异常

    注意,此功能要求用户客户端接受 cookie。

  3. 在配置应用程序后,你应该在带有 ActiveForm 的视图中使用 ActiveForm::beginFormCHtml::endForm 而不是 HTML 表单标签:

    <?php $form = ActiveForm::begin(['id' => 'login-form']); ?>
         <input type='text' name='name'
         .........
    <?php ActiveForm::end(); ?>
    
  4. 或者手动:

    <form action='#' method='POST'>
      <input type="hidden" name="<?= Yii::$app->request->csrfParam ?>" value="<?=Yii::$app->request->getCsrfToken()?>" />
      ....
    </form>
    
  5. 在第一种情况下,Yii 自动添加一个隐藏的令牌字段,如下所示:

          <form action="/csrf/create" method="post">
          <div style="display:none"><input type="hidden" value="e4d1021e79ac
          269e8d6289043a7a8bc154d7115a" name="YII_CSRF_TOKEN" />
    
  6. 如果你将此表单保存为 HTML 并尝试提交,你将收到如下截图所示的提示信息,而不是常规的数据处理:如何操作...

它是如何工作的...

在内部,在表单渲染期间,我们有如下代码:

if ($request->enableCsrfValidation && !strcasecmp($method, 'post')) {
  $hiddenInputs[] = static::hiddenInput($request->csrfParam, $request->getCsrfToken());
}

if (!empty($hiddenInputs)) {
  $form .= "\n" . implode("\n", $hiddenInputs);
}

在前面的代码中,getCsrfToken() 生成一个唯一的令牌值并将其写入到 cookie 中。然后,在随后的请求中,比较 cookie 和 POST 值。如果它们不匹配,将显示错误信息而不是常规的数据处理。

如果你需要执行 POST 请求但不想使用 CHtml 构建表单,则可以传递一个参数,其名称来自 Yii::app()->request->csrfParam,其值来自 Yii::$app->request->getCsrfToken()

还有更多...

让我们看看更多功能。

禁用所有操作的 CSRF 令牌

  1. 如果你有 enableCsrfValidation 的问题,你可以将其关闭。

  2. 要禁用 CSRF,将以下代码添加到你的控制器中:

    public function beforeAction($action) { 
        $this->enableCsrfValidation = false; 
        return parent::beforeAction($action);
    }
    

禁用特定操作的 CSRF 令牌

public function beforeAction($action) { 
    $this->enableCsrfValidation =  ($action->id !== "actionId"); 
    return parent::beforeAction($action);
}

CSRF 验证用于 Ajax 调用

当主布局中的 enableCsrfValidation 选项启用时,添加 csrfMetaTags

<head>
  .......
  <?= Html::csrfMetaTags() ?>
</head>

Now you will be able to simply add it to ajax-call
var csrfToken = $('meta[name="csrf-token"]').attr("content");
$.ajax({
        url: 'request'
        type: 'post',
        dataType: 'json',
        data: {param1: param1, _csrf : csrfToken},
});

此外 [重命名]

如果你的应用程序需要非常高的安全级别,例如银行账户管理系统,可以采取额外措施。

首先,你可以使用 config/main.php 关闭记住我功能,如下所示:

'components' => [
    ..
   'user' => [
        ..
       'enableAutoLogin' => false,
        ..
   ],
    ..
],

注意,如果 enabledSession 选项为 true,则此方法将不起作用。

然后,你可以降低会话超时时间,如下所示:

'components' => [
    ..
   'session' => [
        ..
       'timeout' => 200,
        ..
   ],
    ..
],

这设置了数据被视为垃圾并清理后的秒数。

当然,这些措施将使用户体验更差,但它们将增加一个额外的安全层。

正确使用 GET 和 POST

HTTP 强制不使用会更改数据或状态的 GET 操作。坚持这个规则是良好的实践。它不能阻止所有类型的 CSRF,但至少可以实施一些注入,如 <img src=, pointless>

参见

为了了解更多关于 SQL 注入和通过 Yii 与数据库交互的信息,请参考以下 URL:

使用 RBAC

基于角色的访问控制RBAC)提供简单而强大的集中式访问控制。它是 Yii 中最强大的访问控制方法。它在指南中有描述,但由于它相当复杂且强大,如果不稍微深入了解,可能不容易理解。

在本食谱中,我们将从 definitive guide 中获取角色层次结构,导入它,并解释内部发生的情况。

准备工作

  1. 使用 Composer 包管理器创建一个新的应用程序,如官方指南中所述 www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 创建一个 MySQL 数据库并配置它。

  3. 在你的 config/main.phpconfig/console.php 中配置 authManager 组件,如下所示:

    return [
       // ...
       'components' => [
           'authManager' => [
               'class' => 'yii\rbac\DbManager',
           ],
           // ...
       ],
    ];
    
  4. 运行迁移:

    yii migrate --migrationPath=@yii/rbac/migrations
    

如何做到这一点...

执行以下步骤:

  1. 创建访问规则 rbac/AuthorRule.php

    <?php
    
    namespace app\rbac;
    
    use yii\rbac\Rule;
    
    /**
    * Class AuthorRule.
    * @package app\rbac
    */
    class AuthorRule extends Rule
    {
       public $name = 'isAuthor';
    
       /**
        * @param int|string $user
        * @param \yii\rbac\Item $item
        * @param array $params
        *
        * @return bool
        */
       public function execute($user, $item, $params)
       {
           return isset($params['post']) ? $params['post']->createdBy == $user : false;
       }
    }
    
  2. 创建一个控制台命令,command/RbacController.php,以 init RBAC 规则命令:

    <?php
    
    namespace app\commands;
    
    use app\models\User;
    use Yii;
    use yii\console\Controller;
    
    /**
    * Class RbacController.
    * @package app\commands
    */
    class RbacController extends Controller
    {
       public function actionInit()
       {
           $auth = Yii::$app->authManager;
    
           $createPost = $auth->createPermission('createPost');
           $createPost->description = 'Create a post';
    
           $updatePost = $auth->createPermission('updatePost');
           $updatePost->description = 'Update a post';
    
           $updatePost = $auth->createPermission('updatePost');
           $updatePost->description = 'Update a post';
    
           $deletePost = $auth->createPermission('deletePost');
           $deletePost->description = 'Delete a post';
    
           $readPost = $auth->createPermission('readPost');
           $readPost->description = 'Read a post';
    
           $authorRule = new \app\rbac\AuthorRule();
    
           // add permissions
           $auth->add($createPost);
           $auth->add($updatePost);
           $auth->add($deletePost);
           $auth->add($readPost);
           $auth->add($authorRule);
    
           // add the "updateOwnPost" permission and associate the rule with it.
           $updateOwnPost = $auth->createPermission('updateOwnPost');
           $updateOwnPost->description = 'Update own post';
           $updateOwnPost->ruleName = $authorRule->name;
    
           $auth->add($updateOwnPost);
           $auth->addChild($updateOwnPost, $updatePost);
    
           // create Author role
           $author = $auth->createRole('author');
           $auth->add($author);
           $auth->addChild($author, $createPost);
           $auth->addChild($author, $updateOwnPost);
           $auth->addChild($author, $readPost);
    
           // create Admin role
           $admin = $auth->createRole('admin');
           $auth->add($admin);
           $auth->addChild($admin, $updatePost);
           $auth->addChild($admin, $deletePost);
           $auth->addChild($admin, $author);
    
           // assign roles
           $auth->assign($admin, User::findByUsername('admin')->id);
           $auth->assign($author, User::findByUsername('demo')->id);
    
           echo "Done!\n";
       }
    }
    
  3. 就这样。在控制台中运行它:

    yii rbac/init
    
    
  4. 按如下方式创建 controllers/RbacController.php

    <?php
    
    namespace app\controllers;
    
    use app\models\User;
    use stdClass;
    use Yii;
    use yii\filters\AccessControl;
    use yii\helpers\Html;
    use yii\web\Controller;
    
    /**
    * Class RbacController.
    */
    class RbacController extends Controller
    {
       public function behaviors()
       {
           return [
               'access' => [
                   'class' => AccessControl::className(),
                   'rules' => [
                       [
                           'allow' => true,
                           'actions' => ['delete'],
                           'roles' => ['deletePost'],
                       ],
                       [
                           'allow' => true,
                           'actions' => ['test'],
                       ],
                   ],
               ],
           ];
       }
    
       public function actionDelete()
       {
           return $this->renderContent(
               Html::tag('h1', 'Post deleted.')
           );
       }
    
       /**
        * @param $description
        * @param $rule
        * @param array $params
        *
        * @return string
        */
       protected function renderAccess($description, $rule, $params = [])
       {
           $access = Yii::$app->user->can($rule, $params);
    
           return $description.': '.($access ? 'yes' : 'no');
       }
    
       public function actionTest()
       {
           $post = new stdClass();
           $post->createdBy = User::findByUsername('demo')->id;
    
           return $this->renderContent(
               Html::tag('h1', 'Current permissions').
               Html::ul([
                   $this->renderAccess('Use can create post', 'createPost'),
                   $this->renderAccess('Use can read post', 'readPost'),
                   $this->renderAccess('Use can update post', 'updatePost'),
                   $this->renderAccess('Use can own update post', 'updateOwnPost', [
                       'post' => $post,
                   ]),
                   $this->renderAccess('Use can delete post', 'deletePost'),
               ])
           );
       }
    }
    
  5. 现在运行一次rbac/test以检查对 RBAC 层次结构中创建的所有权限的访问权限:如何操作...

  6. 然后,尝试以demo(密码是demo)身份登录并再次运行rbac/test如何操作...

  7. 然后,尝试以admin(密码是admin)身份登录并再次运行rbac/test如何操作...

  8. demo用户身份登录并运行rbac/delete如何操作...

  9. admin身份登录并运行rbac/delete如何操作...

它是如何工作的…

Yii 实现了遵循NIST RBAC模型的通用层次结构 RBAC。它通过authManagerapplication组件提供 RBAC 功能。

RBAC 层次结构是一个有向无环图,即一组节点及其有向连接或边。有三种类型的节点可用:角色、权限和规则。

角色代表一组权限(例如创建帖子和管理帖子)。一个角色可以分配给一个或多个用户。为了检查用户是否具有指定的权限,我们可以检查用户是否被分配了一个包含该权限的角色。

角色和权限都可以组织成层次结构。特别是,一个角色可能包含其他角色或权限,一个权限也可能包含其他权限。Yii 实现了一个部分顺序的层次结构,它包括更特殊的tree层次结构。虽然一个角色可以包含一个权限,但反过来不一定成立。

为了测试权限,我们创建了两个操作。第一个操作是test,它包含了对创建的权限和角色的检查器。第二个操作是delete,它通过访问过滤器进行限制。访问过滤器的规则包含以下代码:

[
   'allow' => true,
   'actions' => ['delete'],
   'roles' => ['deletePost'],
],

这意味着我们允许所有具有deletePost权限的用户运行deletePost操作。Yii 从deletePost权限开始检查。除了访问规则元素被命名为roles之外,您还可以指定 RBAC 层次结构节点,无论是角色、规则还是权限。检查updatePost是复杂的:

Yii::$app->user->can('updatePost', ['post' => $post]);

我们使用第二个参数来传递一个帖子(在我们的例子中,我们用stdClass模拟了它)。如果用户以demo身份登录,那么为了获取访问权限,我们需要从updatePost到作者。如果你很幸运,你可能只需要通过updatePostupdateOwnPost和作者。

由于updateOwnPost定义了一个规则,它将在传递给checkAccess的参数下运行。如果结果是 true,则将授予访问权限。由于 Yii 不知道最短路径是什么,它会尝试检查所有可能性,直到成功或没有其他选择。

更多内容…

以下是一些有用的技巧,可以帮助您有效地使用 RBAC,这些技巧将在以下小节中讨论。

保持层次结构简单和高效

在可能的情况下遵循以下建议,以最大限度地提高性能并减少层次结构复杂性:

  • 避免将多个角色分配给单个用户

  • 不要连接相同类型的节点;例如,避免将一个任务连接到另一个任务

命名 RBAC 节点

如果不使用某种命名约定,复杂的层次结构就难以理解。以下是一个可能的约定,有助于减少混淆:

  [group_][own_]entity_action

其中 own 用于规则确定只有当当前用户是元素的拥有者且 group 只是一个命名空间时,用户才能修改元素。entity 是我们正在处理的实体的名称,action 是我们正在执行的操作。

例如,如果我们需要创建一个规则来决定用户是否可以删除博客文章,我们将将其命名为 blog_post_delete。如果规则决定用户是否可以编辑自己的博客评论,名称将是 blog_own_comment_edit

参见

为了了解更多关于 SQL 注入和通过 Yii 与数据库交互的信息,请参考以下内容:

加密/解密数据

Yii2 框架包含一个特殊的网络安全组件,该组件提供了一套处理常见安全相关任务的方法。\yii\base\Security 类需要 OpenSSL PHP 扩展而不是 mcrypt

准备工作

  1. 按照官方指南使用 Composer 包管理器创建一个新应用程序,官方指南链接

  2. 按如下方式设置数据库连接并创建一个名为 order 的表:

    DROP TABLE IF EXISTS `order`;
    CREATE TABLE IF NOT EXISTS `order` (
    `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    `client` VARCHAR(255) NOT NULL,
    `total` FLOAT NOT NULL,
    `encrypted_field` BLOB NOT NULL,
    PRIMARY KEY (`id`)
    );
    
  3. 使用 Gii 生成 Order 模型。

如何操作...

  1. config/params.php 中添加一个额外的键参数,如下所示:

    <?php
    
    return [
        'adminEmail' => 'admin@example.com',
        'key' => 'mysecretkey'
    ];
    
  2. behaviorshelper 属性添加到 Order 模型中,如下所示:

    public $encrypted_field_temp;
    
    public function behaviors()
    {
       return [
           [
               'class' => AttributeBehavior::className(),
               'attributes' => [
                   ActiveRecord::EVENT_BEFORE_INSERT => 'encrypted_field',
                   ActiveRecord::EVENT_BEFORE_UPDATE => 'encrypted_field',
               ],
               'value' => function ($event) {
                   $event->sender->encrypted_field_temp = $event->sender->encrypted_field;
                   return Yii::$app->security->encryptByKey(
                       $event->sender->encrypted_field,
                       Yii::$app->params['key']
                   );
               },
           ],
           [
               'class' => AttributeBehavior::className(),
               'attributes' => [
                   ActiveRecord::EVENT_AFTER_INSERT => 'encrypted_field',
                   ActiveRecord::EVENT_AFTER_UPDATE => 'encrypted_field',
               ],
               'value' => function ($event) {
                   return $event->sender->encrypted_field_temp;
               },
           ],
           [
               'class' => AttributeBehavior::className(),
               'attributes' => [
                   ActiveRecord::EVENT_AFTER_FIND => 'encrypted_field',
               ],
               'value' => function ($event) {
                   return Yii::$app->security->decryptByKey(
                       $event->sender->encrypted_field,
                       Yii::$app->params['key']
                   );
               },
           ],
       ];
    }
    
  3. 添加 controllers/CryptoController.php

    <?php
    
    namespace app\controllers;
    
    use app\models\Order;
    use Yii;
    use yii\db\Query;
    use yii\helpers\ArrayHelper;
    use yii\helpers\Html;
    use yii\helpers\VarDumper;
    use yii\web\Controller;
    
    /**
    * Class CryptoController.
    * @package app\controllers
    */
    class CryptoController extends Controller
    {
       public function actionTest()
       {
           $newOrder = new Order();
           $newOrder->client = "Alex";
           $newOrder->total = 100;
           $newOrder->encrypted_field = 'very-secret-info';
           $newOrder->save();
    
           $findOrder = Order::findOne($newOrder->id);
    
           return $this->renderContent(Html::ul([
               'New model: ' . VarDumper::dumpAsString($newOrder->attributes),
               'Find model: ' . VarDumper::dumpAsString($findOrder->attributes)
           ]));
    
       }
    
       public function actionRaw()
       {
           $row = (new Query())->from('order')
               ->where(['client' => 'Alex'])
               ->one();
    
           return $this->renderContent(Html::ul(
               $row
           ));
       }
    }
    
  4. 运行 crypto/test,你将得到以下结果:如何操作...

  5. 要查看原始数据,运行 crypto/raw如何操作...

它是如何工作的...

首先,我们添加了 AttributeBehavior,它会在某些事件发生时自动处理我们的数据。我们的事件是 ActiveRecord::EVENT_AFTER_INSERTActiveRecord::EVENT_AFTER_UPDATEActiveRecord::EVENT_AFTER_FIND

在插入和更新事件期间,我们使用特殊方法Yii::$app->security->encryptByKey();解密我们的数据:在将其保存到数据库之前,使用 HKDF 和随机盐解密我们的数据。从数据库获取数据后,我们还可以使用ActiveRecord::EVENT_AFTER_FIND方法来解密我们的数据。在这种情况下,我们也使用特殊的 Yii2 方法Yii::$app->security->encryptByKey();。此方法接受两个参数:加密数据和密钥。

更多内容...

除了数据加密和解密,安全组件还提供使用标准算法进行密钥派生、数据篡改预防和密码验证。

处理密码

验证密码:

if (Yii::$app->getSecurity()->validatePassword($password, $hash)) {
 // all good, logging user in
} else {
 // wrong password
}

参见

为了了解更多关于 SQL 注入和通过 Yii 与数据库工作的信息,请参阅www.yiiframework.com/doc-2.0/guide-security-passwords.html

第六章。RESTful 网络服务

在本章中,我们将涵盖以下主题:

  • 创建一个 REST 服务器

  • 认证

  • 速率限制

  • 版本控制

  • 错误处理

简介

本章将帮助你了解一些关于 Yii URL 路由器、控制器和视图的实用技巧。你将能够使你的控制器和视图更加灵活。

创建一个 REST 服务器

在下面的菜谱中,我们使用一个示例来说明你可以如何以最小的编码工作量构建和设置 RESTful API。这个菜谱将在本章的其他菜谱中重复使用。

准备工作

  1. 使用官方指南中描述的 Composer 包管理器创建一个新的应用程序,www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 使用以下命令创建创建文章表的迁移:

    ./yii migrate/create create_film_table
    
    
  3. 然后,更新刚刚创建的迁移方法 up,使用以下代码:

    public function up()
        {
            $tableOptions = null;
            if ($this->db->driverName === 'mysql') {
                $tableOptions = 'CHARACTER SET utf8 COLLATE
                    utf8_general_ci ENGINE=InnoDB';
            }
            $this->createTable('{{%film}}', [
                'id' => $this->primaryKey(),
                'title' => $this->string(64)->notNull(),
                'release_year' => $this->integer(4)->notNull(),
            ], $tableOptions);
    
            $this->batchInsert('{{%film}}', ['id','title','release_year'], [
                [1, 'Interstellar', 2014],
                [2, "Harry Potter and the Philosopher's Stone",2001],
                [3, 'Back to the Future', 1985],
                [4, 'Blade Runner', 1982],
                [5, 'Dallas Buyers Club', 2013],
            ]);
        }
    

    使用以下代码更新 down 方法:

    public function down()
    {
        $this->dropTable('film');
    }
    
  4. 运行创建的 create_film_table 迁移。

  5. 使用 Gii 模块生成 Film 模型。

  6. 配置你的应用程序服务器以使用干净的 URL。如果你使用 Apache 并开启了 mod_rewriteAllowOverride,那么你应该在你的 @web 目录下的 .htaccess 文件中添加以下行:

    Options +FollowSymLinks
    IndexIgnore */*
    RewriteEngine on
    # if a directory or a file exists, use it directly
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    # otherwise forward it to index.php
    RewriteRule . index.php
    

如何做……

  1. 创建一个控制器,@app/controller/FilmController.php,使用以下代码:

    <?php
        namespace app\controllers;
    
        use yii\rest\ActiveController;
    
        class FilmController extends ActiveController
        {
            public $modelClass = app\models\Film';
        }
    

    更新 @app/config/web.php 配置文件。添加以下 urlManager 组件的配置:

    'urlManager' => [
        'enablePrettyUrl' => true,
        'enableStrictParsing' => true,
        'showScriptName' => false,
        'rules' => [
            ['class' => 'yii\rest\UrlRule', 'controller' => 'films'],
        ],
    ],
    
  2. @app/config/web.php 中重新配置请求组件:

    'request' => [
        'cookieValidationKey' => 'mySecretKey',
        'parsers' => [
            'application/json' => 'yii\web\JsonParser',
        ],
    ]
    

它是如何工作的……

我们扩展 \yii\rest\ActiveController 来创建我们自己的控制器,然后对于创建的控制器,设置了 modelClass 属性。\yii\rest\ActiveController 类实现了一组通用的操作,以支持对 ActiveRecord 的 RESTful 访问。

通过上述最小限度的努力,你已经完成了创建用于访问电影数据的 RESTful API。

你创建的 API 包括:

  • GET /films:这按页列出所有电影

  • HEAD /films:这显示了电影列表的概要信息

  • POST /films:这创建一个新的电影

  • GET /films/5:这返回电影 5 的详细信息

  • HEAD /films/5:这显示了电影 5 的概要信息

  • PATCH /films/5 和 PUT /films/5:这将更新电影 5

  • DELETE /films/5:这删除电影 5

  • OPTIONS /films:这显示了 /films 端点的支持动词

  • OPTIONS /films/5:这显示了 /films/5 端点的支持动词

它之所以可以这样工作,是因为 \yii\rest\ActiveController 支持以下操作:

  • index:这列出模型

  • view:这返回模型的详细信息

  • create:这创建一个新的模型

  • update:这更新一个现有的模型

  • delete:这将删除一个现有的模型

  • options:这返回允许的 HTTP 方法

此外,还有一个 verbs() 方法,它定义了每个操作的允许请求方法。

为了检查我们的 RESTful API 是否正确工作,让我们发送几个请求。

让我们从GET请求开始。在控制台运行以下命令:

curl -i -H "Accept:application/json" "http://yii-book.app/films"

你将得到以下输出:

HTTP/1.1 200 OK
Date: Wed, 23 Sep 2015 17:46:35 GMT
Server: Apache
X-Powered-By: PHP/5.5.23
X-Pagination-Total-Count: 5
X-Pagination-Page-Count: 1
X-Pagination-Current-Page: 1
X-Pagination-Per-Page: 20
Link: <http://yii-book.app/films?page=1>; rel=self
Content-Length: 301
Content-Type: application/json; charset=UTF-8

[{"id":1,"title":"Interstellar","release_year":2014},{"id":2,"title":"Harry Potter and the Philosopher's Stone","release_year":2001},{"id":3,"title":"Back to the Future","release_year":1985},{"id":4,"title":"Blade Runner","release_year":1982},{"id":5,"title":"Dallas Buyers Club","release_year":2013}]

让我们发送一个POST请求。在控制台运行以下命令:

curl -i -H "Accept:application/json" -X POST -d title="New film" -d release_year=2015 "http://yii-book.app/films"

你将得到以下输出:

HTTP/1.1 201 Created
Date: Wed, 23 Sep 2015 17:48:06 GMT
Server: Apache
X-Powered-By: PHP/5.5.23
Location: http://yii-book.app/films/6
Content-Length: 49
Content-Type: application/json; charset=UTF-8

{"title":"New film","release_year":"2015","id":6}

让我们获取创建的电影。在控制台运行以下命令:

curl -i -H "Accept:application/json" "http://yii-book.app/films/6"

你将得到以下输出:

HTTP/1.1 200 OK
Date: Wed, 23 Sep 2015 17:48:36 GMT
Server: Apache
X-Powered-By: PHP/5.5.23
Content-Length: 47
Content-Type: application/json; charset=UTF-8

{"id":6,"title":"New film","release_year":2015}

让我们发送一个DELETE请求。在控制台运行以下命令:

curl -i -H "Accept:application/json" -X DELETE "http://yii-book.app/films/6"

你将得到以下输出:

HTTP/1.1 204 No Content
Date: Wed, 23 Sep 2015 17:48:55 GMT
Server: Apache
X-Powered-By: PHP/5.5.23
Content-Length: 0
Content-Type: application/json; charset=UTF-8

更多内容...

我们现在将探讨内容协商和自定义 REST URL 规则:

内容协商

你也可以轻松地通过内容协商行为格式化你的响应。

例如,你可以将此代码放入你的控制器中,所有数据都将以 XML 格式返回。

你应该在文档中查看完整的格式列表。

use yii\web\Response;
public function behaviors()
{
    $behaviors = parent::behaviors();
    $behaviors['contentNegotiator']['formats']['application/xml']= Response::FORMAT_XML;
    return $behaviors;
}

在控制台运行以下命令:

curl -i -H "Accept:application/xml" "http://yii-book.app/films"

你将得到以下输出:

HTTP/1.1 200 OK
Date: Wed, 23 Sep 2015 18:02:47 GMT
Server: Apache
X-Powered-By: PHP/5.5.23
X-Pagination-Total-Count: 5
X-Pagination-Page-Count: 1
X-Pagination-Current-Page: 1
X-Pagination-Per-Page: 20
Link: <http://yii-book.app/films?page=1>; rel=self
Content-Length: 516
Content-Type: application/xml; charset=UTF-8

<?xml version="1.0" encoding="UTF-8"?>
<response>
    <item>
        <id>1</id>
        <title>Interstellar</title>
        <release_year>2014
        </release_year>
    </item>
    <item>
        <id>2</id>

<title>Harry Potter and the Philosopher's Stone</title>
        <release_year>2001
        </release_year>
    </item>
    <item>
        <id>3</id>
        <title>Back to the Future</title>
        <release_year>1985
        </release_year>
    </item>
    <item>
        <id>4</id>
        <title>Blade Runner</title>
        <release_year>1982
        </release_year>
    </item>
    <item>
        <id>5</id>
        <title>Dallas Buyers Club</title>
        <release_year>2013
        </release_year>
    </item>
</response>

自定义 REST URL 规则

你必须记住,默认情况下,控制器 ID 是以复数形式定义的。这是因为yii\rest\UrlRule会自动将控制器 ID 复数化。你可以通过将yii\rest\UrlRule::$pluralize设置为 false 来简单地禁用此功能:

'urlManager' => [
    //..
    'rules' => [
        [
            'class' => 'yii\rest\UrlRule',
            'controller' => 'film'
            'pluralize' => false
        ],
    ],
    //..
]

如果你还想指定控制器 ID 在模式中应如何显示,你可以向数组中添加一个自定义名称作为键值对,其中数组键是控制器 ID,数组值是实际的控制器 ID。例如:

'urlManager' => [
    //..
    'rules' => [
        [
            'class' => 'yii\rest\UrlRule',
            'controller' => ['super-films' => 'film']
        ],
    ],
    //..
]

参见

更多信息,请参考以下 URL:

认证

在这个菜谱中,将设置认证模型。

准备工作

从“准备工作”和“如何操作”部分的“创建 REST 服务器”菜谱中重复所有步骤。

如何操作...

  1. @app/controllers/FilmController修改为以下内容:

    <?php
    
        namespace app\controllers;
    
        use app\models\User;
        use Yii;
        use yii\helpers\ArrayHelper;
        use yii\rest\ActiveController;
        use yii\filters\auth\HttpBasicAuth;
    
        class FilmController extends ActiveController
        {
            public $modelClass = 'app\models\Film';
    
            public function behaviors()
            {
                return ArrayHelper::merge(parent::behaviors(),[
                    'authenticator' => [
                    'authMethods' => [
                        'basicAuth' => [
                            'class' =>HttpBasicAuth::className(),
                            'auth' => function ($username,$password) {
                                $user =User::findByUsername($username);
    
                                if ($user !== null && $user->validatePassword($password)){
                                    return $user;
                                }
    
                                return null;
                            },
                        ]
                    ]
                ]
    
            ]);
        }
    }
    

在浏览器中打开http://yii-book.app/films并确保我们已配置 HTTP 基本认证:

如何操作...

让我们尝试进行认证。在控制台运行以下命令:

curl -i -H "Accept:application/json" "http://yii-book.app/films"

你将得到以下内容:

HTTP/1.1 401 Unauthorized
Date: Thu, 24 Sep 2015 01:01:24 GMT
Server: Apache
X-Powered-By: PHP/5.5.23
Www-Authenticate: Basic realm="api"
Content-Length: 149
Content-Type: application/json; charset=UTF-8

{"name":"Unauthorized","message":"You are requesting with an invalid credential.","code":0,"status":401,"type":"yii\\web\\UnauthorizedHttpException"}
  1. 现在尝试使用cURL进行auth操作:

    curl -i -H "Accept:application/json" -u admin:admin "http://yii-book.app/films"
    
    
  2. 你应该得到如下响应:

    HTTP/1.1 200 OK
    Date: Thu, 24 Sep 2015 01:01:40 GMT
    Server: Apache
    X-Powered-By: PHP/5.5.23
    Set-Cookie: PHPSESSID=8b3726040bf8850ebd07209090333103; path=/; HttpOnly
    Expires: Thu, 19 Nov 1981 08:52:00 GMT
    Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
    Pragma: no-cache
    X-Pagination-Total-Count: 5
    X-Pagination-Page-Count: 1
    X-Pagination-Current-Page: 1
    X-Pagination-Per-Page: 20
    Link: <http://yii-book.app/films?page=1>; rel=self
    Content-Length: 301
    Content-Type: application/json; charset=UTF-8
    [{"id":1,"title":"Interstellar","release_year":2014},{"id":2,"title":"Harry Potter and the Philosopher's Stone","release_year":2001},{"id":3,"title":"Back to the Future","release_year":1985},{"id":4,"title":"Blade Runner","release_year":1982},{"id":5,"title":"Dallas Buyers Club","release_year":2013}]
    

它是如何工作的...

我们还向HttpBasicAuth类添加了authenticator行为,因此我们只需使用登录名和密码即可进行认证。你可以实现官方指南中 RESTful 网络服务部分描述的任何认证方法。

更多内容...

发送访问令牌有不同的方法:

  • HTTP 基本认证

  • 查询参数

  • OAuth

Yii 支持所有这些认证方法。

参见

有关更多信息,请参阅 www.yiiframework.com/doc-2.0/guide-rest-rate-limiting.html

速率限制

为了防止滥用,你应该考虑为你的 API 添加速率限制。例如,你可能希望将每个用户的 API 使用限制在最多一分钟内的五个 API 调用。如果在指定的时间内收到来自用户的过多请求,应返回状态码 429(请求过多)的响应。

准备工作

重复 创建 REST 服务器 菜单中的 准备工作如何操作... 部分的所有步骤。

  1. 使用以下命令创建用于创建用户允许表迁移:

     ./yii migrate/create create_user_allowance_table
    
    
  2. 然后,更新刚刚创建的迁移方法 up,使用以下代码:

    public function up()
        {
            $tableOptions = null;
            if ($this->db->driverName === 'mysql') {
                $tableOptions = 'CHARACTER SET utf8 COLLATEutf8_general_ci ENGINE=InnoDB';
           }
           $this->createTable('{{%user_allowance}}', [
               'user_id' => $this->primaryKey(),
               'allowed_number_requests' => $this->integer(10)->notNull(),
               'last_check_time' => $this->integer(10)->notNull()
           ], $tableOptions);
        }
    
  3. 使用以下代码更新 down 方法:

    public function down()
        {
            $this->dropTable('{{%user_allowance}}');
        }
    
  4. 运行创建的 create_film_table 迁移。

  5. 使用 Gii 模块生成 UserAllowance 模型。

如何操作…

首先,你必须使用以下代码更新 @app/controllers/FilmController.php

<?php

    namespace app\controllers;

    use yii\rest\ActiveController;
    use yii\filters\RateLimiter;
    use yii\filters\auth\QueryParamAuth;

    class FilmController extends ActiveController
    {
        public $modelClass = 'app\models\Film';

        public function behaviors()
        {
            $behaviors = parent::behaviors();

            $behaviors['authenticator'] = [
            'class' => QueryParamAuth::className(),
            ];

            $behaviors['rateLimiter'] = [
            'class' => RateLimiter::className(),
            'enableRateLimitHeaders' => true
            ];

        return $behaviors;
    }
}

要启用速率限制,User 模型类应该实现 yii\filters\RateLimitInterface 并需要实现三个方法:getRateLimit()loadAllowance()saveAllowance()。你必须使用 RATE_LIMIT_NUMBERRATE_LIMIT_RESET 常量将它们添加进去:

<?php

    namespace app\models;

    class User extends \yii\base\Object implements \yii\web\IdentityInterface, \yii\filters\RateLimitInterface
    {
        public $id;
        public $username;
        public $password;
        public $authKey;
        public $accessToken;

        const RATE_LIMIT_NUMBER = 5;
        const RATE_LIMIT_RESET = 60;

    //  it means that user allowed only 5 requests per one minute
        public function getRateLimit($request, $action)
        {
            return [self::RATE_LIMIT_NUMBER,self::RATE_LIMIT_RESET];
        }

        public function loadAllowance($request, $action)
        {
            $userAllowance = UserAllowance::findOne($this->id);

            return $userAllowance ?
            [$userAllowance->allowed_number_requests,$userAllowance->last_check_time] :
             $this->getRateLimit($request, $action);
        }

    public function saveAllowance($request, $action,$allowance, $timestamp)
    {
        $userAllowance = ($allowanceModel =UserAllowance::findOne($this->id)) ?$allowanceModel : new UserAllowance();
        $userAllowance->user_id = $this->id;
        $userAllowance->last_check_time = $timestamp;
        $userAllowance->allowed_number_requests =$allowance;
        $userAllowance->save();
    }

   // other User model methods
}

它是如何工作的…

一旦身份类实现了所需接口,Yii 将自动使用配置为 [[yii\filters\RateLimiter]] 的动作过滤器为 [[yii\rest\Controller]] 执行速率限制检查。我们还添加了 'authenticator' 行为和 QueryParamAuth 类。因此,我们现在可以使用通过查询参数传递的访问令牌进行身份验证。你可以在官方指南的 RESTful 网络服务部分添加任何描述的认证方法。

让我们解释我们的方法。它们很容易理解。

getRateLimit():这个方法返回允许的最大请求数量和时间周期(例如,[100, 600] 表示在 600 秒内最多可以有 100 个 API 调用)

loadAllowance():这个方法返回剩余允许的请求数量和上次检查速率限制时对应的 UNIX 时间戳

saveAllowance():这个方法保存剩余允许的请求数量和当前的 UNIX 时间戳

我们将数据存储在 MySQL 数据库中。为了性能,你可能使用一个 NoSQL 数据库或另一个具有更高时间获取和加载数据的存储系统。

现在让我们尝试检查速率限制功能。在控制台中运行以下命令:

curl -i "http://yii-book.app/films?access-token=100-token"

你将得到以下输出:

HTTP/1.1 200 OK
Date: Thu, 24 Sep 2015 01:35:51 GMT
Server: Apache
X-Powered-By: PHP/5.5.23
Set-Cookie: PHPSESSID=495a928978cc732bee853b83f521eba2; path=/; HttpOnly
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
X-Rate-Limit-Limit: 5
X-Rate-Limit-Remaining: 4
X-Rate-Limit-Reset: 0
X-Pagination-Total-Count: 5
X-Pagination-Page-Count: 1
X-Pagination-Current-Page: 1
X-Pagination-Per-Page: 20
Link: <http://yii-book.app/films?access-token=100-token&page=1>; rel=self
Content-Length: 301
Content-Type: application/json; charset=UTF-8

[{"id":1,"title":"Interstellar","release_year":2014},{"id":2,"title":"Harry Potter and the Philosopher's Stone","release_year":2001},{"id":3,"title":"Back to the Future","release_year":1985},{"id":4,"title":"Blade Runner","release_year":1982},{"id":5,"title":"Dallas Buyers Club","release_year":2013}]

让我们了解返回的头部信息。当启用速率限制时,默认情况下,每个响应都会发送包含当前速率限制信息的以下 HTTP 头部:

X-Rate-Limit-Limit:这是在时间周期内允许的最大请求数量

X-Rate-Limit-Remaining:这是当前时间周期内剩余请求数量

X-Rate-Limit-Reset:这是等待以获取最大允许请求数量的秒数

因此,现在尝试超过限制,每分钟请求以下 URL 超过五次,您将看到TooManyRequestsHttpExeption

HTTP/1.1 429 Too Many Requests
Date: Thu, 24 Sep 2015 01:37:24 GMT
Server: Apache
X-Powered-By: PHP/5.5.23
Set-Cookie: PHPSESSID=bb630ca8a641ef92bd210c0a936e3149; path=/; HttpOnly
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
X-Rate-Limit-Limit: 5
X-Rate-Limit-Remaining: 0
X-Rate-Limit-Reset: 60
Content-Length: 131
Content-Type: application/json; charset=UTF-8
{"name":"Too Many Requests","message":"Rate limit exceeded.","code":0,"status":429,"type":"yii\\web\\TooManyRequestsHttpException"}

参见

更多信息,请参阅以下 URL:

版本控制

如果您构建未版本化的 API,那将是可怕的。让我们想象一下,您正在推出一个破坏性变更——基本上是任何与客户端开发者计划相反的变更,例如重命名或删除参数或更改响应格式——您可能会使您的许多(如果不是所有)客户的系统崩溃,导致愤怒的客户支持电话,更糟糕的是,大量流失。这就是为什么您必须保持 API 版本化的原因。在 Yii2 中,可以通过模块轻松地进行版本控制,因此版本将表示为独立的代码块。

准备工作

重复从“创建 REST 服务器”食谱的“准备工作”和“如何操作...”部分的所有步骤。

如何操作…

  1. 在您的应用文件夹中创建以下结构。总共,您必须在@app/modules文件夹中创建包含v1v2文件夹的文件夹。在每个模块的文件夹中,您必须创建控制器和模型文件夹:

    app/
        modules/
            v1/
                controllers/
                    FilmController.php
                Module.php
            v2/
                controllers/
                    FilmController.php
                Module.php
    
  2. 将导入模块添加到@app/config/web.php

    'modules' => [
       'v1' => [
           'class' => 'app\modules\v1\Module',
       ],
        'v2' => [
           'class' => 'app\modules\v2\Module'
       ]
    ],
    
  3. 使用以下代码创建@app/modules/v1/controllers/FilmController.php@app/modules/v2/controllers/FilmController.php

    <?php
    
        namespace app\modules\v1\controllers;
    
        use yii\rest\ActiveController;
    
        class FilmController extends ActiveController
        {
            public $modelClass = 'app\models\Film';
        }
    
        <?php
    
            namespace app\modules\v1\controllers;
    
            use yii\rest\ActiveController;
    
            class FilmController extends ActiveController
            {
                public $modelClass = 'app\models\Film';
            }
    
    <?php
        namespace app\modules\v1;
    
        class Module extends  \yii\base\Module
        {
            public function init()
            {
                parent::init();
            }
        }
    
    <?php
        namespace app\modules\v2;
    
        class Module extends  \yii\base\Module
        {
            public function init()
            {
                parent::init();
            }
        }
    

使用以下代码创建@app/modules/v1/Module.php@app/modules/v2/Module.php

工作原理…

每个模块代表我们 API 的一个独立版本。

现在,您可以通过两种方式指定 API 的版本:

  1. 通过 API 的 URL。您可以指定 v1 或 v2 版本。结果是http://yii-book.app/v1/film将返回版本 1 的电影列表,而http://yii-book.app/v2/film将返回版本 2 的电影列表。

  2. 您还可以通过 HTTP 请求头传递版本号。像往常一样,可以通过Accept头完成:

      // as a vendor content type
      Accept: application/vnd.company.myproject-v1+json
      // via a parameter
      Accept: application/json; version=v1
    

因此,我们现在有两个版本的 API,我们可以轻松地修改 v2 版本而不会感到头疼。我们的老客户继续使用 v1 版本,而新客户或希望升级的客户将使用 v2 版本。

还有更多...

更多信息,请参阅:

错误处理

有时你可能想要自定义默认的错误响应格式。例如,我们需要知道响应的时间戳以及响应是否成功。框架提供了一个简单的方法来实现这一点。

准备工作

重复从“创建 REST 服务器”菜谱中的所有步骤,这些步骤在“准备”和“如何操作…'’部分。

如何操作…

为了实现这个目标,你可以在@app/config/web.php中响应响应组件的beforeSend事件,如下所示:

'response' => [
    'class' => 'yii\web\Response',
    'on beforeSend' => function ($event) {
        $response = $event->sender;
        if ($response->data !== null) {
            $response->data = [
            'success' => $response->isSuccessful,
            'timestamp' => time(),
            'path' => Yii::$app->request->getPathInfo(),
            'data' => $response->data,
            ];
        }
    },
],

它是如何工作的…

为了了解这段代码中发生了什么,让我们稍微玩一下。首先,在控制台中运行以下命令:

curl -i "http://yii-book.app/films/1" 

你将得到以下输出:

HTTP/1.1 200 OK
Date: Thu, 24 Sep 2015 04:24:52 GMT
Server: Apache
X-Powered-By: PHP/5.5.23
Content-Length: 115
Content-Type: application/json; charset=UTF-8

{"success":true,"timestamp":1443068692,"path":"films/1","data":{"id":1,"title":"Interstellar","release_year":2014}}

其次,在你的控制台中运行以下命令:

curl -i "http://yii-book.app/films/1000"

你将得到以下结果:

HTTP/1.1 404 Not Found
Date: Thu, 24 Sep 2015 04:24:26 GMT
Server: Apache
X-Powered-By: PHP/5.5.23
Content-Length: 186
Content-Type: application/json; charset=UTF-8

{"success":false,"timestamp":1443068666,"path":"films/1000","data":{"name":"Not Found","message":"Object not found: 1000","code":0,"status":404,"type":"yii\\web\\NotFoundHttpException"}}

我们在发送响应内容之前已经更改了它。这样,就很容易定义响应是否成功。

相关内容

如需更多信息,请参阅www.yiiframework.com/doc-2.0/guide-rest-error-handling.html

第七章. 官方扩展

在本章中,我们将涵盖以下主题:

  • 认证客户端

  • SwiftMailer 邮件库

  • Faker 固定数据生成器

  • 想象库

  • MongoDB 驱动程序

  • ElasticSearch 引擎适配器

  • Gii 代码生成器

  • Pjax jQuery 插件

  • Redis 数据库驱动程序

简介

Yii2 的官方仓库为一些流行的库、数据库和搜索引擎提供了适配器。在本章中,我们将向您展示如何在项目中安装和使用官方扩展。您还将学习如何编写自己的扩展并与其他开发者共享。

认证客户端

此扩展为 Yii 2.0 框架添加了 OpenID、OAuth 和 OAuth2 消费者。

准备工作

  1. 使用 composer 创建一个新应用程序,如官方指南 www.yiiframework.com/doc-2.0/guide-start-installation.html 中所述。

  2. 使用以下命令安装扩展:

    composer require yiisoft/yii2-authclient
    
    

如何操作…

  1. 打开您的 GitHub 应用程序页面 github.com/settings/applications 并添加您自己的新应用程序:如何操作…

  2. 获取 客户端 ID客户端密钥如何操作…

  3. 配置您的 Web 配置并设置 authClientCollection 组件的相应选项:

    'components' => [
        // ...
        'authClientCollection' => [
            'class' => 'yii\authclient\Collection',
            'clients' => [
                'google' => [
                    'class' =>'yii\authclient\clients\GoogleOpenId'
                ],
                'github' => [
                    'class' => 'yii\authclient\clients\GitHub',
                    'clientId' => '87f0784aae2ac48f78a',
                    'clientSecret' =>'fb5953a54dea4640f3a70d8abd96fbd25592ff18',
                 ],
                    // etc.
            ],
        ],
    ],
    
  4. 打开您的 SiteController 并添加 auth 独立动作和成功回调方法:

    use yii\authclient\ClientInterface;
    
    public function actions()
    {
        return [
            // ...
            'auth' => [
                'class' => 'yii\authclient\AuthAction',
                'successCallback' => [$this, 'onAuthSuccess'],
            ],
        ];
    }
    
    public function onAuthSuccess(ClientInterface $client)
    {
        $attributes = $client->getUserAttributes();
        \yii\helpers\VarDumper::dump($attributes, 10, true);
        exit;
    }
    
  5. 打开 views/site/login.php 文件并插入 AuthChoice 小部件:

    <div class="site-login">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <div class="panel panel-default">
            <div class="panel-body">
                <?= yii\authclient\widgets\AuthChoice::widget(['baseAuthUrl' => ['site/auth'],
                'popupMode' => false,
                ]) ?>
            </div>
        </div>
    
        <p>Please fill out the following fields to login:</p>
       ...
    </div>
    
  6. 您将看到您已配置的提供者的图标:如何操作…

  7. 尝试使用 GitHub 提供商进行授权:如何操作…

  8. 如果成功,您的回调将显示授权用户属性:

    [
        'login' => 'Name'
        'id' => 0000000
        'avatar_url' =>'https://avatars.githubusercontent.com/u/0000000?v=3'
        'gravatar_id' => ''
        'url' => 'https://api.github.com/users/Name'
        'html_url' => 'https://github.com/Name'
        ...
        'name' => 'YourName'
        'blog' =>site.com'
        'email => mail@site.com'
        ...
    ]
    
  9. onAuthSuccess 方法中创建自己的授权代码,例如 github.com/yiisoft/yii2-authclient/blob/master/docs/guide/quick-start.md 中的示例。

工作原理…

该扩展为您的应用程序提供 OpenID、OAuth 和 OAuth2 认证客户端。

AuthChoice 小部件在所选服务网站上打开一个认证页面,并存储 auth 动作 URL。认证后,当前服务将用户重定向回,并通过 POST 请求发送认证数据。AuthAction 接收请求并调用相应的回调。

您可以使用任何现有的客户端或创建自己的客户端。

参见

SwiftMailer 邮件库

许多 Web 应用程序出于安全原因需要通过电子邮件发送通知并确认客户端的操作。Yii2 框架为已建立的库SwiftMailer提供了一个包装器,yiisoft/yii2-swiftmailer

准备工作

使用官方指南www.yiiframework.com/doc-2.0/guide-start-installation.html中描述的方法,通过 composer 创建一个新的应用程序。

基本和高级应用程序都自带这个扩展。

如何做到这一点…

现在我们将尝试从我们的应用程序发送任何类型的电子邮件。

发送纯文本电子邮件

  1. 将邮件器配置设置到config/console.php文件中:

    'components' => [
        // ...
        'mailer' => [
            'class' => 'yii\swiftmailer\Mailer',
            'useFileTransport' => true,
        ],
        // ...
    ],
    
  2. 创建一个测试控制台控制器,MailController,以下代码:

    <?php
    
        namespace app\commands;
    
        use yii\console\Controller;
        use Yii;
    
        class MailController extends Controller
        {
            public function actionSend()
            {
                Yii::$app->mailer->compose()
                ->setTo('to@yii-book.app')
                ->setFrom(['from@yii-book.app' => Yii::$app->name])
                ->setSubject('My Test Message')
                ->setTextBody('My Text Body')
                ->send();
        }
    }
    
  3. 运行以下控制台命令:

    php yii mail/send
    
    
  4. 检查您的runtime/mail目录。它应该包含您的邮件文件。

    注意

    注意:邮件文件包含特殊电子邮件源格式的消息,与任何邮件软件兼容。您也可以将其作为纯文本打开。

  5. useFileTransport参数设置为 false 或从配置中删除此字符串:

    'mailer' => [
        'class' => 'yii\swiftmailer\Mailer',
    ],
    

    然后将您的真实电子邮件 ID 放入setTo()方法中:

     ->setTo('my@real-email.com')
    
  6. 再次运行控制台命令:

    php yii mail/send
    
    
  7. 检查您的inbox目录。

注意

注意:SwiftMailer 默认使用标准的 PHP 函数mail()来发送邮件。请确保您的服务器已正确配置,可以通过mail()函数发送邮件。

许多邮件系统拒绝没有 DKIM 和 SPF 签名(例如,由mail()函数发送)的邮件,或将它们放入Spam文件夹。

发送 HTML 内容

  1. 确保您的应用程序包含mail/layouts/html.php文件,并添加mail/layouts/text.php文件,内容如下:

    <?php
    /* @var $this \yii\web\View */
    /* @var $message \yii\mail\MessageInterface */
    /* @var $content string */
    ?>
    <?php $this->beginPage() ?>
    <?php $this->beginBody() ?>
    <?= $content ?>
    <?php $this->endBody() ?>
    <?php $this->endPage() ?>
    
  2. mail/message-html.php文件中创建自己的视图:

    <?php
    use yii\helpers\Html;
    
    /* @var $this yii\web\View */
    /* @var $name string */
    ?>
    
    <p>Hello, <?= Html::encode($name) ?>!</p>
    

    创建一个mail/message-text.php文件,内容与原文件相同,但不含 HTML 标签:

    <?php
        use yii\helpers\Html;
    
    /* @var $this yii\web\View */
    /* @var $name string */
    ?>
    
         Hello, <?= Html::encode($name) ?>!
    
  3. 创建一个控制台控制器,MailController,以下代码:

    <?php
    
        namespace app\commands;
    
        use yii\console\Controller;
        use Yii;
    
        class MailController extends Controller
        {
            public function actionSendHtml()
            {
                $name = 'John';
    
                Yii::$app->mailer->compose('message-html',['name' => $name])
                ->setTo('to@yii-book.app')
                ->setFrom(['from@yii-book.app' => Yii::$app->name])
                ->setSubject('My Test Message')
                ->send();
            }
    
            public function actionSendCombine()
            {
                $name = 'John';
    
                Yii::$app->mailer->compose(['html' => 
                'message-html', 'text' => 'message-text'], [
                'name' => $name,
                ])
                ->setTo('to@yii-book.app')
                ->setFrom(['from@yii-book.app' 
                    => Yii::$app->name])
                ->setSubject('My Test Message')
                ->send();
            }
    }
    
  4. 运行以下控制台命令:

    php yii mail/send-html
    php yii mail/se
    nd-combine
    
    

使用 SMTP 传输

  1. mailer组件的transport参数设置如下:

    'mailer' => [
        'class' => 'yii\swiftmailer\Mailer',
        'transport' => [
            'class' => 'Swift_SmtpTransport',
            'host' => 'smtp.gmail.com',
            'username' => 'username@gmail.com',
            'password' => 'password',
            'port' => '587',
            'encryption' => 'tls',
        ],
    ],
    
  2. 编写并运行以下代码:

    Yii::$app->mailer->compose()
        ->setTo('to@yii-book.app')
        ->setFrom('username@gmail.com')
        ->setSubject('My Test Message')
        ->setTextBody('My Text Body')
        ->send();
    
  3. 检查您的 Gmail 收件箱。

注意

注意:Gmail 会自动将From字段重写为默认的配置电子邮件 ID,但其他电子邮件系统并不这样做。在传输配置和setFrom()方法中始终使用相同的电子邮件 ID,以传递其他电子邮件系统的反垃圾邮件策略。

附加文件和嵌入图片

添加相应的方法来附加任何文件到您的邮件中:

class MailController extends Controller
{
    public function actionSendAttach()
    {
        Yii::$app->mailer->compose()
            ->setTo('to@yii-book.app')
            ->setFrom(['from@yii-book.app' => Yii::$app->name])
            ->setSubject('My Test Message')
            ->setTextBody('My Text Body')
            ->attach(Yii::getAlias('@app/README.md'))
            ->send();
    }
}

或者,在您的电子邮件视图文件中使用embed()方法将图片粘贴到电子邮件内容中:

<img src="img/<?= $message->embed($imageFile); ?>">

它会自动附加一个图片文件并插入其唯一标识符。

它是如何工作的…

包装器实现了基类\yii\mail\MailerInterface。它的compose()方法返回一个消息对象(\yii\mail\MessageInterface的实现)。

您可以使用 setTextBody()setHtmlBody() 方法手动设置纯文本和 HTML 内容,或者将您的视图和视图参数传递给 compose() 方法。在这种情况下,邮件发送器会调用 \yii\web\View::render() 方法来渲染相应的内容。

useFileTransport 参数将邮件存储在文件中而不是实际发送。这对于本地开发和应用程序测试很有帮助。

参见

Faker 测试数据生成器

fzaninotto/faker 是一个 PHP 库,可以生成多种类型的假数据:姓名、电话、地址、随机字符串和数字等。它可以帮助您生成用于性能和逻辑测试的许多随机记录。您可以通过编写自己的格式化和生成器来扩展您支持的类型集合。

在 Yii2 应用程序骨架中,yiisoft/yii2-faker 包装器包含在 composer.json 文件的 require-dev 部分中,并用于测试代码(第十一章。

如何做到这一点…

  1. 打开 tests/codeception/templates 目录并添加测试数据模板文件,users.txt

    <?php
    /**
     * @var $faker \Faker\Generator
     * @var $index integer
     */
        return [
            'name' => $faker->firstName,
            'phone' => $faker->phoneNumber,
            'city' => $faker->city,
            'about' => $faker->sentence(7, true),
            'password' => Yii::$app->getSecurity()
            ->generatePasswordHash('password_' . $index),
            'auth_key' => Yii::$app->getSecurity()
            ->generateRandomString(),
        ];
    
  2. 运行测试控制台 yii 命令:

    php tests/codeception/bin/yii fixture/generate users --count=2
    
    
  3. 确认迁移生成。

  4. 检查 tests/codeception/fixtures 目录是否包含新的 users.php 文件,并包含如下自动生成数据:

    return [
        [
            'name' => 'Isadore',
            'phone' => '952.877.8545x190',
            'city' => 'New Marvinburgh',
            'about' => 'Ut quidem voluptatem itaque veniam voluptas dolores.',
            'password' => '$2y$13$Fi3LOl/sKlomUH.DLgqBkOB/uCLmgCoPPL1KXiW0hffnkrdkjCzAC',
            'auth_key' => '1m05hlgaAG8zfm0cyDyoRGMkbQ9W6hj1',
        ],
        [
            'name' => 'Raleigh',
            'phone' => '1-655-488-3585x699',
            'city' => 'Reedstad',
            'about' => 'Dolorem quae impedit tempore libero doloribus nobis dicta tempora facere.',
            'password' => '$2y$13$U7Qte5Y1jVLrx/pnhwdwt.1uXDegGXuNVzEQyUsb65WkBtjyjUuYm',
            'auth_key' => 'uWWJDgy5jNRk6KjqpxS5JuPv0OHearqE',
        ],
    ],
    

使用自己的数据类型

  1. 使用您自定义的值生成逻辑创建自己的提供者:

    <?php
        namespace tests\codeception\faker\providers;
    
        use Faker\Provider\Base;
    
        class UserStatus extends Base
        {
            public function userStatus()
            {
                return $this->randomElement([0, 10, 20, 30]);
            }
        }
    
  2. 将提供者添加到 /tests/codeception/config/config.php 文件中的提供者列表中:

    return [
        'controllerMap' => [
            'fixture' => [
                'class' => 'yii\faker\FixtureController',
                'fixtureDataPath' => '@tests/codeception/fixtures',
                'templatePath' => '@tests/codeception/templates',
                'namespace' => 'tests\codeception\fixtures',
                'providers' => [
                    'tests\codeception\faker\providers\UserStatus',
                ],
            ],
        ],
        // ...
    ];
    
  3. status 字段添加到您的测试数据模板文件中:

    <?php
    /**
     * @var $faker \Faker\Generator
     * @var $index integer
     */
        return [
            'name' => $faker->firstName,
            'status' => $faker->userStatus,
        ];
    
  4. 使用控制台命令重新生成测试数据:

    php tests/codeception/bin/yii fixture/generate users --count=2
    
    
  5. 检查生成的 fixtures/users.php 文件中的代码是否包含您的自定义值:

    return [
        [
            'name' => 'Christelle',
            'status' => 30,
        ],
        [
            'name' => 'Theo',
            'status' => 10,
        ],
    ];
    

它是如何工作的…

yii2-faker扩展包含一个控制台生成器(它使用您的模板生成固定数据文件),并提供原始Faker对象的预置实例。您可以在控制台参数中生成所有或特定的固定数据,并可以传递自定义计数或语言。

注意

注意:如果您的测试使用这些固定数据,请小心处理现有的测试文件,因为自动生成会完全重写旧数据。

参见

Imagine 库

Imagine 是一个用于图像处理的 OOP 库。它允许您使用 GD、Imagic 和 Gmagic PHP 扩展对不同的图像进行裁剪、调整大小和其他操作。Yii2-Imagine 是该库的轻量级静态包装器。

准备工作

  1. 按照官方指南使用 composer 创建一个新应用,官方指南可在www.yiiframework.com/doc-2.0/guide-start-installation.html找到。

  2. 使用以下命令安装扩展:

    composer require yiisoft/yii2-imagine
    
    

如何做到这一点…

在您的项目中,您可以使用两种方式使用此扩展:

  • 作为工厂使用

  • 使用内部方法

作为工厂使用

您可以使用原始Imagine库类的实例:

$imagine = new Imagine\Gd\Imagine();
// or
$imagine = new Imagine\Imagick\Imagine();
// or
$imagine = new Imagine\Gmagick\Imagine();

然而,这取决于您系统中现有的相应 PHP 扩展。您可以使用getImagine()方法:

$imagine = \yii\imagine\Image::getImagine();

使用内部方法

您可以使用crop()thumbnail()watermark()text()frame()方法进行常见的通用操作,如下所示:

<?php
    use yii\imagine\Image;
    Image::crop('path/to/image.jpg', 100, 100, ManipulatorInterface::THUMBNAIL_OUTBOUND)
    ->save('path/to/destination/image.jpg', ['quality' => 90]);

\yii\imagine\BaseImage类的源代码中查看所有支持方法的签名以获取更多详细信息。

它是如何工作的…

扩展准备用户数据,创建原始 Imagine 对象,并在其上调用相应的方法。所有方法都返回这个原始图像对象。您可以继续操作图像或将结果保存到您的磁盘上。

参见

MongoDB 驱动程序

此扩展为 Yii2 框架提供了 MongoDB 集成,并允许您通过ActiveRecord-style模型与 MongoDB 集合的记录一起工作。

准备工作

  1. 使用 composer,按照官方指南 www.yiiframework.com/doc-2.0/guide-start-installation.html 中的说明创建一个新的应用程序。

  2. 使用适用于你系统的正确安装过程从 docs.mongodb.org/manual/installation/ 安装 MongoDB。

  3. 安装 php5-mongo PHP 扩展。

  4. 使用以下命令安装组件:

    composer require yiisoft/yii2-mongodb
    
    

如何操作…

  1. 首先,创建新的 MongoDB 数据库。在 mongo-client shell 中运行它并输入数据库名称:

    mongo
    > use mydatabase
    
    
  2. 将此连接信息添加到你的 components 配置部分:

    return [
        // ...
        'components' => [
            // ...
            'mongodb' => [
                'class' => '\yii\mongodb\Connection',
                'dsn' =>
                    'mongodb://localhost:27017/mydatabase',
            ],
        ],
    ];
    
  3. 将新的控制台控制器添加到你的控制台配置文件中:

    return [
        // ...
        'controllerMap' => [
            'mongodb-migrate' =>
            'yii\mongodb\console\controllers\MigrateController'
        ],
    ];
    
  4. 使用 shell 命令创建新的迁移:

    php yii mongodb-migrate/create create_customer_collection
    
    
  5. 将以下代码输入到 up()down() 方法中:

    <?php
    
        use yii\mongodb\Migration;
    
        class m160201_102003_create_customer_collection extends Migration
        {
            public function up()
            {
                $this->createCollection('customer');
            }
    
            public function down()
            {
                $this->dropCollection('customer');
            }
        }
    
  6. 应用迁移:

    php yii mongodb-migrate/up
    
    
  7. 将 MongoDB 调试面板和模型生成器放入你的配置中:

    if (YII_ENV_DEV) {
        // configuration adjustments for 'dev' environment
        $config['bootstrap'][] = 'debug';
        $config['modules']['debug'] = [
            'class' => 'yii\debug\Module',
            'panels' => [
                'mongodb' => [
                    'class' => 'yii\mongodb\debug\MongoDbPanel',
                ],
            ],
        ];
    
        $config['bootstrap'][] = 'gii';
        $config['modules']['gii'] = [
            'class' => 'yii\gii\Module',
            'generators' => [
                'mongoDbModel' => [
                    'class' => 'yii\mongodb\gii\model\Generator'
                ]
            ],
        ];
    }
    
  8. 运行 Gii 生成器:如何操作…

  9. 启动新的 MongoDB 模型生成器 以为你自己的集合生成新模型:如何操作…

  10. 点击 预览生成 按钮。

  11. 确认你有新的模型,app\models\Customer

    <?php
    
        namespace app\models;
    
        use Yii;
        use yii\mongodb\ActiveRecord;
    
    /**
    * This is the model class for collection "customer".
    *
    * @property \MongoId|string $_id
    * @property mixed $name
    * @property mixed $email
    * @property mixed $address
    * @property mixed $status
    */
        class Customer extends ActiveRecord
        {
            public static function collectionName()
            {
                return 'customer';
            }
    
            public function attributes()
            {
                return [
                   '_id',
                   'name',
                   'email',
                   'address',
                   'status',
                ];
            }
    
            public function rules()
            {
                return [
                [['name', 'email', 'address', 'status'], 'safe']
                ];
            }
    
            public function attributeLabels()
            {
                return [
                    '_id' => 'ID',
                    'name' => 'Name',
                    'email' => 'Email',
                    'address' => 'Address',
                    'status' => 'Status',
                ];
            }
        }
    
  12. 再次运行 Gii 并生成 CRUD:如何操作…

  13. 确认你已经生成了 CustomerController 类并运行新的客户管理页面:如何操作…

  14. 你现在可以创建、更新和删除客户的资料。

  15. 在页面页脚中查找 调试 面板:如何操作…

  16. 你可以看到总的 MongoDB 查询次数和总执行时间。点击计数徽章并检查查询:如何操作…

基本用法

你可以通过 \yii\mongodb\Collection 实例访问数据库和集合:

$collection = Yii::$app->mongodb->getCollection('customer');$collection->insert(['name' => 'John Smith', 'status' => 1]);

要执行 find 查询,你应该使用 \yii\mongodb\Query:

use yii\mongodb\Query;
$query = new Query;
// compose the query
$query->select(['name', 'status'])
    ->from('customer')
    ->limit(10);
// execute the query
$rows = $query->all();

注意

注意:MongoDB 文档 id ("_id" 字段) 不是标量,而是 \MongoId 类的实例。

你不必关心从整数或字符串 $id 值到 \MongoId 的转换,因为查询构建器会自动转换它:

$query = new \yii\mongodb\Query;
$row = $query->from('item')
    ->where(['_id' => $id]) // implicit typecast to \MongoId
    ->one();

要获取实际的 Mongo ID 字符串,你应该将 \MongoId 实例转换为字符串:

$query = new Query;

$row = $query->from('customer')->one();
var_dump($row['_id']); // outputs: "object(MongoId)"var_dump((string)$row['_id']);

它是如何工作的…

此扩展的 QueryActiveQueryActiveRecord 类扩展了 yii\db\QueryInterfaceyii\db\BaseActiveRecord,因此它们与内置框架的 QueryActiveQueryActiveRecord 类兼容。

你可以使用 yii\mongodb\ActiveRecord 类为你的模型,并使用 yii\mongodb\ActiveQuery 构建器检索你的模型并在你的数据提供者中使用它们:

use yii\data\ActiveDataProvider;
use app\models\Customer;
$provider = new ActiveDataProvider([
    'query' => Customer::find(),
    'pagination' => [
        'pageSize' => 10,
    ]
]);

关于如何使用 Yii 的 ActiveRecord 的一般信息,请参阅第三章 ActiveRecord, Model, and Database。

参见

ElasticSearch 引擎适配器

此扩展是 ElasticSearch 全文搜索引擎集成到 Yii2 框架中的 ActiveRecord 类似包装器。它允许你使用任何模型数据,并使用 ActiveRecord 模式在 ElasticSearch 集合中检索和存储记录。

准备工作

  1. 按照官方指南使用 composer 创建一个新的应用程序,如 www.yiiframework.com/doc-2.0/guide-start-installation.html 中所述。

  2. 安装位于 www.elastic.co/downloads/elasticsearchElasticSearch 服务。

  3. 使用以下命令安装扩展:

    compose
    r require yiisoft/yii2-elasticsearch
    
    

如何做到这一点…

在你的应用程序配置中设置新的 ElasticSearch 连接:

return [
    //....
    'components' => [
        'elasticsearch' => [
            'class' => 'yii\elasticsearch\Connection',
            'nodes' => [
                ['http_address' => '127.0.0.1:9200'],
                // configure more hosts if you have a cluster
            ],
        ],
    ]
];

使用查询类

你可以使用 Query 类对任何集合进行低级查询:

use  \yii\elasticsearch\Query;

$query = new Query;
$query->fields('id, name')
    ->from('myindex', 'users')
    ->limit(10);

$query->search();

你也可以创建一个命令并直接运行它:

$command = $query->create
Command();
$rows = $command->search();

使用 ActiveRecord

使用 ActiveRecord 是访问你的记录的常见方式。只需扩展 yii\elasticsearch\ActiveRecord 类并实现 attributes() 方法来定义你的文档属性。

例如,你可以编写 Customer 模型:

class Buyer extends \yii\elasticsearch\ActiveRecord
{
    public function attributes()
    {
        return ['id', 'name', 'address', 'registration_date'];
    }
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['buyer_id' => 'id'])->orderBy('id');
    }
}

然后编写 Order 模型:

class Order extends \yii\elasticsearch\ActiveRecord
{
    public function attributes()
    {
        return ['id', 'user_id', 'date'];
    }

    public function getBuyer()
    {
        return $this->hasOne(Customer::className(), ['id' => 'buyer_id']);
    }
}

你可以覆盖 index()type() 来定义此记录表示的索引和类型。

以下是一个使用示例:

$buyer = new Buyer();
$buyer>primaryKey = 1; // it equivalent to $customer->id = 1;
$buyer>name = 'test';
$buyer>save();

$buyer = Buyer::get(1);

$buyer = Buyer::mget([1,2,3]);

$buyer = Buyer::find()->where(['name' => 'test'])->one();

你可以使用查询 DSL 进行特定查询:

$result = Article::find()->query(["match" => ["title" => "yii"]])->all();
        $query = Article::find()->query([
        "fuzzy_like_this" => [
            "fields" => ["title", "description"],
            "like_text" => "Some search text",
            "max_query_terms" => 12
        ]
]);
$query->all();

你可以为你的搜索添加分面:

$query->addStatisticalFacet('click_stats', ['field' => 'visit_count']);
$query->search();

使用 ElasticSearch DebugPanel

此扩展包含一个用于 yii2-debug 模块的专用面板。它允许你查看所有执行的查询。你可以在配置文件中包含此面板:

if (YII_ENV_DEV) {
    // configuration adjustments for 'dev' environment
    $config['bootstrap'][] = 'debug';
    $config['modules']['debug'] =  [
        'class' => 'yii\debug\Module',
        'panels' => [
            'elasticsearch' => [
                'class' => 'yii\elasticsearch\DebugPanel',
            ],
        ],
    ];

    $config['bootstrap'][] = 'gii';
    $config['modules']['gii'] = 'yii\gii\Module';
}

它是如何工作的…

此扩展提供了一个低级命令构建器和高级 ActiveRecord 实现来从 ElasticSearch 索引查询记录。

扩展的 ActiveRecord 使用与第三章 ActiveRecord, Model, and Database 中描述的数据库 ActiveRecord 非常相似,除了 join()groupBy()having()union() 这些 ActiveQuery 操作符。

注意

注意ElasticSearch 默认将返回的记录数限制为十个项目。如果你使用带有 via() 选项的关系时,请注意限制。

参见

Gii 代码生成器

此扩展为 Yii 2 应用程序提供了一个基于 Web 的代码生成器,称为 Gii。您可以使用 Gii 快速生成模型、表单、模块、CRUD 以及更多。

准备工作

  1. 按照官方指南使用 composer 创建一个新的应用程序 www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 使用 shell 命令创建一个新的迁移:

    php yii migrate/create create_customer_table
    
    
  3. 将以下代码放入 up()down() 方法中:

    use yii\db\Schema;
    use yii\db\Migration;
    class m160201_154207_create_customer_table extends Migration
    {
        public function up()
        {
            $tableOptions = null;
            if ($this->db->driverName === 'mysql') {
                $tableOptions = 
                    'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
           }
            $this->createTable('{{%customer}}', [
                'id' => Schema::TYPE_PK,
                'name' => Schema::TYPE_STRING . ' NOT NULL',
                'email' => Schema::TYPE_STRING . ' NOT NULL',
                'address' => Schema::TYPE_STRING,
            ], $tableOptions);
        }
    
        public function down()
        {
            $this->dropTable('{{%customer}}');
        }
    }
    
  4. 应用迁移:

    php yii migrate/up
    

如何操作…

在您的项目中,您可以使用两种方式使用此扩展:

  • 使用图形用户界面

  • 使用命令行界面

使用图形用户界面

  1. 检查您的 Web 配置文件是否包含以下代码:

    if (YII_ENV_DEV) {
        $config['bootstrap'][] = 'gii';
        $config['modules']['gii'] = [
            'class' => 'yii\gii\Module',
        ];
    }
    
  2. 您的 web/index.php 文件将定义开发环境:

    defined('YII_ENV') or define('YII_ENV', 'dev');
    

    之前的配置表明,在开发环境中,应用程序应包含一个名为 gii 的模块,该模块属于 yii\gii\Module 类。

    默认情况下,该模块允许从 IP 地址 127.0.0.1 访问。如果您在其他位置工作,请在 allowedIPs 属性中添加您的地址:

    $config['modules']['gii'] = [
        'class' => 'yii\gii\Module',
        allowedIPs = ['127.0.0.1', '::1', '192.168.0.*'],
    ];
    
  3. 前往您应用程序的 gii 路由:http://localhost/index.php?r=gii使用图形用户界面

  4. 点击 模型生成器 按钮,并在表单中输入您的表名和模型名:使用图形用户界面

  5. 点击 预览 按钮。您必须查看特色文件列表:使用图形用户界面

  6. 如果您想重新生成现有文件,Gii 将将这些文件标记为黄色:使用图形用户界面

  7. 在这种情况下,您可以查看现有文件和新文件之间的差异,并在需要时覆盖目标文件。

  8. 在完成所有这些之后,点击 生成 按钮:使用图形用户界面

  9. 检查新类 \app\models\Customer 是否存在。

  10. CRUD 是在大多数网站上使用数据的四个常见任务的缩写:创建、读取、更新和删除。要使用 Gii 创建 CRUD,请选择 CRUD 生成器 部分。指定您的模型类并输入其他字段:与 GUI 一起工作

  11. 生成新条目:与 GUI 一起工作

  12. 之后,尝试打开新的控制器:与 GUI 一起工作

您将看到一个数据网格,显示数据库表中的客户。尝试创建一个新条目。您可以通过在列标题中输入筛选条件来对网格进行排序或筛选。

使用 CLI 一起工作

Gii 还提供用于代码生成的控制台控制器。

  1. 检查您的控制台配置是否包含 Gii 模块设置:

    return [
        // ...
        'modules' => [
            'gii' => 'yii\gii\Module',
        ],
        // ...
    ];
    
  2. 运行任何 shell 命令以获取帮助:

    php yii help gii
    php yii help gii/model
    
    
  3. 输入以下命令以启动模型生成过程:

    php yii gii/model --tableName=customer --modelClass=Customer --useTablePrefix=1
    
    
  4. 检查新的类 \app\models\Customer 是否存在。

  5. 为您的模型生成 CRUD:

    php yii gii/crud --modelClass=app\\models\\Customer \
        --searchModelClass=app\\models\\CustomerSearch \
        --controllerClass=app\\controllers\\CustomerController
    

它是如何工作的…

Gii 允许您生成一些标准代码元素,而不是手动输入。它提供基于 Web 和控制台界面来与每个生成器一起工作。

参见

Pjax jQuery 插件

Pjax 是一个集成 pjax jQuery 插件的小部件。所有由该小部件包装的内容都将通过 AJAX 重新加载,而不会刷新当前页面。该小部件还使用 HTML5 历史 API 来更改浏览器地址栏中的当前 URL。

准备工作

使用官方指南中描述的方法通过 composer 创建一个新的应用程序:www.yiiframework.com/doc-2.0/guide-start-installation.html

如何做到这一点…

在以下示例中,您可以了解如何使用 yii\grid\GridView 小部件与 Pjax:

<?php
    use yii\widgets\Pjax;
?>
<?php Pjax::begin(); ?>
    <?= GridView::widget([...]); ?>
<?php Pjax::end(); ?>

只需将任何代码片段包裹在 Pjax::begin()Pjax::end() 调用中。

这将渲染以下 HTML 代码:

<div id="w1">
    <div id="w2" class="grid-view">...</div>
</div>

<script type="text/javascript">jQuery(document).ready(function () {
    jQuery(document).pjax("#w1 a", "#w1", {...});
});</script>

所有带有分页和排序链接的包装内容都将通过 AJAX 重新加载。

指定自定义 ID

Pjax 从 AJAX 请求中获取页面内容,然后提取具有相同 ID 的自己的 DOM 元素。您可以通过不使用布局渲染内容来优化页面渲染性能,特别是对于 Pjax 请求:

public function actionIndex()
{
    $dataProvider = …;

    if (Yii::$app->request->isPjax) {
        return $this->renderPartial('_items', [
            'dataProvider' => $dataProvider,
        ]);
    } else {
        return $this->render('index', [
            'dataProvider' => $dataProvider,
        ]);
    }
}

默认情况下,yii\base\Widget::getId 方法在具有递增属性的任何页面上递增标识符和部件:

<nav id="w0">...</nav> // Main navigation
<ul id="w1">...</ul> // Breadcrumbs widget
<div id="w2">...</div> // Pjax widget

要使用 renderPartial()renderAjax() 方法渲染,而不渲染布局,您的页面将只有一个编号为 0 的小部件:

<div id="w0">...</div> // Pjax widget

结果,在下一个请求中,您自己的小部件将找不到其具有 w2 选择器的块。

然而,Pjax 将在 Ajax 响应中找到具有 w2 选择器的相同块。结果,在下一个请求中,您自己的小部件将找不到具有 w2 选择器的块。

因此,您必须手动为所有 Pjax 小部件指定一个唯一的标识符,以避免不同的冲突:

<?php Pjax::begin(['id' => 'countries']) ?>
    <?= GridView::widget([...]); ?>
<?php Pjax::end() ?>

使用 ActiveForm

默认情况下,Pjax 仅与包装块中的链接一起工作。如果您想与 ActiveForm 小部件一起使用它,您必须使用表单的 data-pjax 选项:

<?php
use \yii\widgets\Pjax
use \yii\widgets\ActiveForm;

<?php yii\widgets\Pjax::begin(['id' => 'my-block']) ?>
    <?php $form = ActiveForm::begin(['options' => [
        'data-pjax' => true,
    ]]); ?> 
        <?= $form->field($model, 'name') ?>
    <?php ActiveForm::end(); ?>
<?php Pjax::end(); ?>

它在表单提交事件上添加相应的监听器。

您还可以使用 Pjax 小部件的 $formSelector 选项来指定哪个表单提交可能触发 pjax

与客户端脚本一起工作

您可以订阅容器事件:

<?php $this->registerJs('
    $("#my-block").on("pjax:complete", function() {
        alert('Pjax is completed');
    });
'); ?>

或者,您可以使用其选择器手动重新加载容器:

<?php $this->registerJs('
    $("#my-button").on("click", function() {
        $.pjax.reload({container:"#my-block"});
    });
'); ?>

它是如何工作的…

Pjax 是任何代码片段的简单包装器。它订阅片段中所有链接的点击事件,并替换整个页面,通过 Ajax 调用重新加载。我们可以为包装的表单使用 data-pjax 属性,并且任何表单提交都将触发 Ajax 请求。

小部件将动态加载和更新小部件的主体内容,而无需加载布局资源(JS、CSS)。

您可以配置小部件的 $linkSelector 以指定哪些链接应触发 Pjax,并配置 $formSelector 以指定哪些表单提交可能触发 Pjax。

您可以通过向此链接添加 data-pjax="0" 属性来禁用容器中特定链接的 Pjax。

参见

Redis 数据库驱动程序

此扩展允许您在 Yii2 框架的任何项目中使用 Redis 键值存储。它包含 CacheSession 存储处理程序,以及实现 ActiveRecord 模式以访问 Redis 数据库记录的扩展。

准备工作

  1. 使用 composer 创建一个新的应用程序,具体操作请参考官方指南中的www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 安装存储:redis.io

  3. 使用以下命令安装所有迁移:

    composer require yiisoft/yii2-redis
    
    

如何操作…

首先,在您的配置文件中配置 Connection 类:

return [
    //....
    'components' => [
        'redis' => [
            'class' => 'yii\redis\Connection',
            'hostname' => 'localhost',
            'port' => 6379,
            'database' => 0,
        ],
    ]
];

直接使用

对于使用 Redis 命令的低级操作,您可以使用连接组件的executeCommand方法:

Yii::$app->redis->executeCommand('hmset', ['test_collection', 'key1', 'val1', 'key2', 'val2']);

您也可以使用简化的快捷方式来代替executeCommand调用:

Yii::$app->redi
s->hmset('test_collection', 'key1', 'val1', 'key2', 'val2')

使用 ActiveRecord

要通过ActiveRecord模式访问 Redis 记录,您的记录类需要从yii\redis\ActiveRecord基类扩展并实现attributes()方法:

class Customer extends \yii\redis\ActiveRecord
{
    public function attributes()
    {
        return ['id', 'name', 'address', 'registration_date'];
    }
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }
}

任何模型的键可以通过primaryKey()方法定义,如果没有指定,默认为id。如果未在primaryKey()方法中手动指定,则主键需要放在属性列表中。

以下是一个使用示例:

$customer = new Customer();
$customer->name = 'test';
$customer->save();
echo $customer->id; // id will automatically be incremented if not set explicitly
// find by query
$customer = Customer::find()->where(['name' => 'test'])->one();

它是如何工作的…

扩展提供了Connection组件,用于对 Redis 存储记录进行低级访问。

您还可以使用类似 ActiveRecord 的模型,并限制方法集(where(), limit(), offset(), 和 indexBy())。其他方法不存在,因为 Redis 不支持 SQL 查询。

Redis 中没有表,因此您不能通过连接表名定义关系。您只能通过其他hasMany关系定义多对多关系。

关于如何使用 Yii 的 ActiveRecord 的一般信息,请参阅第三章, ActiveRecord, 模型,和数据库

参见

第八章。扩展 Yii

在本章中,我们将涵盖以下主题:

  • 创建辅助器

  • 创建模型行为

  • 创建组件

  • 创建可重用的控制器操作

  • 创建可重用的控制器

  • 创建小部件

  • 创建 CLI 命令

  • 创建过滤器

  • 创建模块

  • 创建自定义视图渲染器

  • 创建多语言应用程序

  • 使扩展准备分发

简介

在本章中,我们将向您展示如何实现自己的 Yii 扩展,以及如何使您的扩展可重用并对社区有用。此外,我们将关注许多您应该做的事情,以确保您的扩展尽可能高效。

创建辅助器

yii\helpers 命名空间中有很多内置框架辅助器,如 StringHelper。这些包含用于操作字符串、文件、数组和其他主题的静态方法集。

在许多情况下,为了添加额外的行为,你可以创建一个自己的辅助器并将任何静态函数放入其中。例如,我们在本食谱中实现了数字辅助器。

准备工作

使用官方指南中描述的 composer 包管理器创建一个新的 yii2-app-basic 应用程序,请参阅 www.yiiframework.com/doc-2.0/guide-start-installation.html.

如何操作…

  1. 在你的项目中创建 helpers 目录并编写 NumberHelper 类:

    <?php
    namespace app\helpers;
    
    class NumberHelper
    {
        public static function format($value, $decimal = 2)
        {
            return number_format($value, $decimal, '.', ',');
        }
    }
    
  2. actionNumbers 方法添加到 SiteController

    <?php
    ...
    class SiteController extends Controller
    {
        …
    
        public function actionNumbers()
        {
            return $this->render('numbers', ['value' => 18878334526.3]);
        }
    }
    
  3. 添加 views/site/numbers.php 视图:

    <?php
    use app\helpers\NumberHelper;
    use yii\helpers\Html;
    
    /* @var $this yii\web\View */
    /* @var $value float */
    
    $this->title = 'Numbers';
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div class="site-numbers">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <p>
            Raw number:<br />
            <b><?= $value ?></b>
        </p>
        <p>
            Formatted number:<br />
            <b><?= NumberHelper::format($value) ?></b>
        </p>
    </div>
    
  4. 打开操作。你应该看到以下结果:如何操作…

在其他情况下,你可以指定另一个小数位数。观察以下示例:

NumberHelper::format($value, 3)

它是如何工作的…

任何 Yii2 辅助器都只是一组函数,作为相应类中的静态方法实现。

你可以使用辅助器来实现任何不同格式的输出,对任何变量的值进行操作,以及其他情况。

注意

通常,静态辅助器是轻量级的干净函数,具有少量参数。避免将业务逻辑和其他复杂操作放入辅助器中。在其他情况下,使用小部件或其他组件代替辅助器。

参见

关于辅助器的更多信息,请参阅:

www.yiiframework.com/doc-2.0/guide-helper-overview.html.

关于内置辅助器的示例,请参阅框架 helpers 目录中的源代码。对于框架,请参阅:

github.com/yiisoft/yii2/tree/master/framework/helpers.

创建模型行为

当前的 Web 应用程序中有许多类似的解决方案。领先的产品,如 Google 的 Gmail,正在定义良好的 UI 模式。其中之一是软删除。与需要大量确认的永久删除不同,Gmail 允许我们立即标记消息为已删除,然后轻松撤销。同样的行为可以应用于任何对象,如博客文章、评论等。

让我们创建一个允许标记模型为已删除、恢复模型、选择尚未删除的模型、已删除的模型以及所有模型的行为。在这个菜谱中,我们将遵循测试驱动开发的方法来规划行为并测试实现是否正确。

准备工作

  1. 使用官方指南中描述的 composer 创建一个新的yii2-app-basic应用程序,指南链接为www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 为工作和测试创建两个数据库。

  3. config/db.php中配置 Yii 以使用您主应用程序中的第一个数据库。确保测试应用程序使用tests/codeception/config/config.php中的第二个数据库。

  4. 创建一个新的迁移:

    <?php
    use yii\db\Migration;
    
    class m160427_103115_create_post_table extends Migration
    {
        public function up()
        {
            $this->createTable('{{%post}}', [
                'id' => $this->primaryKey(),
                'title' => $this->string()->notNull(),
                'content_markdown' => $this->text(),
                'content_html' => $this->text(),
            ]);
        }
    
        public function down()
        {
            $this->dropTable('{{%post}}');
        }
    }
    
  5. 将迁移应用到工作数据库和测试数据库:

    ./yii migrate
    tests/codeception/bin/yii migrate
    
  6. 创建Post模型:

    <?php
    namespace app\models;
    
    use app\behaviors\MarkdownBehavior;
    use yii\db\ActiveRecord;
    
    /**
     * @property integer $id
     * @property string $title
     * @property string $content_markdown
     * @property string $content_html
     */
    class Post extends ActiveRecord
    {
        public static function tableName()
        {
            return '{{%post}}';
        }
    
        public function rules()
        {
            return [
                [['title'], 'required'],
                [['content_markdown'], 'string'],
                [['title'], 'string', 'max' => 255],
            ];
        }
    }
    

如何做…

首先,让我们准备一个测试环境,从定义Post模型的固定数据开始。创建tests/codeception/unit/fixtures/PostFixture.php文件:

<?php
namespace app\tests\codeception\unit\fixtures;

use yii\test\ActiveFixture;

class PostFixture extends ActiveFixture
{
    public $modelClass = 'app\models\Post';
    public $dataFile = '@tests/codeception/unit/fixtures/data/post.php';
}
  1. 将固定数据文件添加到tests/codeception/unit/fixtures/data/post.php

    <?php
    return [
        [
            'id' => 1,
            'title' => 'Post 1',
            'content_markdown' => 'Stored *markdown* text 1',
            'content_html' => "<p>Stored <em>markdown</em> text 1</p>\n",
        ],
    ];
    
  2. 然后,我们需要创建一个测试用例,tests/codeception/unit/MarkdownBehaviorTest.php

    <?php
    namespace app\tests\codeception\unit;
    
    use app\models\Post;
    use app\tests\codeception\unit\fixtures\PostFixture;
    use yii\codeception\DbTestCase;
    
    class MarkdownBehaviorTest extends DbTestCase
    {
        public function testNewModelSave()
        {
            $post = new Post();
            $post->title = 'Title';
            $post->content_markdown = 'New *markdown* text';
    
            $this->assertTrue($post->save());
            $this->assertEquals("<p>New <em>markdown</em> text</p>\n", $post->content_html);
        }
    
        public function testExistingModelSave()
        {
            $post = Post::findOne(1);
    
            $post->content_markdown = 'Other *markdown* text';
            $this->assertTrue($post->save());
    
            $this->assertEquals("<p>Other <em>markdown</em> text</p>\n", $post->content_html);
        }
    
        public function fixtures()
        {
            return [
                'posts' => [
                    'class' => PostFixture::className(),
                ]
            ];
        }
    }
    
  3. 运行单元测试:

    codecept run unit MarkdownBehaviorTest
    Ensure that tests has not passed:
    Codeception PHP Testing Framework v2.0.9
    Powered by PHPUnit 4.8.27 by Sebastian Bergmann and contributors.
    
    Unit Tests (2) ---------------------------------------------------------------------------
    Trying to test ... MarkdownBehaviorTest::testNewModelSave             Error
    Trying to test ... MarkdownBehaviorTest::testExistingModelSave        Error
    ---------------------------------------------------------------------------
    
    Time: 289 ms, Memory: 16.75MB
    
    
  4. 现在我们需要实现行为,将其附加到模型上,并确保测试通过。创建一个新的目录,behaviors。在这个目录下,创建一个MarkdownBehavior类:

    <?php
    namespace app\behaviors;
    
    use yii\base\Behavior;
    use yii\base\Event;
    use yii\base\InvalidConfigException;
    use yii\db\ActiveRecord;
    use yii\helpers\Markdown;
    
    class MarkdownBehavior extends Behavior
    {
        public $sourceAttribute;
        public $targetAttribute;
    
        public function init()
        {
            if (empty($this->sourceAttribute) || empty($this->targetAttribute)) {
                throw new InvalidConfigException('Source and target must be set.');
            }
            parent::init();
        }
    
        public function events()
        {
            return [
                ActiveRecord::EVENT_BEFORE_INSERT => 'onBeforeSave',
                ActiveRecord::EVENT_BEFORE_UPDATE => 'onBeforeSave',
            ];
        }
    
        public function onBeforeSave(Event $event)
        {
            if ($this->owner->isAttributeChanged($this->sourceAttribute)) {
                $this->processContent();
            }
        }
    
        private function processContent()
        {
            $model = $this->owner;
            $source = $model->{$this->sourceAttribute};
            $model->{$this->targetAttribute} = Markdown::process($source);
        }
    }
    
  5. 让我们将行为附加到Post模型上:

    class Post extends ActiveRecord
    {
        ...
    
        public function behaviors()
        {
            return [
                'markdown' => [
                    'class' => MarkdownBehavior::className(),
                    'sourceAttribute' => 'content_markdown',
                    'targetAttribute' => 'content_html',
                ],
            ];
        }
    }
    
  6. 运行测试并确保通过:

    Codeception PHP Testing Framework v2.0.9
    Powered by PHPUnit 4.8.27 by Sebastian Bergmann and contributors.
    
    Unit Tests (2) ---------------------------------------------------------------------------
    Trying to test ... MarkdownBehaviorTest::testNewModelSave             Ok
    Trying to test ... MarkdownBehaviorTest::testExistingModelSave        Ok
    ---------------------------------------------------------------------------
    
    Time: 329 ms, Memory: 17.00MB
    
    
  7. 就这样。我们已经创建了一个可重用的行为,并且可以通过将其连接到模型来在所有未来的项目中使用它。

它是如何工作的…

让我们从测试用例开始。由于我们想要使用一组模型,我们正在定义固定数据。每次执行测试方法时,固定数据集都会放入“数据库”中。

我们准备单元测试来指定行为应该如何工作:

  • 首先,我们正在测试新模型内容的处理。行为必须将源属性中的 Markdown 文本转换为 HTML,并将第二个存储到目标属性中。

  • 其次,我们正在测试更新现有模型的内容。在更改 Markdown 内容并保存模型后,我们必须获取更新的 HTML 内容。

现在,让我们转到有趣的实现细节。在行为中,我们可以添加自己的方法,这些方法将被混合到附加行为到的模型中。我们还可以订阅所有者组件的事件。我们正在使用它来添加一个自己的监听器:

public function events()
{
    return [
        ActiveRecord::EVENT_BEFORE_INSERT => 'onBeforeSave',
        ActiveRecord::EVENT_BEFORE_UPDATE => 'onBeforeSave',
    ];
}

现在我们可以实现这个监听器:

public function onBeforeSave(Event $event)
{
    if ($this->owner->isAttributeChanged($this->sourceAttribute))
    {
        $this->processContent();
    }
}

在所有方法中,我们可以使用owner属性来获取行为附加到的对象。一般来说,我们可以将任何行为附加到我们的模型、控制器、应用程序和其他扩展yii\base\Component类的组件上。此外,我们还可以将一个行为重复附加到模型上以处理不同的属性:

class Post extends ActiveRecord
{
    ...

    public function behaviors()
    {
        return [
            [
                'class' => MarkdownBehavior::className(),
                'sourceAttribute' => 'description_markdown',
                'targetAttribute' => 'description_html',
            ],
            [
                'class' => MarkdownBehavior::className(),
                'sourceAttribute' => 'content_markdown',
                'targetAttribute' => 'content_html',
            ],
        ];
    }
}

此外,我们可以像yii\behaviors\TimestampBehavior一样扩展yii\base\AttributeBehavior类,以更新任何事件指定的属性。

参见

要了解更多关于行为和事件的信息,请参阅以下页面:

有关 Markdown 语法的更多信息,请参阅daringfireball.net/projects/markdown/

此外,请参阅本章的使扩展准备就绪配方。

创建组件

如果您有一些看起来可以重用的代码,但不知道它是一个行为、小部件还是其他东西,那么它很可能是组件。组件应该继承自yii\base\Component类。稍后,组件可以附加到应用程序中,并使用配置文件的components部分进行配置。与仅使用纯 PHP 类相比,这是主要优势。此外,我们获得了行为、事件、获取器和设置器支持。

对于我们的示例,我们将实现一个简单的交换应用程序组件,该组件将能够从fixer.io站点获取货币汇率,将其附加到应用程序中,并使用它。

准备工作

使用 composer 创建一个新的yii2-app-basic应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guide-start-installation.html

如何做到这一点…

对于获取货币汇率,我们的组件应该向服务 URL(如api.fixer.io/2016-05-14?base=USD)发送 HTTP GET 查询。

服务必须返回最近工作日的所有支持的汇率:

{
    "base":"USD",
    "date":"2016-05-13",
    "rates": {
        "AUD":1.3728,
        "BGN":1.7235,
        ...
        "ZAR":15.168,
        "EUR":0.88121
    }
}

组件应从 JSON 格式的响应中提取针币货币并返回目标汇率:

  1. 在您的应用程序结构中创建components目录。

  2. 使用以下接口创建组件类示例:

    <?php
    namespace app\components;
    
    use yii\base\Component;
    
    class Exchange extends Component
    {
        public function getRate($source, $destination, $date = null)
        {
    
        }
    }
    
  3. 实现组件功能:

    <?php
    namespace app\components;
    
    use yii\base\Component;
    use yii\base\InvalidConfigException;
    use yii\base\InvalidParamException;
    use yii\caching\Cache;
    use yii\di\Instance;
    use yii\helpers\Json;
    
    class Exchange extends Component
    {
        /**
        * @var string remote host
        */
        public $host = 'http://api.fixer.io';
        /**
        * @var bool cache results or not
        */
        public $enableCaching = false;
        /**
        * @var string|Cache component ID
        */
        public $cache = 'cache';
    
        public function init()
        {
            if (empty($this->host)) {
                throw new InvalidConfigException('Host must be set.');
            }
            if ($this->enableCaching) {
                $this->cache = Instance::ensure($this->cache, Cache::className());
            }
            parent::init();
        }
    
        public function getRate($source, $destination, $date = null)
        {
            $this->validateCurrency($source);
            $this->validateCurrency($destination);
            $date = $this->validateDate($date);
            $cacheKey = $this->generateCacheKey($source, $destination, $date);
            if (!$this->enableCaching || ($result = $this->cache->get($cacheKey)) === false) {
                $result = $this->getRemoteRate($source, $destination, $date);
                if ($this->enableCaching) {
                    $this->cache->set($cacheKey, $result);
                }
            }
            return $result;
        }
    
        private function getRemoteRate($source, $destination, $date)
        {
            $url = $this->host . '/' . $date . '?base=' . $source;
            $response = Json::decode(file_get_contents($url));
            if (!isset($response['rates'][$destination])) {
                throw new \RuntimeException('Rate not found.');
            }
            return $response['rates'][$destination];
        }
    
        private function validateCurrency($source)
        {
            if (!preg_match('#^[A-Z]{3}$#s', $source)) {
                throw new InvalidParamException('Invalid currency format.');
            }
        }
    
        private function validateDate($date)
        {
            if (!empty($date) && !preg_match('#\d{4}\-\d{2}-\d{2}#s', $date)) {
                throw new InvalidParamException('Invalid date format.');
            }
            if (empty($date)) {
                $date = date('Y-m-d');
            }
            return $date;
        }
    
        private function generateCacheKey($source, $destination, $date)
        {
            return [__CLASS__, $source, $destination, $date];
        }
    }
    
  4. 将组件附加到您的config/console.phpconfig/web.php配置文件中:

    'components' => [
        'cache' => [
            'class' => 'yii\caching\FileCache',
        ],
        'exchange' => [
            'class' => 'app\components\Exchange',
            'enableCaching' => true,
        ],
        // ...
        db' => $db,
    ],
    
  5. 目前,我们可以直接使用新组件或通过get方法使用:

    echo \Yii::$app->exchange->getRate('USD', 'EUR');
    echo \Yii::$app->get('exchange')->getRate('USD', 'EUR', '2014-04-12');
    
  6. 创建一个演示控制台控制器:

    <?php
    namespace app\commands;
    
    use yii\console\Controller;
    
    class ExchangeController extends Controller
    {
        public function actionTest($currency, $date = null)
        {
            echo \Yii::$app->exchange->getRate('USD', $currency, $date) . PHP_EOL;
        }
    }
    
  7. 现在尝试运行任何命令:

    $ ./yii exchange/test EUR
    > 0.90196
    
    $ ./yii exchange/test EUR 2015-11-24
    > 0.93888
    
    $ ./yii exchange/test OTHER
    > Exception 'yii\base\InvalidParamException' with message 'Invalid currency format.'
    
     $ ./yii exchange/test EUR 2015/24/11
    Exception 'yii\base\InvalidParamException' with message 'Invalid date format.'
    
    $ ./yii exchange/test ASD
    > Exception 'RuntimeException' with message 'Rate not found.'
    

因此,您必须在成功情况下看到汇率值或在错误情况下看到特定的异常。除了创建自己的组件外,您还可以做更多的事情。

覆盖现有应用程序组件

大多数情况下,您不需要创建自己的应用程序组件,因为其他类型的扩展,如小部件或行为,几乎涵盖了所有可重用代码的类型。然而,覆盖核心框架组件是一种常见的做法,并且可以用来根据您的特定需求自定义框架的行为,而无需修改核心。

例如,为了能够使用Yii::app()->formatter->asNumber($value)方法而不是从创建辅助工具配方中的NumberHelper::format方法格式化数字,您可以按照以下步骤操作:

  1. 如下扩展yii\i18n\Formatter组件:

    <?php
    namespace app\components;
    
    class Formatter extends \yii\i18n\Formatter
    {
        public function asNumber($value, $decimal = 2)
        {
            return number_format($value, $decimal, '.', ',');
        }
    }
    
  2. 覆盖内置的formatter组件的类:

    'components' => [
        // ...
        formatter => [
            'class' => 'app\components\Formatter,
        ],
        // ...
    ],
    
  3. 目前,我们可以直接使用此方法:

    echo Yii::app()->formatter->asNumber(1534635.2, 3);
    

    或者,它也可以用作GridViewDetailView小部件的新格式:

    <?= \yii\grid\GridView::widget([
        'dataProvider' => $dataProvider,
        'columns' => [
            'id',
            'created_at:datetime',
            'title',
            'value:number',
        ],
    ]) ?>
    
  4. 此外,您还可以扩展每个现有组件,而无需覆盖其源代码。

它是如何工作的…

要将组件附加到应用程序中,可以从yii\base\Component类扩展。附加操作就像在配置的组件部分添加一个新的数组一样简单。在那里,类值指定组件的类,所有其他值都通过相应组件的公共属性和设置方法设置给组件。

实现本身非常简单;我们将api.fixer.io调用封装到一个方便的 API 中,其中包含验证器和缓存。我们可以通过Yii::$app使用其组件名称访问我们的类。在我们的例子中,它将是Yii::$app->exchange

参见

关于组件的官方信息,请参阅www.yiiframework.com/doc-2.0/guide-concept-components.html

对于NumberHelper类的源代码,请参考创建辅助工具配方。

创建可重用的控制器操作

常见操作,如通过主键删除 AR 模型或获取用于 AJAX 自动完成的 数据,可以移动到可重用的控制器操作,并在需要时附加到控制器。

在本配方中,我们将创建一个可重用的删除操作,该操作将通过主键删除指定的 AR 模型。

准备中

  1. 使用官方指南中描述的 composer 创建一个新的yii2-app-basic应用程序,www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 创建一个新的数据库并对其进行配置。

  3. 创建并应用以下迁移:

    <?php
    use yii\db\Migration;
    
    class m160308_093233_create_post_table extends Migration
    {
        public function up()
        {
            $this->createTable('{{%post}}', [
                'id' => $this->primaryKey(),
                'title' => $this->string()->notNull(),
                'text' => $this->text()->notNull(),
            ]);
        }
    
        public function down()
        {
            $this->dropTable('{{%post}}');
        }
    }
    
  4. 使用 Gii 为帖子生成模型和评论。

  5. 使用 Gii 生成标准的 CRUD 控制器app\controllers\PostController

  6. 确保 CRUD 操作正常工作:准备中

  7. 在成功的情况下,添加一组示例帖子。

如何操作…

执行以下步骤:

  1. 创建操作目录并添加DeleteAction独立操作:

    <?php
    namespace app\actions;
    
    use yii\base\Action;
    use yii\base\InvalidConfigException;
    use yii\web\MethodNotAllowedHttpException;
    use yii\web\NotFoundHttpException;
    
    class DeleteAction extends Action
    {
        public $modelClass;
        public $redirectTo = ['index'];
    
        public function init()
        {
            if (empty($this->modelClass)) {
                throw new InvalidConfigException('Empty model class.');
            }
            parent::init();
        }
    
        public function run($id)
        {
            if (!\Yii::$app->getRequest()->getIsPost()) {
                throw new MethodNotAllowedHttpException('Method not allowed.');
            }
            $model = $this->findModel($id);
            $model->delete();
            return $this->controller->redirect($this->redirectTo);
        }
    
        /**
        * @param $id
        * @return \yii\db\ActiveRecord
        * @throws NotFoundHttpException
        */
        private function findModel($id)
        {
            $class = $this->modelClass;
            if (($model = $class::findOne($id)) !== null) {
                return $model;
            } else {
                throw new NotFoundHttpException('Page does not exist.');
            }
        }
    }
    
  2. 现在我们需要将其附加到 controllers/PostController.php 控制器。删除控制器的 actionDeletebehaviors 方法,并在 action 方法中附加您自己的操作:

    <?php
    namespace app\controllers;
    
    use app\actions\DeleteAction;
    use Yii;
    use app\models\Post;
    use app\models\PostSearch;
    use yii\web\Controller;
    use yii\web\NotFoundHttpException;
    
    class PostController extends Controller
    {
        public function actions()
        {
            return [
                'delete' => [
                    'class' => DeleteAction::className(),
                    'modelClass' => Post::className(),
                ],
            ];
        }
    
        public function actionIndex()  {  ...  }
    
        public function actionView($id)  {  ...  }
    
        public function actionCreate()  {  ...  }
    
        public function actionUpdate($id)  {  ...  }
    
        protected function findModel($id)
        {
            if (($model = Post::findOne($id)) !== null) {
                return $model;
            } else {
                throw new NotFoundHttpException('The requested page does not exist.');
            }
        }
    }
    
  3. 就这样。确保删除操作仍然正确工作,并且在删除后,您将被重定向到相应的索引操作。

它是如何工作的…

要创建外部控制器操作,您需要从 yii\base\Action 类扩展您的类。唯一必须实现的方法是 run。在我们的例子中,它使用 Yii 的自动参数绑定功能从 $_GET 接收名为 $id 的参数,并尝试删除相应的模型。

为了使其可定制,我们创建了两个可以从控制器配置的公共属性。这些是 modelName,它包含我们正在工作的模型名称,以及 redirectTo,它指定用户将被重定向到的路由。

配置本身是通过在控制器中实现 actions 方法来完成的。在那里,您可以一次或多次附加操作并配置其公共属性。

如果需要将其重定向到另一个操作或渲染特定视图,您可以通过控制器属性访问原始控制器对象。

参见

创建可重用控制器

在 Yii 中,您可以创建可重用控制器。如果您正在创建大量应用程序或同类型的控制器,将所有通用代码移动到可重用控制器将为您节省大量时间。

在这个菜谱中,我们尝试创建一个通用的 CleanController,该控制器将清除临时目录并刷新缓存数据。

准备工作

使用官方指南中描述的 composer 创建一个新的 yii2-app-basic 应用程序 www.yiiframework.com/doc-2.0/guide-start-installation.html

如何操作…

执行以下步骤以创建可重用控制器:

  1. 创建名为 cleaner 的目录并添加独立的 CleanController 控制器:

    <?php
    namespace app\cleaner;
    
    use Yii;
    use yii\filters\VerbFilter;
    use yii\helpers\FileHelper;
    use yii\web\Controller;
    
    class CleanController extends Controller
    {
        public $assetPaths = ['@app/web/assets'];
        public $runtimePaths = ['@runtime'];
        public $caches = ['cache'];
    
        public function behaviors()
        {
            return [
                'verbs' => [
                    'class' => VerbFilter::className(),
                    'actions' => [
                        'assets' => ['post'],
                        'runtime' => ['post'],
                        'cache' => ['post'],
                    ],
                ],
            ];
        }
    
        public function actionIndex()
        {
            return $this->render('@app/cleaner/views/index');
        }
    
        public function actionAssets()
        {
            foreach ((array)$this->assetPaths as $path) {
                $this->cleanDir($path);
                Yii::$app->session->addFlash(
                    'cleaner',
                    'Assets path "' . $path . '" is cleaned.'
                );
            }
            return $this->redirect(['index']);
        }
    
        public function actionRuntime()
        {
            foreach ((array)$this->runtimePaths as $path) {
                $this->cleanDir($path);
                Yii::$app->session->addFlash(
                    'cleaner',
                    'Runtime path "' . $path . '" is cleaned.'
                );
            }
            return $this->redirect(['index']);
        }
    
        public function actionCache()
        {
            foreach ((array)$this->caches as $cache) {
                Yii::$app->get($cache)->flush();
                Yii::$app->session->addFlash(
                    'cleaner',
                    'Cache "' . $cache . '" is cleaned.'
                );
            }
            return $this->redirect(['index']);
        }
    
        private function cleanDir($dir)
        {
            $iterator = new \DirectoryIterator(Yii::getAlias($dir));
            foreach($iterator as $sub) {
                if(!$sub->isDot() && $sub->isDir()) {
                    FileHelper::removeDirectory($sub->getPathname());
                }
            }
        }
    }
    
  2. actionIndex 方法创建 cleaner/views/index.php 视图文件:

    <?php
    use yii\helpers\Html;
    /* @var $this yii\web\View */
    $this->title = 'Cleaner';
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div class="clean-index">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <?php if (Yii::$app->session->hasFlash('cleaner')): ?>
        <?php foreach ((array)Yii::$app->session->getFlash('cleaner', []) as $message): ?>
        <div class="alert alert-success">
            <?= $message ?>
        </div>
        <?php endforeach; ?>
        <?php endif; ?>
    
        <p>
            <?= Html::a('Clear Caches', ['cache'], [
                'class' => 'btn btn-primary',
                'data' => [
                    'confirm' => 'Are you sure you want to clear all cache data?',
                    'method' => 'post',
                ],
            ]) ?>
            <?= Html::a('Clear Assets', ['assets'], 
                ['class' => 'btn btn-primary',
                    'data' => [
                        'confirm' => 'Are you sure you want to 
                            clear all temporary assets?',
                    'method' => 'post',
                ],
            ]) ?>
            <?= Html::a('Clear Runtime', ['runtime'], 
                ['class' => 'btn btn-primary',
                    'data' => [
                        'confirm' => 'Are you sure you want to clear all runtime files?',
                            'method' => 'post',
                    ],
                ]) ?>
        </p>
    </div>
    
  3. 通过 config/web.php 配置文件的 controllerMap 部分将控制器附加到应用程序:

    $config = [
        'id' => 'basic',
        'basePath' => dirname(__DIR__),
        'bootstrap' => ['log'],
        'controllerMap' => [
            'clean' => 'app\cleaner\CleanController',
        ],
        'components' => [
            ...
        ]
        ...
    ];
    
  4. 向主菜单添加新项目:

    echo Nav::widget([
        'options' => ['class' => 'navbar-nav navbar-right'],
        'items' => [
            ['label' => 'Home', 'url' => ['/site/index']],
            ['label' => 'Cleaner', 'url' => ['/clean/index']],
            ['label' => 'About', 'url' => ['/site/about']],
            ...
        ],
    ]);
    
  5. 打开控制器并清除资源:如何操作…

  6. 如果您使用的是 yii2-app-advanced 应用程序模板,只需在配置中指定正确的路径:

    'controllerMap' => [
        'clean' => 'app\cleaner\CleanController',
        'assetPaths' => [
            '@backend/web/assets',
            '@frontend/web/assets',
        ],
        'runtimePaths' => [
            '@backend/runtime',
            '@frontend/runtime',
            '@console/runtime',
        ],
    ],
    

现在我们可以将控制器附加到任何应用程序。

它是如何工作的…

当您运行应用程序并传递一个如 clean/index 的路由时,在执行 CleanController::actionIndex 之前,Yii 会检查是否定义了 controllerMap。由于我们已在那里定义了一个干净的控制器,因此 Yii 将执行它而不是走常规路线。

在控制器本身中,我们定义了 assetPathsruntimePathscaches 属性,以便能够将控制器连接到具有不同目录和缓存结构的应用程序。我们在附加控制器时设置它。

参见

创建控件

小部件是视图的可重用部分,它不仅渲染一些数据,而且还根据某些逻辑进行渲染。它甚至可以从模型中获取数据并使用自己的视图,因此它类似于模块的简化可重用版本。

让我们创建一个使用 Google API 绘制饼图的控件。

准备工作

使用官方指南中描述的 composer 创建一个新的 yii2-app-basic 应用程序 www.yiiframework.com/doc-2.0/guide-start-installation.html

如何做…

  1. 创建 widgets 目录并添加 ChartWidget 类:

    <?php
    namespace app\widgets;
    
    use yii\base\Widget;
    
    class ChartWidget extends Widget
    {
        public $title;
        public $width = 300;
        public $height = 200;
        public $data = [];
        public $labels = [];
    
        public function run()
        {
            $path = 'http://chart.apis.google.com/chart';
    
            $query = http_build_query([
                'chtt' => $this->title,
                'cht' => 'pc',
                'chs' => $this->width . 'x' . $this->height,
                'chd' => 't:' . implode(',', $this->data),
                'chds' => 'a',
                'chl' => implode('|', $this->labels),
                'chxt' => 'y',
                'chxl' => '0:|0|' . max($this->data)
            ]);
    
            $url = $path  . '?' . $query;
    
            return $this->render('chart', [
                'url' => $url,
            ]);
        }
    }
    
  2. 创建 widgets/views/chart.php 视图:

    <?php
    use yii\helpers\Html;
    
    /* @var $this yii\web\View */
    /* @var $url string */
    ?>
    
    <div class="chart">
        <?= Html::img($url) ?>
    </div>
    
  3. 现在创建一个 ChartController 控制器:

    <?php
    namespace app\controllers;
    
    use yii\base\Controller;
    
    class ChartController extends Controller
    {
        public function actionIndex()
        {
            return $this->render('index');
        }
    }
    
  4. 添加 views/chart/index.php 视图:

    <?php
    use app\widgets\ChartWidget;
    use yii\helpers\Html;
    
    /* @var $this yii\web\View */
    
    $this->title = 'Chart';
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div class="site-about">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <?= ChartWidget::widget([
            'title' => 'My Chart Diagram',
            'data' => [
                100 - 32,
                    32,
            ],
            'labels' => [
                'Big',
                'Small',
            ],
        ]) ?>
    </div>
    
  5. 现在尝试运行控制器的索引操作。您应该看到如下所示的饼图:如何做…

  6. 您可以使用不同的大小和数据集显示任何图表。

它是如何工作的…

正如在其他任何类型的扩展中一样,我们创建了一些公共属性,我们可以在调用控件的 widget 方法时进行配置。在这种情况下,我们配置了标题、数据集和数据标签。

控件的主要方法是 run()。在我们的控件中,我们生成一个 URL 并渲染控件视图,该视图使用 Google 图表 API 打印 <img> 标签。

参见

创建 CLI 命令

Yii 具有良好的命令行支持,并允许创建可重用的控制台命令。控制台命令的创建速度比 Web 图形用户界面快。如果您需要为您的应用程序创建一些将被开发人员或管理员使用的实用程序,控制台命令是正确的工具。

为了展示如何创建控制台命令,我们将创建一个简单的命令,该命令将清理各种内容,例如资源和临时目录。

准备工作

使用 composer 创建一个新的yii2-app-basic应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guide-start-installation.html

如何操作…

执行以下步骤以创建 CLI 命令:

  1. 使用以下代码创建commands/CleanController.php文件:

    <?php
    namespace app\commands;
    
    use yii\console\Controller;
    use yii\helpers\FileHelper;
    
    /**
    * Removes content of assets and runtime directories.
    */
    class CleanController extends Controller
    {
        public $assetPaths = ['@app/web/assets'];
        public $runtimePaths = ['@runtime'];
    
        /**
        * Removes temporary assets.
        */
        public function actionAssets()
        {
            foreach ((array)$this->assetPaths as $path) {
                $this->cleanDir($path);
            }
    
            $this->stdout('Done' . PHP_EOL);
        }
    
        /**
        * Removes runtime content.
        */
        public function actionRuntime()
        {
            foreach ((array)$this->runtimePaths as $path) {
                $this->cleanDir($path);
            }
    
            $this->stdout('Done' . PHP_EOL);
        }
    
        private function cleanDir($dir)
        {
            $iterator = new \DirectoryIterator(\Yii::getAlias($dir));
            foreach($iterator as $sub) {
                if(!$sub->isDot() && $sub->isDir()) {
                    $this->stdout('Removed ' . $sub->getPathname() . PHP_EOL);
                    FileHelper::removeDirectory($sub->getPathname());
                }
            }
        }
    }
    
  2. 现在我们可以使用我们自己的具有默认设置的 console 控制器。只需运行yiishell 脚本:

    ./yii
    
    
  3. 查找自己的clean命令:

    This is Yii version 2.0.7.
    
    The following commands are available:
    
    - asset                    Allows you to combine...
     asset/compress         Combines and compresses the asset...
     asset/template         Creates template of configuration file...
    
    ...
    
    - clean                    Removes content of assets and runtime directories.
     clean/assets           Removes temporary assets.
     clean/runtime          Removes runtime content.
    
    - fixture                  Manages fixture data loading and unloading.
     fixture/load (default) Loads the specified fixture data.
     fixture/unload         Unloads the specified fixtures.
    
    ...
    
    
  4. 现在运行资产清理:

    .yii clean/assets
    
    
  5. 查看过程报告:

    Removed /yii-book.app/web/assets/25f82b8a
    Removed /yii-book.app/web/assets/9b3b2888
    Removed /yii-book.app/web/assets/f4307424
    Done
    
    
  6. 如果您想在yii2-app-advanced应用程序中使用此控制器,只需指定自定义工作路径:

    return [
        'id' => 'app-console',
        'basePath' => dirname(__DIR__),
        'bootstrap' => ['log'],
        'controllerNamespace' => 'console\controllers',
        'controllerMap' => [
            'clean' => [
                'class' => 'console\controllers\CleanController',
                'assetPaths' => [
                    '@backend/web/assets',
                    '@frontend/web/assets',
                ],
                'runtimePaths' => [
                    '@backend/runtime',
                    '@frontend/runtime',
                    '@console/runtime',
                ],
            ],
        ],
        // ...
    ];
    

它是如何工作的…

所有控制台命令都应该扩展自yii\console\Controller类。由于所有控制台命令都是在yii\console\Application而不是yii\web\Application中运行的,我们没有方法来确定@webroot别名的值。此外,在yii2-app-advanced模板中,我们默认有后端、前端和控制台子目录。为此,我们正在创建可配置的公共属性,称为assetPathsruntimePaths

控制台命令结构本身就像一个典型的控制器。我们正在定义几个可以通过yii <console command>/<command action>运行的操作。

如您所见,没有使用视图,因此我们可以专注于编程任务而不是设计、标记等。然而,您需要提供一些有用的输出,以便用户知道正在发生什么。这是通过简单的 PHP echo 语句完成的。

如果您的命令相对复杂,例如与 Yii 捆绑的消息或迁移,提供一些关于可用选项和操作的额外描述是一个好主意。这可以通过重写getHelp方法来完成:

public function getHelp()
{
    $out = "Clean command allows you to clean up various temporary data Yii and an application are generating.\n\n";
    return $out . parent::getHelp();
}

执行以下命令:

./yii help clean

您可以按以下方式查看完整输出:

DESCRIPTION
Clean command allows you to clean up various temporary data Yii and an application are generating.
Removes content of assets and runtime directories.
SUB-COMMANDS
- clean/assets   Removes temporary assets.
- clean/runtime  Removes runtime content.

默认情况下,当我们运行 shell 命令时:

./yii

我们已经在输出列表中看到了所有命令的简化描述:

- clean                    Removes content of assets and runtime directories.
 clean/assets           Removes temporary assets.
 clean/runtime          Removes runtime content.

此描述将来自类和动作之前的注释:

/**
* Removes content of assets and runtime directories.
*/
class CleanController extends Controller
{
    /**
    * Removes temporary assets.
    */
    public function actionAssets() { … }

    * Removes runtime content.
    */
    public function actionRuntime() { … }
}

为您的类添加描述是可选的。您绝对不应该为您的 CLI 命令这样做。

参见

  • 本章中的创建可重用控制器配方

  • 本章中的制作扩展分发准备就绪配方

创建过滤器

过滤器是一个可以在执行动作之前/之后运行的类。它可以用来修改执行上下文或装饰输出。在我们的例子中,我们将实现一个简单的访问过滤器,它将允许用户在接受用户协议后才能看到私有内容。

准备工作

使用 composer 创建一个新的yii2-app-basic应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guide-start-installation.html

如何操作…

  1. 创建协议表单模型:

    <?php
    namespace app\models;
    
    use yii\base\Model;
    
    class AgreementForm extends Model
    {
        public $accept;
    
        public function rules()
        {
            return [
                ['accept', 'required'],
                ['accept', 'compare', 'compareValue' => 1, 'message' => 'You must agree the rules.'],
            ];
        }
    
        public function attributeLabels()
        {
            return [
                'accept' => 'I completely accept the rules.'
            ];
        }
    }
    
  2. 创建协议检查器服务:

    <?php
    namespace app\services;
    
    use Yii;
    use yii\web\Cookie;
    
    class AgreementChecker
    {
        public function isAllowed()
        {
            return Yii::$app->request->cookies->has('agree');
        }
    
        public function allowAccess()
        {
            Yii::$app->response->cookies->add(new Cookie([
                'name' => 'agree',
                'value' => 'on',
                'expire' => time() + 3600 * 24 * 90, // 90 days
            ]));
        }
    }
    
    1. 它封装了对协议 cookie 的操作。
  3. 创建filter类:

    <?php
    namespace app\filters;
    
    use app\services\AgreementChecker;
    use Yii;
    use yii\base\ActionFilter;
    
    class AgreementFilter extends ActionFilter
    {
        public function beforeAction($action)
        {
            $checker = new AgreementChecker();
            if (!$checker->isAllowed()) {
                Yii::$app->response->redirect(['/content/agreement'])->send();
                return false;
            }
            return true;
        }
    }
    
  4. 创建内容控制器并将过滤器附加到其行为:

    <?php
    namespace app\controllers;
    
    use app\filters\AgreementFilter;
    use app\models\AgreementForm;
    use app\services\AgreementChecker;
    use Yii;
    use yii\web\Controller;
    
    class ContentController extends Controller
    {
        public function behaviors()
        {
            return [
                [
                    'class' => AgreementFilter::className(),
                    'only' => ['index'],
                ],
            ];
        }
    
        public function actionIndex()
        {
            return $this->render('index');
        }
    
        public function actionAgreement()
        {
            $model = new AgreementForm();
            if ($model->load(Yii::$app->request->post()) && $model->validate()) {
                $checker = new AgreementChecker();
                $checker->allowAccess();
                return $this->redirect(['index']);
            } else {
                return $this->render('agreement', [
                    'model' => $model,
                ]);
            }
        }
    }
    
  5. 添加带有私有内容的views/content/index.php视图:

    <?php
    use yii\helpers\Html;
    
    /* @var $this yii\web\View */
    $this->title = 'Content';
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div class="site-about">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <div class="well">
            This is our private page.
        </div>
    </div>
    
  6. 添加带有表单的views/content/agreement.php视图:

    <?php
    use yii\helpers\Html;
    use yii\bootstrap\ActiveForm;
    
    /* @var $this yii\web\View */
    /* @var $form yii\bootstrap\ActiveForm */
    /* @var $model app\models\AgreementForm */
    
    $this->title = 'User agreement';
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div class="site-login">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <p>Please agree with our rules:</p>
    
        <?php $form = ActiveForm::begin(); ?>
    
        <?= $form->field($model, 'accept')->checkbox() ?>
    
        <div class="form-group">
            <?= Html::submitButton('Accept', ['class' => 'btn btn-success']) ?>
            <?= Html::a('Cancel', ['/site/index'], ['class' => 'btn btn-danger']) ?>
        </div>
    
        <?php ActiveForm::end(); ?>
    </div>
    
  7. 将主菜单项添加到views/layouts/main.php文件:

    echo Nav::widget([
        'options' => ['class' => 'navbar-nav navbar-right'],
        'items' => [
            ['label' => 'Home', 'url' => ['/site/index']],
            ['label' => 'Content', 'url' => ['/content/index']],
            ['label' => 'About', 'url' => ['/site/about']],
            ...
        ],
    ]);
    
  8. 尝试打开内容页面。过滤器必须将你重定向到协议页面:如何做…

  9. 只有在接受规则之后,你才能看到私有内容:如何做…

  10. 此外,你还可以将过滤器附加到其他控制器或模块。

它是如何工作的…

过滤器应该扩展yii\base\ActionFilter类,该类扩展了yii\base\Behavior。如果我们想进行后过滤和前过滤,我们可以重写beforeActionafterAction方法。

例如,我们可以在失败情况下检查用户访问并抛出相应的 HTTP 异常。在这个菜谱中,如果特定的 cookie 值不存在,我们将用户重定向到协议页面:

class AgreementFilter extends ActionFilter
{
    public function beforeAction($action)
    {
        $checker = new AgreementChecker();
        if (!$checker->isAllowed()) {
            Yii::$app->response->redirect(['/content/agreement'])->send();
            return false;
        }
        return true;
    }
}

你可以将过滤器附加到任何控制器或模块。要指定必要的路由列表,只需使用onlyexcept选项。例如,我们只为控制器的 index 动作应用我们的过滤器:

public function behaviors() 
{
    return [
        [
            'class' => AgreementFilter::className(),
            'only' => ['index'],
        ],
    ];
}

注意

不要忘记在beforeAction方法中返回一个true值(在成功情况下)。否则,控制器动作将不会执行。

相关内容

关于过滤器的更多信息,请参阅 www.yiiframework.com/doc-2.0/guide-structure-filters.html.

对于内置缓存和访问控制过滤器,请参阅:

创建模块

如果你已经创建了一个复杂的应用程序部分,并希望在下一个项目中以某种程度的定制使用它,那么你很可能需要创建一个模块。在这个菜谱中,我们将看到如何创建一个应用程序日志视图模块。

准备工作

使用官方指南中描述的 composer 创建一个新的yii2-app-basic应用程序,指南链接为 www.yiiframework.com/doc-2.0/guide-start-installation.html.

如何做…

让我们先做一些规划。

在默认配置的yii2-app-basic中,所有日志条目都存储在runtime/logs/app.log文件中。我们可以使用正则表达式从该文件中提取所有消息,并在GridView小部件上显示它们。此外,我们必须允许用户配置自定义日志文件的路径。

执行以下步骤:

  1. 创建modules/log目录,并使用新文件选项创建Module类:

    <?php
    namespace app\modules\log;
    
    class Module extends \yii\base\Module
    {
        public $file = '@runtime/logs/app.log';
    }
    
  2. 创建一个简单的模型,用于从日志文件中传输行:

    <?php
    namespace app\modules\log\models;
    
    use yii\base\Object;
    
    class LogRow extends Object
    {
        public $time;
        public $ip;
        public $userId;
        public $sessionId;
        public $level;
        public $category;
        public $text;
    }
    
  3. 编写一个日志文件读取器类,它将解析文件行,反转其顺序,并返回 LogRow 模型实例的数组:

    <?php
    namespace app\modules\log\services;
    
    use app\modules\log\models\LogRow;
    
    class LogReader
    {
        public function getRows($file)
        {
            $result = [];
            $handle = @fopen($file, "r");
            if ($handle) {
                while (($row = fgets($handle)) !== false) {
                    $pattern =
                        '#^' .
                        '(?P<time>\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}) ' .
                        '\[(?P<ip>[^\]]+)\]' .
                        '\[(?P<userId>[^\]]+)\]' .
                        '\[(?P<sessionId>[^\]]+)\]' .
                        '\[(?P<level>[^\]]+)\]' .
                        '\[(?P<category>[^\]]+)\]' .
                        ' (?P<text>.*?)' .
                        '(\$\_(GET|POST|REQUEST|COOKIE|SERVER) = \[)?' .
                        '$#i';
                    if (preg_match($pattern, $row, $matches)) {
                        if ($matches['text']) {
                            $result[] = new LogRow([
                                'time' => $matches['time'],
                                'ip' => $matches['ip'],
                                'userId' => $matches['userId'],
                                'sessionId' => $matches['sessionId'],
                                'level' => $matches['level'],
                                'category' => $matches['category'],
                                'text' => $matches['text'],
                            ]);
                        }
                    }
                }
                fclose($handle);
            }
            return array_reverse($result);
        }
    }
    
  4. 添加一个用于显示日志级别的美观 HTML 徽章的辅助器:

    <?php
    namespace app\modules\log\helpers;
    
    use yii\helpers\ArrayHelper;
    use yii\helpers\Html;
    
    class LogHelper
    {
        public static function levelLabel($level)
        {
            $classes = [
                'error' => 'danger',
                'warning' => 'warning',
                'info' => 'primary',
                'trace' => 'default',
                'profile' => 'success',
                'profile begin' => 'info',
                'profile end' => 'info',
            ];
    
            $class = ArrayHelper::getValue($classes, $level, 'default');
            return Html::tag('span', Html::encode($level), ['class' => 'label-' . $class]);
        }
    }
    
  5. 创建一个模块控制器,它将从读取器获取行数组并将它们传递到 ArrayDataProvider

    <?php
    namespace app\modules\log\controllers;
    
    use app\modules\log\services\LogReader;
    use yii\data\ArrayDataProvider;
    use yii\web\Controller;
    
    class DefaultController extends Controller
    {
        public function actionIndex()
        {
            $reader = new LogReader();
            $dataProvider = new ArrayDataProvider([
                'allModels' => $reader->getRows($this->getFile()),
            ]);
            return $this->render('index', [
                'dataProvider' => $dataProvider,
            ]);
        }
    
        private function getFile()
        {
            return \Yii::getAlias($this->module->file);
        }
    }
    
  6. 现在,创建 modules/log/default/index.php 视图文件:

    <?php
    use app\modules\log\helpers\LogHelper;
    use app\modules\log\models\LogRow;
    use yii\grid\GridView;
    use yii\helpers\Html;
    
    /* @var $this yii\web\View */
    /* @var $dataProvider yii\data\ArrayDataProvider */
    
    $this->title = 'Application log';
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div class="log-index">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <?= GridView::widget([
            'dataProvider' => $dataProvider,
            'columns' => [
                [
    
                'attribute' => 'time',
                    'format' => 'datetime',
                    'contentOptions' => [
                        'style' => 'white-space: nowrap',
                    ],
                ],
                'ip:text:IP',
                'userId:text:User',
                [
                    'attribute' => 'level',
                    'value' => function (LogRow $row) {
                        return LogHelper::levelLabel($row->level);
                    },
                    'format' => 'raw',
                ],
                'category',
                'text',
            ],
        ]) ?>
    </div>
    
  7. config/web.php 文件中将模块附加到你的应用程序中:

    $config = [
        'id' => 'basic',
        'basePath' => dirname(__DIR__),
        'bootstrap' => ['log'],
        'modules' => [
            'log' => 'app\modules\log\Module',
        ],
        'components' => [
    
        ],
        ...
    ];
    
  8. views/layouts/main.php 文件中添加对控制器的链接:

    echo Nav::widget([
        'options' => ['class' => 'navbar-nav navbar-right'],
        'items' => [
            ['label' => 'Home', 'url' => ['/site/index']],
            ['label' => 'Log', 'url' => ['/log/default/index']],
            ['label' => 'About', 'url' => ['/site/about']],
            ['label' => 'Contact', 'url' => ['/site/contact']],
            ...
        ],
    ]);
    NavBar::end();
    
  9. 前往 url /index.php?r=lo``g 并确保模块正常工作:如何操作…

它是如何工作的...

你可以通过分离的模块将控制器、模型、视图和其他组件分组,并将它们附加到你的应用程序中。你可以使用 Gii 生成模块模板,或者手动创建它。

每个模块都包含一个主模块类,我们可以在其中定义可配置的属性、定义更改路径、附加控制器等。默认情况下,使用 Gii 生成的模块运行默认控制器的 index 动作。

参见

创建自定义视图渲染器

现在有大量的 PHP 模板引擎。Yii2 只提供原生 PHP 模板。如果你想使用现有的模板引擎或创建自己的模板引擎,你必须实现它——当然,如果它还没有被 Yii 社区实现的话。

在这个菜谱中,我们将重新实现 Smarty 模板支持。

准备工作

  1. 使用 composer 创建一个新的 yii2-app-basic 应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 安装 Smarty 库:

    composer require smarty/smarty
    
    

如何操作…

执行以下步骤以创建自定义视图渲染器:

  1. 创建 smarty/ViewRenderer.php 文件:

    <?php
    namespace app\smarty;
    
    use Smarty;
    use Yii;
    
    class ViewRenderer extends \yii\base\ViewRenderer
    {
        public $cachePath = '@runtime/smarty/cache';
        public $compilePath = '@runtime/smarty/compile';
    
        /**
        * @var Smarty
        */
        private $smarty;
    
        public function init()
        {
            $this->smarty = new Smarty();
            $this->smarty->setCompileDir(Yii::getAlias($this->compilePath));
            $this->smarty->setCacheDir(Yii::getAlias($this->cachePath));
            $this->smarty->setTemplateDir([
                dirname(Yii::$app->getView()->getViewFile()),
                Yii::$app->getViewPath(),
            ]);
        }
    
        public function render($view, $file, $params)
        {
            $templateParams = empty($params) ? null : $params;
            $template = $this->smarty->createTemplate($file, null, null, $templateParams, false);
            $template->assign('app', \Yii::$app);
            $template->assign('this', $view);
            return $template->fetch();
        }
    }
    
  2. 现在我们需要将视图渲染器连接到应用程序。在 config/web php 中,我们需要添加视图组件的渲染器:

    'components' => [
        ....
        'view' => [
            'renderers' => [
                'tpl' => [
                    'class' => 'app\smarty\ViewRenderer',
                ],
            ],
        ],
        ...
    ];
    
  3. 现在让我们来测试它。创建一个新的 SmartyController

    <?php
    namespace app\controllers;
    
    use yii\web\Controller;
    
    class SmartyController extends Controller
    {
        public function actionIndex()
        {
            return $this->render('index.tpl', [
                'name' => 'Bond',
            ]);
        }
    }
    
  4. 接下来,我们需要创建 views/smarty/index.tpl 视图:

    <div class="smarty-index">
        <h1>Smarty Example</h1>
        <p>Hello, {$name}!</p>
    </div>
    
  5. 现在尝试运行控制器。在成功的情况下,你应该得到以下输出:如何操作…

它是如何工作的…

视图渲染器是 yii\base\ViewRenderer 抽象类的子类,它只实现了一个名为 render 的方法:

<?php
namespace yii\base;

abstract class ViewRenderer extends Component
{
    /**
    * Renders a view file.
    *
    * This method is invoked by [[View]] whenever it tries to render a view.
    * Child classes must implement this method to render the given view file.
    *
    * @param View $view the view object used for rendering the file.
    * @param string $file the view file.
    * @param array $params the parameters to be passed to the view file.
    * @return string the rendering result
    */
   abstract public function render($view, $file, $params);
}

因此,我们得到了一个视图组件、文件路径和渲染变量。我们需要处理文件并返回渲染结果。在我们的例子中,处理本身是由 Smarty 模板引擎完成的,所以我们需要正确地初始化它并调用其处理方法:

class ViewRenderer extends \yii\base\ViewRenderer
{
    public $cachePath = '@runtime/smarty/cache';
    public $compilePath = '@runtime/smarty/compile';
    private $smarty;

    public function init()
    {
        $this->smarty = new Smarty();
        $this->smarty->setCompileDir(Yii::getAlias($this->compilePath));
        $this->smarty->setCacheDir(Yii::getAlias($this->cachePath));
        $this->smarty->setTemplateDir([
            dirname(Yii::$app->getView()->getViewFile()),
            Yii::$app->getViewPath(),
        ]);
    }
    …
}

将 Yii 临时文件存储在应用程序运行时目录中是一个好习惯。这就是为什么我们将compile目录(Smarty 存储编译成 PHP 的模板的地方)设置为runtime/smarty/compile

渲染本身稍微简单一些:

public function render($view, $file, $params)
{
    $templateParams = empty($params) ? null : $params;
    $template = $this->smarty->createTemplate($file, null, null, $templateParams, false);
    $template->assign('app', \Yii::$app);
    $template->assign('this', $view);
    return $template->fetch();
}

通过$this->render设置的所有数据都原样传递给 Smarty 模板。此外,我们正在创建特殊的 Smarty 模板变量appthis,它们指向Yii::$appYii::$app->view,允许我们在模板内部获取应用程序属性。

然后,我们将渲染模板。

参见

你可以在github.com/yiisoft/yii2-smarty上获取使用 Smarty 视图渲染器以及插件和配置支持的准备信息。

要了解更多关于 Smarty 和一般视图渲染器的信息,请参考以下 URL:

创建多语言应用程序

每天我们都遇到越来越多的国际公司、软件产品和信息资源,它们在多种语言上发布内容。Yii2 为制作多语言应用程序提供了内置的 i18n 支持。

在这个菜谱中,我们将应用程序界面翻译成不同的语言。

准备工作

使用 composer 创建一个新的yii2-app-basic应用程序,具体步骤请参考官方指南中的www.yiiframework.com/doc-2.0/guide-start-installation.html

如何操作...

  1. views/layouts/main.php文件中将主菜单标签更改为使用Yii::t('app/nav', '...')方法:

    echo Nav::widget([
        'options' => ['class' => 'navbar-nav navbar-right'],
        'items' => [
            ['label' => Yii::t('app/nav', 'Home'), 'url' => ['/site/index']],
            ['label' => Yii::t('app/nav', 'About'), 'url' => ['/site/about']],
            ['label' => Yii::t('app/nav', 'Contact'), 'url' => ['/site/contact']],
            ...
        ],
    ]);
    
  2. 将所有标题和面包屑更改为使用通用的Yii::t('app', '...')方法:

    $this->title = Yii::t('app', 'Contact');
    $this->params['breadcrumbs'][] = $this->title;
    
  3. 同时,更改所有按钮的标签:

    <div class="form-group">
        <?= Html::submitButton(Yii::t('app', 'Submit'), ['class' => 'btn btn-primary'']) ?>
    </div>
    

    同时更改其他硬编码的消息:

    <p>
        <?= Yii::t('app', 'The above error occurred while the Web server was processing your request.') ?>
    </p>
    
  4. 更改你的ContactForm模型的属性标签:

    class LoginForm extends Model
    {
        ...
    
        public function attributeLabels()
        {
            return [
                'username' => Yii::t('app/user', 'Username'),
                'password' => Yii::t('app/user', 'Password'),
                'rememberMe' => Yii::t('app/user', 'Remember Me'),
            ];
        }
    }
    

    同时,更改LoginForm模型的属性标签:

    class ContactForm extends Model
    {
        ...
    
        public function attributeLabels()
        {
            return [
                'name' => Yii::t('app/contact', 'Name'),
                'email' => Yii::t('app/contact', 'Email'),
                'subject' => Yii::t('app/contact', 'Subject'),
                'body' => Yii::t('app/contact', 'Body'),
                'verifyCode' => Yii::t('app', 'Verification Code'),
            ];
        }
    }
    

    它将输出当前语言的翻译标签而不是原始标签。

  5. 为了准备翻译,创建messages目录。目前,我们可以为所有需要的语言创建翻译文件。我们可以手动操作,但有一个有用的爬虫可以扫描所有项目文件并从Yii::t()结构中提取所有消息。让我们使用它。

  6. 生成消息扫描器的配置文件:

    ./yii message/config-template config/messages.php
    
    
  7. 打开配置文件并设置以下值:

    <?php
    
    return [
        'sourcePath' => '@app',
        'languages' => ['de', 'fr'],
        'translator' => 'Yii::t',
        'sort' => false,
        'removeUnused' => false,
        'markUnused' => true,
        'only' => ['*.php'],
        'except' => [
            '.svn',
            '.git',
            '.gitignore',
            '.gitkeep',
            '.hgignore',
            '.hgkeep',
            '/messages',
            '/vendor',
        ],
    
        'format' => 'php',
        'messagePath' => '@app/messages',
        'overwrite' => true,
    
        'ignoreCategories' => [
            'yii',
        ],
    ];
    
  8. 在传递此配置文件的同时运行爬虫:

    ./yii message config/messages.php
    
    
  9. 在此过程中,我们必须获得以下目录结构:

    messages
    ├── de
    │   ├── app
    │   │   ├── contact.php
    │   │   ├── nav.php
    │   │   └── user.php
    │   └── app.php
    └── fr
        ├── app
        │   ├── contact.php
        │   ├── nav.php
        │   └── user.php
        └── app.php
    
  10. 例如,messages/de/app/contact文件包含以下内容:

    <?php
    ... 
    return [
        'Body' => '',
        'Email' => '',
        'Name' => '',
        'Subject' => '',
    ];
    
  11. 它是一个普通的 PHP 数组,键是原始句子,值是翻译的消息。

  12. 只需输入翻译德语消息所需的价值:

    <?php
    ... 
    return [
        'Password' => 'Passwort',
        'Remember Me' => 'Erinnere dich an mich',
        'Username' => 'Benutzername',
    ];
    
  13. config/web.php 文件中将这些翻译附加到应用程序的 i18n 组件:

    $config = [
        'id' => 'basic',
        'basePath' => dirname(__DIR__),
        'bootstrap' => ['log'],
        'components' => [
            …
            'i18n' => [
                'translations' => [
                    'app*' => [
                        'class' => 'yii\i18n\PhpMessageSource',
                        'sourceLanguage' => 'en-US',
                    ],
                ],
            ],
            'db' => require(__DIR__ . '/db.php'),
        ],
        'params' => $params,
    ];
    
  14. 使用默认语言打开登录页面:如何做…

  15. 将应用程序语言切换为 de

    $config = [
        'id' => 'basic',
        'language' => 'de',
        'basePath' => dirname(__DIR__),
        'bootstrap' => ['log'],
        ...
    ];
    

    然后刷新登录页面:

    如何做…

  16. 内置框架的消息和默认验证错误将自动翻译。

它是如何工作的…

Yii2 提供了 Yii::t() 方法,通过 i18n 组件翻译界面消息,该组件支持不同类型的源。在本食谱中,我们使用 yii\i18n\hpMessageSource,它将翻译消息存储在纯 PHP 文件中。

框架没有人工智能,并且不会自动翻译消息。你必须将准备好的翻译放入文件或数据库中,并将框架中的消息源获取所需的消息。

你可以手动设置当前语言:

$config = [
    'id' => 'basic',
    'language' => 'de',
    ...
];

而不是在配置文件中设置语言,你可以在运行时切换应用程序语言:

Yii::$app->language = 'fr';

例如,如果你在 User 模型的 lang 字段中存储用户语言,你可以创建语言加载器:

<?php
namespace app\bootstrap;

use yii\base\BootstrapInterface;

class LanguageBootstrap implements BootstrapInterface
{
    public function bootstrap($app)
    {
        if (!$app->user->isGuest) {
            $app->language = $app->user->identity->lang;
        }
    }
}

在引导列表中注册此类:

$config = [
    'id' => 'basic',
    'basePath' => dirname(__DIR__),
    'bootstrap' => ['log', 'app'bootstrap\LanguageBoostrap'],
    ...
];

现在,每个经过身份验证的用户都将看到他们自己的语言界面。

此外,你可以覆盖 yii\web\UrlManager 类,通过 GET 参数或 URL 的前缀传递当前语言。另外,作为一个替代方案,你可以在浏览器 cookie 中存储选定的语言。

当你使用 Gii 生成模型和其他代码时,你可以检查以下选项:

它是如何工作的…

生成代码中的所有标签都将包含在 Yii::t() 调用中。

注意

我们在本食谱中没有涵盖模型内容的翻译。然而,例如,你可以在数据库中单独的表中(如用于帖子模型的 post_lang 表)存储翻译文本,并使用 Yii::$app->language 属性的值来获取当前语言,并通过该值提取模型所需的内容。

参考也

有关 Yii2 国际化的更多信息,请参阅 www.yiiframework.com/doc-2.0/guide-tutorial-i18n.html

使扩展准备分发

在本章中,你学习了如何创建各种类型的 Yii 扩展。现在我们将讨论如何与他人分享你的成果以及为什么这很重要。

准备工作

让我们先为好的扩展制定一个清单。一个好的编程产品应该遵循以下这些要点:

  • 良好的编码风格

  • 人们应该能够找到它

  • 一致、易于阅读和易于使用的 API

  • 良好的文档

  • 扩展应该适用于最常见的用例

  • 应该得到维护

  • 代码经过良好测试,理想情况下带有单元测试

  • 你需要提供对其的支持

当然,拥有所有这些需要大量的工作,但这些对于创建一个好的产品是必要的。

如何做…

  1. 每个现代 PHP 产品都必须遵循www.php-fig.org/psr/指南中的 PSR4 自动加载标准和 PSR1 和 PSR2 编码风格标准。

  2. 让我们更详细地审查我们的列表,从 API 开始。API 应该是一致的、易于阅读和使用的。一致性意味着整体风格不应该改变,所以没有不同的变量命名,没有像isFlag1()isNotFlag2()这样的不一致的名称,等等。一切都应该遵循你为你的代码定义的规则。这允许减少对文档的检查,让你可以专注于编码。

  3. 没有任何文档的代码几乎毫无用处。一个例外是相对简单的代码,但即使只有几行,如果没有关于如何安装和使用的说明,也会感觉不太对劲。好的文档有哪些特点?代码的目的和优点应该尽可能明显,并且应该写得清晰易懂。

  4. 如果开发者不知道代码应该放在哪里以及应用配置中应该有什么,那么代码就毫无用处。不要期望人们知道如何做框架特定的东西。安装指南应该详细说明。大多数开发者更喜欢逐步的形式。如果代码需要 SQL 模式才能工作,请提供它。

  5. 即使你的 API 方法和属性命名得当,你仍然需要用 PHPDoc 注释来记录它们,指定参数类型和返回类型,并为每个方法提供简要的描述。不要忘记受保护的和方法和属性,因为有时阅读这些内容是理解代码工作细节的必要条件。同时,考虑在文档中列出公共方法和属性,以便作为参考。

  6. 提供带有良好注释的代码用例示例。尽量涵盖扩展使用最常见的方式。

  7. 在示例中,不要试图一次解决多个问题,因为这可能会造成混淆。

  8. 让你的代码灵活很重要,这样它就可以适用于许多用例。然而,由于不可能为每个可能的用例编写代码,因此尽量涵盖最常见的用例。

  9. 让人们感到舒适很重要。提供良好的文档是第一步。第二步是提供证明你的代码按预期工作并且会与后续更新一起工作的证据。最好的方式是一套单元测试。

  10. 扩展应该得到维护,至少直到它稳定,没有更多的功能请求和错误报告。因此,预计会有问题和报告,并留出一些时间来进一步工作在代码上。如果你不能投入更多时间来维护扩展,但它非常创新,之前没有人做过,那么仍然值得分享。如果社区喜欢它,肯定有人会提供帮助。

  11. 最后,您需要使扩展可用。从您的扩展创建 Composer 包,将其推送到 GitHub 或其他共享仓库存储,并在packagist.org网站上发布。

  12. 每个扩展都应该有一个版本号和变更日志。这将允许社区检查他们是否拥有最新版本,并在升级前查看发生了什么变化。我们建议遵循来自semver.org网站的语义版本控制规则。

  13. 即使您的扩展相对简单且文档良好,也可能会有问题,而且第一次,唯一能回答这些问题的人就是您。通常,这些问题会在官方论坛上提出,因此最好创建一个人们可以讨论您的代码并在此扩展页面上提供链接的主题。

它是如何工作的…

如果您想与社区分享一个扩展并确保它有用且受欢迎,您需要做的不仅仅是编写代码。使扩展准备分发需要做更多的工作。这甚至可能比创建扩展本身还要多。那么,为什么一开始就要与社区分享扩展呢?

将您在自己的项目中使用的代码开源有其优点。您将获得人们,比您能接触到测试封闭源代码项目的人多得多的测试人员。使用您扩展的人们正在测试它,提供宝贵的反馈,并报告错误。如果您的代码受欢迎,将会有热情的开发者尝试改进您的代码,使其更广泛、更稳定、更可重用。此外,这感觉很好,因为您正在做一件好事。

我们已经涵盖了最重要的内容。尽管如此,还有更多的事情需要检查。在编写自己的扩展之前,尝试使用现有的扩展。如果一个扩展几乎符合要求,尝试联系扩展作者并贡献您的想法。审查现有代码有助于您发现有用的技巧、应该做的和不应该做的。此外,不时查看维基文章和官方论坛;关于创建扩展和一般使用 Yii 进行开发的信息非常丰富。

参见

第九章。性能调整

在本章中,我们将涵盖以下主题:

  • 遵循最佳实践

  • 加快会话处理

  • 使用缓存依赖和链

  • 使用 Yii 分析应用程序

  • 利用 HTTP 缓存

  • 合并和最小化资源

  • 在 HHVM 上运行 Yii2

Yii 是可用的最快框架之一。尽管如此,在开发和部署应用程序时,拥有一些额外的免费性能以及遵循应用程序自身的最佳实践是很好的。在本章中,您将了解如何配置 Yii 以获得额外的性能。此外,您还将学习一些最佳实践,以开发一个即使在非常高的负载下也能平稳运行的应用程序。

遵循最佳实践

在本食谱中,您将了解如何配置 Yii2 以获得最佳性能以及构建响应式应用的一些附加原则。这些原则既普遍又与 Yii 相关。因此,我们甚至在没有使用 Yii2 的情况下也能应用其中的一些。

准备工作

使用 Composer 包管理器创建一个新的 yii2-app-basic 应用程序,如官方指南中所述 www.yiiframework.com/doc-2.0/guidestart-installation.html

如何做到这一点...

  1. 将您的 PHP 更新到最新稳定版本。PHP 的大版本发布可能会带来显著的性能改进。关闭调试模式并设置 prod 环境。这可以通过编辑 web/index.php 来完成,如下所示:

    defined('YII_DEBUG') or define('YII_DEBUG', false);
    defined('YII_ENV') or define('YII_ENV', 'prod');
    

    注意

    注意:在 yii2-app-advanced 应用程序骨架中,您可以使用 shell 命令 php init 并选择生产环境来加载优化的 index.php 和配置文件。

  2. 启用 cache 组件:

    'components' => [
        'cache' => [
            'class' => 'yii\caching\FileCache',
        ],
    ],
    

    您可以使用任何缓存存储而不是 FileCache。您还可以注册多个缓存应用程序组件,并使用 Yii::$app->cacheYii::$app->cache2 来处理不同类型的数据:

    'components' => [
        'cache' => [
            'class' => 'yii\caching\MemCache',
            'useMemcached' => true,
        ],
        'cache2' => [
            'class' => 'yii\caching\FileCache',
        ],
    ],
    

    框架默认在其自身类中使用 cache 组件。

  3. 按如下方式启用 db 组件的表模式缓存:

    return [
        // ...
        'components' => [
            // ...
            'cache' => [
                'class' => 'yii\caching\FileCache',
            ],
            'db' => [
                'class' => 'yii\db\Connection',
                'dsn' => 'mysql:host=localhost;dbname=mydatabase',
                'username' => 'root',
                'password' => '',
                'enableSchemaCache' => true,
    
                // Optional. Default value is 3600 seconds
                schemaCacheDuration' => 3600, 
    
                // Optional. Default value is 'cache'
                'schemaCache' => 'cache',
            ],
        ],
    ];
    
  4. 在列出元素集合时,使用纯数组而不是 Active Record 对象:

    $categoriesArray = Categories::find()->asArray()->all();
    
  5. 在处理大量结果时,使用 each() 而不是 all()foreach 中:

    foreach (Post::find()->each() as $post) {
        // ...
    }
    
  6. 由于 Composer 的自动加载器用于包含大多数第三方类文件,您应该考虑通过执行以下命令来优化它:

    composer dump-autoload
     -o
    
    

它是如何工作的...

YII_DEBUG 设置为 false 时,Yii 关闭所有跟踪级别的日志记录并使用较少的错误处理代码。此外,当您将 YII_ENV 设置为 prod 时,您的应用程序不会加载 Yii 和调试面板模块。

schemaCachingDuration 设置为秒数,允许缓存 Yii 的 Active Record 使用的数据库模式。这对于生产服务器来说非常推荐,并且它可以显著提高 Active Record 的性能。为了使其正常工作,您需要正确配置 cache 组件,如下所示:

'cache' => [
    'class' => 'yii\cache\FileCache',
],

启用缓存也会对其他 Yii 组件产生积极影响。例如,Yii 路由器或 urlManager 开始缓存路由。

当然,你可能会遇到前述设置无法帮助达到足够性能水平的情况。在大多数情况下,这意味着应用程序本身是瓶颈,或者你需要更多的硬件。

  • 服务器端性能只是大局的一部分:服务器端性能只是影响整体性能的因素之一。通过优化客户端,如提供 CSS、图像和 JavaScript 文件,适当的缓存以及最小化 HTTP 请求的数量,即使不优化 PHP 代码,也能获得良好的视觉性能提升。

  • 无需使用 Yii 完成的事项:有些事情最好不用 Yii 来完成。例如,为了避免额外的开销,动态图像缩放最好在单独的 PHP 脚本中完成。

  • Active Record 与查询构建器和 SQL 的比较:在性能关键的应用程序部分使用查询构建器或 SQL。通常,AR 在添加和编辑记录时最有用,因为它添加了一个方便的验证层,而在选择记录时则不太有用。

  • 首先检查慢查询:如果开发者不小心忘记为经常读取的表添加索引,或者相反,或者为经常写入的表添加过多的索引,数据库可能会在瞬间成为瓶颈。同样,选择不必要的数据和不必要的连接操作也会导致这种情况。

  • 缓存或保存重处理的结果:如果你可以避免在每次页面加载时运行重处理过程,那就更好了。例如,保存或缓存解析 Markdown 文本的结果,净化它(这是一个非常耗资源的处理过程)一次,然后使用准备好的可显示 HTML。

  • 处理过多的处理:有时需要立即处理过多的处理。这可能包括构建复杂的报告或简单地发送电子邮件(如果你的项目负载很重)。在这种情况下,最好将其放入队列中,稍后使用 cron 或其他专用工具进行处理。

参见

更多关于性能调整和缓存的信息,请参考以下链接:

加快会话处理

在大多数情况下,PHP 的本地会话处理是不错的。至少有两个可能的原因让你想要改变会话处理的方式:

  • 当使用多个服务器时,你需要为两个服务器都拥有共同的会话存储。

  • 默认的 PHP 会话使用文件,因此可能的最大性能受限于磁盘 I/O。

  • 默认 PHP 会话会阻塞并发会话存储。在这个菜谱中,我们将看到如何为 Yii 会话使用高效的存储。

准备工作

使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,如官方指南www.yiiframework.com/doc-2.0/guide-start-installation.html中所述,并安装 Memcache 服务器和memcache PHP 扩展。

如何做到这一点...

我们将使用 Apache ab工具对网站进行压力测试。它与 Apache 二进制文件一起分发,因此如果您使用 Apache,您可以在bin目录中找到它。

  1. 运行以下命令,将您的网站替换为您实际使用的实际主机名:

    ab -n 1000 -c 5 http://yii-book.app/index.php?r=site/contact
    
    

    这将发送 1,000 个请求,每次 5 个,并将输出以下统计信息:

    This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/
    ...
    Server Software:        nginx
    Server Hostname:        yii-book.app
    Server Port:            80
    
    Document Path:          /index.php?r=site/contact
    Document Length:        14866 bytes
    Concurrency Level:      5
    Time taken for tests:   10.961 seconds
    Complete requests:      1000
    Failed requests:        0
    Total transferred:      15442000 bytes
    HTML transferred:       14866000 bytes
    Requests per second:    91.24 [#/sec] (mean)
    Time per request:       54.803 [ms] (mean)
    Time per request:       10.961 [ms] (mean, across all concurrent requests)
    Transfer rate:          1375.84 [Kbytes/sec] received
    
    Connection Times (ms)
     min  mean[+/-sd] median   max
    Connect:        0    0   0.0      0       0
    Processing:    18   55 324.9     29    4702
    Waiting:       15   41 255.1     24    4695
    Total:         18   55 324.9     29    4702
    
    

    我们对每秒请求数的指标感兴趣。这个数字意味着,如果每次有 5 个请求,网站可以每秒处理 91.24 个请求。

    注意

    注意调试没有关闭,因为我们对会话处理速度的变化感兴趣。

  2. 现在将以下内容添加到/config/web.php组件部分:

    'session' => array(
        'class' => 'yii\web\CacheSession',
        'cache' => 'sessionCache',
    ),
    'sessionCache' => array(
        'class' => 'yii\caching\MemCache',
    ),
    
  3. 使用相同的设置再次运行ab。这次,您应该得到更好的结果。在我的情况下,是每秒 139.07 个请求。这意味着作为会话处理器的Memcache比默认的基于文件的会话处理器性能提高了 52%。

    注意

    不要依赖于这里提供的确切结果。这完全取决于软件版本、设置和使用的硬件。始终尝试在自己的部署环境中运行所有测试。

  4. 通过选择合适的会话处理后端,您可以获得显著的性能提升。Yii 支持更多开箱即用的缓存后端,包括 WinCache、XCache 和与 Zend Server 一起提供的 Zend 数据缓存。此外,您可以实现自己的缓存后端以使用快速 noSQL 存储,如 Redis。

它是如何工作的…

默认情况下,Yii 使用原生 PHP 会话;这意味着在大多数情况下使用文件系统。文件系统无法有效地处理高并发。

在以下情况下,Memcache 或其他平台表现良好:

'session' => array(
    'class' => 'yii\web\CacheSession',
    'cache' => 'sessionCache',
),
'sessionCache' => array(
    'class' => 'yii\caching\MemCache',
),

在前面的配置部分中,我们指导 Yii 使用CacheSession作为会话处理器。使用此组件,我们可以将会话处理委托给cache中指定的缓存组件。这次我们使用MemCache

当使用 memcached 后端时,你应该考虑到,在使用这些解决方案时,如果达到最大缓存容量,应用程序用户可能会丢失会话。

注意

注意,当使用会话的缓存后端时,您不能依赖于会话作为临时数据存储,因为这样就没有内存来存储更多的数据在 memcached 中。在这种情况下,这将只是清除所有数据或删除其中的一部分。

如果你正在使用多个服务器,你不能使用文件存储。服务器之间无法共享会话数据。在 memcached 的情况下,这很容易,因为它可以从你想要的任意多个服务器上轻松访问。

此外,为了共享会话数据,你可以使用DbSession

return [
    // ...
    'components' => [
        'session' => [
            'class' => 'yii\web\DbSession',
        ],
    ],
];

现在,在你的数据库中创建一个新的表:

CREATE TABLE session (
    id CHAR(40) NOT NULL PRIMARY KEY,
    expire INTEGER,
    data BLOB
)

更多内容…

尽快关闭会话是个好主意。如果你在当前请求中不打算在会话中存储任何内容,你甚至可以在控制器动作的非常开始时就关闭它。这样,即使使用文件作为存储,你的应用程序也应该没问题。

使用以下命令:

Yii:$app->session->close();

参见

关于性能和缓存的更多信息,请参考以下 URL:

使用缓存依赖和链

Yii 支持许多缓存后端,但真正使 Yii 缓存灵活的是依赖和依赖链支持。有些情况下,你不能简单地缓存数据一小时,因为缓存的信息可能会随时更改。

在这个菜谱中,我们将看到如何缓存整个页面,并且在更新时仍然总是获取新鲜数据。页面将是仪表板类型的,将显示最近添加的五篇文章以及一个账户的总数。

注意

注意,操作一旦添加就无法编辑,但文章可以编辑。

准备就绪

使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guide-start-installation.html

  1. config/web.php中按照以下方式激活缓存组件:

    return [
        // ...
        'components' => [
            cache => ['class' => 'yii\caching\FileCache,
            ],
        ],
    ];
    
  2. config/db.php中设置一个新的数据库并配置它。

  3. 运行以下迁移:

    <?php
    
    use yii\db\Schema;
    use yii\db\Migration;
    
    class m160308_093233_create_example_tables extends Migration
    {
        public function up()
        {
            $tableOptions = null;
            if ($this->db->driverName === 'mysql') {
                $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
            }
    
            $this->createTable('{{%account}}', [
                'id' => Schema::TYPE_PK,
                'amount' => Schema::TYPE_DECIMAL . '(10,2) NOT NULL',
            ], $tableOptions);
    
            $this->createTable('{{%article}}', [
                'id' => Schema::TYPE_PK,
                'title' => Schema::TYPE_STRING . ' NOT NULL',
                'text' => Schema::TYPE_TEXT . ' NOT NULL',
            ], $tableOptions);
        }
    
        public function down()
        {
            $this->dropTable('{{%article}}');
            $this->dropTable('{{%account}}');
        }
    }
    
  4. 使用 Yii 生成账户和文章表的模型。

  5. 创建protected/controllers/DashboardController.php如下:

    <?php
    
    namespace app\controllers;
    
    use app\models\Account;
    use app\models\Article;
    use yii\web\Controller;
    
    class DashboardController extends Controller
    {
        public function actionIndex()
        {
            $total = Account::find()->sum('amount');
            $articles = Article::find()->orderBy('id DESC')->limit(5)->all();
    
            return $this->render('index', array(
                'total' => $total,
                'articles' => $articles,
            ));
        }
    
        public function actionRandomOperation()
        {
            $rec = new Account();
            $rec->amount = rand(-1000, 1000);
            $rec->save();
    
            echo 'OK';
        }
    
        public function actionRandomArticle()
        {
            $n = rand(0, 1000);
    
            $article = new Article();
            $article->title = "Title #".$n;
            $article->text = "Text #".$n;
            $article->save();
    
            echo 'OK';
        }
    }
    
  6. 创建views/dashboard/index.php如下:

    <?php
    use yii\helpers\Html;
    /* @var $this yii\web\View */
    /* @var $total int */
    /* @var $articles app\models\Article[] */
    ?>
    
    <h1>Total: <?= $total ?></h1>
    <h2>5 latest articles:</h2>
    <?php foreach($articles as $article): ?>
        <h3><?= Html::encode($article->title) ?></h3>
        <div><?= Html::encode($article->text) ?></div>
    <?php endforeach ?>
    
  7. 运行dashboard/random-operationdashboard/random-article几次。然后,运行dashboard/index,你应该会看到一个类似于以下截图的屏幕:准备就绪

  8. 点击页面底部的调试面板中的数据库查询数量:准备就绪

    查看查询列表:

    准备就绪

如何操作…

执行以下步骤:

  1. 我们需要修改控制器代码如下:

    <?php
    
    namespace app\controllers;
    
    use app\models\Account;
    use app\models\Article;
    use yii\caching\DbDependency;
    use yii\caching\TagDependency;
    use yii\web\Controller;
    
    class DashboardController extends Controller
    {
        public function behaviors()
        {
            return [
                'pageCache' => [
                    'class' => 'yii\filters\PageCache',
                    'only' => ['index'],
                    'duration' => 24 * 3600 * 365, // 1 year
                    'dependency' => [
                        'class' => 'yii\caching\ChainedDependency',
                        'dependencies' => [
                            new TagDependency(['tags' => 
                            ['articles']]),
                            new DbDependency(['sql' => 'SELECT MAX(id) FROM ' . Account::tableName()])
                        ]
                    ],
                ],
            ];
        }
    
        public function actionIndex()
        {
            $total = Account::find()->sum('amount');
            $articles = Article::find()->orderBy('id DESC')->limit(5)->all();
    
            return $this->render('index', array(
                'total' => $total,
                'articles' => $articles,
            ));
        }
    
        public function actionRandomOperation()
        {
            $rec = new Account();
            $rec->amount = rand(-1000, 1000);
            $rec->save();
    
            echo 'OK';
        }
    
        public function actionRandomArticle()
        {
            $n = rand(0, 1000);
    
            $article = new Article();
            $article->title = "Title #".$n;
            $article->text = "Text #".$n;
            $article->save();
    
            TagDependency::invalidate(\Yii::$app->cache, 'articles');
    
            echo 'OK';
        }
    }
    
  2. 就这些了。现在,在加载dashboard/index几次之后,你将在最新的快照中只得到一个简单的查询,如图所示:如何操作…

    此外,尝试运行dashboard/random-operationdashboard/random-article,然后刷新dashboard/index。数据应该如下变化:

    如何做…

它是如何工作的…

为了在最小化代码修改的同时实现最佳性能,我们使用以下过滤器实现全页缓存:

public function behaviors()
{
    return [
        'pageCache' => [
            'class' => 'yii\filters\PageCache',
            'only' => ['index'],
            'duration' => 24 * 3600 * 365, // 1 year
            'dependency' => [
                'class' => 'yii\caching\ChainedDependency',
                'dependencies' => [
                    new TagDependency(['tags' => ['articles']]),
                    new DbDependency(['sql' => 'SELECT MAX(id) FROM account'])
                ]
            ],
        ],
    ];
}

上述代码表示我们将全页缓存应用于index操作。页面将被缓存一年,如果依赖数据中的任何一个发生变化,缓存将刷新。因此,一般来说,依赖的工作方式如下:

  • 第一次运行时,获取依赖中描述的新数据,保存以供将来参考,并更新缓存

  • 它获取依赖中描述的新数据,获取保存的数据,然后比较两者

  • 如果它们相等,它将使用缓存数据

  • 如果不是,它将更新缓存,使用新鲜数据,并保存新鲜依赖数据以供将来参考

在我们的案例中,使用了两种依赖类型——标签和数据库。标签依赖通过自定义字符串标签标记数据,并检查它以决定我们是否需要使缓存失效,而数据库依赖使用 SQL 查询结果来完成同样的目的。

你现在可能有的问题是,“为什么我们在一种情况下使用数据库,而在另一种情况下使用标签?”这是一个很好的问题!

使用数据库依赖的目标是替换繁重的计算,并选择一个尽可能少获取数据的轻量查询。这种类型依赖的最好之处在于,我们不需要在现有代码中嵌入任何额外的逻辑。在我们的案例中,我们可以使用这种类型的依赖进行账户操作,但不能用于文章,因为文章内容可能会改变。因此,对于文章,我们设置了一个名为 article 的全局标签,这意味着当我们想要使整个文章缓存失效时,可以手动调用以下操作:

TagDependency::invalidate(\Yii::$app->cache, 'articles');

参见

为了了解更多关于缓存和使用缓存依赖的信息,请参考www.yiiframework.com/doc-2.0/guide-caching-overview.html

使用 Yii 分析应用程序

如果应用了所有关于部署 Yii 应用程序的最佳实践,但你仍然没有得到你想要的表现,那么很可能是应用程序本身存在一些瓶颈。处理这些瓶颈的主要原则是,你永远不应该假设任何事情,并且在尝试优化代码之前,始终测试和评估代码。

在这个菜谱中,我们将尝试在 Yii2 迷你应用程序中找到瓶颈。

准备中

使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,如官方指南中所述,请参阅www.yiiframework.com/doc-2.0/guide-start-installation.html

  1. 设置你的数据库连接并应用以下迁移:

    <?php
    use yii\db\Migration;
    
    class m160308_093233_create_example_tables extends Migration
    {
        public function up()
        {
            $tableOptions = null;
            if ($this->db->driverName === 'mysql') {
                $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
            }
    
            $this->createTable('{{%category}}', [
                'id' => $this->primaryKey(),
                'name' => $this->string()->notNull(),
            ], $tableOptions);
    
            $this->createTable('{{%article}}', [
                'id' => $this->primaryKey(),
                'category_id' => $this->integer()->notNull(),
                'title' => $this->string()->notNull(),
                'text' => $this->text()->notNull(),
            ], $tableOptions);
    
            $this->createIndex('idx-article-category_id', '{{%article}}', 'category_id');
            $this->addForeignKey('fk-article-category_id', '{{%article}}', 'category_id', '{{%category}}', 'id');
        }
    
        public function down()
        {
            $this->dropTable('{{%article}}');
            $this->dropTable('{{%category}}');
        }
    }
    
  2. 使用 Yii 生成每个表的模型。

  3. 输入以下控制台命令:

    <?php
    namespace app\commands;
    
    use app\models\Article;
    use app\models\Category;
    use Faker\Factory;
    use yii\console\Controller;
    
    class DataController extends Controller
    {
        public function actionInit()
        {
            $db = \Yii::$app->db;
            $faker = Factory::create();
    
            $transaction = $db->beginTransaction();
            try {
                $categories = [];
                for ($id = 1; $id <= 100; $id++) {
                    $categories[] = [
                        'id' => $id,
                        'name' => $faker->name,
                    ];
                }
    
                $db->createCommand()->batchInsert(Category::tableName(), ['id', 'name'], $categories)->execute();
    
                $articles = [];
                for ($id = 1; $id <= 100; $id++) {
                    $articles[] = [
                        'id' => $id,
                        'category_id' => $faker->numberBetween(1, 100),
                        'title' => $faker->text($maxNbChars = 100),
                        'text' => $faker->text($maxNbChars = 200),
                    ];
                }
    
                $db->createCommand()
                ->batchInsert(Article::tableName(), ['id', 'category_id', 'title', 'text'], $articles)->execute();
    
                $transaction->commit();
            } catch (\Exception $e) {
                $transaction->rollBack();
                throw $e;
            }
        }
    }
    

    然后执行:

    ./yii data/init
    
    
  4. 添加ArticleController类,如下所示:

    <?php
    namespace app\controllers;
    
    use Yii;
    use app\models\Article;
    use yii\data\ActiveDataProvider;
    use yii\web\Controller;
    
    class ArticleController extends Controller
    {
        public function actionIndex()
        {
            $query = Article::find();
            $dataProvider = new ActiveDataProvider([
                'query' => $query,
            ]);
    
            return $this->render('index', [
                'dataProvider' => $dataProvider,
            ]);
        }
    }
    
  5. 添加views/article/index.php视图,如下所示:

    <?php
    use yii\helpers\Html;
    use yii\widgets\ListView;
    
    /* @var $this yii\web\View */
    /* @var $dataProvider yii\data\ActiveDataProvider */
    
    $this->title = 'Articles';
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div class="article-index">
        <h1><?= Html::encode($this->title) ?></h1>
        <?= ListView::widget([
            'dataProvider' => $dataProvider,
            'itemOptions' => ['class' => 'item'],
            'itemView' => '_item',
        ]) ?>
    /div>
    

    然后添加views/article/_item.php

    <?php
    use yii\helpers\Html;
    
    /* @var $this yii\web\View */
    /* @var $model app\models\Article */
    ?>
    
    <div class="panel panel-default">
        <div class="panel-heading"><?= Html::encode($model->title); ?></div>
        <div class="panel-body">
            Category: <?= Html::encode($model->category->name) ?>
        </div>
    </div>
    

如何操作…

按照以下步骤使用 Yii 对应用程序进行性能分析:

  1. 打开文章页面:如何操作…

  2. 打开views/article/index.php文件,并在ListView小部件前后添加性能分析调用:

    <div class="article-index">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <?php Yii::beginProfile('articles') ?>
    
        <?= ListView::widget([
            'dataProvider' => $dataProvider,
            'itemOptions' => ['class' => 'item'],
            'itemView' => '_item',
        ]) ?>
    
        <?php Yii::endProfile('articles') ?>
    
    </div>
    

    现在刷新页面。

  3. 展开页面底部的调试面板,并点击时间徽章(在我们的例子中为73 ms):如何操作…

    现在检查性能分析报告:

    如何操作…

    我们可以看到,我们的文章块接近 40 毫秒。

  4. 打开我们的控制器,并为文章的category关系添加预加载,如下所示:

    class ArticleController extends Controller
    {
        public function actionIndex()
        {
            $query = Article::find()->with('category');
    
            $dataProvider = new ActiveDataProvider([
                'query' => $query,
            ]);
            return $this->render('index', [
                'dataProvider' => $dataProvider,
            ]);
        }
    }
    
  5. 返回网站,刷新页面,并再次打开性能分析报告:如何操作…

目前文章列表的生成接近 25 毫秒,因为应用程序通过相关模型的预加载执行了较少的 SQL 查询。

它是如何工作的…

你可以使用Yii::beginProfileYii::endProfile调用将任何源代码片段括起来:

Yii::beginProfile('articles');
// ...
Yii::endProfile('articles');

页面执行后,你可以在调试模块的性能分析页面看到包含所有时长的报告。

此外,你还可以使用嵌套的性能分析调用,如下所示:

Yii::beginProfile('outer');
    Yii::beginProfile('inner');
        // ...
    Yii::endProfile('inner');
Yii::endProfile('outer');

注意

注意:在此情况下,请注意正确地打开和关闭调用以及正确的块命名。如果你遗漏了Yii::endProfile调用,或者将Yii::endProfile('inner')Yii::endProfile('outer')的顺序颠倒,性能分析将不会工作。

参见

利用 HTTP 缓存

除了仅服务器端缓存实现外,你还可以通过特定的 HTTP 头部使用客户端缓存。

在本食谱中,我们将基于Last-ModifiedETag头部进行全页缓存。

准备工作

使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,具体方法请参阅官方指南中的www.yiiframework.com/doc-2.0/guide-start-installation.html

  1. 按照以下方式创建和运行迁移:

    <?php
    use yii\db\Migration;
    
    class m160308_093233_create_example_tables extends Migration
    {
        public function up()
        {
            $this->createTable('{{%article}}', [
                'id' => $this->primaryKey(),
                'created_at' => $this->integer()->unsigned()-
                >notNull(),
                'updated_at' => $this->integer()->unsigned()->notNull(),
                'title' => $this->string()->notNull(),
                'text' => $this->text()->notNull(),
            ]);
        }
    
        public function down()
        {
            $this->dropTable('{{%article}}');
        }
    }
    
  2. 创建一个Article模型,如下所示:

    <?php
    namespace app\models;
    
    use Yii;
    use yii\behaviors\TimestampBehavior;
    use yii\db\ActiveRecord;
    
    class Article extends ActiveRecord
    {
        public static function tableName()
        {
            return '{{%article}}';
        }
    
        public function behaviors()
        {
            return [
                TimestampBehavior::className(),
            ];
        }
    }
    
  3. 创建一个具有以下操作的博客控制器:

    <?php
    namespace app\controllers;
    
    use app\models\Article;
    use yii\web\Controller;
    use yii\web\NotFoundHttpException;
    class BlogController extends Controller
    {
        public function actionIndex()
        {
            $articles = Article::find()->orderBy(['id' => SORT_DESC])->all();
            return $this->render('index', array(
                'articles' => $articles,
            ));
        }
    
        public function actionView($id)
        {
            $article = $this->findModel($id);
            return $this->render('view', array(
                'article' => $article,
            ));
        }
    
        public function actionCreate()
        {
            $n = rand(0, 1000);
            $article = new Article();
            $article->title = 'Title #' . $n;
            $article->text = 'Text #' . $n;
            $article->save();
            echo 'OK';
        }
    
        public function actionUpdate($id)
        {
            $article = $this->findModel($id);
            $n = rand(0, 1000);
            $article->title = 'Title #' . $n;
            $article->text = 'Text #' . $n;
            $article->save();
            echo 'OK';
        }
        private function findModel($id)
        {
            if (($model = Article::findOne($id)) !== null) {
                return $model;
            } else {
                throw new NotFoundHttpException('The requested page does not exist.');
            }
        }
    }
    
  4. 添加views/blog/index.php视图:

    <?php
    use yii\helpers\Html;
    
    $this->title = 'Articles';;
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    
    <?php foreach($articles as $article): ?>
        <h3><?= Html::a(Html::encode($article->title), ['view', 'id' => $article->id]) ?></h3>
        <div>Created <?= Yii::$app->formatter->asDatetime($article->created_at) ?></div>
        <div>Updated <?= Yii::$app->formatter->asDatetime($article->updated_at) ?></div>
    <?php endforeach ?>
    
  5. 添加views/blog/view.php视图文件:

    <?php
    use yii\helpers\Html;
    
    $this->title = $article->title;
    $this->params['breadcrumbs'][] = ['label' => 'Articles', 'url' => ['index']];
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    
    <h1><?= Html::encode($article->title) ?></h1>
    <div>Created <?= Yii::$app->formatter->asDatetime($article->created_at) ?></div>
    <div>Updated <?= Yii::$app->formatter->asDatetime($article->updated_at) ?></div>
    <hr />
    <p><?= Yii::$app->formatter->asNtext($article->text) ?></p>
    

如何操作…

按照以下步骤利用 HTTP 缓存:

  1. 访问以下 URL yii-book.app/index.php?r=blog/create三次以生成三篇文章。

  2. 打开以下博客页面:如何操作…

  3. 在您的浏览器中打开开发者控制台,查看每次重新加载博客页面时的 200 OK 响应状态:如何操作…

  4. 打开 BlogController 并附加以下行为:

    class BlogController extends Controller
    {
        public function behaviors()
        {
            return [
                [
                    'class' => 'yii\filters\HttpCache',
                    'only' => ['index'],
                    'lastModified' => function ($action, $params) {
                        return Article::find()->max('updated_at');
                    },
                ],
                [
                    'class' => 'yii\filters\HttpCache',
                    'only' => ['view'],
                    'etagSeed' => function ($action, $params) {
                        $article = $this->findModel(\Yii::$app->request->get('id'));
                        return serialize([$article->title, $article->text]);
                    },
                ],
            ];
        }
    
        // ...
    }
    
  5. 然后,重新加载页面几次,并检查服务器返回的是 304 Not Modified 状态而不是 200 OK如何操作…

  6. 使用以下 URL 打开相关页面以更新随机文章:http://yii-book.app/index.php?r=blog/update

  7. 更新博客页面后,检查服务器在第一次请求时返回 200 OK,之后返回 304 Not Modified,并验证您是否在页面上看到了新的更新时间:如何操作…

  8. 打开我们文章中的任何页面,如下所示:如何操作…

验证服务器在第一次请求时返回 200 OK,在后续请求时返回 304 Not Modified

它是如何工作的…

有基于时间和基于内容的方法,通过 HTTP 头部帮助您的浏览器检查缓存响应内容的可用性。

Last-Modified

这种方法表明服务器必须返回每个文档的最后修改日期。在存储日期后,我们的浏览器可以在每个后续请求的 If-Modified-Since 头部中附加它。

我们必须将 action 过滤器附加到我们的控制器中,并指定 lastModified 回调如下:

class BlogController extends Controller
{
    public function behaviors()
    {
        return [
            [
                'class' => 'yii\filters\HttpCache',
                'only' => ['index'],
                'lastModified' => function ($action, $params) {
                    return Article::find()->max('updated_at');
                },
            ],
           // ...
        ];
    }

    // ...
}

\yii\filters\HttpCache 类调用回调并比较返回值与 $_SERVER['HTTP_IF_MODIFIED_SINCE'] 系统变量。如果文档尚未更改,HttpCache 将发送一个轻量级的 304 响应头,而不运行操作。

然而,如果文档已更新,缓存将被忽略,服务器将返回一个完整响应。

请求 响应
第一次请求,包含完整响应

|

GET /index.php?r=blog HTTP 1.1

|

HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
Last-Modified: Thu, 21 Apr 2016 00:56:02 GMT

<!DOCTYPE html>
<html lang="en-US">
...

|

第二次请求,使用 If-Modified-Since 且响应为空

|

GET /index.php?r=blog HTTP 1.1
If-Modified-Since: Thu, 21 Apr 2016 00:56:02 GMT

|

HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=3600

|

第三次请求,更新帖子后包含完整响应

|

GET /index.php?r=blog HTTP 1.1
If-Modified-Since: Thu, 21 Apr 2016 00:56:02 GMT

|

HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
Last-Modified: Thu, 21 Apr 2016 01:12:02 GMT

<!DOCTYPE html>
<html lang="en-US">
...

|

作为 Last-Modified 头部变量的替代或补充,您可以使用 ETag

Entity Tag

在我们不在文档或页面上存储最后修改日期的情况下,我们可以使用自定义哈希,这些哈希可以在文档内容的基础上生成。

例如,我们可以使用文档的标题来对特定的标签进行哈希:

class BlogController extends Controller
{
    public function behaviors()
    {
        return [
            [
                'class' => 'yii\filters\HttpCache',
                'only' => ['view'],
                'etagSeed' => function ($action, $params) {
                    $article = $this->findModel(\Yii::$app->request->get('id'));
                    return serialize([$article->title, $article->text]);
                },
            ],
        ];
    }
    // ...
}

HttpCache 过滤器将此标签附加到服务器响应的 ETag 头部变量。

存储了 ETag 后,我们的浏览器可以在每个后续请求的 If-None-Match 头部中附加它。

如果文档尚未更改,HttpCache 将发送一个轻量级的 304 响应头,而不运行操作。

请求 响应
第一次请求,包含完整响应

|

GET index.php?r=blog/view&id=3 HTTP 1.1

|

HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
Etag: "VYkwdOXBzV23KhnzTTJXU"

<!DOCTYPE html>
<html lang="en-US">
...

|

第二次请求,使用 If-None-Match 且响应为空

|

GET index.php?r=blog/view&id=3 HTTP 1.1
If-None-Match: "VYkwdOXBzV23KhnzTTJXU"

|

HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=3600
Etag: "VYkwdOXBzV23KhnzTTJXU"

|

第三次请求,更新帖子后包含完整响应

|

GET index.php?r=blog/view&id=3 HTTP 1.1
If-None-Match: "VYkwdOXBzV23KhnzTTJXU"

|

HTTP/1.1 200 OK
Cache-Control: public, max-age=3600Etag: "Ur4Ghd6hdYthrn82Ph44dhF"

<!DOCTYPE html>
<html lang="en-US">
...

|

当缓存有效时,我们的应用程序将发送304 Not Modified HTTP 响应头而不是页面内容,并且不会重复运行控制器和操作。

参考以下

合并和最小化资源

如果您的网页包含许多 CSS 和/或 JavaScript 文件,页面将非常慢地打开,因为浏览器会通过分离的线程发送大量的 HTTP 请求来下载每个文件。为了减少请求和连接的数量,我们可以在生产模式下将多个 CSS/JavaScript 文件合并并压缩成一个或非常少的文件,然后在页面上包含这些压缩文件而不是原始文件。

准备工作

如何操作…

按照以下步骤合并和最小化资源:

  1. 打开应用程序index页面的源 HTML 代码。检查它是否与以下结构相似:

    <!DOCTYPE html>
    <html lang="en-US">
    <head>
        ...
        <title>My Yii Application</title>
        <link href="/assets/9b3b2888/css/bootstrap.css" rel="stylesheet">
        <link href="/css/site.css" rel="stylesheet">
    </head>
    <body>
        ...
        <script src="img/jquery.js"></script>
        <script src="img/yii.js"></script>
        <script src="img/bootstrap.js"></script>
    </body>
    </html>
    

    页面包含三个 JavaScript 文件。

  2. 打开config/console.php文件并添加@webroot@web别名定义:

    <?php
    Yii::setAlias('@webroot', __DIR__ . '/../web');
    Yii::setAlias('@web', '/');
    
  3. 打开控制台并运行以下命令:

    yii asset/template assets.php
    
    
  4. 打开生成的assets.php文件并按以下方式配置:

    <?php
    return [
        'jsCompressor' => 'java -jar compiler.jar --js {from} --js_output_file {to}',
        'cssCompressor' => 'java -jar yuicompressor.jar --type css {from} -o {to}',
        'bundles' => [
            'app\assets\AppAsset',
            'yii\bootstrap\BootstrapPluginAsset',
        ],
        'targets' => [
            'all' => [
                'class' => 'yii\web\AssetBundle',
                'basePath' => '@webroot/assets',
                'baseUrl' => '@web/assets',
                'js' => 'all-{hash}.js',
                'css' => 'all-{hash}.css',
            ],
        ],
        'assetManager' => [
            'basePath' => '@webroot/assets',
            'baseUrl' => '@web/assets',
        ],
    ];
    
  5. 运行合并命令yii asset assets.php config/assets-prod.php。如果成功,您必须得到具有以下配置的config/assets-prod.php文件:

    <?php
    return [
        'all' => [
            'class' => 'yii\\web\\AssetBundle',
            'basePath' => '@webroot/assets',
            'baseUrl' => '@web/assets',
            'js' => [
                'all-fe792d4766bead53e7a9d851adfc6ec2.js',
            ],
            'css' => [
                'all-37cfb42649f74eb0a4bfe0d0e715c420.css',
            ],
        ],
        'yii\\web\\JqueryAsset' => [
            'sourcePath' => null,
            'js' => [],
            'css' => [],
            'depends' => [
                'all',
            ],
        ],
        'yii\\web\\YiiAsset' => [
            'sourcePath' => null,
            'js' => [],
            'css' => [],
            'depends' => [
                'yii\\web\\JqueryAsset',
                'all',
            ],
        ],
        'yii\\bootstrap\\BootstrapAsset' => [
            'sourcePath' => null,
            'js' => [],
            'css' => [],
            'depends' => [
                'all',
            ],
        ],
        'app\\assets\\AppAsset' => [
            'sourcePath' => null,
            'js' => [],
            'css' => [],
            'depends' => [
                'yii\\web\\YiiAsset',
                'yii\\bootstrap\\BootstrapAsset',
                'all',
            ],
        ],
        'yii\\bootstrap\\BootstrapPluginAsset' => [
            'sourcePath' => null,
            'js' => [],
            'css' => [],
            'depends' => [
                'yii\\web\\JqueryAsset',
                'yii\\bootstrap\\BootstrapAsset',
                'all',
            ],
        ],
    ];
    
  6. assetManager组件的配置添加到config/web.php文件中:

    'components' => [
        // ...
        'assetManager' => [
            'bundles' => YII_ENV_PROD ? require(__DIR__ . '/assets-prod.php') : [],
        ],
    ],
    
  7. web/index.php中开启生产模式:

    defined('YII_ENV') or define('YII_ENV', 'prod');
    
  8. 在浏览器中重新加载页面并再次查看 HTML 代码。现在它必须包含单行以包含我们的压缩文件:

    <!DOCTYPE html>
    <html lang="en-US">
        <head>
            ...
            <title>My Yii Application</title>
            <link href="/assets/all-37cfb42649f74eb0a4bfe0d0e715c420.css" rel="stylesheet">
        </head>
        <body>
            ...
            <script src="img/all-fe792d4766bead53e7a9d851adfc6ec2.js"></script>
        </body>
    </html>
    

它是如何工作的…

首先,我们的页面包含了一系列的文件:

<link href="/assets/9b3b2888/css/bootstrap.css" rel="stylesheet">
<link href="/css/site.css" rel="stylesheet">
...
<script src="img/jquery.js"></script>
<script src="img/yii.js"></script>
<script src="img/bootstrap.js"></script>

接下来,我们生成了assets.php配置文件并指定了用于压缩的包:

'bundles' => [
    'app\assets\AppAsset',
    'yii\bootstrap\BootstrapPluginAsset',
],

注意

注意:我们可以指定所有中间资产包,例如yii\web\JqueryAssetyii\web\YiiAsset,但这些资产已经作为AppAssetBootstrapPluginAsset的依赖项指定,压缩命令会自动解决所有这些依赖。

AssetManager 将所有资产发布到web/assets中的经典子目录,发布后运行压缩器将所有 CSS 和 JS 文件合并为all-{hash}.jsall-{hash}.css

检查 CSS 文件是否包含其他资源,例如bootstrap.css文件通过相对路径:

@font-face {
    font-family: 'Glyphicons Halflings';
    src: url('../fonts/glyphicons-halflings-regular.eot');
}

如果是这样,那么在合并文件中,我们的压缩器将所有相对路径更改为以下形式:

@font-face{
    font-family: 'Glyphicons Halflings';
    src: url('9b3b2888/fonts/glyphicons-halflings-regular.eot');
}

处理后,我们得到包含assetManager组件的包配置的assets-prod.php文件。它定义了新的虚拟资产作为原始包的干净副本的依赖项:

return [
    'all' => [
        'class' => 'yii\\web\\AssetBundle',
        'basePath' => '@webroot/assets',
        'baseUrl' => '@web/assets',
        'js' => [
            'all-fe792d4766bead53e7a9d851adfc6ec2.js',
        ],
        'css' => [
            'all-37cfb42649f74eb0a4bfe0d0e715c420.css',
        ],
    ],
    'yii\\web\\JqueryAsset' => [
        'sourcePath' => null,
        'js' => [],
        'css' => [],
        'depends' => [
            'all',
        ],
    ],
    // ...
]

现在我们可以将此配置引入config/web.php文件:

'components' => [
    // ...
    'assetManager' => [
        'bundles' => require(__DIR__ . '/assets-prod.php'),
    ],
],

或者,我们只能为生产环境引入文件:

'components' => [
    // ...
    'assetManager' => [
        'bundles' => YII_ENV_PROD ? require(__DIR__ . '/assets-prod.php') : [],
    ],
],

注意

注意:不要忘记在原始资源任何更新后重新生成所有压缩和合并的文件。

参考以下

在 HHVM 上运行 Yii2

HipHop 虚拟机HHVM)是 Facebook 基于即时编译(JIT)的一个进程虚拟机。HHVM 将 PHP 代码转换为中间的HipHop 字节码HHBC),并将 PHP 代码动态转换为机器码,这将得到优化并本地执行。

准备工作

使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,如官方指南中所述,该指南可在www.yiiframework.com/doc-2.0/guidestart-installation.html找到。

如何操作…

按照以下步骤在 HHVM 上运行 Yii:

  1. 安装 Apache2 或 Nginx 网络服务器。

  2. 按照在 Linux 或 Mac 上安装 HHVM 的指南进行操作,该指南可在docs.hhvm.com/hhvm/installation/introduction找到。例如,在 Ubuntu 上,您必须运行以下命令:

    sudo apt-get install software-properties-common
    sudo apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0x5a16e7281be7a449
    sudo add-apt-repository "deb http://dl.hhvm.com/ubuntu $(lsb_release -sc) main"
    sudo apt-get update
    sudo apt-get install hhvm
    After installing, you will see the following tips in your terminal:
    ********************************************************************
    * HHVM is installed.
    *
    * Running PHP web scripts with HHVM is done by having your
    * webserver talk to HHVM over FastCGI. Install nginx or Apache,
    * and then:
    * $ sudo /usr/share/hhvm/install_fastcgi.sh
    * $ sudo /etc/init.d/hhvm restart
    * (if using nginx)  $ sudo /etc/init.d/nginx restart
    * (if using apache) $ sudo /etc/init.d/apache restart
    *
    * Detailed FastCGI directions are online at:
    * https://github.com/facebook/hhvm/wiki/FastCGI
    *
    * If you're using HHVM to run web scripts, you probably want it
    * to start at boot:
    * $ sudo update-rc.d hhvm defaults
    *
    * Running command-line scripts with HHVM requires no special setup:
    * $ hhvm whatever.php
    *
    * You can use HHVM for /usr/bin/php even if you have php-cli
    * installed:
    * $ sudo /usr/bin/update-alternatives \
    *    --install /usr/bin/php php /usr/bin/hhvm 60
    ********************************************************************
    
    
  3. 尝试手动为您网站启动内置服务器:

    cd web
    hhvm -m server -p 8080
    
    

    在您的浏览器中打开localhost:8080主机:

    如何操作…

    目前您可以使用 HHVM 开发您的项目。

  4. 如果您使用 Nginx 或 Apache2 服务器,那么 HHVM 会自动在 /etc/nginx/etc/apache2 目录中创建自己的配置文件。在 Nginx 的情况下,它创建 /etc/nginx/hhvm.conf 模板以将配置文件包含到您的项目中。例如,让我们创建一个名为 yii-book-hhvm.app 的新虚拟主机:

    server {
        listen 127.0.0.1:80;
        server_name .yii-book-hhvm.app;
        root /var/www/yii-book-hhvm.app/web;
        charset utf-8; 
        index index.php index.html index.htm;    
        include /etc/nginx/hhvm.conf;
    }
    

    将主机名添加到您的 /etc/hosts 文件中:

    127.0.0.1	yii-book-hhvm.app
    
    

    现在重新启动 Nginx 服务器:

    sudo service nginx restart
    
    

    最后,在浏览器中打开新主机。

    如何操作…

您的服务器已成功设置。

它是如何工作的…

您可以在 fastcgi 模式下使用 HHVM 作为 PHP 进程的替代品。默认情况下,它监听 9000 端口。您可以在 /etc/hhvm/server.ini 文件中更改 fastcgi 进程的默认端口:

hhvm.server.port = 9000

/etc/hhvm/php.ini 文件中配置特定的 PHP 选项。

参见

关于安装 HHVM 的更多信息,请参阅以下网址:

为了了解更多关于 HHVM 使用的信息,请参阅docs.hhvm.com/hhvm/.

第十章 部署

在本章中,我们将介绍以下菜谱:

  • 修改 Yii 目录布局

  • 移动应用程序 webroot

  • 修改高级应用程序模板

  • 将配置部分移动到单独的文件中

  • 使用多个配置来简化部署

  • 实现和执行 cron 作业

  • 维护模式

  • 部署工具

简介

在本章中,我们将介绍一些特别有用的提示,这些提示在应用程序部署期间非常有用;这些提示在团队开发应用程序或只是想要使你的开发环境更舒适时也非常有用。

修改 Yii 目录布局

默认情况下,我们有基本的和高级的 Yii2 应用程序骨架,具有不同的目录结构。但这些结构并非教条,如果需要,我们可以自定义它们。

例如,我们可以将运行时目录从项目中移出。

准备工作

使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,如官方指南中所述www.yiiframework.com/doc-2.0/guidestart-installation.html

如何操作...

修改运行时目录位置

打开config/web.phpconfig/console.php并定义runtimePath参数:

$config = [
    'id' => 'basic',
    'basePath' => dirname(__DIR__),
    'bootstrap' => ['log'],
    'runtimePath' => '/tmp/runtime',
    'components' => [
        // ...
    ],
]

将运行时目录移动到新位置。

修改供应商目录位置

  1. 打开config/web.phpconfig/console.php并定义vendorPath参数:

    $config = [
        'id' => 'basic',
        'basePath' => dirname(__DIR__),
        'bootstrap' => ['log'],
        'vendorPath' => dirname(__DIR__), '/../vendor,
        'components' => [
            // ...
        ],
    ]
    
  2. 将包含composer.jsoncomposer.lock文件的vendor目录移动到新位置。

  3. 打开web/index.phpyii文件,并找到以下行:

    require(__DIR__ . '/../vendor/autoload.php');
    require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
    
  4. 更改包含路径。

修改控制器位置

  1. commands目录重命名为console

  2. app\commands\HelloController的命名空间更改为app\console\HelloController

  3. 打开config/console.php并重新定义controllerNamespace参数:

    $config = [
        'id' => 'basic-console',
        'basePath' => dirname(__DIR__),
        'bootstrap' => ['log'],
        'controllerNamespace' => 'app\console,
        'components' => [
            // ...
        ],
    ]
    

修改视图目录位置

  1. 打开config/web.php并定义viewPath参数:

    $config = [
        'id' => 'basic',
        'basePath' => dirname(__DIR__),
        'bootstrap' => ['log'],
        'viewPath' => '@app/myviews',
        'components' => [
            // ...
        ],
    ]
    
  2. 重命名你的views目录。

它是如何工作的...

yii\base\Application::preInit方法中,我们的应用程序定义了basePathruntimePathvendorPath参数。

默认情况下,这些值指向根应用程序目录、根目录中的runtimevendor路径。

例如,如果你想与同一项目的某些实例共享供应商目录,可以重新定义vendorPath。但请注意包的版本兼容性。

yii\base\Application类扩展了yii\base\Module,其中包含controllerNamespaceviewPath参数。第一个参数允许你更改应用程序和模块的基本命名空间。如果你想在同一个模块目录中提供前端和后端控制器,这将很有帮助。只需将controllers目录更改为前端和后端,或者创建子目录并配置你的前端和后端应用程序:

return [
    'id' => 'app-frontend',
    'basePath' => dirname(__DIR__),
    'controllerNamespace' => frontend\controllers',
    'bootstrap' => ['log'],
    'modules' => [
        'user' => [
            'my\user\Module',
            'controllerNamespace' => 'my\user\controllers\frontend',
        ]
    ],
    // ...
]
return [
    'id' => 'app-backend',
    'basePath' => dirname(__DIR__),
    'controllerNamespace' => 'backend\controllers',
    'bootstrap' => ['log'],
    'modules' => [
        'user' => [
            'my\user\Module',
            'controllerNamespace' => 'my\user\controllers\backend',
        ]
    ],
    // ...
]

参见

为了了解更多关于应用程序结构的信息,请参阅www.yiiframework.com/doc-2.0/guide-structure-applications.html

移动应用程序 webroot

默认情况下,Yii2 应用程序从 web 目录运行你的网站入口脚本。但是,共享主机环境在配置和目录结构方面通常非常有限。你不能更改你网站的运行目录。大多数服务器只为你的网站入口脚本提供 public_html 目录。

准备工作

使用官方指南中描述的 Composer 包管理器创建一个新的 yii2-app-basic 应用程序,官方指南请见www.yiiframework.com/doc-2.0/guidestart-installation.html

如何操作...

让我们讨论移动应用程序 webroot 的方法。

将文件放置在根目录

  1. 将应用程序文件上传到你的主机。

  2. web 目录重命名为 public_html

  3. 确认网站运行正常。

将文件放置在子目录

主机用户目录可能包含其他文件和文件夹。以下是你可以将文件移动到子目录的方法:

  1. 创建 applicationpublic_html 目录。

  2. 将应用程序文件移动到 application 目录。

  3. application/web 目录的内容移动到 public_html

  4. 打开 public_html/index.php 文件并更改包含路径:

    require(__DIR__ . '/../application/vendor/autoload.php');
    require(__DIR__ . '/../application/vendor/yiisoft/yii2/Yii.php');
    

它是如何工作的...

Yii2 应用程序会自动基于入口脚本的位置设置 @web@webroot 别名路径。因此,我们可以轻松地移动或重命名 web 目录而无需更改应用程序配置。

对于 yii2-app-advanced,你可以将 web 目录的内容从 backend 移动到子目录,例如 admin

public_html
    index.php
    ...
    admin
        index.php
        ...       
backend
common
console
frontend
...

参见

要获取在共享主机环境中安装 Yii 的更多信息,请参阅www.yiiframework.com/doc-2.0/guide-tutorial-shared-hosting.html

修改高级应用程序模板

默认情况下,Yii2 的高级模板包含 consolefrontendbackend 应用程序。然而,在你的特定情况下,你可以重命名现有的应用程序并创建自己的应用程序。例如,如果你为你的网站开发 API,你可以添加 api 应用程序。

准备工作

使用官方指南中描述的 Composer 包管理器创建一个新的 yii2-app-advanced 项目,官方指南请见github.com/yiisoft/yii2-app-advanced/blob/master/docs/guide/start-installation.md

如何操作...

  1. backend 目录的内容复制到应用程序根目录下的新 api 目录。

  2. 打开 api/config/main.php 文件并更改 controllerNamespace 选项的值:

    return [
        'id' => 'app-manager',
        'basePath' => dirname(__DIR__),
        'controllerNamespace' => 
        'api\controllers',
        // ....
    ]
    
  3. 打开api/assets/AppAsset.phpapi/controllers/SiteController.php,并将命名空间从backend更改为api,如下所示:

    namespaces api\assets;
    namespaces api\controllers;
    
  4. 打开api/views/layouts/main.php文件,找到以下行:

    use backend\assets\AppAsset;
    

    更改为这个:

    use api\assets\AppAsset;
    
  5. 打开common/config/bootstrap.php并为新应用程序添加@api别名:

    <?php
    Yii::setAlias('@common', dirname(__DIR__));
    Yii::setAlias('@frontend', dirname(dirname(__DIR__)) . '/frontend');
    Yii::setAlias('@backend', dirname(dirname(__DIR__)) . '/backend');
    Yii::setAlias('@console', dirname(dirname(__DIR__)) . '/console');
    Yii::setAlias('@api', dirname(dirname(__DIR__)) . '/api);
    
  6. 打开environments目录,并在devprod子目录中将api目录复制为backend

  7. 打开environments/index.php文件并为api应用程序添加行:

    return [
        'Development' => [
            'path' => 'dev',
            'setWritable' => [
                'backend/runtime',
                'backend/web/assets',
                'frontend/runtime',
                'frontend/web/assets',
                'api/runtime',
                'api/web/assets',
            ],
            'setExecutable' => [
                'yii',
                'tests/codeception/bin/yii',
            ],
            'setCookieValidationKey' => [
                'backend/config/main-local.php',
                'frontend/config/main-local.php',
                'api/config/main-local.php',
            ],
        ],
        'Production' => [
            'path' => 'prod',
            'setWritable' => [
                'backend/runtime',
                'backend/web/assets',
                'frontend/runtime',
                'frontend/web/assets',
                'api/runtime',
                'api/web/assets',
            ],
            'setExecutable' => [
                'yii',
            ],
            'setCookieValidationKey' => [
                'backend/config/main-local.php',
                'frontend/config/main-local.php',
                'api/config/main-local.php',
            ],
        ],
    ];
    

现在您有了consolefrontendbackendapi应用程序。

它是如何工作的...

高级应用程序模板是一组具有自定义别名的应用程序,例如@frontend@backend@common@console,以及相应的命名空间,而不是Basic模板中的简单@app别名。

如果需要,您可以轻松地添加、删除或重命名这些应用程序(包括它们的别名和命名空间)。

参见

若想了解更多关于应用程序目录结构的使用信息,请参阅github.com/yiisoft/yii2-app-advanced/tree/master/docs/guide

将配置部分移动到单独的文件中

在基本应用程序模板中,我们已经将 Web 和控制台配置文件分开。通常,我们在两个配置文件中设置一些应用程序组件。

此外,当我们开发大型应用程序时,可能会遇到一些不便。例如,如果我们需要调整一些设置,我们很可能会在 Web 应用程序配置和控制台应用程序配置中重复更改。

准备工作

通过使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,如官方指南中所述www.yiiframework.com/doc-2.0/guidestart-installation.html

如何操作...

  1. 打开config/web.php文件并在组件配置中添加urlManager部分:

    'components' => [
        // ...
        'db' => require(__DIR__ . '/db.php'),
        'urlManager' => [
            'class' => 'yii\web\UrlManager',
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'rules' => [
                '' => 'site/index',
                '<_c:[\w\-]+>/<id:\d+>' => '<_c>/view',
                '<_c:[\w\-]+/<_a:[\w\-]+>>/<id:\d+>' => '<_c>/<_a>',
                '<_c:[\w\-]+>' => 
                '<_c>/index',
            ],
        ],
    ],
    
  2. 创建config/urlRules.php文件并将规则数组移动到其中:

    <?php
    return [
        '' => 'site/index',
        '<_c:[\w\-]+>/<id:\d+>' => '<_c>/view',
        '<_c:[\w\-]+/<_a:[\w\-]+>>/<id:\d+>' => '<_c>/<_a>',
        '<_c:[\w\-]+>' => '<_c>/index',
    ];
    
  3. 将规则数组替换为需要此文件的文件:

    'urlManager' => [
        'class' => 'yii\web\UrlManager',
        'enablePrettyUrl' => true,
        'showScriptName' => false,
        'rules' => require(__DIR__ .  '/urlRules.php'),
    ],
    

它是如何工作的...

上述技术依赖于 Yii 配置文件是本地的 PHP 文件,包含数组:

<?php
return [...];

让我们看看require结构:

'rules' => require(__DIR__ . '/urlRules.php'),

当我们使用这个时,它会读取指定的文件,如果这个文件中有return语句,它会返回一个值。

因此,将一部分从主配置文件移出并放入单独的文件中需要创建一个单独的文件,将配置部分移动到return语句之后,并在主配置文件中使用require

如果单独的应用程序(在我们的例子中,这些是 Web 应用程序和控制台应用程序)需要一些共同的配置部分,那么我们可以使用require将它们移动到一个单独的文件中。

参见

为了了解更多关于 PHP requireinclude语句的信息,请参阅以下 URL:

使用多个配置简化部署

高级应用程序模板为每个应用程序使用不同的配置文件:

common
    config
        main.php
        main-local.php
        params.php
        params-local.php
console
    config
        main.php
        main-local.php
        params.php
        params-local.php
backend
    config
        main.php
        main-local.php
        params.php
        params-local.php
frontend
    config
        main.php
        main-local.php
        params.php
        params-local.php

每个web/index.php脚本合并自己的配置文件集:

$config = yii\helpers\ArrayHelper::merge(
    require(__DIR__ . '/../../common/config/main.php'),
    require(__DIR__ . '/../../common/config/main-local.php'),
    require(__DIR__ . '/../config/main.php'),
    require(__DIR__ . '/../config/main-local.php')
);
$application = new yii\web\Application($config);
$application->run();

每个config/main.php文件合并参数:

<?php
$params = array_merge(
    require(__DIR__ . '/../../common/config/params.php'),
    require(__DIR__ . '/../../common/config/params-local.php'),
    require(__DIR__ . '/params.php'),
    require(__DIR__ . '/params-local.php')
);
return [
    // ... 
    'params' => $params,
];

这个系统允许你配置我们应用程序的公共和特定属性以及组件。我们可以在版本控制系统上存储默认配置文件,并忽略所有*-local.php文件。

所有本地文件模板都在environments目录中准备。当你通过控制台运行php init并选择一个针环境时,这个初始化脚本会复制相应的文件并将它们放置到目标文件夹中。

但基本应用程序模板不包含敏捷配置系统,只提供了以下文件:

config
    console.php
    web.php
    db.php
    params.php

让我们尝试向yii2-app -basic应用程序模板添加一个高级配置系统。

准备工作

按照官方指南使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,如www.yiiframework.com/doc-2.0/guidestart-installation.html中所述。

如何做到这一点...

  1. 创建config/common.php文件:

    <?php
    $params = array_merge(
        require(__DIR__ . '/params.php'),
        require(__DIR__ . '/params-local.php')
    );
    return [
        'basePath' => dirname(__DIR__),
        'components' => [
            'cache' => [
                'class' => 'yii\caching\FileCache',
            ],
            'mailer' => [
                'class' => 'yii\swiftmailer\Mailer',
            ],
            'db' => [],
        ],
        'params' => $params,
    ];
    
  2. 创建config/common-local文件:

    <?php
    return [
        'components' => [
            'db' => [
                'class' => 'yii\db\Connection',
                'dsn' => 'mysql:host=localhost;dbname=yii2basic',
                'username' => 'root',
                'password' => '',
                'charset' => 'utf8',
            ],
            'mailer' => [
                'useFileTransport' => true,
            ],
        ],
    ];
    
  3. 移除config/db.php文件。

  4. config/console.php中移除重复的代码:

    <?php
    Yii::setAlias('@tests', dirname(__DIR__) . '/tests');
    return [
        'id' => 'basic-console',
        'bootstrap' => ['log', 'gii'],
        'controllerNamespace' => 'app\commands',
        'modules' => [
            'gii' => 'yii\gii\Module',
        ],
        'components' => [
            'log' => [
                'targets' => [
                    [
                        'class' => 'yii\log\FileTarget',
                        'levels' => ['error', 'warning'],
                    ],
                ],
            ],
        ],
    ];
    
  5. 创建一个空的config/console-local.php文件:

    <?php
    return [
    ];
    
  6. 修改config/web.php文件:

    $config = [
        'id' => 'basic',
        'bootstrap' => ['log'],
        'components' => [
            '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'],
                    ],
                ],
            ],
        ],
    ];
    if (YII_ENV_DEV) {
        // configuration adjustments for 'dev' environment
        $config['bootstrap'][] = 'debug';
        $config['modules']['debug'] = 'yii\debug\Module';
    
        $config['bootstrap'][] = 'gii';
        $config['modules']['gii'] = 'yii\gii\Module';
    }
    return $config;
    
  7. request配置移动到config/web-local.php

    <?php
    return [
        'components' => [
            'request' => [
                'cookieValidationKey' => 'TRk9G1La5kvLFwqMEQTp6PmC1NHdjtkq',
            ],
        ],
    ];
    
  8. config/params.php中移除电子邮件 ID:

    <?php
    return [
        'adminEmail' => '',
    ];
    
  9. 将 ID 粘贴到config/params-local.php

    <?php
    return [
        'adminEmail' => 'admin@example.com',
    ];  
    
  10. tests/codeception/config/config.php中移除dsn字符串:

    <?php
    /**
     * Application configuration shared by all test types
     */
    return [
        'controllerMap' => [
            // ...
        ],
        'components' => [
            'db' => [
                'dsn' => '',
            ],
            'mailer' => [
                'useFileTransport' => true,
            ],
            'urlManager' => [
                'showScriptName' => true,
            ],
        ],
    ];
    
  11. 将字符串放入新的tests/codeception/config/config-local.php文件:

    <?php
    return [
        'components' => [
            'db' => [
                'dsn' => 'mysql:host=localhost;dbname=yii2_basic_tests',
            ],
        ],
    ];
    
  12. 将配置合并添加到web/index.php文件:

    $config = yii\helpers\ArrayHelper::merge(
        require(__DIR__ . '/../config/common.php'),
        require(__DIR__ . '/../config/common-local.php'),
        require(__DIR__ . '/../config/web.php'),
        require(__DIR__ . '/../config/web-local.php')
    );
    
  13. 将配置合并添加到控制台入口脚本yii

    $config = yii\helpers\ArrayHelper::merge(
        require(__DIR__ . '/config/common.php'),
        require(__DIR__ . '/config/common-local.php'),
        require(__DIR__ . '/config/console.php'),
        require(__DIR__ . '/config/console-local.php')
    );
    
  14. 将配置合并添加到单元测试、功能测试和验收测试的测试配置中,从tests/codeception/config

    return yii\helpers\ArrayHelper::merge(
        require(__DIR__ . '/../../../config/common.php'),
        require(__DIR__ . '/../../../config/common-local.php'),
        require(__DIR__ . '/../../../config/web.php'),
        require(__DIR__ . '/../../../config/web-local.php'),
        require(__DIR__ . '/config.php'),
        require(__DIR__ . '/config-local.php'),
        [
            // ...
        ]
    );
    
  15. 将配置合并添加到测试环境控制台的入口脚本tests/codeception/bin/yii

    $config = yii\helpers\ArrayHelper::merge(
        require(YII_APP_BASE_PATH . '/config/common.php'),
        require(YII_APP_BASE_PATH . '/config/common-local.php'),
        require(YII_APP_BASE_PATH . '/config/console.php'),
        require(YII_APP_BASE_PATH . '/config/console-local.php'),
        require(__DIR__ . '/../config/config.php'),
        require(__DIR__ . '/../config/config-local.php')
    );
    
  16. 结果,你必须在你的配置目录中得到以下内容:

    config
        common.php
        common-local.php
        console.php
        console-local.php
        web.php
        web-local.php
        params.php
        params-local.php
    
  17. 最后,你可以在configtests/codeception/config目录中添加一个包含此内容的新的.gitignore文件,这样你就可以通过 Git 版本控制系统忽略本地配置文件。

    /*-local.php
    

它是如何工作的...

你可以在config/common.php文件中存储公共应用程序组件配置,并为 Web 和命令行应用程序设置特定配置。你可以将你的临时和安全的配置数据放入*-local.php文件中。

此外,你也可以从yii2-app-advanced复制初始化 shell 脚本。

  1. 创建一个新的environments目录并将你的模板复制进去:

    environments
        dev
            config
                common-local.php
                console-local.php
                web-local.php
                params-local.php
            web
                index.php
                index-test.php
            tests
                codeception
                    config
                        config.php
                        config-local.php
            yii
        prod
            config
                common-local.php
                console-local.php
                web-local.php
                params-local.php
            web
                index.php
            yii
    
  2. 使用以下代码创建environments/index.php文件:

    <?php
    return [
        'Development' => [
            'path' => 'dev',
            'setWritable' => [
                'runtime',
                'web/assets',
            ],
            'setExecutable' => [
                'yii',
                'tests/codeception/bin/yii',
            ],
            'setCookieValidationKey' => [
                'config/web-local.php',
            ],
        ],
        'Production' => [
            'path' => 'prod',
            'setWritable' => [
                'runtime',
                'web/assets',
            ],
            'setExecutable' => [
                'yii',
            ],
            'setCookieValidationKey' => [
                'config/web-local.php',
            ],
        ],
    ];
    
  3. 从你的composer.json中移除默认的Installer::postCreateProject配置:

    "extra": {
        "asset-installer-paths": {
            "npm-asset-library": "vendor/npm",
            "bower-asset-library": "vendor/bower"
        }
    }
    
  4. 从高级模板github.com/yiisoft/yii2-app-advanced复制initinit.bat脚本,并在从仓库克隆项目后使用命令php init运行初始化过程。

参见

关于应用程序配置的更多信息,请参阅www.yiiframework.com/doc-2.0/guide-concept-configurations.html

实现和执行 cron 作业

有时,应用程序需要一些后台任务,例如重新生成网站地图或刷新统计数据。实现这一点的常见方法是使用 cron 作业。当使用 Yii 时,有一种方法可以使用命令作为作业运行。

在这个菜谱中,我们将看到如何实现这两个。对于我们的菜谱,我们将实现将当前时间戳写入受保护目录下的t imestamp.txt文件。

准备工作

使用 Composer 创建一个新的yii2-app-basic应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guide-startinstallation.html

如何做...

运行 Hello 命令

让我们尝试将app\commands\HelloController::actionIndex作为一个 shell 命令来运行:

<?php
namespace app\commands;
use yii\console\Controller;

/**
 * This command echoes the first argument that you have entered.
 */
class HelloController extends Controller
{
    /**
    * This command echoes what you have entered as the message.
    * @param string $message the message to be echoed.
    */
    public function actionIndex($message = 'hello world')
    {
        echo $message . "\n";
    }
}
  1. 在你的应用程序目录中打开 shell 并执行此命令:

    php yii
    
    

    或者,你也可以调用以下内容并确保 shell 工作正常:

    ./yii
    
    
  2. 输入以下命令以显示hello

    ./yii help hello
    
    
  3. 框架必须显示一些信息:

    DESCRIPTION
    This command echoes what you have entered as the message.
    
    USAGE
    yii hello [message] [...options...]
    - message: string (defaults to 'hello world')
     the message to be echoed.
    
    
  4. 运行默认命令动作:

    ./yii hello
    
    

    或者,运行具体的index动作:

    ./yii hello/index
    
    
  5. 你现在应该看到默认短语:

    Hello world
    
  6. 使用任何参数运行命令并查看响应:

    ./yii hello 'Bond, James Bond'
    
    

创建自己的命令

你也可以创建自己的控制台控制器。例如,创建一个commands/CronController.php文件,包含以下示例代码:

<?php
namespace app\commands;

use yii\console\Controller;
use yii\helpers\Console;
use Yii;

/**
* Console crontab actions
*/
class CronController extends Controller
{
    /**
    * Regenerates timestamp
    */
    public function actionTimestamp()
    {
        file_put_contents(Yii::getAlias('@app/timestamp.txt'), time());
        $this->stdout('Done!', Console::FG_GREEN, Console::BOLD);
        $this->stdout(PHP_EOL);
    }
}

一切完成后,在 shell 中运行命令:

./yii cron/timestamp

然后,检查响应文本和新的文件是否存在,即timestamp.txt

设置 cron 计划

在你的 Linux 服务器上创建/etc/cron.d/myapp,并添加以下行以在午夜运行我们的命令:

0 0 
* * * www-data /path/to/yii cron/timestamp >/dev/null

它是如何工作的...

控制台命令被定义为扩展自yii\console\Controller的控制器类。在控制器类中,你定义一个或多个动作,这些动作对应于控制器的子命令。在每个动作中,你编写代码以实现特定子命令的适当任务。

当运行一个命令时,您需要指定控制器操作的路径。例如,路径 migrate/create 调用与 MigrateController::actionCreate() 操作方法相对应的子命令。如果在执行过程中提供的路由不包含操作 ID,则将执行默认操作(类似于网络控制器)。

请注意,您的控制台控制器应放置在 web/console.php 配置中定义的 c ontrollerNamespace 选项指定的目录中。

参见

维护模式

有时,需要微调一些应用程序设置或从备份中恢复数据库。在处理此类任务时,不希望允许每个人使用应用程序,因为这可能导致丢失最近的用户消息或显示应用程序实现细节。

在这个菜谱中,我们将看到如何向除开发者外的人显示维护消息。

准备工作

通过使用官方指南中描述的 Composer 软件包管理器创建一个新的 yii2-app-basic 应用程序,如 www.yiiframework.com/doc-2.0/guidestart-installation.html 所述。

如何操作...

执行以下步骤:

  1. 首先,我们需要创建 protected/controllers/MaintenanceController.php。我们这样做如下:

    class MaintenanceController extends Controller
    {
        public function actionIndex()
        {
            $this->renderPartial("index");
        }
    }
    
  2. 然后,我们创建一个名为 views/maintenance/index.php 的视图,如下所示:

    <?php
    use yii\helpers\Html;
    ?>
    <!doctype html>
    <head>
        <meta charset="utf-8" />
        <title><?php echo 
        Html::encode(Yii::$app->name)?>is under maintenance</title>
    </head>
    <body>
        <h1><?php echo CHtml::encode(Yii::$app->name)?>is under maintenance</h1>
        <p>We'll be back soon. If we aren't back for too long,please drop a message to <?php echo Yii::$app->params['adminEmail']?>.</p>
        <p>Meanwhile, it's a good time to get a cup of coffee,to read a book or to check email.</p>
    </body>
    
  3. 现在我们需要在 config/web.php 中添加一行代码,如下所示:

    $config = [
        'catchAll' => file_exists(dirname(__DIR__) .'/.maintenance') 
        && !(isset($_COOKIE['secret']) && $_COOKIE['secret']=="password") ?
        ['maintenance/index'] : null,
        // …
    ]
    
  4. 现在为了进入维护模式,您需要在您的站点目录中创建一个名为 .maintenance 的文件。完成此操作后,您应该看到这个页面。

为了恢复正常,您只需将其删除。要查看维护模式下的网站,您可以在您的站点目录中创建一个名为 secret 的 cookie,其值等于 password

它是如何工作的...

Yii 网络应用程序提供了一种拦截所有可能的请求并将这些请求路由到单个控制器操作的方法。您可以通过将 yii\web\Application::catchAll 设置为包含应用程序路由的数组来实现这一点,如下所示:

'catchAll' => ['maintenance/index'],

维护控制器本身并没有什么特别之处;它只是渲染一个包含一些文本的视图。

我们需要一个简单的方法来开启和关闭维护模式。由于应用程序配置是一个常规的 PHP 文件,我们可以通过简单地检查文件是否存在来实现,如下所示:

file_exists(dirname(__DIR__) . '/.maintenance')

此外,我们检查 cookie 值以能够覆盖维护模式。我们这样做如下:

!
(isset($_COOKIE['secret']) && $_COOKIE['secret']=="password")

参见

为了了解如何在 Yii 应用程序中捕获所有请求以及检查生产就绪解决方案的维护,请参阅www.yiiframework.com/doc-2.0/yii-web-application.html#$catchAll-detail

部署工具

如果你使用 Git 等版本控制系统来管理项目代码并将发布版本推送到远程仓库,你可以通过 git pull 命令将代码部署到生产服务器,而不是手动上传文件。此外,你可以编写自己的 shell 脚本来拉取新的仓库提交、更新供应商、应用迁移以及执行更多操作。

然而,有许多工具可用于自动化部署过程。在本配方中,我们考虑名为 Deployer 的工具。

准备工作

通过使用官方指南中描述的 Composer 包管理器创建一个新的 yii2-app-basic 应用程序,官方指南请参阅www.yiiframework.com/doc-2.0/guidestart-installation.html

如何操作...

如果你有一个共享的远程仓库,你可以将其用作部署源。

第 1 步 - 准备远程主机

  1. 前往你的远程主机并安装 Composer 和 asset-plugin

    global require 'fxp/composer-asset-plugin:~1.1.1'
    
    
  2. 通过 ssh-keygen 生成 SSH 密钥。

  3. ~/.ssh/id_rsa.pub 文件内容添加到 GitHub、Bitbucket 或其他存储库的仓库设置中的部署 SSH 密钥页面。

  4. 尝试手动克隆你的仓库:

    git clone git@github.com:user/repo.git
    
    
  5. 如果系统要求你这样做,添加 Github 地址和已知主机列表。

第 2 步 - 准备本地主机

  1. 在本地主机上全局安装 deploy.phar

    sudo wget http://deployer.org/deployer.phar
    sudo mv deployer.phar /usr/local/bin/dep
    sudo chmod +x /usr/local/bin/dep
    
  2. 添加带有部署配置的 deploy.php 文件:

    <?php
    require 'recipe/yii2-app-basic.php';
    
    set('shared_files', [
        'config/db.php',
        'config/params.php',
        'web/index.php',
        'yii',
    ]);
    
    server('prod', 'site.com', 22) // SSH access to remote server
        ->user('user')
        // ->password(password) // uncomment for authentication by password
        // ->identityFile()               // uncomment for authentication by SSH key
        ->stage('production')
        ->env('deploy_path', '/var/www/project');
    
    set('repository', 'git@github.com:user/repo.git');
    
  3. 尝试准备远程项目目录结构:

    dep deploy:prepare prod
    
    

第 3 步 - 添加远程配置

  1. 打开服务器的 /var/www/project 目录。初始化后,它有两个子目录:

    project
    ├── releases
    └── shared
    
  2. shared 目录中创建具有私有配置的原始文件,如下所示:

    project
    ├── releases
    └── shared
            ├── config
            │       ├── db.php
            │       └── params.php
            ├── web
            │       └── index.php
            └── yii
    

Deployer 工具将通过符号链接将这些文件包含在每个发布子目录中。

share/config/db.php 中指定你的私有配置:

<?php
return [
    'class' => 'yii\db\Connection',
    dsn' => 'mysql:host=localhost;dbname=catalog',
    'username' => 'root',
    'password' => 'root',
    'charset' => 'utf8',
];

还要在 share/config/params.php 中指定它:

<?php
return [
    'adminEmail' => 'admin@example.com',
];

设置 share/web/index.php 的内容:

<?php
defined('YII_DEBUG') or define('YII_DEBUG', false);
defined('YII_ENV') or define('YII_ENV', 'prod');

$dir = dirname($_SERVER['SCRIPT_FILENAME']);

require($dir . '/../vendor/autoload.php');
require($dir . '/../vendor/yiisoft/yii2/Yii.php');

$config = require($dir . '/../config/web.php');

(new yii\web\Application($config))->run();

还要设置 share/yii 文件的内容:

#!/usr/bin/env php
<?php
defined('YII_DEBUG') or define('YII_DEBUG', false);
defined('YII_ENV') or define('YII_ENV', 'prod');

$dir = dirname($_SERVER['SCRIPT_FILENAME']);

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);

注意

注意:我们故意使用 dirname($_SERVER['SCRIPT_FILENAME']) 代码而不是原始的 __DIR__ 常量,因为当文件通过符号链接包含时,__DIR__ 将返回不正确的值。

注意:如果你使用 yii2-app-advanced 模板,你只能重新声明每个(后端、前端、控制台和公共)的 config/main-local.phpconfig/params-local.php 文件,因为 web/index.phpyii 文件将由 init 命令自动创建。

第 4 步 - 尝试部署

  1. 使用 deploy.php 文件回到本地主机并运行部署命令:

    dep deploy prod
    
    
  2. 如果成功,您将看到部署报告:步骤 4 - 尝试部署

  3. Deployer 在您的远程服务器上创建了一个新的发布子目录,并从您的项目到共享项目以及从 current 目录到当前发布添加了符号链接:

    project
    ├── current -> releases/20160412140556
    ├── releases
    │    └── 20160412140556
    │        ├── ...
    │        ├── runtime -> /../../shared/runtime
    │        ├── web
    │        ├── vendor
    │        ├── ...
    │        └── yii -> /../../shared/yii
    └── shared
       ├── config
       │   ├── db.php
       │   └── params.php
       ├── runtime
       ├── web
       │   └── index.php
       └── yii
    
  4. 所有工作完成后,您必须在 project/current/web 目录中设置服务器的 DocumentRoot

  5. 如果在部署过程中出现问题,您可以回滚到先前的有效发布:

    dep rollback prod
    
    

    current 目录将指向您的前一个发布文件。

它是如何工作的...

大多数部署工具都执行相同的任务:

  • 创建新的发布子目录

  • 克隆仓库文件

  • 从项目到共享目录以及到本地配置文件的符号链接

  • 安装 Composer 包

  • 应用项目迁移

  • 将符号链接从服务器的 DocumentRoot 路径切换到当前发布目录

Deployer 工具为流行的框架预定义了食谱。您可以为任何现有食谱扩展或为特定情况编写一个新的食谱。

参见

第十一章。测试

在本章中,我们将介绍以下主题:

  • 使用 Codeception 测试应用程序

  • 使用 PHPUnit 进行单元测试

  • 使用 Atoum 进行单元测试

  • 使用 Behat 进行单元测试

简介

在本章中,您将学习如何使用最佳的测试技术,如 Codeception、PhpUnit、Atoum 和 Behat。您将了解如何编写简单的测试以及如何避免应用程序中的回归错误。

使用 Codeception 测试应用程序

默认情况下,基本和高级 Yii2 应用程序骨架使用 Codeception 作为测试框架。Codeception 支持开箱即用的单元、功能和验收测试。对于单元测试,它使用 PHPUnit 测试框架,这将在下一个菜谱中介绍。

准备中

  1. 使用 Composer 包管理器创建一个新的 yii2-app-basic 应用程序,具体操作请参考官方指南 www.yiiframework.com/doc-2.0/guide-start-installation.html

    注意

    注意:如果您使用的是基本应用程序的 2.0.9 版本(或更早版本),只需手动升级 tests 目录,并添加 config/test.phpconfig/test_db.phpweb/index-test.php 文件。此外,您必须复制 composer.json 文件的 requirerequire-dev 部分,并运行 composer update

  2. 创建并应用以下迁移:

    <?php
    use yii\db\Migration;
    
    class m160309_070856_create_post extends Migration
    {
        public function up()
        {
            $this->createTable('{{%post}}', [
                'id' => $this->primaryKey(),
                'title' => $this->string()->notNull(),
                'text' => $this->text()->notNull(),
                'status' => $this->smallInteger()->notNull()-
                >defaultValue(0),
            ]);
        }
    
        public function down()
        {
            $this->dropTable('{{%post}}');
        }
    }
    
  3. 创建 Post 模型:

    namespace app\models;
    
    use Yii;
    use yii\db\ActiveRecord;
    
    /**
     * @property integer $id
     * @property string $title
     * @property string $text
     * @property integer $status
     * @property integer $created_at
     * @property integer $updated_at
     */
    class Post extends ActiveRecord
    {
        const STATUS_DRAFT = 0;
        const STATUS_ACTIVE = 1;
    
        public static function tableName()
        {
            return '{{%post}}';
        }
    
        public function rules()
        {
            return [
                [['title', 'text'], 'required'],
                [['text'], 'string'],
                ['status', 'in', 'range' => [self::STATUS_DRAFT, self::STATUS_ACTIVE]],
                ['status', 'default', 'value' => self::STATUS_DRAFT],
                [['title'], 'string', 'max' => 255],
            ];
        }
    
        public function behaviors()
        {
            return [
                TimestampBehavior::className(),
            ];
        }
    
        public static function getStatusList()
        {
            return [
                self::STATUS_DRAFT => 'Draft',
                self::STATUS_ACTIVE => 'Active',
            ];
        }
        public function publish()
        {
            if ($this->status == self::STATUS_ACTIVE) {
                throw new \DomainException('Post is already published.');
            }
            $this->status = self::STATUS_ACTIVE;
        }
    
        public function draft()
        {
            if ($this->status == self::STATUS_DRAFT) {
                throw new \DomainException('Post is already drafted.');
            }
            $this->status = self::STATUS_DRAFT;
        }
    }
    
  4. 生成 CRUD:准备中

  5. 此外,在 views/admin/posts/_form.php 中为 status 字段添加状态下拉列表,并为提交按钮命名:

    <div class="post-form">
    
        <?php $form = ActiveForm::begin(); ?>
    
        <?= $form->field($model, 'title')->textInput(['maxlength' => true])  ?>
    
        <?= $form->field($model, 'text')->textarea(['rows' => 6]) ?>
    
        <?= $form->field($model, 'status')->dropDownList(Post::getStatusList()) ?>
    
        <div class="form-group">
            <?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update', [
                'class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary',
                'name' => 'submit-button',
            ]) ?>
        </div>
    
        <?php ActiveForm::end(); ?>
    
    </div>
    
  6. 现在检查控制器是否正常工作:准备中

创建任何演示帖子。

如何做到这一点...

准备测试

按照以下步骤准备测试:

  1. 创建 yii2_basic_tests 或其他测试数据库,并通过应用迁移来更新它:

    tests/bin/yii migrate
    
    

    需要在测试目录中运行此命令。您可以在配置文件 /config/test_db.php 中指定您的测试数据库选项。

  2. Codeception 使用自动生成的 Actor 类为自身的测试套件。使用以下命令构建它们:

    composer exec codecep
    t build
    
    

运行单元和功能测试

我们现在可以运行应用程序的任何类型的测试:

# run all available tests
composer exec codecept run

# run functional tests
composer exec codecept run functional

# run unit tests
composer exec codecept run unit

结果,您可以查看如下测试报告:

运行单元和功能测试

获取覆盖率报告

您可以为您的代码获取代码覆盖率报告。默认情况下,代码覆盖率在 tests/codeception.yml 配置文件中是禁用的;您应该取消注释必要的行以便能够收集代码覆盖率:

coverage:
   enabled: true
   whitelist:
       include:
           - models/*
           - controllers/*
           - commands/*
           - mail/*
   blacklist:
       include:
           - assets/*
           - config/*
           - runtime/*
           - vendor/*
           - views/*
           - web/*
           - tests/*

您必须从 xdebug.org 安装 XDebug PHP 扩展。例如,在 Ubuntu 或 Debian 上,您可以在终端中输入以下命令:

sudo apt-get install php5-xdebug

在 Windows 上,您必须打开 php.ini 文件,并添加带有您 PHP 安装目录路径的自定义代码:

[xdebug]
zend_extension_ts=C:/php/ext/php_xdebug.dll

或者,如果您使用的是非线程安全版本,请输入以下命令:

[xdebug]
zend_extension=C:/php/ext/php_xdebug.dll

最后,您可以使用以下命令运行测试并收集覆盖率报告:

#collect coverage for all tests
composer exec codecept run --coverage-html

#collect coverage only for unit tests
composer exec codecept run unit --coverage-html

#collect coverage for unit and functional tests
composer exec codecept run functional,unit --coverage-html

你可以在终端中看到文本代码覆盖率输出:

Code Coverage Report: 
 2016-03-31 08:13:05 

 Summary: 
 Classes: 20.00% (1/5) 
 Methods: 40.91% (9/22) 
 Lines:   30.65% (38/124)

\app\models::ContactForm
 Methods:  33.33% ( 1/ 3)   Lines:  80.00% ( 12/ 15)
\app\models::LoginForm
 Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 18/ 18)
\app\models::User
 Methods:  57.14% ( 4/ 7)   Lines:  53.33% (  8/ 15)
Remote CodeCoverage reports are not printed to console

HTML report generated in coverage

你还可以在tests/codeception/_output/coverage目录下看到 HTML 报告:

获取覆盖率报告

你可以点击任何类并分析在测试过程中哪些代码行没有被执行。

运行验收测试

在验收测试中,你可以使用 PhpBrowser 通过 Curl 请求服务器。这有助于检查你的网站控制器并解析 HTTP 和 HTML 响应代码。但如果你想测试 CSS 或 JavaScript 行为,你必须使用真实浏览器。

Selenium Server 是一个交互式工具,它可以集成到 Firefox 和其他浏览器中,允许打开网站页面并模拟人类行为。

为了与真实浏览器协同工作,我们必须安装 Selenium Server:

  1. 使用完整的 Codeception 包而不是基本包:

    composer require --dev codeception/codeception
    composer remove --dev codeception/base
    
    
  2. 下载以下软件:

  3. 在新的终端窗口中启动带有驱动程序的服务器:

    java -jar -Dwebdriver.gecko.driver=~/geckodriver ~/selenium-server-standalone-x.xx.x.jar
    
    
  4. tests/acceptance.suite.yml.example复制到tests/acceptance.suite.yml文件,并配置如下:

    class_name: AcceptanceTester
    modules:
     enabled:
     - WebDriver:
     url: http://127.0.0.1:8080/
     browser: firefox
     - Yii2:
     part: orm
     entryScript: index-test.php
     cleanup: false
    
    
  5. 打开新的终端窗口并启动 Web 服务器:

    tests/bin/yii serve
    
    
  6. 运行验收测试:

    composer exec codecept run acceptance
    
    

你应该看到 Selenium 如何启动浏览器并检查所有网站页面。

创建数据库固定数据

在运行自己的测试之前,我们必须清除自己的测试数据库并将特定的测试数据加载到其中。yii2-codeception扩展提供了ActiveFixture基类,用于为自己的模型创建测试数据集。按照以下步骤创建数据库固定数据:

  1. Post模型创建固定数据类:

    <?php
    namespace tests\fixtures;
    
    use yii\test\ActiveFixture;
    
    class PostFixture extends ActiveFixture
    {
        public $modelClass = 'app\modules\Post';
        public $dataFile = '@tests/_data/post.php';
    }
    
  2. test/_data/post.php文件中添加演示数据集:

    <?php
    return [
        [
            'id' => 1,
            'title' => 'First Post',
            'text' => 'First Post Text',
            'status' => 1,
            'created_at' => 1457211600,
            'updated_at' => 1457211600,
        ],
        [
            'id' => 2,
            'title' => 'Old Title For Updating',
            'text' => 'Old Text For Updating',
            'status' => 1,
            'created_at' => 1457211600,
            'updated_at' => 1457211600,
        ],
        [
            'id' => 3,
            'title' => 'Title For Deleting',
            'text' => 'Text For Deleting',
            'status' => 1,
            'created_at' => 1457211600,
            'updated_at' => 1457211600,
        ],
    ];
    
  3. 激活单元和验收测试的固定数据支持。只需将fixtures部分添加到unit.suite.yml文件中:

    class_name: UnitTester
    modules:
       enabled:
         - Asserts
         - Yii2:
               part: [orm, fixtures, email]
    

    此外,将fixtures部分添加到acceptance.suite.yml

    class_name: AcceptanceTester
    modules:
       enabled:
           - WebDriver:
               url: http://127.0.0.1:8080/
               browser: firefox
           - Yii2:
               part: [orm, fixtures]
               entryScript: index-test.php
               cleanup: false
    
  4. 通过以下命令重新生成tester类以应用这些更改:

    composer exec codecept build
    
    

编写单元或集成测试

单元和集成测试检查我们项目的源代码。

单元测试只检查当前类或其方法,与其他类和资源(如数据库、文件等)隔离。

集成测试检查你的类与其他类和资源集成的运行情况。

在 Yii2 中,ActiveRecord 模型始终使用数据库来加载表模式,因为我们必须创建一个真实的测试数据库,并且我们的测试将是集成性的。

  1. 编写测试以检查模型验证、保存和更改其状态:

    <?php
    namespace tests\unit\models;
    
    use app\models\Post;
    use Codeception\Test\Unit;
    use tests\fixtures\PostFixture;
    
    class PostTest extends Unit
    {
        /**
        * @var \UnitTester
        */
        protected $tester;
    
        public function _before()
        {
            $this->tester->haveFixtures([
                'post' => [
                    'class' => PostFixture::className(),
                    'dataFile' => codecept_data_dir() . 'post.php'
                ]
            ]);
        }
    
        public function testValidateEmpty()
        {
            $model = new Post();
    
            expect('model should not validate', $model->validate())->false();
    
            expect('title has error', $model->errors)->hasKey('title');
            expect('title has error', $model->errors)->hasKey('text');
        }
    
        public function testValidateCorrect()
        {
             $model = new Post([
                 'title' => 'Other Post',
                 'text' => 'Other Post Text',
             ]);
    
             expect('model should validate', $model->validate())->true();
        }
    
        public function testSave()
        {
            $model = new Post([
                'title' => 'Test Post',
                'text' => 'Test Post Text',
            ]);
    
             expect('model should save', $model->save())->true();
    
            expect('title is correct', $model->title)->equals('Test Post');
            expect('text is correct', $model->text)->equals('Test Post Text');
            expect('status is draft', $model->status)->equals(Post::STATUS_DRAFT);
            expect('created_at is generated', $model->created_at)->notEmpty();
            expect('updated_at is generated', $model->updated_at)->notEmpty();
        }
    
        public function testPublish()
        {
            $model = new Post(['status' => Post::STATUS_DRAFT]);
    
            expect('post is drafted', $model->status)->equals(Post::STATUS_DRAFT);
            $model->publish();
            expect('post is published', $model->status)->equals(Post::STATUS_ACTIVE);
        }
    
        public function testAlreadyPublished()
        {
            $model = new Post(['status' => Post::STATUS_ACTIVE]);
    
            $this->setExpectedException('\LogicException');
            $model->publish();
        }
    
        public function testDraft()
        {
            $model = new Post(['status' => Post::STATUS_ACTIVE]);
    
            expect('post is published', $model->status)->equals(Post::STATUS_ACTIVE);
            $model->draft();
            expect('post is drafted', $model->status)->equals(Post::STATUS_DRAFT);
        }
    
        public function testAlreadyDrafted()
        {
            $model = new Post(['status' => Post::STATUS_ACTIVE]);
    
            $this->setExpectedException('\LogicException');
            $model->publish();
        }
    }
    
  2. 运行测试:

    composer exec codecept run unit
    
    
  3. 现在看看结果:编写单元或集成测试

那就是全部了。如果你故意或偶然地破坏了任何模型的方法,你将看到失败的测试。

编写功能测试

功能测试检查您的应用程序是否运行正确。此套件准备 $_GET$_POST 和其他请求变量,并调用 Application::handleRequest 方法。它有助于测试您的控制器及其响应,而无需运行真实服务器。

现在我们可以为我们的管理员 CRUD 编写测试:

  1. 生成一个新的测试类:

    codecept generate:cest functional admin/Posts
    
  2. 在生成的文件中修复命名空间并编写自己的测试:

    <?php
    namespace tests\functional\admin;
    
    use app\models\Post;
    use FunctionalTester;
    use tests\fixtures\PostFixture;
    use yii\helpers\Url;
    
    class PostsCest
    {
        function _before(FunctionalTester $I)
        {
            $I->haveFixtures([
                'user' => [
                    'class' => PostFixture::className(),
                    'dataFile' => codecept_data_dir() . 'post.php'
                ]
            ]);
        }
    
        public function testIndex(FunctionalTester $I)
        {
            $I->amOnPage(['admin/posts/index']);
            $I->see('Posts', 'h1');
        }
    
        public function testView(FunctionalTester $I)
        {
            $I->amOnPage(['admin/posts/view', 'id' => 1]);
            $I->see('First Post', 'h1');
        }
    
        public function testCreateInvalid(FunctionalTester $I)
        {
            $I->amOnPage(['admin/posts/create']);
            $I->see('Create', 'h1');
    
            $I->submitForm('#post-form', [
                'Post[title]' => '',
                'Post[text]' => '',
            ]);
    
            $I->expectTo('see validation errors');
            $I->see('Title cannot be blank.', '.help-block');
            $I->see('Text cannot be blank.', '.help-block');
        }
    
        public function testCreateValid(FunctionalTester $I)
        {
            $I->amOnPage(['admin/posts/create']);
            $I->see('Create', 'h1');
    
            $I->submitForm('#post-form', [
                'Post[title]' => 'Post Create Title',
                'Post[text]' => 'Post Create Text',
                'Post[status]' => 'Active',
            ]);
    
            $I->expectTo('see view page');
            $I->see('Post Create Title', 'h1');
        }
    
        public function testUpdate(FunctionalTester $I)
        {
            // ...
        }
        public function testDelete(FunctionalTester $I)
        {
            $I->amOnPage(['/admin/posts/view', 'id' => 3]);
            $I->see('Title For Deleting', 'h1');
    
            $I->amGoingTo('delete item');
            $I->sendAjaxPostRequest(Url::to(['/admin/posts/delete', 'id' => 3]));
            $I->expectTo('see that post is deleted');
            $I->dontSeeRecord(Post::className(), [
                'title' => 'Title For Deleting',
            ]);
        }
    }
    
  3. 使用以下命令运行测试:

    composer exec codecept run functional
    
    
  4. 现在查看结果:编写功能测试

所有测试都通过。在其他情况下,您可以在 tests/_output 目录中查看失败的测试的页面快照。

编写验收测试

  1. 验收测试人员从测试服务器直接访问真实网站,而不是调用 Application::handleRequest 方法。高级验收测试看起来像中级功能测试,但在 Selenium 的情况下,它允许检查真实浏览器中的 JavaScript 行为。

  2. 您必须在 tests/acceptance 目录中获取以下类:

    <?php
    namespace tests\acceptance\admin;
    
    use AcceptanceTester;
    use tests\fixtures\PostFixture;
    use yii\helpers\Url;
    
    class PostsCest
    {
        function _before(AcceptanceTester $I)
        {
            $I->haveFixtures([
                'post' => [
                    'class' => PostFixture::className(),
                    'dataFile' => codecept_data_dir() . 'post.php'
                ]
            ]);
        }
    
        public function testIndex(AcceptanceTester $I)
        {
            $I->wantTo('ensure that post index page works');
            $I->amOnPage(Url::to(['/admin/posts/index']));
            $I->see('Posts', 'h1');
        }
    
        public function testView(AcceptanceTester $I)
       {
            $I->wantTo('ensure that post view page works');
            $I->amOnPage(Url::to(['/admin/posts/view', 'id' => 1]));
            $I->see('First Post', 'h1');
        }
    
        public function testCreate(AcceptanceTester $I)
        {
            $I->wantTo('ensure that post create page works');
            $I->amOnPage(Url::to(['/admin/posts/create']));
            $I->see('Create', 'h1');
    
            $I->fillField('#post-title', 'Post Create Title');
            $I->fillField('#post-text', 'Post Create Text');
            $I->selectOption('#post-status', 'Active');
    
            $I->click('submit-button');
            $I->wait(3);
    
            $I->expectTo('see view page');
            $I->see('Post Create Title', 'h1');
        }
    
        public function testDelete(AcceptanceTester $I)
        {
            $I->amOnPage(Url::to(['/admin/posts/view', 'id' => 3]));
            $I->see('Title For Deleting', 'h1');
    
            $I->click('Delete');
            $I->acceptPopup();
            $I->wait(3);
    
            $I->see('Posts', 'h1');
        }
    }
    

    不要忘记调用 wait 方法以等待页面打开或重新加载。

  3. 在新的终端窗口中运行 PHP 测试服务器:

    tests/bin/yii serve
    
    
  4. 运行验收测试:

    composer exec codecept run acceptance
    
    
  5. 查看结果:编写验收测试

Selenium 将启动 Firefox 网络浏览器并执行我们的测试命令。

创建 API 测试套件

除了单元、功能和验收套件之外,Codeception 允许创建特定的测试套件。例如,我们可以为支持 XML 和 JSON 解析的 API 测试创建一个。

  1. Post 模型创建 REST API 控制器 controllers/api/PostsController.php

    <?php
    namespace app\controllers\api;
    
    use yii\rest\ActiveController;
    
    class PostsController extends ActiveController
    {
        public $modelClass = '\app\models\Post';
    }
    
  2. config/web.php 中为 UrlManager 组件添加 REST 路由:

    'components' => [
        // ...
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'rules' => [
                ['class' => 'yii\rest\UrlRule', 'controller' => 'api/posts'],
            ],
        ],
    ],
    

    并在 config/test.php 中添加一些配置(但启用 showScriptName 选项):

    'components' => [
        // ...
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => true,
            'rules' => [
                ['class' => 'yii\rest\UrlRule', 'controller' => 'api/posts'],
            ],
         ],
    ],
    
  3. 添加以下内容的 web/.htaccess 文件:

    RewriteEngine On
    
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . index.php
    
  4. 检查 api/posts 控制器是否工作:创建 API 测试套件

  5. 使用 REST 模块创建 API 测试套件 tests/api.suite.yml 配置文件:

    class_name: ApiTester
    modules:
       enabled:
           - REST:
               depends: PhpBrowser
               url: 'http://127.0.0.1:8080/index-test.php'
               part: [json]
           - Yii2:
               part: [orm, fixtures]
               entryScript: index-test.php
    

    现在重新构建测试人员:

    composer exec codecept build
    
    
  6. 创建 tests/api 目录并生成新的测试类:

    composer exec codecept generate:cest api Posts
    
    
  7. 为您的 REST-API 编写测试:

    <?php
    namespace tests\api;
    
    use ApiTester;
    use tests\fixtures\PostFixture;
    use yii\helpers\Url;
    
    class PostsCest
    {
       function _before(ApiTester $I)
       {
           $I->haveFixtures([
               'post' => [
                   'class' => PostFixture::className(),
                   'dataFile' => codecept_data_dir() . 'post.php'
               ]
           ]);
       }
    
       public function testGetAll(ApiTester $I)
       {
           $I->sendGET('/api/posts');
           $I->seeResponseCodeIs(200);
           $I->seeResponseIsJson();
           $I->seeResponseContainsJson([0 => ['title' => 'First Post']]);
       }
    
       public function testGetOne(ApiTester $I)
       {
           $I->sendGET('/api/posts/1');
           $I->seeResponseCodeIs(200);
           $I->seeResponseIsJson();
           $I->seeResponseContainsJson(['title' => 'First Post']);
       }
    
       public function testGetNotFound(ApiTester $I)
       {
           $I->sendGET('/api/posts/100');
           $I->seeResponseCodeIs(404);
           $I->seeResponseIsJson();
           $I->seeResponseContainsJson(['name' => 'Not Found']);
       }
    
       public function testCreate(ApiTester $I)
       {
           $I->sendPOST('/api/posts', [
               'title' => 'Test Title',
               'text' => 'Test Text',
           ]);
           $I->seeResponseCodeIs(201);
           $I->seeResponseIsJson();
           $I->seeResponseContainsJson(['title' => 'Test Title']);
       }
    
       public function testUpdate(ApiTester $I)
       {
           $I->sendPUT('/api/posts/2', [
               'title' => 'New Title',
           ]);
           $I->seeResponseCodeIs(200);
           $I->seeResponseIsJson();
           $I->seeResponseContainsJson([
               'title' => 'New Title',
               'text' => 'Old Text For Updating',
           ]);
       }
    
       public function testDelete(ApiTester $I)
       {
           $I->sendDELETE('/api/posts/3');
           $I->seeResponseCodeIs(204);
       }
    }
    
  8. 运行应用程序服务器:

    tests/bin yii serve
    
    
  9. 运行 API 测试:

    composer exec codecept run api
    
    

    现在查看结果:

    创建 API 测试套件

所有测试都通过,并且我们的 API 运行正确。

它是如何工作的…

Codeception 是一个基于 PHPUnit 包的高级测试框架,用于提供编写单元、集成、功能和验收测试的基础设施。

我们可以使用 Codeception 内置的 Yii2 模块,它允许我们加载固定数据、与模型和其他事物一起工作,来自 Yii 框架。

参见

使用 PHPUnit 进行单元测试

PHPUnit 是最受欢迎的 PHP 测试框架。它配置和使用的简单性。此外,该框架支持代码覆盖率报告,并且有许多附加插件。前一个菜谱中的 Codeception 使用 PHPUnit 进行自己的工作和编写单元测试。在这个菜谱中,我们将使用 PHPUnit 测试创建一个演示购物车扩展。

准备工作

使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guidestart-installation.html

如何做到这一点...

首先,我们必须为我们的扩展创建一个新的空目录。

准备扩展结构

  1. 首先,为您的扩展创建目录结构:

    book
    └── cart
        ├── src
        └── tests
    

    要将扩展作为 Composer 包使用,准备book/cart/composer.json文件如下:

    {
        "name": "book/cart",
        "type": "yii2-extension",
        "require": {
            "yiisoft/yii2": "~2.0"
        },
        "require-dev": {
            "phpunit/phpunit": "4.*"
        },
        "autoload": {
            "psr-4": {
                "book\\cart\\": "src/",
                "book\\cart\\tests\\": "tests/"
            }
        },
        "extra": {
            "asset-installer-paths": {
                "npm-asset-library": "vendor/npm",
                "bower-asset-library": "vendor/bower"
            }
        }
    }
    
  2. 添加book/cart/.gitignore文件,包含以下行:

    /vendor
    /composer.lock
    
  3. 将以下行添加到 PHPUnit 默认配置文件book/cart/phpunit.xml.dist中,如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <phpunit bootstrap="./tests/bootstrap.php"
             colors="true"
             convertErrorsToExceptions="true"
             convertNoticesToExceptions="true"
             convertWarningsToExceptions="true"
             stopOnFailure="false">
        <testsuites>
            <testsuite name="Test Suite">
                <directory>./tests</directory>
            </testsuite>
        </testsuites>
        <filter>
            <whitelist>
                <directory suffix=".php">./src/</directory>
            </whitelist>
        </filter>
    </phpunit>
    
  4. 安装扩展的所有依赖项:

    composer install
    
    
  5. 现在我们必须得到以下结构:

    book
    └── cart
        ├── src
        ├── tests
        ├── .gitignore
        ├── composer.json
        ├── phpunit.xml.dist
        └── vendor
    

编写扩展代码

要编写扩展代码,请按照以下步骤操作:

  1. src目录中创建book\cart\Cart类:

    <?php
    namespace book\cart;
    
    use book\cart\storage\StorageInterface;
    use yii\base\Component;
    use yii\base\InvalidConfigException;
    
    class Cart extends Component
    {
        /**
         * @var StorageInterface
         */
        private $_storage;
        /**
         * @var array
         */
        private $_items;
    
        public function setStorage($storage)
        {
            if (is_array($storage)) {
                $this->_storage = \Yii::createObject($storage);
            } else {
                $this->_storage = $storage;
            }
        }
    
        public function add($id, $amount = 1)
        {
            $this->loadItems();
            if (isset($this->_items[$id])) {
                $this->_items[$id] += $amount;
            } else {
                $this->_items[$id] = $amount;
            }
            $this->saveItems();
        }
    
        public function set($id, $amount)
        {
            $this->loadItems();
            $this->_items[$id] = $amount;
            $this->saveItems();
        }
    
        public function remove($id)
        {
            $this->loadItems();
            if (isset($this->_items[$id])) {
                unset($this->_items[$id]);
            }
            $this->saveItems();
        }
    
        public function clear()
        {
            $this->loadItems();
            $this->_items = [];
            $this->saveItems();
        }
    
        public function getItems()
        {
            $this->loadItems();
            return $this->_items;
        }
    
        public function getCount()
        {
            $this->loadItems();
            return count($this->_items);
        }
    
        public function getAmount()
        {
            $this->loadItems();
            return array_sum($this->_items);
        }
    
        private function loadItems()
        {
            if ($this->_storage === null) {
                throw new InvalidConfigException('Storage must be set');
            }
            if ($this->_items === null) {
                $this->_items = $this->_storage->load();
            }
        }
    
        private function saveItems()
        {
             $this->_storage->save($this->_items);
        }
    }
    
  2. src/storage子目录中创建StorageInterface接口:

    <?php
    namespace book\cart\storage;
    
    interface StorageInterface
    {
        /**
         * @return array
         */
        public function load();
    
        /**
         * @param array $items
         */
        public function save(array $items);
    }
    

    以及 SessionStorage 类:

    namespace book\cart\storage;
    
    use Yii;
    
    class SessionStorage implements StorageInterface
    {
        public $sessionKey = 'cart';
    
        public function load()
        {
            return Yii::$app->session->get($this->sessionKey, []);
        }
    
        public function save(array $items)
        {
            Yii::$app->session->set($this->sessionKey, $items);
        }
    }
    
  3. 现在我们必须得到以下结构:

    book
    └── cart
        ├── src
        │   ├── storage
        │   │   ├── SessionStorage.php
        │   │   └── StorageInterface.php
        │   └── Cart.php
        ├── tests
        ├── .gitignore
        ├── composer.json
        ├── phpunit.xml.dist
        └── vendor
    

编写扩展测试

要进行扩展测试,请按照以下步骤操作:

  1. 添加book/cart/tests/bootstrap.php入口脚本以供 PHPUnit 使用:

    <?php
    
    defined('YII_DEBUG') or define('YII_DEBUG', true);
    defined('YII_ENV') or define('YII_ENV', 'test');
    
    require(__DIR__ . '/../vendor/autoload.php');
    require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
    
  2. 在每个测试之前初始化 Yii 应用程序并在之后销毁应用程序以创建一个测试基类:

    <?php
    namespace book\cart\tests;
    
    use yii\di\Container;
    use yii\web\Application;
    
    abstract class TestCase extends \PHPUnit_Framework_TestCase
    {
        protected function setUp()
        {
            parent::setUp();
            $this->mockApplication();
        }
    
        protected function tearDown()
        {
            $this->destroyApplication();
            parent::tearDown();
        }
    
        protected function mockApplication()
        {
            new Application([
                'id' => 'testapp',
                'basePath' => __DIR__,
                'vendorPath' => dirname(__DIR__) . '/vendor',
            ]);
        }
    
        protected function destroyApplication()
        {
            \Yii::$app = null;
            \Yii::$container = new Container();
        }
    }
    
  3. 添加一个基于内存的清洁模拟类,该类实现了StorageInterface接口:

    <?php
    
    namespace book\cart\tests\storage;
    
    use book\cart\storage\StorageInterface;
    
    class FakeStorage implements StorageInterface
    {
        private $items = [];
    
        public function load()
        {
            return $this->items;
        }
    
        public function save(array $items)
        {
            $this->items = $items;
        }
    }
    

    它将项目存储到私有变量中,而不是与真实会话一起工作。它允许独立运行测试(没有真实存储驱动程序),并且也提高了测试性能。

  4. 添加CartTest类:

    <?php
    namespace book\cart\tests;
    
    use book\cart\Cart;
    use book\cart\tests\storage\FakeStorage;
    
    class CartTest extends TestCase
    {
        /**
         * @var Cart
         */
        private $cart;
    
        public function setUp()
        {
            parent::setUp();
            $this->cart = new Cart(['storage' => new FakeStorage()]);
        }
    
        public function testEmpty()
        {
            $this->assertEquals([], $this->cart->getItems());
            $this->assertEquals(0, $this->cart->getCount());
            $this->assertEquals(0, $this->cart->getAmount());
        }
    
        public function testAdd()
        {
            $this->cart->add(5, 3);
            $this->assertEquals([5 => 3], $this->cart->getItems());
    
            $this->cart->add(7, 14);
            $this->assertEquals([5 => 3, 7 => 14], $this->cart->getItems());
    
            $this->cart->add(5, 10);
            $this->assertEquals([5 => 13, 7 => 14], $this->cart->getItems());
        }
    
        public function testSet()
        {
            $this->cart->add(5, 3);
            $this->cart->add(7, 14);
            $this->cart->set(5, 12);
            $this->assertEquals([5 => 12, 7 => 14], $this->cart->getItems());
        }
    
        public function testRemove()
        {
            $this->cart->add(5, 3);
            $this->cart->remove(5);
            $this->assertEquals([], $this->cart->getItems());
        }
    
        public function testClear()
        {
            $this->cart->add(5, 3);
            $this->cart->add(7, 14);
            $this->cart->clear();
            $this->assertEquals([], $this->cart->getItems());
        }
    
        public function testCount()
        {
            $this->cart->add(5, 3);
            $this->assertEquals(1, $this->cart->getCount());
    
            $this->cart->add(7, 14);
            $this->assertEquals(2, $this->cart->getCount());
        }
    
        public function testAmount()
        {
            $this->cart->add(5, 3);
            $this->assertEquals(3, $this->cart->getAmount());
    
            $this->cart->add(7, 14);
            $this->assertEquals(17, $this->cart->getAmount());
        }
    
        public function testEmptyStorage()
        {
            $cart = new Cart();
            $this->setExpectedException('yii\base\InvalidConfigException');
            $cart->getItems();
        }
    }
    
  5. 添加一个单独的测试来检查SessionStorage类:

    <?php
    namespace book\cart\tests\storage;
    
    use book\cart\storage\SessionStorage;
    use book\cart\tests\TestCase;
    
    class SessionStorageTest extends TestCase
    {
        /**
         * @var SessionStorage
         */
        private $storage;
    
        public function setUp()
        {
            parent::setUp();
            $this->storage = new SessionStorage(['key' => 'test']);
        }
    
        public function testEmpty()
        {
            $this->assertEquals([], $this->storage->load());
        }
    
        public function testStore()
        {
            $this->storage->save($items = [1 => 5, 6 => 12]);
    
            $this->assertEquals($items, $this->storage->load());
        }
    }
    
  6. 目前我们必须得到以下结构:

    book
    └── cart
        ├── src
        │   ├── storage
        │   │   ├── SessionStorage.php
        │   │   └── StorageInterface.php
        │   └── Cart.php
        ├── tests
        │   ├── storage
        │   │   ├── FakeStorage.php
        │   │   └── SessionStorageTest.php
        │   ├── bootstrap.php
        │   ├── CartTest.php
        │   └── TestCase.php
        ├── .gitignore
        ├── composer.json
        ├── phpunit.xml.dist
        └── vendor
    

运行测试

在使用composer install命令安装所有依赖项期间,Composer 包管理器将PHPUnit包安装到vendor目录,并将可执行文件phpunit放置在vendor/bin子目录中。

现在我们可以运行以下脚本:

cd book/cart
vendor/bin/phpunit

我们必须查看以下测试报告:

PHPUnit 4.8.26 by Sebastian Bergmann and contributors.

..........

Time: 906 ms, Memory: 11.50MB

OK (10 tests, 16 assertions)

每个点表示对应测试的成功结果。

尝试通过注释unset操作故意破坏自己的购物车:

class Cart extends Component
{
    …

    public function remove($id)
    {
        $this->loadItems();
        if (isset($this->_items[$id])) {
            // unset($this->_items[$id]);
        }
        $this->saveItems();
    }

    ...
}

再次运行测试:

PHPUnit 4.8.26 by Sebastian Bergmann and contributors.

...F......

Time: 862 ms, Memory: 11.75MB

There was 1 failure:

1) book\cart\tests\CartTest::testRemove
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
+    5 => 3
 )

/book/cart/tests/CartTest.php:52

FAILURES!
Tests: 10, Assertions: 16, Failures: 1

在这种情况下,我们看到了一个失败(用F代替点)和失败报告。

分析代码覆盖率

您必须从 xdebug.org 安装 XDebug PHP 扩展。例如,在 Ubuntu 或 Debian 上,您可以在终端中输入以下内容:

sudo apt-get install php5-xdebug

在 Windows 上,您必须打开 php.ini 文件,并将路径添加到您的 PHP 安装目录中的自定义代码:

[xdebug]
zend_extension_ts=C:/php/ext/php_xdebug.dll

或者,如果您使用的是非线程安全版本,请输入以下内容:

[xdebug]
zend_extension=C:/php/ext/php_xdebug.dll

安装 XDebug 后,再次运行测试,并使用 --coverage-html 标志指定报告目录:

vendor/bin/phpunit --coverage-html tests/_output

运行后,在浏览器中打开 tests/_output/index.html 文件,您将看到每个目录和类的显式覆盖率报告:

分析代码覆盖率

您可以点击任何类并分析在测试过程中未执行的代码行。例如,打开我们的 Cart 类报告:

分析代码覆盖率

在我们的例子中,我们忘记测试从数组配置创建存储。

组件的使用

在 Packagist 上发布扩展后,我们可以安装一个一对一的任何项目:

composer require book/cart

此外,在应用程序配置文件中启用组件:

'components' => [
    // …
    'cart' => [
        'class' => 'book\cart\Cart',
        'storage' => [
            'class' => 'book\cart\storage\SessionStorage',
        ],
    ],
],

作为不发布扩展到 Packagist 的替代方法,我们必须设置 @book 别名以启用正确的类自动加载:

$config = [
    'id' => 'basic',
    'basePath' => dirname(__DIR__),
    'bootstrap' => ['log'],
    'aliases' => [
        '@book' => dirname(__DIR__) . '/book',
    ],
    'components' => [
        'cart' => [
            'class' => 'book\cart\Cart',
            'storage' => [
                'class' => 'book\cart\storage\SessionStorage',
            ],
        ],
        // ...
    ],
]

无论如何,我们可以在项目中将其用作 Yii::$app->cart 组件:

Yii::$app->cart->add($product->id, $amount);

它是如何工作的…

在创建自己的测试之前,您只需在项目的根目录中创建任何子目录,并添加 phpunit.xmlphpunit.xml.dist 文件:

<?xml version="1.0" encoding="utf-8"?>
<phpunit bootstrap="./tests/bootstrap.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Test Suite">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory suffix=".php">./src/</directory>
        </whitelist>
    </filter>
</phpunit>

如果工作目录中不存在第一个文件,PHPUnit 从第二个文件加载配置。您还可以通过初始化自动加载器和您的框架环境来创建 bootstrap.php 文件:

<?php
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'test');
require(__DIR__ . '/../vendor/autoload.php');
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');

最后,您可以通过 Composer(本地或全局)安装 PHPUnit,并在包含 XML 配置文件的目录中使用 phpunit 控制台命令。

PHPUnit 扫描测试目录,并找到以 *Test.php 后缀的文件。您所有的测试类都必须扩展 PHPUnit_Framework_TestCase 类,并包含具有 test* 前缀的公共方法,如下所示:

class MyTest extends TestCase
{
    public function testSomeFunction()
    {
        $this->assertTrue(true);
    }
}

在测试的主体中,您可以使用任何现有的 assert* 方法:

$this->assertEqual('Alex', $model->name);
$this->assertTrue($model->validate());
$this->assertFalse($model->save());
$this->assertCount(3, $items);
$this->assertArrayHasKey('username', $model->getErrors());
$this->assertNotNull($model->author);
$this->assertInstanceOf('app\models\User', $model->author);

此外,您还可以覆盖 setUp()tearDown() 方法以添加在每次测试方法前后运行的表达式。

例如,您可以通过重新初始化 Yii 应用程序来定义自己的基础 TestCase 类:

<?php
namespace book\cart\tests;

use yii\di\Container;
use yii\web\Application;

abstract class TestCase extends \PHPUnit_Framework_TestCase
{
    protected function setUp()
    {
        parent::setUp();
        $this->mockApplication();
    }

    protected function tearDown()
    {
        $this->destroyApplication();
        parent::tearDown();
    }

    protected function mockApplication()
    {
        new Application([
            'id' => 'testapp',
            'basePath' => __DIR__,
            'vendorPath' => dirname(__DIR__) . '/vendor',
        ]);
    }

    protected function destroyApplication()
    {
        \Yii::$app = null;
        \Yii::$container = new Container();
    }
}

现在,您可以在子类中扩展此类。即使您的 test 方法也会使用应用程序的独立实例。这有助于避免副作用并创建独立的测试。

注意

Yii 2.0.* 使用旧的 PHPUnit 4.* 版本以与 PHP 5.4 兼容。

参见

使用 Atoum 进行单元测试

除了 PHPUnit 和 Codeception,Atoum 还是一个简单的单元测试框架。您可以使用此框架来测试您的扩展或测试应用程序的代码。

准备工作

为新项目创建一个空目录。

如何做到这一点…

在这个菜谱中,我们将创建一个带有 Atoum 测试的演示购物车扩展。

准备扩展结构

  1. 首先,为您的外部扩展创建目录结构:

    book
    └── cart
        ├── src
        └── tests
    
  2. 为了将扩展作为 composer 包使用,按照以下方式准备book/cart/composer.json文件:

    {
        "name": "book/cart",
        "type": "yii2-extension",
        "require": {
            "yiisoft/yii2": "~2.0"
        },
        "require-dev": {
            "atoum/atoum": "².7"
        },
        "autoload": {
            "psr-4": {
                "book\\cart\\": "src/",
                "book\\cart\\tests\\": "tests/"
            }
        },
        "extra": {
            "asset-installer-paths": {
                "npm-asset-library": "vendor/npm",
                "bower-asset-library": "vendor/bower"
            }
        }
    }
    
  3. 将以下行添加到book/cart/,gitignore文件中:

    /vendor
    /composer.lock
    
  4. 安装扩展的所有依赖项:

    composer install
    
  5. 现在我们将得到以下结构:

    book
    └── cart
        ├── src
        ├── tests
        ├── .gitignore
        ├── composer.json
        ├── phpunit.xml.dist
        └── vendor
    

编写扩展代码

使用 PHPUnit 进行单元测试菜谱中复制CartStorageInterfaceSessionStorage类。

最后,我们必须得到以下结构:

book
└── cart
    ├── src
    │   ├── storage
    │   │   ├── SessionStorage.php
    │   │   └── StorageInterface.php
    │   └── Cart.php
    ├── tests
    ├── .gitignore
    ├── composer.json
    └── vendor

编写扩展测试

  1. 添加book/cart/tests/bootstrap.php入口脚本:

    <?php
    defined('YII_DEBUG') or define('YII_DEBUG', true);
    defined('YII_ENV') or define('YII_ENV', 'test');
    require(__DIR__ . '/../vendor/autoload.php');
    require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
    
  2. 通过在每个测试之前初始化 Yii 应用程序并在测试之后销毁应用程序来创建一个测试基类:

    <?php
    
    namespace book\cart\tests;
    
    use yii\di\Container;
    use yii\console\Application;
    use mageekguy\atoum\test;
    
    abstract class TestCase extends test
    {
        public function beforeTestMethod($method)
        {
            parent::beforeTestMethod($method);
            $this->mockApplication();
        }
    
        public function afterTestMethod($method)
        {
            $this->destroyApplication();
            parent::afterTestMethod($method);
        }
    
        protected function mockApplication()
        {
            new Application([
                'id' => 'testapp',
                'basePath' => __DIR__,
                'vendorPath' => dirname(__DIR__) . '/vendor',
                'components' => [
                    'session' => [
                        'class' => 'yii\web\Session',
                    ],
                ]
            ]);
        }
    
        protected function destroyApplication()
        {
            \Yii::$app = null;
            \Yii::$container = new Container();
        }
    }
    
  3. 添加一个基于内存的清洁模拟类,该类实现了StorageInterface接口:

    <?php
    namespace book\cart\tests;
    
    use book\cart\storage\StorageInterface;
    
    class FakeStorage implements StorageInterface
    {
        private $items = [];
    
        public function load()
        {
            return $this->items;
        }
    
        public function save(array $items)
        {
            $this->items = $items;
        }
    }
    

    这将把项目存储到私有变量中,而不是与真实会话一起工作。这允许我们独立运行测试(没有真实存储驱动程序),并且也提高了测试性能。

  4. 添加Cart测试类:

    <?php
    namespace book\cart\tests\units;
    
    use book\cart\tests\FakeStorage;
    use book\cart\Cart as TestedCart;
    use book\cart\tests\TestCase;
    
    class Cart extends TestCase
    {
        /**
         * @var TestedCart
         */
        private $cart;
    
        public function beforeTestMethod($method)
        {
            parent::beforeTestMethod($method);
            $this->cart = new TestedCart(['storage' => new FakeStorage()]);
        }
    
        public function testEmpty()
        {
            $this->array($this->cart->getItems())->isEqualTo([]);
            $this->integer($this->cart->getCount())->isEqualTo(0);
            $this->integer($this->cart->getAmount())->isEqualTo(0);
        }
    
        public function testAdd()
        {        
            $this->cart->add(5, 3);
            $this->array($this->cart->getItems())->isEqualTo([5 => 3]);
    
            $this->cart->add(7, 14);
            $this->array($this->cart->getItems())->isEqualTo([5 => 3, 7 => 14]);
    
            $this->cart->add(5, 10);
            $this->array($this->cart->getItems())->isEqualTo([5 => 13, 7 => 14]);
        }
    
        public function testSet()
        {
            $this->cart->add(5, 3);
            $this->cart->add(7, 14);
            $this->cart->set(5, 12);
            $this->array($this->cart->getItems())->isEqualTo([5 => 12, 7 => 14]);
        }
    
        public function testRemove()
        {
            $this->cart->add(5, 3);
            $this->cart->remove(5);
            $this->array($this->cart->getItems())->isEqualTo([]);
        }
    
        public function testClear()
        {
            $this->cart->add(5, 3);
            $this->cart->add(7, 14);
            $this->cart->clear();
            $this->array($this->cart->getItems())->isEqualTo([]);
        }
    
        public function testCount()
        {
            $this->cart->add(5, 3);
            $this->integer($this->cart->getCount())->isEqualTo(1);
    
            $this->cart->add(7, 14);
            $this->integer($this->cart->getCount())->isEqualTo(2);
        }
    
        public function testAmount()
        {
            $this->cart->add(5, 3);
            $this->integer($this->cart->getAmount())->isEqualTo(3);
    
            $this->cart->add(7, 14);
            $this->integer($this->cart->getAmount())->isEqualTo(17);
        }
    
        public function testEmptyStorage()
        {
            $cart = new TestedCart();
    
            $this->exception(function () use ($cart) {
                $cart->getItems();
            })->hasMessage('Storage must be set');
        }
    }
    
  5. 添加一个单独的测试来检查SessionStorage类:

    <?php
    namespace book\cart\tests\units\storage;
    
    use book\cart\storage\SessionStorage as TestedStorage;
    use book\cart\tests\TestCase;
    
    class SessionStorage extends TestCase
    {
        /**
         * @var TestedStorage
         */
        private $storage;
    
        public function beforeTestMethod($method)
        {
            parent::beforeTestMethod($method);
            $this->storage = new TestedStorage(['key' => 'test']);
        }
    
        public function testEmpty()
        {
            $this
                ->given($storage = $this->storage)
                ->then
                    ->array($storage->load())
                        ->isEqualTo([]);
        }
    
        public function testStore()
        {
            $this
                ->given($storage = $this->storage)
                ->and($storage->save($items = [1 => 5, 6 => 12]))
                ->then
                    ->array($this->storage->load())
                        ->isEqualTo($items)
            ;
        }
    }
    
  6. 现在我们将得到以下结构:

    book
    └── cart
        ├── src
        │   ├── storage
        │   │   ├── SessionStorage.php
        │   │   └── StorageInterface.php
        │   └── Cart.php
        ├── tests
        │   ├── units
        │   │   ├── storage
        │   │   │   └── SessionStorage.php
        │   │   └── Cart.php
        │   ├── bootstrap.php
        │   ├── FakeStorage.php
        │   └── TestCase.php
        ├── .gitignore
        ├── composer.json
        └── vendor
    

运行测试

在使用composer install命令安装所有依赖项期间,Composer 包管理器将Atounm包安装到vendor目录,并将可执行文件atoum放置在vendor/bin子目录中。

现在我们可以运行以下脚本:

cd book/cart
vendor/bin/atoum -d tests/units -bf tests/bootstrap.php

此外,我们还必须看到以下测试报告:

> atoum path: /book/cart/vendor/atoum/atoum/vendor/bin/atoum
> atoum version: 2.7.0
> atoum path: /book/cart/vendor/atoum/atoum/vendor/bin/atoum
> atoum version: 2.7.0
> PHP path: /usr/bin/php5
> PHP version:
=> PHP 5.5.9-1ubuntu4.16 (cli)
> book\cart\tests\units\Cart...
[SSSSSSSS__________________________________________________][8/8]
=> Test duration: 1.13 seconds.
=> Memory usage: 3.75 Mb.
> book\cart\tests\units\storage\SessionStorage...
[SS________________________________________________________][2/2]
=> Test duration: 0.03 second.
=> Memory usage: 1.00 Mb.
> Total tests duration: 1.15 seconds.
> Total tests memory usage: 4.75 Mb.
> Code coverage value: 16.16%

每个S符号表示对应测试的成功结果。

尝试通过注释unset操作来故意破坏购物车:

class Cart extends Component
{
    ...

    public function remove($id)
    {
        $this->loadItems();
        if (isset($this->_items[$id])) {
            // unset($this->_items[$id]);
        }
        $this->saveItems();
    }

    ...
}

再次运行测试:

> atoum version: 2.7.0
> PHP path: /usr/bin/php5
> PHP version:
=> PHP 5.5.9-1ubuntu4.16 (cli)
book\cart\tests\units\Cart...
[SSFSSSSS__________________________________________________][8/8]
=> Test duration: 1.09 seconds.
=> Memory usage: 3.25 Mb.
> book\cart\tests\units\storage\SessionStorage...
[SS________________________________________________________][2/2]
=> Test duration: 0.02 second.
=> Memory usage: 1.00 Mb.
...
Failure (2 tests, 10/10 methods, 0 void method, 0 skipped method, 0 uncompleted method, 1 failure, 0 error, 0 exception)!
> There is 1 failure:
=> book\cart\tests\units\Cart::testRemove():
In file /book/cart/tests/units/Cart.php on line 53, mageekguy\atoum\asserters\phpArray() failed: array(1) is not equal to array(0)
-Expected
+Actual
@@ -1 +1,3 @@
-array(0) {
+array(1) {
+  [5] =>
+  int(3)

在这种情况下,我们看到了一个失败(用F代替点)和一份失败报告。

分析代码覆盖率

您必须从xdebug.org安装 XDebug PHP 扩展。例如,在 Ubuntu 或 Debian 上,您可以在终端中输入以下内容:

sudo apt-get install php5-xdebug

在 Windows 上,您必须打开php.ini文件,并添加带有您 PHP 安装目录路径的自定义代码:

[xdebug]
zend_extension_ts=C:/php/ext/php_xdebug.dll

或者,如果您使用的是非线程安全版本,请输入以下内容:

[xdebug]
zend_extension=C:/php/ext/php_xdebug.dll

在安装 XDebug 之后,创建一个包含覆盖率报告选项的book/cart/coverage.php配置文件:

<?php
use \mageekguy\atoum;
/** @var atoum\scripts\runner $script */
$report = $script->addDefaultReport();
$coverageField = new atoum\report\fields\runner\coverage\html('Cart', __DIR__ . '/tests/coverage');
$report->addField($coverageField);

现在再次运行测试,使用-c选项来使用此配置:

vendor/bin/atoum -d tests/units -bf tests/bootstrap.php -c coverage.php

运行测试后,在浏览器中打开tests/coverage/index.html文件。您将看到每个目录和类的明确覆盖率报告:

分析代码覆盖率

您可以点击任何类并分析在测试过程中未执行的代码行。

它是如何工作的…

Atoum 测试框架支持 行为驱动设计 (BDD) 语法流,如下所示:

public function testSome()
{
    $this
        ->given($cart = new TestedCart())
        ->and($cart->add(5, 13))
        ->then
            ->sizeof($cart->getItems())
                ->isEqualTo(1)
            ->array($cart->getItems())
                ->isEqualTo([5 => 3])
            ->integer($cart->getCount())
                ->isEqualTo(1)
            ->integer($cart->getAmount())
                ->isEqualTo(3);
}

然而,您可以使用类似于 PHPUnit 的常规语法来编写单元测试:

public function testSome()
{
    $cart = new TestedCart();
    $cart->add(5, 3);

    $this
        ->array($cart->getItems())->isEqualTo([5 => 3])
        ->integer($cart->getCount())->isEqualTo(1)
        ->integer($cart->getAmount())->isEqualTo(3)
    ;
}

Atoum 还支持代码覆盖率报告,用于分析测试质量。

参见

使用 Behat 进行单元测试

Behat 是一个用于测试代码的 BDD 框架,它使用人类可读的句子描述各种用例中的代码行为。

准备就绪

为新项目创建一个空目录。

如何做到这一点…

在本菜谱中,我们将创建一个带有 Behat 测试的演示购物车扩展。

准备扩展结构

  1. 首先,为您的扩展创建目录结构:

    book
    └── cart
        ├── src
        └── features
    
  2. 要将扩展作为 Composer 包使用,请按如下准备 book/cart/composer.json 文件:

    {
        "name": "book/cart",
        "type": "yii2-extension",
        "require": {
            "yiisoft/yii2": "~2.0"
        },
        "require-dev": {
            "phpunit/phpunit": "4.*",
            "behat/behat": "³.1"
        },
        "autoload": {
            "psr-4": {
                "book\\cart\\": "src/",
                "book\\cart\\features\\": "features/"
            }
        },
        "extra": {
            "asset-installer-paths": {
                "npm-asset-library": "vendor/npm",
                "bower-asset-library": "vendor/bower"
            }
        }
    }
    
  3. 将以下行添加到 book/cart/.gitignore 文件中:

    /vendor
    /composer.lock
    
  4. 安装扩展的所有依赖项:

    composer install
    
  5. 现在我们得到以下结构:

    book
    └── cart
        ├── src
        ├── features
        ├── .gitignore
        ├── composer.json
        └── vendor
    

编写扩展代码

使用 PHPUnit 进行单元测试 菜谱中复制 CartStorageInterfaceSessionStorage 类。

最后,我们得到以下结构:

book
└── cart
    ├── src
    │   ├── storage
    │   │   ├── SessionStorage.php
    │   │   └── StorageInterface.php
    │   └── Cart.php
    ├── features
    ├── .gitignore
    ├── composer.json
    └── vendor

编写扩展测试

  1. 添加 book/cart/features/bootstrap/bootstrap.php 入口脚本:

    <?php
    defined('YII_DEBUG') or define('YII_DEBUG', true);
    defined('YII_ENV') or define('YII_ENV', 'test');
    
    require_once __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
    
  2. 创建 features/cart.feature 文件并编写购物车测试场景:

    Feature: Shopping cart
      In order to buy products
      As a customer
      I need to be able to put interesting products into a cart
    
      Scenario: Checking empty cart
        Given there is a clean cart
        Then I should have 0 products
        Then I should have 0 product
        And the overall cart amount should be 0
    
      Scenario: Adding products to the cart
        Given there is a clean cart
        When I add 3 pieces of 5 product
        Then I should have 3 pieces of 5 product
        And I should have 1 product
        And the overall cart amount should be 3
    
        When I add 14 pieces of 7 product
        Then I should have 3 pieces of 5 product
        And I should have 14 pieces of 7 product
        And I should have 2 products
        And the overall cart amount should be 17
    
        When I add 10 pieces of 5 product
        Then I should have 13 pieces of 5 product
        And I should have 14 pieces of 7 product
        And I should have 2 products
        And the overall cart amount should be 27
    
      Scenario: Change product count in the cart
        Given there is a cart with 5 pieces of 7 product
        When I set 3 pieces for 7 product
        Then I should have 3 pieces of 7 product
    
      Scenario: Remove products from the cart
        Given there is a cart with 5 pieces of 7 product
        When I add 14 pieces of 7 product
        And I clear cart
        Then I should have empty cart
    
  3. 添加存储测试 features/storage.feature 文件:

    Feature: Shopping cart storage
      I need to be able to put items into a storage
    
      Scenario: Checking empty storage
        Given there is a clean storage
        Then I should have empty storage
    
      Scenario: Save items into storage
        Given there is a clean storage
        When I save 3 pieces of 7 product to the storage
        Then I should have 3 pieces of 7 product in the storage
    
  4. features/bootstrap/CartContext.php 文件中添加所有步骤的实现:

    <?php
    use Behat\Behat\Context\SnippetAcceptingContext;
    use book\cart\Cart;
    use book\cart\features\bootstrap\storage\FakeStorage;
    use yii\di\Container;
    use yii\web\Application;
    
    require_once __DIR__ . '/bootstrap.php';
    
    class CartContext implements SnippetAcceptingContext
    {
        /**
         * @var Cart
         * */
        private $cart;
    
        /**
         * @Given there is a clean cart
         */
        public function thereIsACleanCart()
        {
            $this->resetCart();
        }
    
        /**
         * @Given there is a cart with :pieces of :product product
         */
        public function thereIsAWhichCostsPs($product, $amount)
        {
            $this->resetCart();
            $this->cart->set($product, floatval($amount));
        }
    
        /**
         * @When I add :pieces of :product
         */
        public function iAddTheToTheCart($product, $pieces)
        {
            $this->cart->add($product, $pieces);
        }
    
        /**
         * @When I set :pieces for :arg2 product
         */
        public function iSetPiecesForProduct($pieces, $product)
        {
            $this->cart->set($product, $pieces);
        }
    
        /**
         * @When I clear cart
         */
        public function iClearCart()
        {
            $this->cart->clear();
        }
    
        /**
         * @Then I should have empty cart
         */
        public function iShouldHaveEmptyCart()
        {
            PHPUnit_Framework_Assert::assertEquals(
                0,
                $this->cart->getCount()
            );
        }
    
        /**
         * @Then I should have :count product(s)
         */
        public function iShouldHaveProductInTheCart($count)
        {
            PHPUnit_Framework_Assert::assertEquals(
                intval($count),
                $this->cart->getCount()
            );
        }
    
        /**
         * @Then the overall cart amount should be :amount
         */
        public function theOverallCartPriceShouldBePs($amount)
        {
            PHPUnit_Framework_Assert::assertSame(
                intval($amount),
                $this->cart->getAmount()
            );
        }
    
        /**
         * @Then I should have :pieces of :product
         */
        public function iShouldHavePiecesOfProduct($pieces, $product)
        {
            PHPUnit_Framework_Assert::assertArraySubset(
                [intval($product) => intval($pieces)],
                $this->cart->getItems()
            );
        }
    
        private function resetCart()
        {
            $this->cart = new Cart(['storage' => new FakeStorage()]);
        }
    }
    
  5. features/bootstrap/StorageContext.php 文件中,添加以下内容:

    <?php
    use Behat\Behat\Context\SnippetAcceptingContext;
    use book\cart\Cart;
    use book\cart\features\bootstrap\storage\FakeStorage;
    use book\cart\storage\SessionStorage;
    use yii\di\Container;
    use yii\web\Application;
    
    require_once __DIR__ . '/bootstrap.php';
    
    class StorageContext implements SnippetAcceptingContext
    {
        /**
         * @var SessionStorage
         * */
        private $storage;
    
        /**
         * @Given there is a clean storage
         */
        public function thereIsACleanStorage()
        {
            $this->mockApplication();
            $this->storage = new SessionStorage(['key' => 'test']);
        }
    
        /**
         * @When I save :pieces of :product to the storage
         */
        public function iSavePiecesOfProductToTheStorage($pieces, $product)
        {
            $this->storage->save([$product => $pieces]);
        }
    
        /**
         * @Then I should have empty storage
         */
        public function iShouldHaveEmptyStorage()
        {
            PHPUnit_Framework_Assert::assertCount(
                0,
                $this->storage->load()
            );
        }
    
        /**
         * @Then I should have :pieces of :product in the storage
         */
        public function iShouldHavePiecesOfProductInTheStorage($pieces, $product)
        {
            PHPUnit_Framework_Assert::assertArraySubset(
                [intval($product) => intval($pieces)],
                $this->storage->load()
            );
        }
    
        private function mockApplication()
        {
            Yii::$container = new Container();
            new Application([
                'id' => 'testapp',
                'basePath' => __DIR__,
                'vendorPath' => __DIR__ . '/../../vendor',
            ]);
        }
    }
    
  6. 添加 features/bootstrap/CartContext/FakeStorage.php 文件,其中包含一个模拟存储类:

    <?php
    namespace book\cart\features\bootstrap\storage;
    
    use book\cart\storage\StorageInterface;
    
    class FakeStorage implements StorageInterface
    {
        private $items = [];
    
        public function load()
        {
            return $this->items;
        }
    
        public function save(array $items)
        {
            $this->items = $items;
        }
    }
    
  7. 添加 book/cart/behat.yml 并定义上下文:

    default:
        suites:
            default:
                contexts:
                    - CartContext
                    - StorageContext
    
  8. 现在我们将得到以下结构:

    book
    └── cart
        ├── src
        │   ├── storage
        │   │   ├── SessionStorage.php
        │   │   └── StorageInterface.php
        │   └── Cart.php
        ├── features
        │   ├── bootstrap
        │   │   ├── storage
        │   │   │   └── FakeStorage.php
        │   │   ├── bootstrap.php
        │   │   ├── CartContext.php
        │   │   └── StorageContext.php
        │   ├── cart.feature
        │   └── storage.feature
        ├── .gitignore
        ├── behat.yml
        ├── composer.json
        └── vendor
    

现在我们可以运行我们的测试。

运行测试

在使用 composer install 命令安装所有依赖项期间,Composer 包管理器将 Behat 包安装到 vendor 目录,并将可执行文件 behat 放在 vendor/bin 子目录中。

现在我们可以运行以下脚本:

cd book/cart
vendor/bin/behat

此外,我们还必须查看以下测试报告:

Feature: Shopping cart
 In order to buy products
 As a customer
 I need to be able to put interesting products into a cart

 Scenario: Checking empty cart             # features/cart.feature:6
 Given there is a clean cart             # thereIsACleanCart()
 Then I should have 0 products           # iShouldHaveProductInTheCart()
 Then I should have 0 product            # iShouldHaveProductInTheCart()
 And the overall cart amount should be 0 # theOverallCartPriceShouldBePs()

 ...

Feature: Shopping cart storage
 I need to be able to put items into a storage

 Scenario: Checking empty storage   # features/storage.feature:4
 Given there is a clean storage   # thereIsACleanStorage()
 Then I should have empty storage # iShouldHaveEmptyStorage()

 ...

6 scenarios (6 passed)
31 steps (31 passed)
0m0.23s (13.76Mb)

尝试通过注释 unset 操作来故意破坏购物车:

class Cart extends Component
{
    …

    public function set($id, $amount)
    {
        $this->loadItems();
        // $this->_items[$id] = $amount;
        $this->saveItems();
    }

    ...
}

现在再次运行测试:

Feature: Shopping cart
  In order to buy products
  As a customer
Feature: Shopping cart
  In order to buy products
  As a customer
  I need to be able to put interesting products into a cart

  ...

  Scenario: Change product count in the cart       # features/cart.feature:31
    Given there is a cart with 5 pieces of 7 prod  # thereIsAWhichCostsPs()
    When I set 3 pieces for 7 product              # iSetPiecesForProduct()
    Then I should have 3 pieces of 7 product       # iShouldHavePiecesOf()
      Failed asserting that an array has the subset Array &0 (
          7 => 3
      ).

  Scenario: Remove products from the cart         # features/cart.feature:36
    Given there is a cart with 5 pieces of 7 prod # thereIsAWhichCostsPs()
    When I add 14 pieces of 7 product             # iAddTheToTheCart()
    And I clear cart                              # iClearCart()
    Then I should have empty cart                 # iShouldHaveEmptyCart()

--- Failed scenarios:

    features/cart.feature:31

6 scenarios (5 passed, 1 failed)
31 steps (30 passed, 1 failed)
0m0.22s (13.85Mb)

在这种情况下,我们看到了一次失败和一份失败报告。

它是如何工作的…

Behat 是一个 BDD 测试框架。它简化了编写先导的人类可读测试场景到低级技术实现的转换。

当我们为每个功能编写场景时,我们可以使用一组操作符:

Scenario: Adding products to the cart
    Given there is a clean cart
    When I add 3 pieces of 5 product
    Then I should have 3 pieces of 5 product
    And I should have 1 product
    And the overall cart amount should be 3

Behat 解析我们的句子,并在上下文类中找到与句子关联的实现:

class FeatureContext implements SnippetAcceptingContext
{    
    /**
     * @When I add :pieces of :product
     */
    public function iAddTheToTheCart($product, $pieces)
    {
        $this->cart->add($product, $pieces);
    }
}

您可以创建一个单个的 FeatureContex t 类(默认)或为功能组和场景创建一组特定上下文。

参见

有关 Behat 的更多信息,请参阅以下 URL:

要获取有关替代测试框架的更多信息,请参阅本章中的其他食谱。

第十二章。调试、日志记录和错误处理

在本章中,我们将涵盖以下主题:

  • 使用不同的日志路由

  • 分析 Yii 错误堆栈跟踪

  • 记录和使用上下文信息

  • 显示自定义错误

  • 调试扩展的自定义面板

简介

如果应用程序相对复杂,那么不可能创建一个无错误的程序,因此开发者必须尽快检测错误并处理它们。Yii 提供了一套良好的实用功能来处理日志记录和错误处理。此外,在调试模式下,如果出现错误,Yii 会提供堆栈跟踪。使用它,你可以更快地修复错误。

在本章中,我们将回顾日志记录、分析异常堆栈跟踪以及实现我们自己的错误处理器。

使用不同的日志路由

日志记录是在你无法调试应用程序时理解应用程序实际行为的关键。信不信由你,即使你 100%确信应用程序将按预期行为,在生产环境中,它可能做许多你不知道的事情。这是正常的,因为没有人能了解一切。因此,如果我们期望出现异常行为,我们需要尽快了解它,并拥有足够的详细信息来重现它。这就是日志记录发挥作用的地方。

Yii 不仅允许开发者记录消息,还可以根据消息级别和类别以不同的方式处理它们。例如,你可以将消息写入数据库,发送电子邮件,或者只是在浏览器中显示。

在本食谱中,我们将以明智的方式处理日志消息:最重要的消息将通过电子邮件发送,不太重要的消息将保存在文件 A 和 B 中,性能分析将路由到 Firebug。此外,在开发模式下,所有消息和性能信息都将显示在屏幕上。

准备工作

使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,如官方指南中所述,www.yiiframework.com/doc-2.0/guide-start-installation.html

如何操作...

执行以下步骤:

  1. 使用config/web.php配置日志:

    'components' => [
        'log' => [
            'traceLevel' => 0,
            'targets' => [
                [
                    'class' => 'yii\log\EmailTarget',
                    'categories' => ['example'],
                    'levels' => ['error'],
                    'message' => [
                        'from' => ['log@example.com'],
                        'to' => ['developer1@example.com', 'developer2@example.com'],
                        'subject' => 'Log message',
                    ],
                ],
                [
                    'class' => 'yii\log\FileTarget',
                    'levels' => ['error'],
                    'logFile' => '@runtime/logs/error.log',
                ],
                [
                    'class' => 'yii\log\FileTarget',
                    'levels' => ['warning'],
                    'logFile' => '@runtime/logs/warning.log',
                ],
                [
                    'class' => 'yii\log\FileTarget',
                    'levels' => ['info'],
                    'logFile' => '@runtime/logs/info.log',
                ],
            ],
        ],
    
        'db' => require(__DIR__ . '/db.php'),
    ],
    
  2. 现在,我们将在protected/controllers/LogController.php中生成一些日志消息,如下所示:

    <?php
    namespace app\controllers;
    
    use yii\web\Controller;
    use Yii;
    
    class LogController extends Controller
    {
        public function actionIndex()
        {
            Yii::trace('example trace message', 'example');
            Yii::info('info', 'example');
            Yii::error('error', 'example');
            Yii::trace('trace', 'example');
            Yii::warning('warning','example');
    
            Yii::beginProfile('preg_replace', 'example');
            for($i=0;$i<10000;$i++){
                preg_replace('~^[ a-z]+~', '', 'test it');
            }
            Yii::endProfile('preg_replace', 'example');
    
            return $this->render('index');
        }
    }
    

    并查看views/log/index.php

    <div class="log-index">
        <h1>Log</h1>
    </div>
    
  3. 现在多次运行前面的操作。在屏幕上,你应该看到日志标题和一个带有日志消息数量的调试面板:如何操作...

  4. 如果你点击17,你将看到一个类似于以下截图的网页日志:如何操作...

  5. 日志包含我们记录的所有消息以及堆栈跟踪、时间戳、级别和类别。

  6. 现在打开性能分析页面。你应该能看到分析器消息,如下面的截图所示:如何操作...

    性能分析信息显示自身代码块的总执行时间。

  7. 由于我们只更改了日志文件名而没有更改路径,你应该在 runtime/logs 中查找名为 error.logwarning.loginfo.log 的日志文件。

  8. 在内部,你可以找到以下消息:

    2016-03-06 07:28:35 [127.0.0.1][-][-][error][example] error
    ...
    2016-03-06 07:28:35 [127.0.0.1][-][-][warning][example] warning
    ...
    2016-03-06 07:28:35 [127.0.0.1][-][-][info][example] inf
    o
    
    

它是如何工作的…

当使用 Yii::errorYii::warningYii::infoYii::trace 记录消息时,Yii 会将其传递给日志路由器。

根据其配置方式,它将消息传递给一个或多个目标,例如,通过 Yii::$app->mailer 组件通过电子邮件发送错误,将调试信息写入文件 A,并将警告信息写入文件 B。

yii\log\Dispatcher 类的对象通常附加到名为 log 的应用程序组件。因此,为了配置它,我们应该在配置文件的 components 部分设置其属性。那里唯一可配置的属性是 targets,它包含一个日志路由及其配置的数组。

我们已经定义了四个日志路由。以下是如何回顾它们的:

[
    'class' => 'yii\log\EmailTarget',
    'categories' => ['example'],
    'levels' => ['error'],
    // 'mailer' => 'mailer',
    'message' => [
        'from' => ['log@example.com'],
        'to' => ['developer1@example.com', 'developer2@example.com'],
        'subject' => 'Log error,
    ],
],

EmailTarget 默认通过 Yii::$app->mailer 组件通过电子邮件发送日志消息。我们限制类别为 example 和级别为 error。一封电子邮件将从 log@example.com 发送到两位开发者,主题为 Log error

[
    'class' => 'yii\log\FileTarget',
    'levels' => [warning],
    'logFile' => '@runtime/logs/warning.log',
],

FileTarget 将错误消息追加到指定的文件。我们限制消息级别为警告,并使用名为 warning.log 的文件。我们使用名为 Info.log 的文件以相同的方式为 info 级别的消息执行相同的操作。

此外,我们可以使用 yii\log\SyslogTarget 将消息写入 Unix /var/log/syslog 系统文件,以及使用 yii\log\DbTarget 将日志写入数据库。对于后者,你必须应用它们的迁移:

./yii migrate --migrationPath=@yii/log/migrations/

还有更多…

关于 Yii 日志还有更多有趣的事情,将在以下子节中介绍。

Yii::trace 与 Yii::getLogger()->log 的比较

Yii::traceYii::log 的简单包装:

public static function trace($message, $category = 'application')
{
    if (YII_DEBUG) {
        static::getLogger()->log($message, Logger::LEVEL_TRACE, $category);
    }
}

因此,如果 Yii 处于 debug 模式,Yii::trace 会记录一个具有跟踪级别的消息。

Yii::beginProfileYii::endProfile

这些方法用于测量应用程序代码某些部分的执行时间。在我们的 LogController 中,我们测量了 preg_replace 的 10,000 次执行,如下所示:

Yii::beginProfile('preg_replace', 'example');
for($i=0;$i<10000;$i++){
    preg_replace('~^[ a-z]+~', '', 'test it');
}
Yii::endProfile('preg_replace', 'example');

Yii::beginProfile 标记了代码块开始进行性能分析。我们必须为每个代码块设置一个唯一的令牌,并可选地指定一个类别:

public static function beginProfile($token, $category = 'application') { … }

Yii::endProfile 必须与具有相同类别名称的先前 beginProfile 调用相匹配:

public static function endProfile($token, $category = 'application') { … }

begin-end- 调用也必须正确嵌套。

立即记录日志

默认情况下,Yii 会将所有日志消息保存在内存中,直到应用程序终止。这样做是为了性能原因,并且通常工作得很好。

然而,如果有一个运行时间较长的控制台应用程序,日志消息将不会立即写入。为了确保你的消息在任何时候都会被记录,你可以显式地使用 Yii::$app->getLogger()->flush(true) 或更改控制台应用程序配置中的 flushIntervalexportInterval

'components' => ['log' => ['flushInterval' => 1,'targets' => [['class' => 'yii\log\FileTarget','exportInterval' => 1,],],    ],
],

参见

分析 Yii 错误堆栈跟踪

当发生错误时,Yii 可以显示错误堆栈跟踪以及错误信息。堆栈跟踪在需要知道真正导致错误的原因,而不仅仅是错误发生的事实时特别有用。

准备工作

  1. 使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,如官方指南中所述,请参阅www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 配置数据库并导入以下迁移:

    <?php
    use yii\db\Migration;
    class m160308_093234_create_article_table extends Migration
    {
        public function up()
        {
            $this->createTable('{{%article}}', [
                'id' => $this->primaryKey(),
                'alias' => $this->string()->notNull(),
                'title' => $this->string()->notNull(),
                'text' => $this->text()->notNull(),
            ]);
        }
    
        public function down()
        {
            $this->dropTable('{{%article}}');
        }
    }
    
  3. 使用 Yii 生成一个Article模型。

如何操作...

执行以下步骤:

  1. 现在我们需要创建一些代码来工作。创建protected/controllers/ErrorController.php如下:

    <?php
    
    namespace app\controllers;
    
    use app\models\Article;
    use yii\web\Controller;
    
    class ErrorController extends Controller
    {
        public function actionIndex()
        {
            $article = $this->findModel('php');
    
            return $article->title;
        }
    
        private function findModel($alias)
        {
            return Article::findOne(['allas' => $alias]);
        }
    }
    
  2. 运行前面的动作后,我们应该得到以下错误:如何操作...

  3. 此外,堆栈跟踪显示了以下错误:如何操作...

它是如何工作的...

从错误信息中,我们知道数据库中没有别名列,但我们却在代码的某个地方使用了它。在我们的例子中,只需通过搜索所有项目文件就能轻松找到它,但在大型项目中,一个列可能存储在一个变量中。此外,我们可以在显示堆栈跟踪的屏幕上修复错误,而无需离开。我们只需仔细阅读即可。

堆栈跟踪显示了一系列调用,从导致错误的调用开始,顺序相反。通常,我们不需要阅读整个跟踪来了解发生了什么。框架代码本身经过了良好的测试,因此错误的可能性较小。这就是为什么 Yii 显示应用程序跟踪条目展开,而框架跟踪条目折叠。

因此,我们选择第一个展开的部分,查找别名。在找到它之后,我们可以立即告诉它在ErrorController.php的第 19 行被使用。

参见

日志记录和使用上下文信息

有时日志消息不足以修复错误。例如,如果你遵循最佳实践,并使用所有可能出现的错误报告开发和测试应用程序,你可能会得到一个错误消息。然而,没有执行上下文,它只是在告诉你有一个错误,但并不清楚实际上是什么导致了它。

对于我们的示例,我们将使用一个非常简单且编码质量差的动作,该动作只是回显Hello, <username>!,其中username直接从$_GET获取。

准备工作

使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,具体操作请参考官方指南中的www.yiiframework.com/doc-2.0/guidestart-installation.html

如何操作...

执行以下步骤:

  1. 首先,我们需要一个控制器来工作。因此,创建protected/controllers/LogController.php如下:

    <?php
    namespace app\controllers;
    
    use yii\web\Controller;
    
    class LogController extends Controller
    {
        public function actionIndex()
        {
            return 'Hello, ' . $_GET['username'];
        }
    }
    
  2. 现在,如果我们运行索引操作,我们将得到错误消息Undefined index: username。让我们配置记录器将这类错误写入文件:

    config/web.php:
    
    'components'=>array(
        ...
        'log' => [
            'targets' => [
                [
                    'class' => 'yii\log\FileTarget',
                    'levels' => ['error'],
                    'logFile' => '@runtime/logs/errors.log',
                ],
            ],
        ],
    ],
    
  3. 再次运行索引操作并检查runtime/logs/errors.log文件。应该会有如下类似的日志信息:

    2016-03-06 09:27:09 [127.0.0.1][-][-][error][yii\base\ErrorException:8] exception 'yii\base\ErrorException' with message 'Undefined index: username' in /controllers/LogController.php:11
    Stack trace:
    #0 /yii2/base/InlineAction.php(55): ::call_user_func_array()
    #1 /yii2/base/Controller.php(151): yii\base\InlineAction->runWithParams()
    #2 /yii2/base/Module.php(455): yii\base\Controller->runAction()
    #3 /yii2/web/Application.php(84): yii\base\Module->runAction()
    #4 /yii2/base/Application.php(375): yii\web\Application->handleRequest()
    #5 /web/index.php(12): yii\base\Application->run()
    #6 {main}
    2016-03-06 09:27:09 [127.0.0.1][-][-][info][application] $_GET = [
     'r' => 'log/index'
    ]
    
    $_COOKIE = [
     '_csrf' => 'ca689043348e...a69ea:2:{i:0;s:...\"DSS...KJ\";}'
     'PHPSESSID' => '30584oqhat4ek8b0hrqsapsbf4'
    ]
    
    $_SERVER = [
     'USER' => 'www-data'
     'HOME' => '/var/www'
     'FCGI_ROLE' => 'RESPONDER'
     'QUERY_STRING' => 'r=log/index'
     ...
     'PHP_SELF' => '/index.php'
     'REQUEST_TIME_FLOAT' => 1459934829.3067
     'REQUEST_TIME' => 1459934829
    ]
    
    
  4. 现在,我们可以将我们的应用程序交给测试团队,并定期检查错误日志。默认情况下,错误报告日志包含$_GET$_POST$_FILES$_COOKIE$_SESSION$_SERVER变量的值。如果你不想显示所有值,你可以指定一个自定义变量列表:

    'log' => [
        'targets' => [
            [
                'class' => 'yii\log\FileTarget',
                'levels' => ['error'],
                'logVars' => ['_GET', '_POST'],
                'logFile' => '@runtime/logs/errors.log',
            ],
        ],
    ],
    
  5. 在这种情况下,报告将只包含$_GET$_POST数组:

    ...
    2016-04-06 09:49:08 [127.0.0.1][-][-][info][application] $_GET = [ 'r' => 'log/index'
    ]
    

它是如何工作的...

在记录错误信息的情况下,Yii 会在执行上下文和环境信息中添加完整信息。如果我们手动记录消息,那么我们可能知道需要哪些信息,因此我们可以设置一些目标选项,只写入我们真正需要的信息:

'log' => [
    'targets' => [
        [
            'class' => 'yii\log\FileTarget',
            'levels' => ['error'],
            'logVars' => ['_GET', '_POST'],
            'logFile' => '@runtime/logs/errors.log',
        ],
    ],
],

上述代码将错误记录到名为 errors 的文件中。除了消息本身外,如果$_GET$_POST变量不为空,它还会记录这些变量的内容。

参见

显示自定义错误

在 Yii 中,错误处理非常灵活,因此你可以为特定类型的错误创建自己的错误处理器。在这个菜谱中,我们将以智能的方式处理 404 未找到错误。我们将显示一个自定义的 404 页面,该页面将根据地址栏中输入的内容建议内容。

准备工作

  1. 使用 Composer 包管理器创建一个新的yii2-app-basic应用程序,具体操作请参考官方指南中的www.yiiframework.com/doc-2.0/guide-start-installation.html

  2. 将失败操作添加到你的SiteController中:

    class SiteController extends Controller
    {
        // …
    
        public function actionFail()
        {
            throw new ServerErrorHttpException('Error message example.');
        }
    }
    
  3. 添加以下内容的web/.htaccess文件:

    RewriteEngine on
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . index.php
    
  4. 在你的config/web.php文件中为urlManager组件配置漂亮的 URL:

    'components' => [
        // …
        'urlManager' => [
            'enablePrettyUrl' => true,
                'showScriptName' => false,
            ],
    ],
    
  5. 检查框架是否为不存在的 URL 显示Not found异常:准备工作

  6. 此外,检查框架是否为我们的actionFail显示Internal Server Error异常:准备工作

  7. 现在,我们想要为Not Found页面创建一个自定义页面。让我们开始吧。

如何操作...

现在我们需要更改 Not Found 页面的内容,但保留其他错误类型的原始内容。为了实现这一点,请按照以下步骤操作:

  1. 打开 SiteController 类,并查找 actions() 方法:

    class SiteController extends Controller
    {
        // ...
        public function actions()
        {
            return [
                'error' => [
                    'class' => 'yii\web\ErrorAction',
                ],
                'captcha' => [
                    'class' => 'yii\captcha\CaptchaAction',
                    'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
                ],
            ];
        }
        // ...
    }
    
  2. 删除默认的 error 部分,并将 actions() 保留如下:

    class SiteController extends Controller
    {
        // ...
        public function actions()
        {
            return [
                'captcha' => [
                    'class' => 'yii\captcha\CaptchaAction',
                    'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
                ],
            ];
        }
        // ...
    }
    
  3. 添加自己的 actionError() 方法:

    class SiteController extends Controller
    {
        // ...
        public function actionError()
        {
    
        }
    }
    
  4. 打开原始的 \yii\web\ErrorAction 类,并将其操作内容复制到我们的 actionError() 中,并对其进行自定义,以便渲染自定义的 error-404 视图,用于具有 404 代码的 Not Found 错误:

    // ...
    use yii\base\Exception;
    use yii\base\UserException;
    
    class SiteController extends Controller
    {
        // ...
        public function actionError()
        {
            if (($exception = Yii::$app->getErrorHandler()->exception)== null) {
                $exception = new HttpException(404, Yii::t('yii', 'Page not found.'));
            }
    
            if ($exception instanceof HttpException) {
                $code = $exception->statusCode;
            } else {
                $code = $exception->getCode();
            }
            if ($exception instanceof Exception) {
                $name = $exception->getName();
            } else {
                $name = Yii::t('yii', 'Error');
            }
            if ($code) {
                $name .= " (#$code)";
            }
    
            if ($exception instanceof UserException) {
                $message = $exception->getMessage();
            } else {
                $message = Yii::t('yii', 'An internal server error occurred.');
            }
    
            if (Yii::$app->getRequest()->getIsAjax()) {
                return "$name: $message";
            } else {
                if ($code == 404) {
                    return $this->render('error-404');
                } else {
                    return $this->render('error', [
                        'name' => $name,
                        'message' => $message,
                        'exception' => $exception,
                    ]);
                }
            }
        }
    
    }
    
  5. 添加带有自定义信息的 views/site/error-404.php 视图文件:

    <?php
    use yii\helpers\Html;
    
    /* @var $this yii\web\View */
    
    $this->title = 'Not Found!'
    ?>
    <div class="site-error-404">
    
        <h1>Oops!</h1>
    
        <p>Sorry, but requested page not found.</p>
    
        <p>
            Please follow to <?= Html::a('index page', ['site/index'])?>
            to continue reading. Thank you.
        </p>
    
    </div>
    
  6. 就这样。现在尝试访问一个不存在的 URL,并从 error-404.php 视图中查看我们的内容:如何操作...

  7. 然而,对于失败的操作,我们必须看到来自 error.php 文件中的默认内容:如何操作...

它是如何工作的...

默认情况下,在 yii2-app-basic 应用程序中,我们在配置文件 config/web.php 中为 errorHandler 组件配置 errorActionsite/error。这意味着框架将使用此路由来显示每个处理的异常:

'components' => [
    'errorHandler' => [
        'errorAction' => 'site/error',
    ],
],

SiteController 类中,我们使用内置的独立 yii\web\ErrorAction 类,该类渲染所谓的 error.php 视图:

class SiteController extends Controller
{
    // ...
    public function actions()
    {
        return [
            'error' => [
                'class' => 'yii\web\ErrorAction',
            ],
            'captcha' => [
                'class' => 'yii\captcha\CaptchaAction',
                'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
            ],
        ];
    }
    // ...
}

如果我们想覆盖其实现,我们可以在内联的 actionError() 方法中用我们自己的自定义内容替换它。

在这个菜谱中,我们添加了自己的 if 语句,以便基于错误代码渲染特定的视图:

if ($code == 404) {
    return $this->render('error-404');
} else {
    return $this->render('error', [
        'name' => $name,
        'message' => $message,
        'exception' => $exception,
    ]);
}

此外,我们还可以为 Not Found 页面使用自定义设计。

参见

为了了解更多关于在 Yii 中处理错误的信息,请参阅 www.yiiframework.com/doc-2.0/guide-runtime-handling-errors.html

调试扩展的自定义面板

Yii2-debug 扩展是一个强大的调试工具,可以用于调试自己的代码、分析请求信息或数据库查询等。因此,您可以为任何自定义报告添加自己的面板。

准备工作

按照官方指南使用 Composer 包管理器创建一个新的 yii2-app-basic 应用程序,官方指南请见 www.yiiframework.com/doc-2.0/guidestart-installation.html

如何操作...

  1. 在您网站的根路径上创建 panels 目录。

  2. 添加一个新的 UserPanel 类:

    <?php
    namespace app\panels;
    
    use yii\debug\Panel;
    use Yii;
    
    class UserPanel extends Panel
    {
        public function getName()
        {
            return 'User';
        }
    
        public function getSummary()
        {
            return Yii::$app->view->render('@app/panels/views/summary', ['panel' => $this]);
        }
    
        public function getDetail()
        {
            return Yii::$app->view->render('@app/panels/views/detail', ['panel' => $this]);
        }
    
        public function save()
        {
            $user = Yii::$app->user;
    
            return !$user->isGuest ? [
                'id' => $user->id,
                'username' => $user->identity->username,
            ] : null;
        }
    }
    
  3. 使用以下代码创建 panels/view/summary.php 视图:

    <?php
    /* @var $panel app\panels\UserPanel */
    use yii\helpers\Html;
    ?>
    <div class="yii-debug-toolbar__block">
        <?php if (!empty($panel->data)): ?>
            <a href="<?= $panel->getUrl() ?>">
                User
                <span class="yii-debug-toolbar__label yii-debug-toolbar__label_info">
                    <?= Html::encode($panel->data['username']) ?>
                </span>
            </a>
        <?php else: ?>
            <a href="<?= $panel->getUrl() ?>">Guest session</a>
        <?php endif; ?>
    </div>
    
  4. 添加 panels/view/detail.php 视图,代码如下:

    <?php
    /* @var $panel app\panels\UserPanel */
    use yii\widgets\DetailView;
    ?>
    <h1>User profile</h1>
    <?php if (!empty($panel->data)): ?>
        <?= DetailView::widget([
            'model' => $panel->data,
            'attributes' => [
                'id',
                'username',
            ]
        ]) ?>
    <?php else: ?>
        <p>Guest session.</p>
    <?php endif;?>
    
  5. config/web.php 配置文件中打开您的工具栏:

    if (YII_ENV_DEV) {
        $config['bootstrap'][] = 'debug';
        $config['modules']['debug'] = [
            'class' => 'yii\debug\Module',
            'panels' => [
                'views' => ['class' => 'app\panels\UserPanel'],
            ],
        ];
        $config['bootstrap'][] = 'gii';
        $config['modules']['gii'] = 'yii\gii\Module';
    }
    
  6. 重新加载 index 页面,并在调试面板的末尾查找 Guest Session 单元:如何操作...

  7. 使用 admin 用户名和 admin 密码登录到您的网站。在成功的情况下,您必须在主菜单中看到您的用户名:如何操作...

  8. 再次观察调试面板。现在,您将看到 admin 用户名:如何操作...

  9. 您可以在调试面板中点击用户名,查看详细的用户信息:如何操作...

它是如何工作的...

要为yii2-debug模块创建自己的面板,我们需要扩展yii\debug\Panel类并重写其模板方法:

  • getName(): 调试详细页面上的菜单项标签

  • getSummary(): 调试面板单元格代码

  • getDetail(): 详细页面视图代码

  • save(): 您的信息将被保存在调试存储中,并在加载回$panel->data字段

您的对象可以存储任何调试数据,并在面板的摘要块和详细页面上显示它。

在我们的示例中,我们存储用户信息:

public function save()
{
    $user = Yii::$app->user;
    return !$user->isGuest ? [
        'id' => $user->id,
        'username' => $user->identity->username,
    ] : null;
}

$panel->data字段在摘要和详细页面上显示它。

处理事件

您可以在init()方法中订阅应用程序或任何组件的任何事件。例如,内置的yii\debug\panels\MailPanel面板收集并存储所有发送的消息:

class MailPanel extends Panel
{
    private $_messages = [];

    public function init()
    {
        parent::init();
        Event::on(
            BaseMailer::className(),
            BaseMailer::EVENT_AFTER_SEND,
            function ($event) {
                $message = $event->message;
                $messageData = [
                    // ...
                ];
                $this->_messages[] = $messageData;
            }
        );
    }

    // …

    public function save()
    {
        return $this->_messages;
    }
}

此外,它还在我们的详细页面上显示存储消息的列表网格。

参见

posted @ 2025-09-09 11:30  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报