Yii2-示例-全-

Yii2 示例(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书从零开始介绍 Yii2 框架的使用,直至构建一个完整的 Web 应用程序。

Yii 是一个高性能的 PHP 框架,最适合开发提供快速、安全、专业功能的 Web 2.0 应用程序,以快速创建健壮的项目。然而,这种快速开发需要将常见任务组织在一起以构建完整的应用程序。很容易对这些技术的使用感到困惑。

因此,通过实际示例的讲解将帮助您理解这些概念必须如何使用,并实现一个成功的应用程序。

本书涵盖的内容

第一章, 从 Yii2 开始,提供了关于 Yii2 框架的基本知识,从需求开始解释每个功能。然后,我们将使用调试和日志工具跟踪我们的代码并提供错误查找。最后,我们将基于基本模板编写我们的第一个项目。

第二章, 创建简单的新闻阅读器,创建了我们的第一个控制器和相关视图。我们将探索静态和动态视图,学习如何在布局中渲染视图以及从控制器传递数据到视图,然后查看通过部分视图和块重用视图的方法。

第三章, 创建漂亮的 URL,展示了如何实现漂亮的 URL,这对于搜索引擎优化很有用。我们还将创建使用自定义规则解析和创建 URL 的示例。最后,我们将学习如何通过 Rule 类构建更定制的 URL 规则。

第四章, 通过表单创建房间,展示了如何从头开始构建 Model 类,并使用 Yii2 ActiveForm 小部件从视图发送数据到控制器,该小部件创建表单。我们还将查看常用的格式化数据和方法以及从表单发送文件的方法。

第五章, 开发预订系统,解释了如何配置数据库连接并从零开始执行带有 DAO 框架支持的 SQL 查询。接下来,我们将了解如何使用 Gii 以及它在从数据库表结构创建模型方面的优势。Gii 创建的模型扩展了 ActiveRecord 类,通过使用它,我们最终将学习如何操作数据。

第六章, 使用网格显示数据和关系,介绍了用于显示数据(直接或相关)的 GridView 小部件。GridView 中的一个基本主题是数据提供者,即向 GridView 提供数据的方式。我们将学习如何根据可用来源从 ActiveRecord、数组或 SQL 获取数据提供者。

第七章, 处理用户界面,讨论了用户界面以及 Yii 如何通过其核心功能帮助我们。

第八章, 登录应用,展示了如何将用户认证和授权应用于应用。第一步是创建对应用的认证访问。为此,我们将创建一个数据库表来管理用户,并通过扩展 IdentityInterface 的用户模型将其与 Yii 用户组件关联。

第九章, 面向所有人的房间前端,解释了如何使用 Yii 基于前端和后端应用程序构建现代 Web 项目。我们将发现基本模板和高级模板之间的区别,并安装我们的第一个基于高级模板的高级项目。

第十章, 本地化应用,展示了如何在我们的应用中配置多种语言。我们将发现有两种存储选项来处理国际化:文件和数据库。

第十一章, 为移动应用创建 API,通过使用 Yii 提供的强大工具创建用于移动应用的 API。我们将采用创建新应用的方法来分发 RESTful Web 服务,而不是混合 Web 和 API 控制器。

第十二章, 创建控制台应用程序来自动执行周期性任务,解释了如何编写控制台应用程序,并允许你发现 Web 应用程序和控制台应用程序之间的主要区别。

第十三章, 最终重构,帮助你使用小部件和组件重用代码。我们将创建一些实际示例,展示如何使用它们。

你需要为这本书准备的东西

这本书的最低要求是:一个基于 PHP 5.4 环境的 Web 主机,无论是本地还是远程,并且安装了一个 MySQL 数据库服务器(没有对具体版本的要求)。

对于编写代码,只需要一个简单的语法高亮编辑器就足够了,例如块记事本、TextEdit、Notepad++、PSPad、Aptana 等等。

这本书面向的对象

这本书旨在为任何想要发现 Yii 框架或掌握其实践概念的人提供帮助。入门级用户将在每一章中找到一些介绍性理论,解释所讨论的主题,以及大量代码展示其实际方面。高级用户将找到许多具有特殊案例说明和常见错误解决的示例。

需要具备使用 PHP 和面向对象编程的基本编程经验。

习惯用法

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

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称应如下所示:“现在,使用以下内容在basic/views/my-authentication/login.php中创建视图。”

代码块应如下设置:

<?php
return [
    2 => [
        'operator',
    ],
    1 => [
        'admin',
    ],
];

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

$ curl -H "Accept: application/json" http://hostname/yiiadv/api/web/test-rest/index

[{"id":1,"name":"Albert","surname":"Einstein"},{"id":2,"name":"Enzo","surname":"Ferrari"},{"id":4,"name":"Mario","surname":"Bros"}]

注意

警告或重要注意事项以如下框中的形式出现。

提示

技巧和窍门如下所示。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们来说非常重要,因为它帮助我们开发出您真正能从中受益的书籍。

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

如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南,网址为www.packtpub.com/authors

客户支持

现在您是 Packt 书籍的骄傲拥有者,我们有多个方面可以帮助您从购买中获得最大收益。

下载示例代码

您可以从您在www.packtpub.com的账户下载示例代码文件,适用于您购买的所有 Packt 出版社的书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support,并注册以将文件直接发送给您。

错误

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

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

盗版

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

请通过链接发送至 <copyright@packtpub.com> 与我们联系,以提供涉嫌盗版材料的链接。

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

问题和建议

如果您对本书的任何方面有问题,您可以发送邮件至 <questions@packtpub.com> 与我们联系,我们将尽力解决问题。

第一章. 从 Yii2 开始

Yii2 是对最著名的 PHP 框架之一的第一版本的完全重写。它是一个文档齐全的框架,拥有非常活跃的社区。

正式来说,我们可以找到三种类型的支持:指南,可以在www.yiiframework.com/doc-2.0/guide-index.html中找到,以全面导航框架主题;参考,可以在www.yiiframework.com/doc-2.0/index.html中探索组成框架的所有类;最后,论坛支持在www.yiiframework.com/forum/

在本章中,我们将介绍以下内容:

  • 需求和工具

  • 使用 Composer 安装 Yii2

  • 应用结构

  • 应用属性

    • 常见的应用组件

    • 处理应用事件

    • Yii2 中的 MVC 模式

  • 命名规范

    • 配置调试工具栏

    • 使用日志记录器

    • 示例 - 使用 Yii 基本模板和 bootstrap 模板从头开始创建 hello world

需求和工具

Yii2 的基本需求是一个网络服务器(本地或远程)和 PHP v.5.4(或更高版本)。建议我们能够访问存储代码的机器(本地或远程)的 shell(或命令行),因为有一些脚本在开发复杂应用程序时非常有用。我们也可以在本地开发应用程序,并在我们希望测试它时将其上传到 Web 服务器。

对于远程托管,有多种选择。我们可以使用简单的支持 PHP v.5.4 的 Web 托管服务,或者选择虚拟或专用服务器托管。记住,在使用前者的情况下,如果服务器不符合 PHP 要求,更改任何问题都可能很困难。

Yii2 有一个名为requirements.php的脚本,用于检查我们的托管是否符合运行 Yii2 应用程序的要求。

使用 Composer 安装 Yii2

Composer 是 PHP 中依赖管理的工具。Yii2 使用它来安装自身和其他供应商的模块(例如,bootstrap)。

也可以使用旧的方式安装 Yii2,即下载完整包并将其传输到主机,无论是本地还是远程,框架都将安装在那里。然而,Composer 会给我们带来许多好处,比如可以轻松更新框架并确保所有包依赖都得到满足。Composer 实际上是安装和维护项目的标准方式,所以我建议从一开始就使用它。如果你不确定如何使用 Composer,值得一提的是,大多数用户最多只需要学习两到三个命令,所以学习曲线并不陡峭。

Yii2 提供了两个可用的模板来开始:基本和高级。我们将从基本模板开始,但接下来几章我们也会看到如何使用高级模板。

因此,让我们看看如何使用 Composer 安装 Yii2。我们需要通过控制台访问文件夹,其中 web 服务器的 httpdocs 指向并运行以下命令:

curl -s http://getcomposer.org/installer | php
php composer.phar global require "fxp/composer-asset-plugin:1.0.0"
php composer.phar create-project --prefer-dist yiisoft/yii2-app-basic basic

如果我们在 Linux 或 Mac 环境中,这些命令很有用。在 Windows 上,您需要从 Composer 的官方网站下载Composer-Setup.exe并运行它。

第一个命令获取getcomposer.org/installer URL 并将其传递给 PHP 以创建composer.phar文件。

第二个命令安装了 Composer 资产插件,这使我们能够通过 Composer 管理 bower 和 npm 包依赖。

第三个也是最后的命令是在名为basic的目录中安装 Yii2。如果您愿意,可以选择不同的目录名称。

注意

在安装过程中,Composer 可能会要求我们提供 GitHub 登录凭证,这是正常的,因为 Composer 需要获取足够的 API 速率限制来从 GitHub 检索依赖包信息。如果您还没有 GitHub 账户,现在是创建一个新账户的好时机!

如果我们使用 Windows,我们需要从getcomposer.org下载它并运行。最后两个命令将是相同的。

我们已经安装了 Yii2!

要测试它,请指向http://hostname/basic/web,我们应该看到我的 Yii 应用程序页面。

应用程序结构

Yii2 的应用程序结构非常清晰、精确且冗余(对于高级应用程序)。

basic文件夹的内容应该如下:

文件夹名称 描述
assets 这包括在网页中引用的文件(.js.css)以及应用程序的依赖项。
commands 这包括从命令行使用的控制器。
config 这包括从 web 使用的控制器。
mail 这是邮件布局仓库。
models 这包括整个应用程序中使用的模型。
runtime 这用于 Yii2 存储运行时数据作为日志。
tests 这包括所有测试的仓库(单元、功能、固定等)。
vendor 这包括由 Composer 管理的第三方模块仓库。
views 这包含 PHP 文件,分为文件夹,这些文件夹与控制器名称相关联,用于渲染页面模板的主要内容。它主要从控制器动作中调用以渲染显示输出。一个名为 layout 的文件夹包含页面模板的 PHP 文件。
web 这是网页的入口点

打开web/index.php以查看内容:

<?php
// comment out the following two lines when deployed to production
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');

require(__DIR__ . '/../vendor/autoload.php');
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');

$config = require(__DIR__ . '/../config/web.php');

(new yii\web\Application($config))->run();

在这里,前两个常量定义非常重要。

YII_DEBUG定义了您是否处于调试模式。如果我们设置了它,我们将有更多的日志信息,并且会看到详细的错误调用堆栈。

YII_ENV 定义了我们正在工作的环境模式,默认值是 prod。可用的值有 testdevprod。这些值在配置文件中使用,例如定义不同的数据库连接(本地数据库与远程数据库不同)或其他值,始终在配置文件中。

由于我们处于项目的开始阶段,建议将 YII_DEBUG 设置为 true,以便在代码中出错时获得更详细的信息,而不是无用的空白。

下表包含了一个列表,列出了所有 Yii2 的对象:

对象 描述

| 模型、视图和控制器 | 这些是应用中常用的对象,可以应用 MVC 模式:

  • 模型是数据表示和操作,通常来自数据库

  • 视图用于向最终用户展示数据

  • 控制器是处理请求并生成响应的对象

|

组件 这些是包含逻辑的对象。用户可以编写自己的组件来创建可重用的功能。例如,一个组件可以是货币转换对象,可以在我们的应用中的多个实例中使用。
应用组件 它们是可以在应用中的任何位置调用的单例对象。单例意味着在整个应用中只实例化一次对象(因此对象始终是相同的)。应用组件与组件之间的区别在于,前者在整个应用中只有一个实例。
小部件 这些是包含逻辑和渲染代码的可重用视图对象。例如,一个小部件可以是显示今天天气信息的框。
过滤器 这些是在控制器动作执行前后运行的对象。过滤器可以用来改变页面响应输出的格式,例如,从 HTML 到 JSON。
模块 这包含了一个应用中所有的对象,例如模型(Models)、视图(Views)、控制器(Controller)、组件(Components)等;我们可以把它们看作是子应用,包含可重用的部分(例如,用户管理)。
扩展 扩展是打包的模块,我们可以使用 Composer 轻松管理它们。

应用属性

一个 Yii2 应用可以通过多个属性进行配置。

以下表格列出了任何应用中需要配置的属性:

属性 描述
id 这表示一个唯一的 ID,用于区分此应用与其他应用。它主要用于程序化操作。这个属性的例子是 basic
basePath 这指定了应用的根目录。这个路径是所有其他类型的应用对象(如模型、控制器、视图)的起点。这个属性的例子是 dirname(__DIR__)

以下表格列出了其他常见的属性:

属性 描述
aliases 这表示路径定义的别名。它们使用键/值数组定义,当我们需要将路径设置为在整个应用程序中存在的常量时非常有用。我们输入一个以@字符为前缀的别名。此属性的示例是'@fileupload' => 'path/to/files/uploaded'
bootstrap 此属性允许您配置在应用程序启动过程中运行的组件数组。常见用法是加载日志或配置文件组件、gii 或任何其他组件。请注意不要加载太多组件,否则您页面的响应性能可能会下降。此属性的示例是'log''gii'
catchAll 此属性捕获每个请求,并在网站的维护模式下使用。
components 此属性指出可以在整个应用程序中使用的应用程序组件列表。
language 此属性指定用于显示内容的语言。此属性的示例是'language' => 'en'
modules 此属性指出应用程序中可以使用的应用程序模块列表。
name 此属性指示您的应用程序名称。此属性的示例是'name' => 'My App'
params 此属性指定一个参数数组,通过键/值对。这是一个全局参数的容器,例如管理员的电子邮件地址。
timeZone 此属性指示应用程序应使用的时间区域。此属性的示例是'timeZone' => 'Europe/Rome'
charset 此属性指出应用程序中使用的字符集。默认值是UTF-8
defaultRoute 此属性包含一个在请求未指定时使用的路由。根据我们使用的环境,此属性有不同的默认值。对于 Web 应用程序,此值将是site,以便SiteController可以用来处理这些请求。对于控制台应用程序,此值将是help,以便可以使用yii\console\controllers\HelpController调用其索引操作,该操作将显示帮助信息。

常见应用程序组件

以下是最常用的应用程序组件列表:

  • request:此组件处理所有客户端请求,并提供从服务器全局变量(如$_SERVER$_POST$_GET$_COOKIES)中轻松获取参数的方法。

    默认状态下,enableCookieValidation设置为 true,因此您需要设置cookieValidationKey参数,如下例所示:

    'request' => [
    'cookieValidationKey' => 'hPpnJs7tvs0T4N2OGAY',
    ],
    
  • cache:此组件帮助您处理缓存数据。Yii2 默认使用FileCache实例作为缓存,但我们也可以配置ApcCacheDbCacheMemCache等。

    以下是对 Yii2 的标准安装:

    'cache' => [                     
    'class' => 'yii\caching\FileCache',
    ],
    
  • user:此组件处理应用程序中的用户身份验证。最重要的参数是 identityClass 参数,它定义了包含用户模型数据的类,以便有特定的登录或注销用户的应用程序方法。

    考虑以下示例:

    'user' => [
    'identityClass' => 'app\models\User',
             'enableAutoLogin' => true,
     ],
    
  • errorHandler:此组件提供处理未捕获的错误和异常的功能。可以通过指定要运行的操作进行配置。

    考虑以下示例:

    'errorHandler' => [
    'errorAction' => 'site/error',
    ],
    
  • mailer:此组件配置邮件发送系统的连接参数。通常,它是托管我们网站的同一台机器,因此默认值可能正确。

    考虑以下示例:

    'mailer' => [
      'class' => 'yii\swiftmailer\Mailer',
      // send all mails to a file by default. You have to set
      // 'useFileTransport' to false and configure a transport
         // for the mailer to send real emails.
         'useFileTransport' => true,
    ],
    
  • log:此组件主要用于调试环境中的应用程序执行日志。我们可以设置调试级别和目的地。

    考虑以下示例:

    'log' => [
               'traceLevel' => YII_DEBUG ? 3 : 0,
                'targets' => [
                    [
                        'class' => 'yii\log\FileTarget',
                        'levels' => ['error', 'warning'],
                    ],
                ],
     ],
    
  • db:此组件处理数据库连接。在我们的应用程序中可以有多个 db 配置;在这种情况下,我们可以定义更多位于 yii\db\Connection 类的组件。

    考虑以下示例:

    db => [
        'class' => 'yii\db\Connection',
        'dsn' => 'mysql:host=localhost;dbname=yii2basic',
        'username' => 'dbuser'',
        'password' => 'dbpassword',
        'charset' => 'utf8',
    ],
    

处理应用程序事件

在其生命周期中,一个应用程序可以触发许多事件。这些事件可以在应用程序配置中声明或通过程序方式声明。常见的触发器有 beforeRequestafterRequestbeforeActionafterAction,但每个对象都可以有自己的事件。

例如,事件的一个常见用途是设置 mysql db timezone

要在 db 组件配置中将时区设置为 UTC,我们必须为 afterOpen 事件定义一个处理程序:

'db' => [
  'class' => 'yii\db\Connection',
  'dsn' => 'mysql:host=localhost;dbname=mydb',
  'username' => 'dbuser',
  'password' => 'dbpassword',
  'charset' => 'utf8',

  'on afterOpen' => function($event) {
    $event->sender->createCommand("SET time_zone = '+00:00'")->execute();
       }
  ],

一个匿名函数,附加到 on afterOpen 事件处理程序,有一个 $event 参数,它是 yii\base\ActionEvent 类的实例。此类有一个 $sender 对象,它引用事件的发送者。在这种情况下,$sender 指的是数据库组件(db)的实例。当此事件是类级别事件时,此属性也可能为 null。

Yii2 中的 MVC 模式

Yii2 是根据 模型-视图-控制器MVC)设计模式构建的。

模型,代表逻辑,是从 \yii\base\Model 扩展的对象,提供了许多功能,例如属性、属性标签、大量赋值(直接为数组填充对象属性)、验证规则和数据导出。

通常,在常见应用程序中,模型将从数据库生成,扩展 yii\db\ActiveRecord,该类实现了 Active Record 设计模式,具有许多用于操作数据的方法。Yii2 提供了 Gii 工具,该工具用于直接从数据库的表结构生成模型类。

控制器,视图和模型之间的桥梁,是扩展自 yii\base\Controller 的类实例,用于处理请求和生成响应。

控制器主要包含以动作前缀开头的函数,这使得框架能够识别这些函数作为路由,可以请求。

最后,我们将查看处理向最终用户显示数据的视图,这些数据主要在页面布局中由控制器渲染。

命名规范

为了允许自动加载,Yii2 使用一个简单的标准来设置名称。

指向请求的模块、控制器和操作的路线采用以下格式:

ModuleID/ControllerID/ActionID

我们将如下详细查看每个元素:

  • ModuleID 是可选的,因此通常格式是 ControllerID/ActionID

  • 必须在模块的配置属性中指定 ModuleID,名称相同

  • ControllerID 和 ActionID 应仅包含小写英文字符、数字、下划线、破折号和正斜杠

路由的一个示例是 http://hostname/index.php?r=site/index,其中 site 是 ControllerID,index 是 ActionID。

从 ControllerID 开始,创建控制器类名非常简单。只需将每个由破折号分隔的单词的首字母转换为大写,然后删除破折号并附加后缀 Controller。如果 ControllerID 包含斜杠,只需将规则应用于 ID 中最后一个斜杠之后的部分。这是可能的,因为控制器可以从 app\controllers 开始收集在子文件夹中。

以下是一些示例:

  • 商店指向 app\controllers\ShopController

  • 优先数字指向 app\controllers\PreferredNumberController

  • 管理员/用户账户指向 app\controllers\admin\UsersAccountController命名规范

路由通过 r 参数传递到入口脚本 basic/web/index.php

注意

默认页面 http://hostname/basic/web/index.php 等同于 http://hostname/basic/web/index.php?r=site/index

配置调试工具栏

拥有一个丰富的工具集,以便在显示有关请求和响应的有用信息时使开发更容易,这是很重要的。

为了这个目的,Yii2 提供了一个显示多种信息的工具栏。

激活调试工具栏的一种常见方法是在 config/web.php 中设置:

'bootstrap' => ['debug'],
'modules' => [
  'debug' => 'yii\debug\Module',
]

现在,你可以设置以下值:

  • debug 配置节点更改为 bootstrap

  • debug 配置节点更改为 modules 配置节点,使用 yii\debug\ 下的 Module

Yii2 基本模板的默认安装已经启用了调试工具栏,正如我们可以在 config/web.php 配置文件的底部看到的那样。Gii 模块也已启用,但我们将稍后处理它。

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';
}

此配置条目仅在 YII_ENV_DEV 模式下有效。因此,我们必须检查 web/index.php YII_ENV 变量是否具有 dev 值(如默认安装所示)。

配置调试工具栏

调试工具栏已关闭

在这些检查之后,如果我们尝试重新加载 basic/web/index.php 上的网页,我们应该看到以下截图:

配置调试工具栏

调试工具栏已打开

右箭头报告调试工具栏处于活动状态但已关闭。如果我们点击它,完整的工具栏将打开。现在,点击任何项目,调试面板将显示。

默认情况下,调试工具栏只能在本地主机上使用。但是,如果我们正在使用 Yii2 在远程托管环境中,我们将设置 debug 模块的 allowedIPs 属性。

$config['modules']['debug'] = [
    'class' => 'yii\debug\Module',
    'allowedIPs' => [ '127.0.0.1', '::1']
];

allowedIPs 中只有本地主机(IPv4 和 IPv6 形式)。我们需要在这里放置我们的互联网连接和 IP 源地址,这可以通过使用互联网上的任何 我的 IP 服务轻松找到,例如 www.whatismyip.com/

如果我们的 IP 源是,例如,1.2.3.4,我们必须将此条目添加到 allowedIPs 中,这样:

$config['modules']['debug'] = [
    'class' => 'yii\debug\Module',
    'allowedIPs' => [ '127.0.0.1', '::1', '1.2.3.4']
];

记住,如果我们没有静态 IP 的互联网连接,这个 IP 可能会改变。因此,我们需要检查 allowedIPs 是否包含我们的当前 IP。

您也可以使用星号 * 允许所有 IP 地址,这样您就不必处理动态 IP 问题。如果您这样做,您需要记住在部署之前删除星号。最后,在我们的当前配置 config/web.php 的底部,您将看到以下代码:

if (YII_ENV_DEV) {
    // configuration adjustments for 'dev' environment
    $config['bootstrap'][] = 'debug';
    $config['modules']['debug'] = [
        'class' => 'yii\debug\Module',
            'allowedIPs' => [ '127.0.0.1', '::1', '1.2.3.4']
    ];
    $config['bootstrap'][] = 'gii';
    $config['modules']['gii'] = 'yii\gii\Module';
}

让我们回到 basic/web/index.php 网页并查看调试信息面板。

调试信息分布在菜单中:

  • 配置:这包括已安装的 PHP 版本和配置,以及已安装的 Yii2 框架版本。

  • 请求:这是关于刚刚发送的请求的信息,显示请求的参数、请求的头部和其他有用的数据,如响应和会话数据。

  • 日志:这涉及 Yii2 在执行期间执行的操作。本节中还有额外的过滤器,用于选择要显示的日志类型。

  • 性能分析:这包括关于处理过程的时间和持续时间的详细信息。

  • 数据库:这包括关于所有数据库查询发生的详细信息;我们可以过滤查询类型以定位特定查询。

可以使用内部网格过滤器过滤所有数据,或者过滤所有、最新或选择内容面板顶部最后 10 行日志。

使用记录器

在 Yii2 应用程序中,调试信息是通过日志组件存储的。我们可以在开发和生产环境中使用此工具,但出于性能和安全的考虑,在生产环境中,我们应该只记录重要消息。

Yii2 基本模板的默认配置文件在 config/web.phpcomponents 属性中提供日志条目:

'log' => [
  'traceLevel' => YII_DEBUG ? 3 : 0,
      'targets' => [
      [
             'class' => 'yii\log\FileTarget',
             'levels' => ['error', 'warning'],
      ],
    ],
],

示例 - 使用 Yii 基本模板和 Bootstrap 模板从头开始创建“Hello world”

现在是时候使用 Yii2 编写我们的第一个项目了。

如果我们还没有安装 Yii2,我们现在将使用 Composer 来完成安装,如下所示:

  1. 打开到 Web 服务器的命令提示符。

  2. 前往 Web 服务器文档根目录(Linux 机器上的 /var/www)。

  3. 启动这些命令(如 使用 Composer 安装 Yii 部分所述):

    curl -s http://getcomposer.org/installer | php
    php composer.phar global require "fxp/composer-asset-plugin:1.0.0"
    php composer.phar create-project --prefer-dist yiisoft/yii2-app-basic basic
    
    

现在,我们需要在 Web 服务器文档根的基本文件夹中安装一个全新的 Yii2。将浏览器指向http:/hostname/basic/web,我们应该看到 Yii2 的祝贺页面:

示例 – 使用 Yii 基本模板和 Bootstrap 模板从头开始创建 Hello world

Hello world 页面的示例

我们将创建第一个动作,在屏幕上显示一个难忘的hello world

从“应用程序属性”部分我们知道,在默认路由条目中,当请求未指定路由时,将调用SiteController控制器。

因此,我们进入basic/controllers并打开默认控制器SiteController.php

SiteController类定义中,我们在顶部添加了一个新方法,称为actionHelloWorld,没有参数。

public function actionHelloWorld()
{
    echo 'hello world'
}

让我们保存文件,并将浏览器指向http://hostname/basic/web/index.php?r=site/hello-world

你应该看到一个空白页面,上面显示hello world

注意

使用名称路由约定时请注意。大写字母将被转换为小写字母和短横线。

这很棒,但现在我们只想在页面模板中放置hello world

我们现在需要创建一个包含响应内容“hello world!”的视图。为了做到这一点,我们需要在views/site下创建一个名为helloWorld.php的文件,作为操作的名称。命名约定在这里不一定必须相同,因为视图文件不是由框架自动调用的。

此文件仅包含hello world文本。

我们使用以下代码更新SiteController

public function actionHelloWorld()
{
    return $this->render('helloWorld');
}

actionHelloWorld()方法中,$this指的是SiteController的实例,render()将插入views/helloWorld.php文件内容到主要内容布局页面。

视图文件的扩展名.php是由框架自动添加的,用于查看传递给渲染方法的名称参数。

如果我们想向actionHelloWorld()传递一个参数,例如名称,怎么办?正式来说,我们只需要在SiteController中向actionHelloWorld()添加一个参数,如下所示:

public function actionHelloWorld($nameToDisplay)
{
    return $this->render('helloWorld',
  [ 'nameToDisplay' => $nameToDisplay ]
    );
}

然后,在view/site/helloWorld.php下添加以下代码:

Hello World <?php echo $nameToDisplay ?>

通过更新actionHelloWorld(),我们将作为第二个参数传递一个变量数组,这些变量将在视图中可见并使用。

当我们在动作函数中使用参数时,我们必须记住它们将是必需的,并且我们必须在传递给请求时尊重顺序。

为了避免这个义务,我们可以使用旧方法,将参数解析到函数中:

public function actionHelloWorld()
{
    $nameToDisplay = Yii::$app->request->get('nameToDisplay');
    // Equivalent to
// $nameToDisplay = isset($_GET['nameToDisplay'])?$_GET['nameToDisplay']:null;

    return $this->render('helloWorld',
    [ 'nameToDisplay' => $nameToDisplay ]
    );
}

使用这种解决方案,我们可以决定是否将nameToDisplay参数传递给请求。nameToDisplay参数的默认值将是 null,但我们可以决定分配不同的值。

以下是一个传递nameToDisplay参数Foo的 URL 示例:

http://hostname/basic/web/index.php?r=site/hello-world&nameToDisplay=Foo

摘要

在本章中,我们探讨了 Yii2 框架的基本理解,从需求开始解释其主要特性。然后我们使用了调试和日志工具来追踪我们的代码,并能够找到错误。最后,我们基于基本模板编写了我们的第一个项目。

接下来,你将学习如何创建我们的控制器和视图,以创建与前端用户的自定义交互。

第二章:创建简单的新闻阅读器

本章解释了如何编写第一个控制器以显示新闻条目列表和详情,实现控制器和视图之间的交互,然后自定义视图布局。

本章,我们将介绍以下内容:

  • 创建控制器和动作

  • 创建一个视图来显示新闻列表

  • 控制器如何将数据发送到视图

    • 示例 - 创建一个控制器来显示静态新闻条目列表和详情
  • 将公共视图内容拆分为可重用的视图

    • 示例 - 在视图中渲染部分
  • 创建静态页面

  • 在视图和布局之间共享数据

    • 示例 - 根据 URL 参数更改布局背景
  • 带有动态块的布局

    • 示例 - 添加动态框来显示广告信息
  • 使用多个布局

    • 示例 - 使用不同的布局为同一视图创建响应式和非响应式布局

创建控制器和动作

为了处理请求,首先要做的是创建一个新的控制器。

创建文件控制器时必须记住的事项如下:

  • 顶部的命名空间(在基本应用程序中通常是app\controllers

  • 使用类所用的use路径

  • 控制器类必须扩展yii\web\Controller

  • 动作由以action开头且每个单词首字母大写的控制器函数处理

让我们指向basic/controllers并创建一个名为NewsController.php的文件。

然后,创建一个与文件同名的类,并从控制器扩展它;最后,创建一个名为index的动作来管理news/index的请求:

<?php

// 1\. specify namespace at the top (in basic application usually app\controllers);
namespace app\controllers;

// 2\. specify 'use' path for used class;
use Yii;
use yii\web\Controller;

// 3\. controller class must extend yii\web\Controller class;
// This line is equivalent to
// class NewsController extends yii\web\Controller
class NewsController extends Controller
{
// 4\. actions are handled from controller functions whose name starts with 'action' and the first letter of each word is uppercase;
    public function actionIndex()
    {
            echo "this is my first controller";
    }
}

如果我们尝试将浏览器指向http://hostname/basic/web/index.php?r=news/index,我们将看到一个空白页面,并显示通知这是我的第一个控制器

现在,让我们看看当我们忽略本章开头提到的四个注意事项时,可能会发生哪些常见错误。

命名空间定义了我们应用程序中使用的名称的层次结构组织。如果我们忘记声明命名空间,当YII_DEBUGweb/index.php中设置为 true 时,Yii2 将显示以下错误消息:

创建控制器和动作

缺少的控制器命名空间

Yii2 以极好的方式报告错误,它给了我们检查是否缺少命名空间来解决问题的可能性。

然后,使用Use关键字来指定应用程序中类的完整路径。如果一个类具有path/to/class/ClassName的完整路径,我们只需在命名空间声明后立即添加use path/to/class/ClassName,就可以在应用程序中使用ClassName来引用该类。

然而,如果我们只使用ClassName而没有在文件顶部定义use声明,可能会出现以下错误:

创建控制器和动作

这个错误很容易解释,但很难找到,尤其是对于初学者来说。

在这种情况下,截图显示在第 9 行已经使用了 Controller 名称(在 extends 关键字之后)。由于没有 Controller 类名的完整路径,Yii2 将尝试在 app\controllers 下查找 Controller 类,但没有找到。

要解决这个问题,我们必须在第 9 行将 Controller 替换为 yii\web\Controller,以及所有后续将使用 Controller 类名而不定义完整类路径的行,或者在该文件顶部插入 use 声明,我们必须使用 yii\web\Controller

控制器始终是 yii\web\Controller 的子类,或者如果我们使用了 use 关键字,那么是 Controller 的子类。动作名称遵循前一章中描述的规则。

创建一个视图来显示新闻列表

现在,我们将在名为 itemsList 的视图中创建一个简单的新闻列表。我们将从 NewsController 指向此视图,因此我们必须:

  • basic/views 下创建一个 news 文件夹,NewsController 将使用该文件夹作为基本文件夹来搜索要渲染的视图(根据前一章中解释的视图名称规则)。

  • basic/views/news 下创建一个 itemsList.php 文件

现在,打开 basic/views/news/itemsList.php,创建一个包含数据列表的数组,并使用简单的项目表格显示输出:

<?php
    $newsList = [
        [ 'title' => 'First World War', 'date' => '1914-07-28' ],
        [ 'title' => 'Second World War', 'date' => '1939-09-01' ],
        [ 'title' => 'First man on the moon', 'date' => '1969-07-20' ]
    ];
?>

<table>
    <tr>
        <th>Title</th>
        <th>Date</th>
    </tr>
    <?php foreach($newsList as $item) { ?>
    <tr>
        <td><?php echo $item['title'] ?></td>
        <td><?php echo $item['date'] ?></td>
    </tr>
    <?php } ?>
</table>

然后,我们需要创建一个名为 actionItemsList 的动作,它将通过 http://hostname/basic/web/index.php?r=news/items-list 渲染。

提示

下载示例代码

您可以从您在 www.packtpub.com 的账户下载示例代码文件,以获取您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了此书,您可以访问 www.packtpub.com/support 并注册以直接将文件通过电子邮件发送给您。

注意

注意路由、控制器和动作的命名:

  • 此动作的路由为 news/items-list(小写,单词由破折号分隔);

  • 控制器类名为 NewsController(大写,以 Controller 结尾)。

  • NewsController 中的动作函数名为 actionItemsList(函数名以 action 字词为前缀,路由中的破折号被移除,每个单词的首字母大写);

需要添加到 NewsController 类中的函数如下:

public function actionItemsList()
{
     return $this->render('itemsList');
}

属于 \yii\web\Controllerrender() 方法,在布局中显示传递给第一个参数的视图内容。当框架查找视图时,它将 .php 扩展名附加到 render() 方法的第一个参数名称,并在 basic/view/news 中查找它。路径的最后一个成员是调用 render() 方法的名称。

现在,我们可以指向 http://hostname/basic/web/index.php?r=news/items-list,以查看我们的美丽表格!

控制器如何向视图发送数据

在上一段中,我们看到了如何显示内容视图。然而,视图只应该负责显示数据,而不应该进行操作。因此,任何数据操作都应该在控制器动作中完成,然后传递给视图。

控制器动作中的 render() 方法有一个第二个参数,它是一个数组,其键是变量的名称,值是这些变量在视图上下文中的内容。

现在,让我们将 itemsList 示例中的所有数据操作移到控制器中,只留下格式化输出(如 HTML)的代码。

以下是在 actionItemsList() 控制器中的内容:

public function actionItemsList()
{
  $newsList = [
    [ 'title' => 'First World War', 'date' => '1914-07-28' ],
    [ 'title' => 'Second World War', 'date' => '1939-09-01' ],
    [ 'title' => 'First man on the moon', 'date' => '1969-07-20' ]
  ];

  return $this->render('itemsList', ['newsList' => $newsList]);
}

views/news/itemsList.php 中,我们只有以下代码:

<?php // $newsList is from actionItemsList ?>
<table>
    <tr>
        <th>Title</th>
        <th>Date</th>
    </tr>
    <?php foreach($newsList as $item) { ?>
    <tr>
        <th><?php echo $item['title'] ?></th>
        <th><?php echo $item['date'] ?></th>
    </tr>
    <?php } ?>
</table>

因此,我们已经正确地分割了控制器和视图的工作。

示例 – 创建一个控制器,使用 bootstrap 模板显示静态新闻条目列表和详情

我们下一个目标是完成新闻阅读器,在另一页面上显示单条新闻的详细信息。

由于我们将使用相同的数据来显示列表和详情,我们将从动作中提取 $newsList 数据到一个函数中,以便在更多动作中重用。

NewsController 中,我们将有以下代码:

public function dataItems()
{
  $newsList = [
    [ 'title' => 'First World War', 'date' => '1914-07-28' ],
    [ 'title' => 'Second World War', 'date' => '1939-09-01' ],
    [ 'title' => 'First man on the moon', 'date' => '1969-07-20' ]
  ];

  return $newsList;
}

public function actionItemsList()
{
  $newsList = $this->dataItems();

  return $this->render('itemsList', ['newsList' => $newsList]);
}

然后,我们将在 NewsController 中创建一个新的函数 actionItemDetail,用于处理新闻条目详情的请求。此函数将期望一个参数,允许从 $newsList 中过滤出正确的条目,例如标题。

以下是在 actionItemDetail 中的内容:

public function actionItemDetail($title)
{
  $newsList = $this->dataItems();

  $item = null;
  foreach($newsList as $n)
  {
    if($title == $n['title']) $item = $n;
  }

  return $this->render('itemDetail', ['item' => $item]);
}

接下来,我们必须在 views/news 中创建一个新的视图文件,命名为 itemDetail.php

以下是在 views/news/ 目录下 itemDetail.php 的内容:

<?php // $item is from actionItemDetail ?>

<h2>News Item Detail<h2>
<br />
Title: <b><?php echo $item['title'] ?></b>
<br />
Date: <b><?php echo $item['date'] ?></b>

如果我们不传递标题参数,指向 http://hostname/basic/web/index.php?r=news/item-detail,我们将看到以下截图:

示例 – 创建一个控制器,使用 bootstrap 模板显示静态新闻条目列表和详情

它显示了一个错误,告诉我们标题参数缺失。

尝试将 First%20%World%20War 作为标题参数传递到 URL 中,如下所示 http://hostname/basic/web/index.php?r=news/item-detail&title=First%20World%20War;以下将是输出:

示例 – 创建一个控制器,使用 bootstrap 模板显示静态新闻条目列表和详情

这正是我们期望的!

最后,我们希望将 itemsListitemDetail 连接起来。在 views/news/itemsList.php 中,我们必须将标题内容更改为锚点元素,如下所示:

<?php // $newsList is from actionItemsList ?>
<table>
  <tr>
    <th>Title</th>
    <th>Date</th>
  </tr>
  <?php foreach($newsList as $item) { ?>
  <tr>
    <th><a href="<?php echo Yii::$app->urlManager->createUrl(['news/item-detail' , 'title' => $item['title']]) ?>"><?php echo $item['title'] ?></a></th>
    <th><?php echo $item['date'] ?></th>
  </tr>
  <?php } ?>
</table>

要创建链接,有一个可用的组件 urlManager,它允许我们通过 createUrl() 方法创建链接。createUrl() 中的参数是一个包含路由路径和要传递给 URL 的变量的数组。要了解更多关于这个方法的信息,请参考链接 www.yiiframework.com/doc-2.0/yii-web-urlmanager.html#createUrl%28%29-detail

在我们的例子中,我们有一个 news/item-detail 作为要调用的路由,以及要传递给 URL 的 title 参数。

注意

日期可以使用内置的格式化组件进行格式化。例如,要显示 d/m/Y 格式的日期,可以使用以下代码:d/m/Y : Yii::$app->formatter->asDatetime($item['date'], "php:d/m/Y");

建议使用唯一的标识符在路由之间传递数据。为此,我们添加了一个名为 id 的第三个参数,用于唯一标识记录。

以下为 NewsController 的内容:

public function dataItems()
{
  $newsList = [
    [ 'id' => 1, 'title' => 'First World War', 'date' => '1914-07-28' ],
    [ 'id' => 2, 'title' => 'Second World War', 'date' => '1939-09-01' ],
    [ 'id' => 3, 'title' => 'First man on the moon', 'date' => '1969-07-20' ]
  ];
  return $newsList;
}

public function actionItemsList()
{
  $newsList = $this->dataItems();
  return $this->render('itemsList', ['newsList' => $newsList]);
}
public function actionItemDetail($id)
{
  $newsList = $this->dataItems();

  $item = null;
  foreach($newsList as $n)
  {
    if($id == $n['id']) $item = $n;
  }

  return $this->render('itemDetail', ['item' => $item]);
}

然后,修改 views/news/itemsList.php 中的 createUrl 参数:

<table>
  <tr>
    <th>Title</th>
     <th>Date</th>
  </tr>
  <?php foreach($newsList as $item) { ?>
  <tr>
    <th><a href="<?php echo Yii::$app->urlManager->createUrl(['news/item-detail' , 'id' => $item['id']]) ?>"><?php echo $item['title'] ?></a></th>
    <th><?php echo Yii::$app->formatter->asDatetime($item['date'], "php:d/m/Y"); ?></th>
  </tr>
  <?php } ?>
</table>

将通用视图内容拆分为可重用视图

有时,视图会共享相同的内容部分。在到目前为止的例子中,我们已经看到 itemsListitemDetail 的通用区域可以是版权数据,它显示有关版权信息的免责声明。

为了做到这一点,我们必须将通用内容放在一个单独的视图中,并使用控制器中的 renderPartial() 方法来调用它 (www.yiiframework.com/doc-2.0/yii-base-controller.html#renderPartial%28%29-detail)。它具有与 render() 方法相同的参数类型;render()renderPartial() 方法的主要区别在于,render() 将视图内容写入布局,而 renderPartial() 只将视图内容写入输出。

示例 - 在视图中渲染部分内容

在这个例子中,我们为 itemsListitemDetail 创建了一个关于版权数据的通用视图。

views/news 目录下创建一个名为 _copyright.php 的视图文件。

注意

通常,在 Yii2 的应用中,以下划线开头的视图名称表示通用可重用视图。

在这个文件中,只将版权文本放入 views/news/_copyright.php

<div>
     This is text about copyright data for news items
</div>

现在,我们想在 itemsListitemDetail 视图中显示这个视图。

将位于 views/news/ 目录下的 itemsList.php 文件中的内容修改如下:

<?php echo $this->context->renderPartial('_copyright'); ?>
<table>
  <tr>
    <th>Title</th>
    <th>Date</th>
  </tr>
  <?php foreach($newsList as $item) { ?>
  <tr>
    <th><a href="<?php echo Yii::$app->urlManager->createUrl(['news/item-detail' , 'id' => $item['id']]) ?>"> <?php echo $item['title'] ?> </a></th>
    <th><?php echo Yii::$app->formatter->asDatetime($item['date'], 'php:d/m/Y'); ?></th>
  </tr>
  <?php } ?>
</table>

然后,将位于 views/news/ 目录下的 itemDetail.php 文件中的内容修改如下:

<?php // $item is from actionItemDetail ?>
<?php echo $this->context->renderPartial('_copyright'); ?>
<h2>News Item Detail<h2>
<br />
Title: <b><?php echo $item['title'] ?></b>
<br />
Date: <b><?php echo $item['date'] ?></b>

我们在两个视图中都把通用代码放在了文件顶部:

<?php echo $this->context->renderPartial('_copyright'); ?>

这将渲染 _copyright.php 视图的内容,但不包含布局。

注意

注意!由于 renderPartial()Controller 类的一个方法,而 $this 在视图文件中指向 View 类,为了从 $this 访问 renderPartial(),我们将使用上下文成员,它在 View 对象中代表 Controller 对象。

创建静态页面

所有网站都包含静态页面,其内容是静态的。

要以常规方式创建一个静态页面,我们需要:

  • Controller中创建一个执行操作的函数(action

  • 创建静态内容的视图

将以下操作追加到Controller

public function actionInfo()
{
    return $this->render('info');
}

然后,在views/controller/action-name.php中创建一个视图。这个过程很简单,但太长且冗余。

Yii2 提供了一个快速替代方案,将静态页面添加到Controlleractions()方法中,如下所示:

public function actions()
{
  return [
    'pages' => [
    'class' => 'yii\web\ViewAction',
    ],
  ];
}

通过这个简单的声明,我们可以将所有静态内容放在views/controllerName/pages下。

最后,我们可以通过路由controller_name/page指向 URL,以及通过视图文件名称(如http://hostname/basic/web/index.php?r=controllerName/pages&view=name_of_view)来指定view参数。

示例 - 添加一个联系页面

在我们学习了如何创建静态页面之后,现在是时候编写一个联系页面了。

让我们在views/site/pages/contact.php中放置一段简短的静态内容,如下所示:

To contact us, please write to info@example.com

然后,让我们在Controlleractions()方法返回的数组中添加一个page属性。为了简化,我们将使用具有此默认actions()方法实现的SiteController

  public function actions()
  {
    return [
    'error' => [
       'class' => 'yii\web\ErrorAction',
    ],
    'captcha' => [
      'class' => 'yii\captcha\CaptchaAction',
      'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
    ],
  ];
  }

在最后一个属性之后,我们将追加page属性,以下将是结果:

  public function actions()
  {
    return [
    'error' => [
      'class' => 'yii\web\ErrorAction',
    ],
    'captcha' => [
      'class' => 'yii\captcha\CaptchaAction',
      'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
    ],
    'pages' => [
      'class' => 'yii\web\ViewAction',
    ],
  ];
  }

现在,每个对site/pages/的请求都使用ViewAction类进行路由,它通过简单地渲染相对视图的静态内容来处理它。

通过点击http://hostname/basic/web/index.php?r=site/pages&view=contact进行测试,我们应该看到以下内容:

示例 - 添加一个联系页面

我们可以通过这些更改自定义路由的最后部分:

  • Controlleractions()方法返回的数组中的属性名

  • ViewAction类声明中的viewPrefix属性设置为我们要使用的 URL 的第一部分,以到达页面

  • 更改views/controllerName下的子文件夹名称

例如,我们想在SiteController中通过 URL 的最后部分使用static来访问静态页面。

我们想指向http://hostname/basic/web/index.php?r=site/static&view=contact以显示联系视图。

这将是SiteControlleractions()方法数组中的ViewAction节点:

    'static' => [
    'class' => 'yii\web\ViewAction',
    'viewPrefix' => 'static'
    ],  

我们还必须更改静态页面子文件夹的名称,将其从views/site/pages重命名为views/site/static,并且我们可以指向http://hostname/basic/web/index.php?r=site/static&view=contact

视图和布局之间的数据共享

Yii2 通过视图组件的params属性提供了一种标准解决方案,该属性可用于在视图之间共享数据。

注意

这是一个标准解决方案,因为params属性存在于所有视图中,并且它附加到视图组件上。

这个属性,params,是一个我们可以无限制使用的数组。

想象一下,我们想填充布局中的面包屑元素以跟踪导航路径。

打开主布局 views/layouts/main.php;你应该在声明页脚之前找到默认的面包屑实现:

        <div class="container">
            <?= Breadcrumbs::widget([
                'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [],
            ]) ?>
         </div>

我们需要在视图中填充 params 的面包屑属性,以便从任何视图显示到布局的定制路径。例如,我们希望在 SiteController 的 index 中显示面包屑。

访问 views/site/index.php 并在文件顶部添加以下代码:

$this->params['breadcrumbs'][] = 'My website';

注意

由于我们处于视图文件中,$this 指的是视图组件。

访问 http://hostname/basic/web/index.php?r=site/index 以查看页面顶部的面包屑栏出现:

在视图和布局之间共享数据

示例 - 根据 URL 参数更改布局背景

视图和布局之间通信的另一个例子是,例如,根据 URL 参数更改布局的背景颜色。

我们需要通过 URL 中的 bckg 参数更改路由 site/index 的背景。

因此,我们必须打开 views/site/index.php 并将此代码放在顶部:

<?php
$backgroundColor = isset($_REQUEST['bckg'])?$_REQUEST['bckg']:'#FFFFFF';
$this->params['background_color'] = $backgroundColor;

如果没有传递到 bckg 参数,此代码将设置 $``backgroundColor#FFFFFF(白色),否则它将传递一个值。

然后,设置视图组件的 params 属性,以便在布局中写入其内容。

打开 views/layout/main.php,并在 body 标签中,根据从视图传递的 params['background_color'] 应用样式。

然后,让我们使用以下方式更改 body 标签的布局:

<?php
$backgroundColor = isset($this->params['background_color'])?$this->params['background_color']:'#FFFFFF'; ?>
<body style="background-color:<?php echo $backgroundColor ?>">

最后,访问 http://hostname/basic/web/index.php?r=site/index&bckg=yellow 以获得黄色背景,或访问 http://hostname/basic/web/index.php?r=site/index&bckg=#FF0000 以获得红色背景。

注意

在此示例中,我们仅在 views/site/index.php 中设置 paramsbackground 属性。其他视图没有设置此属性,因此如果我们没有检查布局文件中是否存在 background_color 属性,我们将收到框架缺少属性的错误,这意味着:

$backgroundColor = isset($this->params['background_color'])?$this->params['background_color']:'#FFFFFF';

带有动态块的布局

使用 params 属性允许视图和布局之间的通信,对于简单情况是可取的,但还有一些更复杂的情况,我们必须共享 HTML 块。

例如,考虑布局中的广告框(通常是模板的左侧或右侧列),它可以根据显示的视图而改变。

在这种情况下,我们需要从视图传递整个 HTML 块代码到布局。

为了这个目的,这个框架提供了 Block 语句,我们可以定义整个数据块从视图发送到布局。

使用 Blocks 的意思是,在视图中定义 Block 语句并在另一个视图中显示它,通常是布局。

我们在视图中定义了 Block 语句如下:

<?php $this->beginBlock('block1'); ?>
...content of block1...
$this->endBlock(); ?>

在这里,beginBlockendBlock 定义了名为 block1 的语句的开始和结束。此内容被保存到具有 block1 属性的视图组件的 blocks 属性中。

我们可以通过在每一个视图(包括布局)中使用$view>blocks[$blockID]来访问这个块。

如果有布局视图可用,要渲染一个块,可以使用以下代码:

<?php if(isset($this->blocks['block1']) { ?>
     <?php echo $this->blocks['block1'] ?>
<?php } else { ?>
    … default content if missing block1 attribute
<?php } ?>

显然,我们可以定义我们想要的全部块。

示例 - 添加一个动态框以显示广告信息

在这个例子中,我们将看到如何显示,当可用时,一个带有广告信息的框,该框显示从视图发送的数据。

首件事是向布局中添加一个显示数据的块。

views/layouts/main.php中输入并更改具有容器类的div如下:

<div class="container">
    <?= Breadcrumbs::widget([
      'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [],
    ]) ?>

    <div class="well">
        This is content for blockADV from view
        <br />
        <?php if(isset($this->blocks['blockADV'])) { ?>
            <?php echo $this->blocks['blockADV']; ?>
        <?php } else { ?>
               <i>No content available</i>
        <?php } ?>    
    </div>

    <?= $content ?>
</div>

我们已经添加了一个带有well类的div来显示blockADV的内容,如果有的话。如果blockADV$this->blocks中可用,它将显示其内容;否则,它将显示no content available,作为一个礼貌信息。

现在,我们将在NewsController中创建一个新的动作,名为advTest,然后创建一个全新的视图。

让我们从在views/news/advTest.php中创建一个包含以下内容的文件开始:

<span>
This is a test where we display an adv box in layout view
</span>
<?php $this->beginBlock('blockADV'); ?>

    <b>Buy this fantastic book!</b>

<?php $this->endBlock(); ?>

我们可以在块中插入任何内容;在这种情况下,我们放入了文本。

注意

视图中定义块的位置并不重要。

然后,打开NewsController并添加一个新的动作advTest

public function actionAdvTest()
{
        return $this->render('advTest');
}

现在,将浏览器指向http://hostname/basic/web/index.php?r=news/adv-test,我们将看到以下截图:

示例 - 添加一个动态框以显示广告信息

所有其他页面在截图中只会显示no content available

使用多个布局

在构建网站或网络应用程序的过程中,通常可能需要渲染具有不同布局的不同视图。例如,考虑本章中制作的新闻列表和详情。

布局由Controller$layout属性管理;main是这个属性的默认值。

只需设置此属性以更改渲染视图内容布局的文件。

在编写$layout属性的值时,有一些重要的规则:

  • 路径别名(例如,@app/views/layouts/main)。

  • 绝对路径(例如,/main)是指布局值以斜杠开头的地方。实际的布局文件将在应用程序布局路径下查找,默认为@app/views/layouts

  • 相对路径(例如,main)是指实际的布局文件将在上下文模块的布局路径下查找,默认为模块目录下的views/layouts目录。

  • 布尔值false表示不应用任何布局。

    注意

    如果布局值不包含文件扩展名,它将使用默认的.php

示例 - 使用不同的布局为同一视图创建响应式和非响应式内容布局

在这个例子中,我们将在NewsController中创建一个新的动作,该动作将根据 URL 中传递的值改变其布局。

首先,在NewsController中添加一个名为actionResponsiveContentTest的新动作:

public function actionResponsiveContentTest()
{
  $responsive =  Yii::$app->request->get('responsive', 0);

  if($responsive)
  {
    $this->layout = 'responsive';
  }
  else
  {
    $this->layout = 'main';
  }

  return $this->render('responsiveContentTest', ['responsive' => $responsive]);
}

在这个操作中,我们从 URL 中获取一个响应参数,并将$responsive变量设置为这个值或如果没有传递则设置为 0。

然后,根据$responsive值将Controller$layout属性设置为响应或不响应,并将这个变量传递给视图。

然后,在views/news/responsiveContentTest.php中创建一个新的视图:

<?php if($responsive) { ?>
  This layout contains responsive content
<?php } else { ?>
  This layout does not contain responsive content
<?php } ?>

根据 $responsive 值显示不同的文本块。

最后,将主布局克隆到views/layouts/responsive.php中,复制views/layouts/main.php,并在新的文件views/layouts/responsive.php中进行更改:

<div class="container"> in <div class="container-fluid" style="padding-top:60px">

这个更改使 div 容器变为流体(响应式),换句话说,其内容根据水平空间中可用的百分比进行缩放(而不是固定值)。

如果我们指向http://hostname/basic/web/index.php?r=news/responsive-content-test,我们将看到固定布局的内容。相反,如果我们传递带有值 1 的responsive参数,即http://hostname/basic/web/index.php?r=news/responsive-content-test&responsive=1,我们将看到全宽屏幕的内容。

摘要

在本章中,在了解 Yii2 应用的架构之后,我们创建了我们的第一个控制器和相关的视图。我们看到了静态和动态视图,我们学习了如何在布局中渲染视图,如何从控制器传递数据到视图,然后我们通过部分视图和块来查看视图的重用。

最后,我们已操纵布局,有条件地改变它们。

在下一章中,我们将以美观的格式显示 URL,这对于网站上所有搜索引擎优化SEO)活动非常重要。然后,我们将学习如何创建自定义 URL 处理程序来管理任何所需的 URL 自定义。

第三章. 制作漂亮的 URL

本章解释了如何配置 URL 规则并使 URL 变得漂亮,特别是对于搜索引擎。在本章中,我们将涵盖以下主题:

  • 使用漂亮的 URL

  • 自定义 URL 规则

    • 示例 - 按年份或类别列出新闻条目
  • 规则中的默认参数

    • 示例 - 显示列表链接的索引页面
  • 完整的 URL 规则参数

  • 支持多语言视图的 URL 模式

  • 创建规则类

使用漂亮的 URL

URL 格式对于 SEO 非常重要。人们不会关注 URL(一些浏览器甚至完全不显示它们),但搜索引擎会在页面中的文本和 URL 之间建立对应关系。

到目前为止,我们使用了这种类型的 URL index.php?r=site/indexindex.php?r=site/about,其中r表示要遵循的参数路由。现在,我们将看到如何更改site/indexsite/about的这些格式,它们更易于阅读,并且对搜索引擎更有用。

为了使用漂亮的 URL,我们需要配置 Yii2 来处理它们,这可以在几分钟内完成。

首先,我们必须确保所有请求都被重写到web/index.php。在 Linux 中,我们可以使用 Apache 更改 Web 服务器配置,并在 Yii2 的应用程序根目录中插入.htaccess文件,如果该文件不存在。.htaccess文件允许我们覆盖 Web 服务器的一些默认配置。

注意

在 Linux 环境中,以点开头的文件名表示该文件是隐藏的。

.htaccess的内容与 Yii1 相同:

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 . web/index.php

如果应用程序根目录是/var/www/vhosts/yiiapp/basic,我们将.htaccess插入到/var/www/vhosts/yiiapp/basic

第一行激活了 Web 服务器的RewriteEngine;然后,在第二行和第三行,脚本检查请求是否不在现有的文件或文件夹中;最后,请求被重写为web/index.php。通过这些更改,所有不是现有文件或路径文件夹的请求都将被重写为web/index.php

注意

如果我们有访问 Apache 配置这一级别的权限,我们也可以在 Apache 配置中配置重写规则,而不是.htaccess文件。

如果.htaccess配置已被忽略,请检查是否已将AllowOverride设置为All,如下所示:

<Directory /var/www/path/to/folder>
   AllowOverride All
</Directory>

而这个选项并没有设置为None

现在最后要做的就是配置 Yii2 以处理漂亮的 URL。

让我们打开config/web.php并在components属性中添加以下内容:

'urlManager' => [
  'enablePrettyUrl' => true,
],

添加enablePrettyUrl属性,我们刚刚配置了urlManager以启用漂亮的 URL,切换漂亮的 URL 格式。

之前的 URL index.php?r=site/index 变成了 /index.php/site/index,而 index.php?r=site/about 变成了 /index.php/site/about

使用enablePrettyUrl属性,我们将再次拥有index.php前缀。我们可以选择是否保留它;然而,为了限制 URL 长度,建议移除它。

为了控制index.php前缀的存在,我们使用另一个名为showScriptName的属性。

如果我们将此属性设置为false,我们将删除 URL 的第一部分。这是我们的更新配置:

'urlManager' => [
    'enablePrettyUrl' => true,
    'showScriptName' => false,
],

现在,将浏览器指向http://hostname/basic/web/site/index以查看 Yii2 应用程序的第一页,并检查其他链接是否以美观的格式显示。

最后,还有一个用于urlManager组件的属性,用于仅基于给定的 URL 规则启用 URL 解析,该属性名为enableStrictParsing。如果此属性为true,则仅执行在urlManager中定义的规则;如果没有与请求匹配的 URL,将显示错误。

自定义 URL 规则

Yii2 给我们提供了自定义 URL 规则的机会,正如我们想要的。这可以通过在urlManager中使用rules属性来完成,其中键是模式,值是对应的路由。模式是常见的正则表达式模式,因此有必要对正则表达式有一些了解。

模式可以包含参数,这些参数将被传递到路由中。在下一个示例中,我们将显示一个可以通过年份或类别参数进行筛选的新闻列表,这些参数基于传递到 URL 的参数。

示例 - 按年份或类别列出新闻条目

在这个示例中,我们将在controllers/NewsController.php中创建一个新的名为News的控制器。在这个新的控制器中,我们将插入一个包含测试数据的数组data()函数,以及一个名为actionItemsList的函数。

首先要做的事情是在config/web.php文件下的urlManager组件中配置rules属性:

'rules' => [
    news/<year:\d{4}>/items-list' => ' news/items-list',
    'news/<category:\w+>/items-list' => 'test-rules/items-list',
],

这里,我们有两种模式:

  • news/<year:\d{4}>/items-list

  • news/<category:\w+>/items-list

第一个模式捕获带有四位数字的数字参数的请求,该参数作为news/items-list路由的year GET 参数传递。我们可以请求news/2014/items-listnews/2015/items-list

第二个模式捕获带有单词参数的请求,该参数作为news/items-list路由的category GET 参数传递。我们可以请求news/business/items-listnews/shopping/items-list

然后,我们创建NewsController,在其中定义data()函数,以返回作为数据源使用的静态数据,以及处理对news/year/or/category/itemsList请求的actionItemsList()函数:

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;

class NewsController extends Controller
{
  public function data()
  {
    return [
    [ "id" => 1, "date" => "2015-04-19", "category" => "business", "title" => "Test news of 2015-04-19" ],
    [ "id" => 2, "date" => "2015-05-20", "category" => "shopping", "title" => "Test news of 2015-05-20" ],
    [ "id" => 3, "date" => "2015-06-21", "category" => "business", "title" => "Test news of 2015-06-21" ],
    [ "id" => 4, "date" => "2016-04-19", "category" => "shopping", "title" => "Test news of 2016-04-19" ],
    [ "id" => 5, "date" => "2017-05-19", "category" => "business", "title" => "Test news of 2017-05-19" ],
    [ "id" => 6, "date" => "2018-06-19", "category" => "shopping", "title" => "Test news of 2018-06-19" ]
    ];
  }

  public function actionItemsList()
  {
    // if missing, value will be null
    $year = Yii::$app->request->get('year');
    // if missing, value will be null
    $category = Yii::$app->request->get('category');

    $data = $this->data();
    $filteredData = [];

    foreach($data as $d)
    {
      if(($year != null)&&(date('Y', strtotime($d['date'])) == $year)) $filteredData[] = $d;
      if(($category != null)&&($d['category'] == $category)) $filteredData[] = $d;
    }

    return $this->render('itemsList', ['year' => $year, 'category' => $category, 'filteredData' => $filteredData] );
    }

最后,我们在views/news/itemsList.php中创建一个视图,显示使用的参数,年份或类别,以及结果列表:

<?php if($year != null) { ?>
<b>List for year <?php echo $year ?></b>
<?php } ?>
<?php if($category != null) { ?>
<b>List for category <?php echo $category ?></b>
<?php } ?>

<br /><br />

<table border="1">
    <tr>
        <th>Date</th>
        <th>Category</th>
        <th>Title</th>
    </tr>

<?php foreach($filteredData as $fd) { ?>
    <tr>
        <td><?php echo $fd['date'] ?></td>
        <td><?php echo $fd['category'] ?></td>
        <td><?php echo $fd['title'] ?></td>
    </tr>
<?php } ?>
</table>

现在,让我们指向http://hostname/basic/web/news/2015/items-list以显示通过年份筛选出的项目列表:

示例 - 按年份或类别列出新闻条目

按年份筛选列表项

尝试在新闻和条目列表之间更改年份,以查看列表中的数据结果如何变化。创建的规则允许我们按分类显示条目列表。指向http://hostname/basic/web/news/business/items-list以查看按商业分类筛选的列表:

示例 – 按年份或分类列出新闻条目

按分类筛选的列表项

我们还可以指向http://hostname/basic/web/news/shopping/items-list以查看按购物分类筛选的列表。

规则中的默认参数

在规则中,所有声明的参数都是必需的;如果 URL 缺少某些参数,则规则将不会应用。这个问题可以使用规则的默认属性来解决。

URL 规则结构有一个名为defaults的参数,包含作为默认值传递的默认参数。参数默认值是一个数组,其中键是参数名称,值是它们对应的值。

例如,将第二条规则更改为完整的数组,并添加['category' => 'shopping']作为默认属性规则:

'rules' => [
    'news/<year:\d{4}>/items-list' => 'news/items-list',
    [
        'pattern' => 'news/<category:\w+>/items-list',
        'route' => 'news/items-list',
        'defaults' => ['category' => 'shopping']
    ]
],

现在,如果我们指向http://hostname/basic/web/news/items-list而不指定年份或分类参数,第一条规则将被跳过,第二条规则将使用默认值“购物”执行,因为缺少分类参数。

示例 – 显示链接列表的索引页面

现在,创建一个索引页面以查看如何创建这些自定义 URL。在这个页面中,我们将显示按年份(过去 5 年)筛选数据和按分类(购物和商业)筛选数据的 URL 链接。

使用yii\helpers\Urlto()方法创建 URL,其中第一个参数可以是:

第一个参数可以是:

  • 一个将要传递给toRoute()方法以生成 URL 的数组。这个数组的第一个元素是要渲染的路由,其他元素是要传递给路由的参数;例如,Url::to(['news/items-list', 'year' => 2015])

  • 一个以@开头的前缀字符串;这被视为一个别名,并将返回相应的别名字符串

  • 一个空字符串,它将返回当前请求的 URL。

  • 一个将按原样返回的正常字符串。

NewsController中创建一个简单的actionIndex操作:

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

然后,在views/news/index.php下为索引操作创建一个视图:

<?php

use yii\helpers\Url;
use yii\helpers\Html;

?>

<b>Filter data by year:</b>
<br />
<ul>
  <?php $currentYear = date('Y'); ?>
  <?php for($year=$currentYear;$year>($currentYear-5);$year--) { ?>
  <li><?php echo Html::a( 'List items by year '.$year, Url::to(['news/items-list', 'year' => $year]) ) ?></li>
  <?php } ?>
</ul>

<br />

<b>Filter data by category:</b>
<br />
<ul>
  <?php $categories = ['business', 'shopping']; ?>
  <?php foreach($categories as $category) { ?>
  <li><?php echo Html::a( 'List items by category '.$category, Url::to(['news/items-list', 'category' => $category]) ) ?></li>
  <?php } ?>
</ul>

<br /><br />

指向http://hostname/news/index,它将显示:

示例 – 显示链接列表的索引页面

可用筛选数据的索引

完整的 URL 规则参数

URL 规则包含以下参数:

  • defaults:正如我们所见,我们可以声明这个规则提供的默认 GET 参数

  • encodeParams:此值表示是否应对参数进行编码

  • host:这是 URL 的主机信息部分

  • mode:这表示此规则是否用于解析请求的 URL 或创建 URL

  • name:这是规则的名称

  • pattern:这是用于解析和创建 URL 路径信息部分的模式

  • route:这是控制器操作的路径

  • suffix:这是用于此规则的 URL 后缀(.json.html等)

  • verb:这是此规则应与之匹配的 HTTP 动词(GET、POST、DELETE 等)

支持多语言视图的 URL 模式

在不同语言中显示相同视图有不同的方法。支持多语言视图的基本方法可以在路由的开头插入一个语言代码。例如,之前的路由news/index在英语中变为en/news/index,在意大利语中变为it/news/index,在法语中变为fr/news/index,依此类推。

将此规则附加到UrlManagerrules属性:

[
    'pattern' => '<lang:\w+>/<controller>/<action>',
    'route' => '<controller>/<action>',
],

所有在路径信息中以语言 ID 作为前缀的请求都将匹配并传递到带有 GET 中传递的$lang参数的<controller>/<action>路由。

现在,在NewsController中创建一个名为actionInternationalIndex的新操作来测试多语言支持:

public function actionInternationalIndex()
{
    // if missing, value will be 'en'
    $lang = Yii::$app->request->get('lang', 'en');

    Yii::$app->language = $lang;

    return $this->render('internationalIndex');
}

在此操作中,$lang是从 GET 参数中获取的。如果请求不包含$lang参数,则默认使用en值。

views/news/internationalIndex.php中创建新的视图来检查传递给 URL 的语言代码。

Requested language for this page is:
<br />
<b><?php echo Yii::$app->language ?></b>

通过访问http://hostname/news/international-index来验证此操作是否正确:

支持多语言视图的 URL 模式

设置英语

我们正在用英语可视化此页面,因为没有将语言代码传递给 URL。因此,已使用默认语言代码en。然而,如果我们把语言代码写入 URL,结果将改变。

例如,指向http://hostname/basic/web/it/news/international-index将显示以下内容:

支持多语言视图的 URL 模式

设置意大利语

这个响应确认我们已经使用了it作为语言代码。

注意

在支持多语言这种简单的方法中,我们从请求中获取$lang值,就像我们在actionInternationalIndex中所做的那样;然而,这是多余的,并且必须在所有请求中通用化。我们可以创建一个BaseController类作为每个 Controller 的基础类,然后覆盖beforeAction()方法,在那里我们可以设置Yii::$app->language参数。

创建规则类

以模式-路由对的形式声明的 URL 规则可以覆盖大多数项目。然而,对于 URL 可以是任何格式和数据库中存储的值的动态数据,这种规则不够灵活。

现在,我们需要使用只包含项目标题的 URL 来显示项目详情,例如http://hostname/basic/web/news/Test 2015-04-19 的新闻

就像我们现在所做的那样,没有方法可以用 URL 规则解决这个问题。

解析和创建 URL 请求的更通用解决方案是使用Rule类。

Rule类扩展了Object并实现了UrlRuleInterface

下一个示例将解释如何显示项目详情,从标题(在对象的data()数组中定义)中找到它,并使用Rule类解析和创建路由。

浏览器中显示的路由将具有news/title格式。

为了这个目的,如果基本文件夹下不存在,请在基本文件夹下创建一个名为components的新文件夹,并创建components/NewsUrlRule.php,内容如下:

<?php

namespace app\components;

use yii\web\UrlRuleInterface;
use yii\base\Object;

class NewsUrlRule extends Object implements UrlRuleInterface
{

  public function createUrl($manager, $route, $params)
  {
    if ($route === 'news/item-detail') {
      if (isset($params['title'])) {
        return 'news/'.$params['title'];
      }
    }
    return false;  // this rule does not apply
  }

  public function parseRequest($manager, $request)
  {
    $pathInfo = $request->getPathInfo();

    if (preg_match('%^([^\/]*)\/([^\/]*)$%', $pathInfo, $matches)) {
      if($matches[1] == 'news')
      {
        $params = [ 'title' => $matches[2]];
        return ['news/item-detail', $params];
      }
      else
      {
        return false;
      }
    }
    return false;  // this rule does not apply
  }
}

第一种方法,createUrl()接收$manager$route$params。有了路由和参数,框架会构建 URL。在这种情况下,我们检查传递的路由是否等同于news/item-detail,如果是,则返回相应的 URL。

第二种方法,parseRequest()接收$manager$request。将使用自定义正则表达式进行匹配,以使用$request数据提取所需的部分。这个过程将返回要执行的路由。

现在,将这些组件链接到位于config/web.php文件的urlManager,在urlManager组件的rule属性中追加以下行:

[
'class' => 'app\components\NewsUrlRule',
// ...configure other properties...
],

下一步要做的是在NewsController中创建actionItemDetail,如下所示:

public function actionItemDetail()
{
    $title = Yii::$app->request->get('title');

    $data = $this->data();

    $itemFound = null;

    foreach($data as $d)
    {
        if($d['title'] == $title) $itemFound = $d;
    }        

    return $this->render('itemDetail', ['title' => $title, 'itemFound' => $itemFound]);
}

在这个操作中,我们简单地从路由中接收到的标题开始查找项目。我们将标题和itemFound传递给视图。

最后要创建的文件是views下的itemDetail.php

Detail item with title <b><?php echo $title ?></b>
<br /><br />
<?php if($itemFound != null) { ?>
    <table border="1">
        <?php foreach($itemFound as $key=>$value) { ?>
        <tr>
            <th><?php echo $key ?></th>
            <td><?php echo $value ?></td>
        </tr>
        <?php } ?>
    </table>

    <br />

    Url for this items is: <?php echo yii\helpers\Url::to(['news/item-detail', 'title' => $title]); ?>

<?php } else { ?>
    <i>No item found</i>
<?php } ?>

创建规则类

项目详情输出

在这个视图中,将显示项目详情(如果找到项目),以及如何构建项目详情的 URL。

摘要

在本章中,我们看到了如何实现漂亮的 URL,这对于搜索引擎优化很有用。我们还创建了使用自定义规则解析和创建 URL 的示例。最后,我们学习了如何通过规则类构建更定制的 URL 规则。

在下一章中,我们将介绍数据库的使用,这是每个 Web 应用的基本方面。我们将从数据库连接的配置开始,一直到 Yii2 为开发者提供的工具,并使用框架小部件构建基于数据库数据的完整预订系统。

第四章:创建房间通过表单

本章解释了如何编写模型类来存储将使用表单从视图发送到控制器使用的数据,包括验证输入、格式化数据和上传文件。在本章中,我们将涵盖以下主题:

  • 创建模型

    • 示例 - 存储房间数据的模型
  • 使用 ActiveForm

    • 示例 - 从 HTML 表单创建新房间
  • 格式化日期、时间和数字

  • 上传文件

    • 示例 - 上传房间图片

创建模型

在视图和控制器之间操作数据的第一个步骤是创建一个模型。模型是一个类,它扩展了位于 yii\base\ 下的 Model 类,这是数据模型的基础。

这是一个合适的类,用于提供简单的解决方案,以封装数据、从数组(表单数据)分配内容,并使用规则验证数据。模型基类实现了以下常用功能:

  • 属性声明:默认情况下,每个公共类成员都被视为模型属性;我们可以使用模型的 attributes 属性访问所有成员。

  • 属性标签:每个属性都可以与一个用于显示的标签相关联;我们可以扩展 attributeLabels() 方法以返回与模型公共成员相关的标签。

  • 大量属性赋值:我们可以通过传递一个包含所有值的整个数组来填充模型的成员内容。当我们需要用表单中的数据填充模型时,这很方便。

  • 基于场景的验证:模型提供了验证数据的规则。我们可以根据场景选择哪些规则适用,场景是一个定义要应用规则的键词。

在执行数据验证时,模型还会引发以下事件:

  • EVENT_BEFORE_VALIDATE: 这是在 validate() 方法开始时引发的事件

  • EVENT_AFTER_VALIDATE: 这是在 validate() 方法结束时引发的事件

您可以直接使用模型来存储模型数据或对其进行自定义扩展。

示例 - 存储房间数据的模型

现在,让我们创建一个模型来存储房间数据。为了创建这个模型,我们选择将所有字段命名为小写字母,并用下划线分隔。

我们可以这样识别模型的这些字段:

  • floor: 在更通用的情况下,我们将其视为字符串成员

  • room_number: 这是一个整型成员

  • has_conditioner: 这是一个整型成员,有两个值 0 和 1

  • has_tv: 这是一个整型成员,有两个值 0 和 1

  • has_phone: 这是一个整型成员,有两个值 0 和 1

  • available_from: 这是一个日期成员,在 PHP 中用字符串表示

  • price_per_day: 这是一个浮点型成员

  • assistance_email: 这是一个包含电子邮件地址的字符串成员

  • description: 这是一个字符串成员

现在,创建名为 RoomModel 类,作为基类,在之前的字段列表中,在 basic/models/Room.php 下创建一个文件,内容如下:

<?php
namespace app\models;
use Yii;
use yii\base\Model;
class Room extends Model {
    public $floor;
    public $room_number;
    public $has_conditioner;
    public $has_tv;
    public $has_phone;
    public $available_from;
    public $price_per_day;
    public $description;
}

第二件事是附加attributeLabels()方法,以便为每个成员分配一个标签。这不是必需的,但这是一个有用的方法,可以在最终用户前端显示标签。

public function attributeLabels()
{
    return [
        'floor' => 'Floor',
        'room_number' => 'Room number',
        'has_condition' => 'Condition available',
        'has_tv' => 'TV available',
        'has_phone' => 'Phone available',
        'available_from' => 'Available from',
        'price_per_day' => 'Price (EUR/day)',
        'description' => 'Description',
    ];
}

最后,创建规则以验证数据。规则基于验证器,其默认值如下:

  • boolean: yii\validators\BooleanValidator

  • captcha: yii\captcha\CaptchaValidator

  • compare: yii\validators\CompareValidator

  • date: yii\validators\DateValidator

  • double: yii\validators\NumberValidator

  • email: yii\validators\EmailValidator

  • exist: yii\validators\ExistValidator

  • file: yii\validators\FileValidator

  • filter: yii\validators\FilterValidator

  • image: yii\validators\ImageValidator

  • in: yii\validators\RangeValidator

  • integer: yii\validators\NumberValidator

  • match: yii\validators\RegularExpressionValidator

  • required: yii\validators\RequiredValidator

  • safe: yii\validators\SafeValidator

  • string: yii\validators\StringValidator

  • trim: yii\validators\FilterValidator

  • unique: yii\validators\UniqueValidator

  • url: yii\validators\UrlValidator

规则是一个数组,其值按以下顺序排列:

  • 一个字符串或数组,用于定义要应用规则的属性或属性列表

  • 验证器的类型

  • 使用on属性来定义要使用哪种场景

  • 其他参数取决于所使用的验证器

编写Room模型类的rules()方法:

/**
 * @return array the validation rules.
 */
public function rules()
{
    return [
        ['floor', 'integer', 'min' => 0],
        ['room_number', 'integer', 'min' => 0],
        [['has_conditioner', 'has_tv', 'has_phone'], 'integer', 'min' => 0, 'max' => 1],
        ['available_from', 'date', 'format' => 'php:Y-m-d'],
        ['price_per_day', 'number', 'min' => 0],
        ['description', 'string', 'max' => 500]
    ];
}

前面的代码解释如下:

  • 第一条规则规定floor是一个整数,最小值为0

  • 第二条规则规定room_number是一个整数,最小值为0;我们可以将楼层和房间合并为一个规则,将它们作为单个规则的第一个参数放入数组中

  • 第三条规则规定has_conditionhas_tvhas_phone是介于 0 和 1 之间的整数(形式上为布尔值)

  • 第四条规则规定available_from是一个日期

  • 第五条规则规定price_per_day是一个数字,其最小值为 0

  • 最后一条规则规定description是一个最多包含 500 个字符的字符串

当调用Modelvalidate()方法时,将应用这些规则。当我们尝试调用save()方法时,此方法会自动调用。

使用 ActiveForm

现在,我们将在视图中创建一个 HTML 表单,以便从视图发送数据到控制器。我们可以使用标准方式通过表单标签和输入字段来构建表单,但 Yii2 提供了简化表单及其内容构建的辅助类。

为了这个目的,我们将使用ActiveForm,这是一个用于为单个或多个数据模型构建交互式 HTML 表单的控件。

对于任何 Yii2 控件,我们将使用begin()静态方法来指示开始使用它的时刻,以及使用end()静态方法来指示停止使用它的时刻,从yii\widgets\ActiveForm开始。这些方法之间的代码将被放置在表单中:

$form = ActiveForm::begin();
... content here ...
ActiveForm::end();

第一个方法,begin(),返回一个对象,我们可以在内容中使用它来创建输入字段。此方法接受一个数组作为参数,以指示要应用的配置属性。最后一个方法,end(),标记小部件的结束,因此可以将其内容渲染出来。

现在,我们需要在代码中插入一些输入字段,这是通过使用我们刚刚创建的ActiveForm实例的field()方法来完成的。此方法需要两个参数:模型和字段名,并返回一个类型为ActiveField的对象。使用此方法,我们只是要求ActiveForm创建一个新的字段;然而,在这种情况下,我们还需要指定我们想要的字段类型。

此操作是通过调用与实例输入类型相关的ActiveField方法来完成的。最常见的是:

  • label(): 这用于生成一个标签标签

  • textInput(): 这用于生成类型为text的输入字段

  • textarea(): 这用于生成textarea标签

  • radio(): 这用于生成类型为radio的输入字段

  • checkbox(): 这用于生成类型为checkbox的输入字段

示例 – 从 HTML 表单创建新房间

首先,在basic/controllers/RoomsController.php下创建一个新的控制器,名为RoomsController,并有一个名为create的操作:

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\Room;

class RoomsController extends Controller
{
    public function actionCreate()
    {
        $model = new Room();
        $modelCanSave = false;

        if ($model->load(Yii::$app->request->post()) && $model->validate()) {
            $modelCanSave = true;
        }

        return $this->render('create', [
            'model' => $model,
            'modelSaved' => $modelCanSave
        ]);
    }
}

create()方法开始时,我们为$model变量创建了一个新的Room类实例。load()方法将数据填充到$model属性中,这些数据是从作为参数传递的数组中名为$model->formName()的关键位置获取的。默认情况下,$model->formName()返回对象的类名,如下面的代码所示:

$model->load(Yii::$app->request->post())

前面的代码等同于:

if (isset($_POST[$model->formName()])) {
  $this->setAttributes($_POST[$model->formName()]);
}

回到load()&&validate()条件,如果load()返回 true,则validate()也会被执行,并且模型rules()方法中的所有规则都将被评估。

在这种情况下,Model已准备好保存到数据存储(在下一章中将在数据库中保存)。现在,用简单变量$modelCanSave标记此条件非常重要,并将其传递给create视图。

basic/views/rooms/create.php下创建create视图的文件:

<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
use yii\helpers\Url;
use yii\helpers\ArrayHelper;
?>

<?php if($modelCanSave) { ?>
<div class="alert alert-success">
    Model ready to be saved!
</div>
<?php } ?>

<?php $form = ActiveForm::begin(); ?>
<div class="row">
    <div class="col-lg-12">
        <h1>Room form</h1>
        <?= $form->field($model, 'floor')->textInput() ?>
        <?= $form->field($model, 'room_number')->textInput() ?>
        <?= $form->field($model, 'has_conditioner')->checkbox() ?>
        <?= $form->field($model, 'has_tv')->checkbox() ?>
        <?= $form->field($model, 'has_phone')->checkbox() ?>
        <?= $form->field($model, 'available_from')->textInput() ?>
        <?= $form->field($model, 'price_per_day')->textInput() ?>
        <?= $form->field($model, 'description')->textarea() ?>
   </div>
</div>
<div class="form-group">
    <?= Html::submitButton('Create' , ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>

如果$modelCanSave变量为 true,将显示一个带有绿色背景的alert div,以通知$model已加载并验证(准备在数据库中保存)。

对于测试代码,指向http://hostname/basic/web/rooms/create。以下屏幕应出现:

示例 – 从 HTML 表单创建新房间

创建房间 HTML 表单

框架会自动处理输入字段的验证检查,这些检查对应于模型rules()方法中的规则列表。我们可以通过在Floor输入框中输入字符来检查这一点。我们应该看到以下截图:

示例 – 从 HTML 表单创建新房间

整数字段的验证检查

验证通知我们 Floor 必须是一个整数,正如规则列表中要求的那样。一旦所有字段都填写了正确的值(日期格式,yyyy-mm-dd),只需点击 创建 按钮,我们应该会看到一个绿色背景的框显示 模型准备保存

格式化日期、时间和数字

现在,让我们看看如何格式化日期、时间和数字字段。Yii2 为这些类型提供了辅助工具。

要格式化一个值,我们将使用 Yii::$app->formatter;此对象属于位于 yii\i18n\ 下的 Formatter 类,支持多种格式化类型。用于此目的的所有方法都以 as 前缀开头。因此,asDate 方法将用于格式化日期,而 asCurrency 方法将用于格式化货币。

每个格式化方法的第一个参数是要格式化的值,其他字段指的是要使用的格式以及其他可选参数。

让我们通过添加准备保存的模型内容来更改视图内容:

<?php if($modelCanSave) { ?>
<div class="alert alert-success">
    Model ready to be saved!
    <br /><br />
    These are values: <br />
    Floor: <?php echo $model->floor; ?> <br />
    Room Number: <?php echo $model->room_number; ?> <br />
    Has conditioner: <?php echo Yii::$app->formatter->asBoolean($model->has_conditioner); ?> <br />
    Has TV: <?php echo Yii::$app->formatter->asBoolean($model->has_tv); ?> <br />
    Has phone: <?php echo Yii::$app->formatter->asBoolean($model->has_phone); ?> <br />
    Available from (mm/dd/yyyy): <?php echo Yii::$app->formatter->asDate($model->available_from,'php:m/d/Y'); ?> <br />
    Price per day: <?php echo Yii::$app->formatter->asCurrency($model->price_per_day,'EUR'); ?> <br />

</div>
<?php } ?>

如果 $model 准备好保存,在绿色背景的框中,我们将看到模型每个字段的输出。

在此示例中,我们使用了:

  • has_conditionhas_tvhas_phone 成员的 boolean 格式化器使用默认的 false 和 true 值表示;默认情况下,false 为 No,true 为 Yes,但我们可以通过 Yii::$app->formatter$booleanFormat 成员来更改此行为设置。

  • available_from 成员的 date 格式化器将使用的日期格式作为第二个参数;此日期格式可以用 PHP 日期函数风格或 ICU 标准表示。

  • price_per_day 成员的 currency 格式化器是第二个参数,包含三个字符类型的货币要使用。

这就是包含模型内容的框的显示方式:

格式化日期、时间和数字

当验证成功时显示模型内容的摘要。

上传文件

当数据从视图发送到控制器时,常见的任务是上传文件。在这种情况下,Yii2 也提供了一个方便的辅助工具来处理此任务:yii\web\UploadedFile。此类有两个重要方法:getInstance()(复数形式 getInstances())和 saveAs()

第一种方法 getInstance() 允许我们从表单的输入字段中获取文件,而第二种方法 saveAs(),正如其名称所暗示的,允许我们将文件输入字段的内容保存到服务器文件系统中。

在开始示例之前,创建一个将包含上传文件的文件夹是很重要的。创建此文件夹的最佳位置是应用程序的根目录。因此,在 basic/ 文件夹下创建一个名为 uploadedfiles 的文件夹。

注意

确保此文件夹可写。

接下来,为了集中配置,为这个新文件夹定义一个别名,这样我们就可以从应用程序配置中更改此路径。在basic/config/web.php中输入,如果不存在,则将这些行追加到$config数组中:

'aliases' =>
[
        '@uploadedfilesdir' => '@app/uploadedfiles'
],

注意

@app是一个系统别名,它定义了应用程序的根目录。

示例 - 上传一个房间的图片

在这个示例中,我们将看到如何上传一个房间的图片。

我们需要在模型、视图和控制器中进行更改。让我们从模型开始。

在模型中,我们需要添加一个新的属性,命名为fileImage,并为其指定特定的规则。

这是模型的最终版本:

<?php
namespace app\models;
use Yii;
use yii\base\Model;
class Room extends Model
{
    public $floor; 
    public $room_number;
    public $has_conditioner;
    public $has_tv;
    public $has_phone;
    public $available_from;
    public $price_per_day;
    public $description;

    public $fileImage;

    public function attributeLabels()
    {
        return [
            'floor' => 'Floor',
            'room_number' => 'Room number',
            'has_conditioner' => 'Conditioner available',
            'has_tv' => 'TV available',
            'has_phone' => 'Phone available',
            'available_from' => 'Available from',
            'price_per_day' => 'Price (Eur/day)',
            'description' => 'Description',
            'fileImage' => 'Image'
        ];
    }

    /**
     * @return array the validation rules.
     */
    public function rules()
    {
        return [
            ['floor', 'integer', 'min' => 0],
            ['room_number', 'integer', 'min' => 0],
            [['has_conditioner', 'has_tv', 'has_phone'], 'integer', 'min' => 0, 'max' => 1],
            ['available_from', 'date', 'format' => 'php:Y-m-d'],
            ['price_per_day', 'number', 'min' => 0],
            ['description', 'string', 'max' => 500],

            ['fileImage', 'file']
        ];
    } 
}

在规则中,对于fileImage字段,我们可以添加多种类型的验证;例如,检查是否必需,检查 MIME 类型(.gif.jpeg.png)。

接下来,我们将使用UploadedFile类的静态方法getInstance()在控制器中获取输入文件字段中的文件,然后使用saveAs将其保存到特定文件夹。这是RoomsController的最终版本:

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\Room;

class RoomsController extends Controller
{
    public function actionCreate()
    {
        $model = new Room();
        $modelCanSave = false;

        if ($model->load(Yii::$app->request->post()) && $model->validate()) {

            $model->fileImage = UploadedFile::getInstance($model, 'fileImage');

            if ($model->fileImage) { 
                $model->fileImage->saveAs(Yii::getAlias('@uploadedfilesdir/' . $model->fileImage->baseName . '.' . $model->fileImage->extension)));
            } 

            $modelCanSave = true;
        }

        return $this->render('create', [
            'model' => $model,
            'modelSaved' => $modelCanSave
        ]);
    }
}

UploadedFile::getInstance$_FILES数组中获取文件,以填充 Model 的fileImage属性并使用其数据。

最后要做的事情是更新create视图内容,通过添加fileInput字段。这是最终版本:

<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
use yii\helpers\Url;
use yii\helpers\ArrayHelper;
?>

<?php if($modelCanSave) { ?>
<div class="alert alert-success">
    Model ready to be saved!
    <br /><br />
    These are values: <br />
    Floor: <?php echo $model->floor; ?> <br />
    Room Number: <?php echo $model->room_number; ?> <br />
    Has conditioner: <?php echo Yii::$app->formatter->asBoolean($model->has_conditioner); ?> <br />
    Has TV: <?php echo Yii::$app->formatter->asBoolean($model->has_tv); ?> <br />
    Has phone: <?php echo Yii::$app->formatter->asBoolean($model->has_phone); ?> <br />
    Available from (mm/dd/yyyy): <?php echo Yii::$app->formatter->asDate($model->available_from,'php:m/d/Y'); ?> <br />
    Price per day: <?php echo Yii::$app->formatter->asCurrency($model->price_per_day,'EUR'); ?> <br />
    Image:
    <?php if(isset($model->fileImage)) { ?>
        <img src="img/'.$model->fileImage->name) ?>" />
    <?php } ?>
</div>
<?php } ?>

<?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data']]); ?>  
<div class="row">
    <div class="col-lg-12">
        <h1>Room form</h1>
        <?= $form->field($model, 'floor')->textInput() ?>
        <?= $form->field($model, 'room_number')->textInput() ?>
        <?= $form->field($model, 'has_conditioner')->checkbox() ?>
        <?= $form->field($model, 'has_tv')->checkbox() ?>
        <?= $form->field($model, 'has_phone')->checkbox() ?>
        <?= $form->field($model, 'available_from')->textInput() ?>
        <?= $form->field($model, 'price_per_day')->textInput() ?>
        <?= $form->field($model, 'description')->textarea() ?>

        <?= $form->field($model, 'fileImage')->fileInput() ?>
   </div>
</div>
<div class="form-group">
    <?= Html::submitButton('Create' , ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>

注意最后一行此示例,ActiveForm::end()关闭了在文件顶部使用ActiveForm::begin()方法定义的$form小部件的主体。

注意

在这个示例中,ActiveForm小部件是通过将配置数组的enctype属性填充为multipart/form-data值创建的,这允许我们发送除了表单文本参数之外的二进制数据。然而,这并不涉及 Yii 或 PHP,因为这是一个 HTML 要求,用于通知浏览器如何将文件发送到服务器。

在这个视图中,如果模型已经经过验证并且fileImage属性已填充,则将显示相应的图片。

摘要

在本章中,我们看到了如何从头开始构建 Model 类,并使用 Yii2 ActiveForm 小部件创建的表单从视图向控制器发送数据。我们还探讨了格式化数据以及从表单发送文件的常用方法。

在下一章中,你将学习如何与数据库交互并将视图表单中的模型数据保存到数据库中。

第五章. 开发预订系统

在本章中,你将学习如何配置和管理数据库,直接使用 SQL 或 ActiveRecord,然后你将看到如何解决常见任务,例如从表单保存单个和多个模型,以及如何创建数据聚合和过滤视图。

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

  • 配置数据库连接:

    • 例如,创建房间、客户和预订表
  • 例如,测试连接并执行 SQL 查询

  • 使用 Gii 创建房间、客户和预订模型

  • 使用 ActiveRecord 操作数据:

    • 例如,使用 ActiveRecord 查询房间列表
  • 处理关系:

    • 例如,使用关系连接房间、预订和客户
  • 如何从表单中保存模型:

    • 例如,从表单创建和更新房间
  • 设置 GMT 时区

  • 使用多个数据库连接:

    • 例如,配置第二个数据库连接以将数据导出到本地 SQLite 数据库

配置数据库连接

Yii2 提供了一个高级层来访问数据库,它建立在PHP 数据对象PDO)之上。

此框架允许我们通过使用 ActiveRecord 对象来操作数据库表的内容。这封装了访问单个或多个记录的方法,以及以直观的方式过滤、连接和排序数据。

再次强调,我们可以使用纯 SQL 与数据库交互,但这意味着我们必须处理通过不同数据库(MySQL、SQL Server、Postgres、Oracle 等)传递的 SQL 语言差异,这意味着会失去 Yii2 的功能。

数据库对象连接是yii\db\Connection的一个实例:

$db = new yii\db\Connection([
    'dsn' => 'mysql:host=localhost;dbname=my_database',
    'username' => 'my_username',
    'password' => 'my_password',
    'charset' => 'utf8',
]);

在本例中,我们连接到一个 MySQL 服务器,使用mysql连接字符串连接到数据库my_databases,将my_username设置为username,将my_password设置为password。此外,我们将charset设置为utf8,以确保使用标准字符集。这是一个标准的数据库连接条目。

其他常见的可用连接字符串包括:

  • MySQL 和 MariaDB: mysql:host=localhost;dbname=mydatabase

  • SQLite: sqlite:/path/to/database/file

  • PostgreSQL: pgsql:host=localhost;port=5432;dbname=mydatabase

  • MS SQL Server(通过mssql驱动程序):mssql:host=localhost;dbname=mydatabase

  • Oracle: oci:dbname=//localhost:1521/mydatabase

注意

如果我们不提供直接到数据库的驱动程序,而必须使用 ODBC,我们将提供一个 ODBC 连接对象的示例如下:

$db = new yii\db\Connection([
     'driverName' => 'mysql',
    'dsn' => 'odbc:Driver={MySQL};Server=localhost;Database=my_database',
    'username' => 'my_username',
    'password' => 'my_password',
    'charset' => 'utf8',
]);

为了方便,我们将数据库连接设置为应用程序组件,因为它将在应用程序的许多地方被使用。在basic/config/web.php中:

return [
    // ...
    'components' => [
        // ...
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=my_database',
            'username' => 'my_username',
            'password' => 'my_password',
            'charset' => 'utf8',
        ],
    ],
    // ...
];

注意

在基本模板中,数据库配置在一个单独的文件中,通常是basic/config/db.php

如果我们打开basic/config/web.php,我们可以看到db.php文件填充了主配置的db属性。

示例 - 创建房间、客户和预订表

现在,我们需要一个 MySQL 数据库实例来工作。打开 DB 管理面板(如果提供)或使用控制台直接访问 DB,创建一个名为 my_database 的新数据库,关联用户名 my_username 和密码 my_password

在这个例子中,我们将创建三个数据库表来管理房间、客户和预订数据。

房间将有以下字段:

  • id 作为整数

  • floor 作为整数

  • room_number 作为整数

  • has_conditioner 作为整数

  • has_tv 作为整数

  • has_phone 作为整数

  • available_from 作为日期

  • price_per_day 作为小数

  • description 作为文本

room 表的脚本将是:

CREATE TABLE `room` (
  `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
  `floor` int(11) NOT NULL,
  `room_number` int(11) NOT NULL,
  `has_conditioner` int(1) NOT NULL,
  `has_tv` int(1) NOT NULL,
  `has_phone` int(1) NOT NULL,
  `available_from` date NOT NULL,
  `price_per_day` decimal(20,2) DEFAULT NULL,
  `description` text);

客户将有以下字段:

  • id 作为整数

  • name 作为字符串

  • surname 作为字符串

  • phone_number 作为字符串

customer 表的脚本将是

CREATE TABLE `customer` (
 `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
 `name` varchar(50) NOT NULL,
 `surname` varchar(50) NOT NULL,
 `phone_number` varchar(50) DEFAULT NULL
);

预订将有以下字段:

  • id 作为整数

  • room_id 作为整数,它是房间表的引用

  • customer_id 作为整数,它是客户表的引用

  • price_per_day 作为小数

  • date_from 指定入住日期

  • date_to 指定退房日期

  • reservation_date 作为创建的时间戳

  • days_stay 作为整数

reservation 表的脚本将是:

CREATE TABLE `reservation` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `room_id` int(11) NOT NULL,
 `customer_id` int(11) NOT NULL,
 `price_per_day` decimal(20,2) NOT NULL,
 `date_from` date NOT NULL,
 `date_to` date NOT NULL,
 `reservation_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
);

最后,将 basic/config/web.php 放在 components 属性中:

$db = new yii\db\Connection([
    'dsn' => 'mysql:host=localhost;dbname=my_database',
    'username' => 'my_username',
    'password' => 'my_password',
    'charset' => 'utf8',
]);

然后,我们就准备好测试到数据库的连接了。

示例 - 测试连接和执行 SQL 查询

现在我们来看看如何测试数据库连接。

在数据库表中放入一些房间数据:

INSERT INTO `my_database`.`room` (`id`, `floor`, `room_number`, `has_conditioner`, `has_tv`, `has_phone`, `available_from`, `price_per_day`, `description`)
VALUES
(NULL, '1', '101', '1', '0', '1', '2015-05-20', '120', NULL), (NULL, '2', '202', '0', '1', '1', '2015-05-30', '118', NULL);

数据库查询使用 yii\db\Command 对象进行,该对象通过 yii\db\Connection::createCommand() 方法静态创建。

从命令中检索数据的最重要方法有:

  • queryAll(): 此方法返回查询的所有行,其中每个数组元素都是一个表示数据行的数组;如果查询没有返回数据,则响应为一个空数组

  • queryOne(): 此方法返回查询的第一行,即一个数组,表示数据行;如果查询没有返回数据,则响应为 false 布尔值

  • queryScalar(): 此方法返回查询结果第一行的第一列的值;如果没有值,则返回 false

  • query(): 这是返回 yii\db\DataReader 对象的最常见响应;如果没有值,则返回 false

现在我们将以不同的方式显示 room 表的内容。

我们将在 basic/controllers/RoomsController.php 中更新 RoomsController。在这个文件中,我们将添加一个索引操作来获取数据并将其传递给视图:

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;

class RoomsController extends Controller
{
    public function actionIndex()
    {
        $sql = 'SELECT * FROM room ORDER BY id ASC';

        $db = Yii::$app->db;

        $rooms = $db->createCommand($sql)->queryAll();

    // same of
     // $rooms = Yii::$app->db->createCommand($sql)->queryAll();

        return $this->render('index', [ 'rooms' => $rooms ]);
    }
}

actionIndex() 的内容非常简单。定义 $sql 变量,包含要执行的 SQL 语句,然后将查询结果填充到 $rooms 数组中,最后渲染 index 视图,传递房间变量。

basic/views/rooms/index.php的视图内容中,我们将以表格形式显示$rooms数组,以利用 Bootstrap CSS 的优势,并将table类应用到表格 HTML 标签上。

这是basic/views/rooms/index.php中的内容,我们也可以看到所使用的数据格式化器:

<table class="table">
    <tr>
        <th>Floor</th>
        <th>Room number</th>
        <th>Has conditioner</th>
        <th>Has tv</th>
        <th>Has phone</th>
        <th>Available from</th>
        <th>Available from (db format)</th>
        <th>Price per day</th>
        <th>Description</th>
    </tr>
    <?php foreach($rooms as $item) { ?>
    <tr>
        <td><?php echo $item['floor'] ?></td>
        <td><?php echo $item['room_number'] ?></td>
        <td><?php echo Yii::$app->formatter->asBoolean($item['has_conditioner']) ?></td>
        <td><?php echo Yii::$app->formatter->asBoolean($item['has_tv']) ?></td>
        <td><?php echo ($item['has_phone'] == 1)?'Yes':'No' ?></td>
        <td><?php echo Yii::$app->formatter->asDate($item['available_from']) ?></td>
        <td><?php echo Yii::$app->formatter->asDate($item['available_from'], 'php:Y-m-d') ?></td>
        <td><?php echo Yii::$app->formatter->asCurrency($item['price_per_day'], 'EUR') ?></td>
        <td><?php echo $item['description'] ?></td>
    </tr>
    <?php } ?>
</table>

floorroom_number字段直接显示。

下两个字段has_conditionerhas_tv通过使用 Yii2 提供的布尔格式化器进行显示;布尔格式化器将使用在 Yii2 配置期间定义的区域设置。

下一个字段has_phone以与前两个字段相同的方式渲染其值;这样做的原因是为了指示如何以标准 PHP 风格产生布尔格式化器的相同输出。

然后,available_from字段以两种不同的方式使用日期格式化器进行渲染,直接渲染和传递要使用的格式。或者,如果没有传递参数,它将采用默认格式。

再次,price_per_day字段通过货币格式化器进行渲染,传递货币作为参数。如果没有传递参数,将使用默认值。最后一个字段description直接显示。将您的浏览器指向http://hostname/basic/web/rooms/index以查看以下内容:

示例 - 测试连接并执行 SQL 查询

房间列表

使用 Gii 创建房间、客户和预订模型

Yii2 提供了一个强大的工具来生成模型、控制器和 CRUD(创建、读取、更新和删除)操作、表单、模块和扩展:Gii。

basic/config/web.php文件的底部,放置在基本标准配置中,有一段代码启用了 Gii:

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';
}

确认这些行存在,否则在web.php文件的return $config语句之前将它们附加到文件底部。最后的检查在basic/web/index.php中。确认YII_ENVdev,如下行:

defined('YII_ENV') or define('YII_ENV', 'dev');

现在,我们可以将我们的浏览器指向http://hostname/basic/web/gii,我们应该看到这个错误页面:

使用 Gii 创建房间、客户和预订模型

禁止访问 Gii

由于 Gii 被密码锁定,此页面将显示。

我们需要向gii模块添加额外的配置,传递其他允许的 IP。Gii 的配置有一个名为allowedIPs的属性,它允许指定哪些 IP 地址可以访问 Gii 页面:

 'allowedIPs' => ['127.0.0.1', '::1', '192.168.178.20']

在这个摘录中,Gii 将接受来自本地主机(以 IPv4 形式的 127.0.0.1 和 IPv6 形式的::1)以及192.168.178.20的访问,这应该是我们私有网络中的 IP 地址。

如果 Yii2 应用程序运行在外部托管上,我们将在此允许的 IP 列表中设置我们的公网 IP 地址。例如,如果我们的 IP 是66.249.64.76,此条目将被附加到现有条目(如果我们想保持其他允许的访问点):

 'allowedIPs' => ['127.0.0.1', '::1', '192.168.178.20', '66.249.64.76']

为了允许从任何地方访问(在开发阶段很有用),我们可以在列表中添加*,这意味着 Gii 页面可以从任何 IP 地址访问:

'allowedIPs' => ['127.0.0.1', '::1', '192.168.178.20', '*']

因此,gii]['gii'] = 'yii\gii\Module'的内容是:

    $config['modules']['gii'] = [
        'class' => 'yii\gii\Module',
        'allowedIPs' => ['127.0.0.1', '::1', '192.168.178.20', '*'] ]; configuration in basic/config/web.php will be:
if (YII_ENV_DEV) {
    // configuration adjustments for 'dev' environment
    $config['bootstrap'][] = 'debug';
    $config['modules']['debug'] = 'yii\debug\Module';

    $config['bootstrap'][] = 'gii';
    //$config'modules'
}

现在,我们能够从任何 IP 地址访问 Gii。

通过点击页面http://hostname/basic/web/gii刷新浏览器,我们最终可以看到其内容:

![使用 Gii 创建房间、客户和预订模型成功访问 Gii 现在,点击模型生成器开始按钮;我们将有一个模型生成器的表单,其中表名是唯一需要填写的字段。当我们开始输入表名时,将显示可能的选项。完成此操作后,当我们移动到模型类字段时,框架将自动填充它。其他字段可以保留默认设置。在表名中输入room,然后点击模型类字段。此字段将填充为Room,这是models文件夹中的文件名。点击预览按钮将显示文件将被创建的路径以及将要应用的操作(它应该是覆盖值,因为我们是在上一章中创建的)。最后,点击生成按钮以完成此操作。响应消息将给我们关于此操作执行的信息。这是成功结果的形式:使用 Gii 创建房间、客户和预订模型

Gii 模型生成器

对其他两个表:预订和客户重复此操作。

现在,我们在basic/models文件夹中有三个模型:Room.phpReservation.phpCustomer.php

让我们解释 Gii 做了什么。打开basic/models/Room.php文件,我们有三个方法:

  • tableName()

  • rules()

  • attributeLabels()

第一种方法,tableName(),简单地返回此模型链接的表名:

    public static function tableName()
    {
        return 'room';
    }

第二种方法,rules(),很重要,因为它包含在validate()方法启动时(在save()方法中自动启动)或大量属性赋值时需要检查的验证规则:

$model->attributes = arrayWithData;

这是rules()方法的内容:

    public function rules()
    {
        return [
            [['floor', 'room_number', 'has_conditioner', 'has_tv', 'has_phone', 'available_from'], 'required'],
            [['floor', 'room_number', 'has_conditioner', 'has_tv', 'has_phone'], 'integer'],
            [['available_from'], 'safe'],
            [['price_per_day'], 'number'],
            [['description'], 'string']
        ];
    }

第一条规则指定字段floorroom_numberhas_conditionhas_tvavaiable_from是必填的,因为它们传递给必需的验证器。此外,它们必须是整数,如第二条规则所要求的。

注意

不在规则中的字段,在大量赋值时将被跳过,因为它们被认为是不可靠的(因为它们不在规则中)。因此,当一个字段没有验证规则时,它必须在'safe'验证器中有一个条目。

第四条规则指定price_per_day字段是一个数字,而最后一条规则说明description是一个字符串。

注意

这些规则会自动从数据库字段类型和约束中读取。

最后一个方法attributeLabels()指定了在显示视图(如表单、网格等)中字段的表示。

这是attributeLabels()的内容:

    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'floor' => 'Floor',
            'room_number' => 'Room Number',
            'has_conditioner' => 'Has Conditioner',
            'has_tv' => 'Has Tv',
            'has_phone' => 'Has Phone',
            'available_from' => 'Available From',
            'price_per_day' => 'Price Per Day',
            'description' => 'Description',
        ];
    }

Yii2 在模型中报告数据库中存在的任何表之间的关系。我们有一个Reservation模型,它与RoomCustomer有关联。

按照以下说明操作,使框架能够在模型中创建关系:

  1. 确认数据库表使用 InnoDB 引擎(支持关系和外键)。

  2. Reservation表中,为room_idcustomer_id字段分别添加两个索引:

    ALTER TABLE `reservation` ADD INDEX ( `room_id` ) ;
    ALTER TABLE `reservation` ADD INDEX ( `customer_id` ) ;
    
  3. Reservation表中,向roomcustomer表添加两个约束:

    ALTER TABLE `reservation` ADD FOREIGN KEY ( `room_id` ) REFERENCES `room` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT ;
    ALTER TABLE `reservation` ADD FOREIGN KEY ( `customer_id` ) REFERENCES `customer` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT ;
    

    注意

    在这些约束中,我们为DELETEUPDATE操作使用了RESTRICTRESTRICT避免了删除引用客户或房间的预订。因此,要删除在预订中出现的客户或房间,我们首先需要删除预订。

    这种行为确保在删除房间或客户时,重要数据如预订永远不会自动(级联)删除。当你尝试对与客户或房间链接的预订执行此操作时,将显示错误消息。

    在其他上下文中,常用的关键字是CASCADE,它删除所有引用链接表的数据。

再次打开 Gii,导航到http://hostname/basic/web/gii,然后在模型生成器中点击开始按钮,并在表名中输入room。点击页面底部的预览按钮,这次你会看到models/Room.php存在,并且动作是覆盖,未标记。

点击“覆盖”旁边的复选框,然后点击生成按钮。这样,我们就强制覆盖了Room模型,以从Room表中获取关系数据。

现在,basic/models/Room.php在底部包含一个名为getReservations的新方法,其内容如下:

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getReservations()
    {
       return $this->hasMany(Reservation::className(), ['room_id' => 'id']);
    }

此方法返回一个 ActiveQuery 实例,用于构建要发送到数据库的查询。

注意

当作为属性调用时,此方法将返回与模型链接的预订列表。

你可能会遇到这种情况,即$modelRoom类的一个实例,例如:$reservationsList = $model->reservations;

在这种情况下,将$reservationsList变量填充与这个Room模型相关的预订列表。

这并不奇怪,尽管hasMany方法返回一个ActiveQuery对象。

如果我们探索BaseActiveRecord__get()方法(它是 ActiveRecord 的基类),处理属性要求,我们可以看到这些代码行:

            $value = parent::__get($name);
            if ($value instanceof ActiveQueryInterface) {
                return $this->_related[$name] = $value->findFor($name, $this);
            } else {
                return $value;
            }

$value内容是ActiveQueryInterface(由ActiveQuery类实现的接口)的实例时,此方法返回关联结果。

使用 ActiveRecord 操作数据

ActiveRecord 提供了一种方便的方式来访问和操作存储在数据库中的数据。这个类与一个数据库表相关联,并代表了这个关联表的行。它的属性是表的字段,它的方法允许我们执行数据库的常见操作,如选择、插入或更新 SQL 语句。

ActiveRecord 支持许多常见数据库,例如:

  • MySQL

  • PostgreSQL

  • SQLite

  • Oracle

  • 微软 SQL Server

此外,一些 NoSQL 数据库受到支持,例如:

  • Redis

  • MongoDB

ActiveRecord 在每次实例化时都会读取表结构,并将表列作为其属性提供。对表结构的任何更改都会立即在 ActiveRecord 对象中可用。

因此,如果一个表包含字段 idfloorroom_number,并且如果 $modelyii\db\ActiveRecord 的一个实例,为了访问这些字段,只需输入:

$id = $model->id;
$floor = $model->floor;
$room_number = $model->room_numer;

ActiveRecord 使用 __get 魔法方法处理属性请求,并捕获相应表列的内容。在上一个段落中,您看到了如何使用 Gii 从数据库表创建模型类,以扩展 yii\db\ActiveRecord。ActiveRecord 使用的语法简单且冗余,因此容易记住。现在让我们看看如何使用 ActiveRecord 从数据库中查询数据。

数据通过一个 \yii\db\ActiveQuery 对象从数据库中检索来构建查询,最后调用 one()all() 方法来获取一个 ActiveRecord 对象或 ActiveRecord 对象的列表。

通过调用其静态方法 ::find() 从 ActiveRecord 对象返回一个 ActiveQuery 对象。

如果 Room 是一个模型(并且是 ActiveRecord 的子类),则从以下内容返回一个 ActiveQuery:

// $query is an ActiveQuery object
$query = Room::find();

ActiveQuery 对象提供了一些方法来构建查询,这些方法的名称类似于 SQL 表达式。

最常见的一些是:

  • 使用 where() 来添加条件

  • 使用 orderBy() 来应用排序

  • 使用 groupBy() 来进行聚合

几乎所有这些方法都支持一个可以是一个字符串或数组的参数。如果是字符串,它将原样传递到 SQL 查询中;如果是数组,则键用作列名,值用作相应的值。例如,我们想要构建查询来找到一楼上的房间:

$query = Room::find()->where('floor = 1');
// equivalent to
$query = Room::find()->where(['floor' => 1]);

对于复杂条件,where() 支持操作符格式,其中条件是一个数组,包含:

[operator, operand1, operand2, …]

例如,我们想要构建一个查询来找到一楼上的房间:

$query = Room::find()->where(['>=', 'floor', 1]);
// equivalent to
$query = Room::find()->where('floor >= 1';

可以使用 andWhere()orWhere() 添加其他条件,只需使用 andor 逻辑连接符。

where() 方法的数组参数比字符串更可取,因为我们可以轻松地将字段名与其内容分开,并将 where() 方法的第二个参数设置为包含参数键值对的数组。

创建查询对象后,要从 ActiveQuery 获取数据,我们将有:

  • one():此方法返回一个 ActiveRecord 对象或未找到时返回 null

  • all(): 此方法返回 ActiveRecord 对象列表或未找到时返回空数组

因此,要获取一楼房间,我们必须编写:

$query = Room::find()->where(['floor' => 1]);
$items = $query->all();
// equivalent to
$items = Room::find()->where(['floor' => 1])->all();

注意

从 ActiveRecord 获取数据有一个更简洁的语法:findOne()findAll()方法,它们返回单个 ActiveRecord 或 ActiveRecord 列表。与前述方法唯一的不同之处在于,它们接受单个参数,可以是:

  • 通过主键过滤的数字

  • 一个由主键值列表组成的标量值数组,用于过滤(仅适用于findAll(),因为findOne()返回单个 ActiveRecord)

  • 一个由属性值组成的键值对数组,用于过滤一组属性值

ActiveRecord 的其他常用方法包括:

  • validate(): 此方法用于将规则验证应用于模型的属性

  • save(): 此方法用于保存新模型或更新已存在的模型(如果save()方法应用于获取的 ActiveRecord 对象)

  • delete(): 此方法用于删除模型

示例 - 使用 ActiveRecord 查询房间列表

在此示例中,我们将使用 ActiveRecord 查询房间列表,并通过以下字段进行过滤:floorroom_numberprice_per_day,使用操作符(>=<==)。

数据过滤器将使用SearchFilter容器来封装所有过滤数据到一个数组中。

从视图开始,创建一个路径为basic/views/rooms/indexFiltered.php的新文件。

在此视图中,我们将搜索过滤器放在顶部,然后是一个表格来显示结果。

我们有三个过滤字段:floorroom_numberprice_per_day,所有字段都带有操作符。数据过滤器将传递给控制器,并在控制器中执行actionIndexFiltered后保留所选的过滤器。

这是关于过滤表单的视图内容:

<?php
use yii\helpers\Url;

$operators = [ '=', '<=', '>=' ];

$sf = $searchFilter;

?>

<form method="post" action="<?php echo Url::to(['rooms/index-filtered']) ?>">
    <input type="hidden" name="<?= Yii::$app->request->csrfParam; ?>" value="<?= Yii::$app->request->csrfToken; ?>" />

    <div class="row">
        <?php $operator = $sf['floor']['operator']; ?>
        <?php $value = $sf['floor']['value']; ?>
        <div class="col-md-3">
            <label>Floor</label>
            <br />    
            <select name="SearchFilter[floor][operator]">
                <?php foreach($operators as $op) { ?>
                    <?php $selected = ($operator == $op)?'selected':''; ?>
                    <option value="<?=$op?>" <?=$selected?>><?=$op?></option>
                <?php } ?>=
            </select>
            <input type="text" name="SearchFilter[floor][value]" value="<?=$value?>" />
        </div>

        <?php $operator = $sf['room_number']['operator']; ?>
        <?php $value = $sf['room_number']['value']; ?>
        <div class="col-md-3">
            <label>Room Number</label>
            <br />    
            <select name="SearchFilter[room_number][operator]">
                <?php foreach($operators as $op) { ?>
                    <?php $selected = ($operator == $op)?'selected':''; ?>
                    <option value="<?=$op?>" <?=$selected?>><?=$op?></option>
                <?php } ?>
            </select>
            <input type="text" name="SearchFilter[room_number][value]" value="<?=$value?>" />
        </div>

        <?php $operator = $sf['price_per_day']['operator']; ?>
        <?php $value = $sf['price_per_day']['value']; ?>
        <div class="col-md-3">
            <label>Price per day</label>
            <br />    
            <select name="SearchFilter[price_per_day][operator]">
                <?php foreach($operators as $op) { ?>
                    <?php $selected = ($operator == $op)?'selected':''; ?>
                    <option value="<?=$op?>" <?=$selected?>><?=$op?></option>
                <?php } ?>
            </select>
            <input type="text" name="SearchFilter[price_per_day][value]" value="<?=$value?>" />
        </div>    
    </div>
    <br />
    <div class="row">
        <div class="col-md-3">
            <input type="submit" value="filter" class="btn btn-primary" />
            <input type="reset" value="reset" class="btn btn-primary" />

        </div>
    </div>
</form>

注意

请注意:

在视图的开始处有一个关键字use,它解释了Url类的完整路径。如果我们删除它,框架将在当前命名空间(即app/controllers)中搜索在<form>标签中请求的Url类。

在声明<form>标签后,我们插入:

<input type="hidden" name="<?= Yii::$app->request->csrfParam; ?>" value="<?= Yii::$app->request->csrfToken; ?>" />

这对于允许框架验证发送的表单数据是强制性的。

$searchFilter变量用作$sf以提供更简洁的表单。

现在更新basic/controllers/RoomsController.php中的RoomsController,并添加一个名为actionIndexFiltered的新操作。从Room创建一个 ActiveQuery 对象,并检查$_POST数组中的SearchFilter关键字是否有内容。

对于每个现有的过滤器,将使用andWhere方法向$query添加一个条件,传递一个操作符、字段名和值。为了使操作内容的形式更简洁,我们在循环中放置一个过滤字段,因为它们具有相同的冗余结构(操作符和值):

    public function actionIndexFiltered()
    {
        $query = Room::find();

        $searchFilter = [
            'floor' => ['operator' => '', 'value' => ''],
            'room_number' => ['operator' => '', 'value' => ''],
            'price_per_day' => ['operator' => '', 'value' => ''],
        ];

        if(isset($_POST['SearchFilter']))
        {
            $fieldsList = ['floor', 'room_number', 'price_per_day'];

            foreach($fieldsList as $field)
            {
                $fieldOperator = $_POST['SearchFilter'][$field]['operator'];
                $fieldValue = $_POST['SearchFilter'][$field]['value'];

                $searchFilter[$field] = ['operator' => $fieldOperator, 'value' => $fieldValue];

                if( $fieldValue != '' )
                {
                    $query->andWhere([$fieldOperator, $field, $fieldValue]);
                }
            }
        }

        $rooms = $query->all();

        return $this->render('indexFiltered', [ 'rooms' => $rooms, 'searchFilter' => $searchFilter ]);

    }

最后,我们需要以表格格式显示结果。因此,在视图底部添加一个表格来显示筛选房间的内容(从 basic/views/rooms/index.php 复制):

<table class="table">
    <tr>
        <th>Floor</th>
        <th>Room number</th>
        <th>Has conditioner</th>
        <th>Has tv</th>
        <th>Has phone</th>
        <th>Available from</th>
        <th>Available from (db format)</th>
        <th>Price per day</th>
        <th>Description</th>
    </tr>
    <?php foreach($rooms as $item) { ?>
    <tr>
        <td><?php echo $item['floor'] ?></td>
        <td><?php echo $item['room_number'] ?></td>
        <td><?php echo Yii::$app->formatter->asBoolean($item['has_conditioner']) ?></td>
        <td><?php echo Yii::$app->formatter->asBoolean($item['has_tv']) ?></td>
        <td><?php echo ($item['has_phone'] == 1)?'Yes':'No' ?></td>
        <td><?php echo Yii::$app->formatter->asDate($item['available_from']) ?></td>
        <td><?php echo Yii::$app->formatter->asDate($item['available_from'], 'php:Y-m-d') ?></td>
        <td><?php echo Yii::$app->formatter->asCurrency($item['price_per_day'], 'EUR') ?></td>
        <td><?php echo $item['description'] ?></td>
    </tr>
    <?php } ?>
</table>

现在将浏览器指向 http://hostname/basic/web/rooms/index-filtered,应该会显示:

示例 – 使用 ActiveRecord 查询房间列表

带有筛选器的房间列表

我们可以通过更改筛选值和运算符来创建尽可能多的测试。

处理关系

ActiveRecord 为我们提供了处理数据库表之间关系的能力。Yii2 使用两种方法来建立当前 ActiveRecord 类与其他 ActiveRecord 类之间的关系:hasOnehasMany,它们根据关系的多度返回一个 ActiveQuery。

第一个方法 hasOne() 返回最多一个与该关系设定的标准匹配的相关记录,而 hasMany() 返回与该关系设定的标准匹配的多个相关记录。

两种方法都需要第一个参数是相关 ActiveRecord 的类名,第二个参数是涉及关系的键对:第一个键相对于外键 ActiveRecord,第二个键与当前 ActiveRecord 相关。

通常,hasOne()hasMany() 是从标识哪些对象(或对象)将被返回的属性中访问的。

本例中的方法是:

class Room extends ActiveRecord
{
    public function getReservations()
    {
return $this->hasMany(Reservation::className(), ['room_id' => 'id']);
    }
}

通过调用 $room->reservations,框架将执行此查询:

SELECT * FROM `reservation` WHERE `room_id` = id_of_room_model

hasOne() 方法的用法类似,以下是一个示例:

class Reservation extends ActiveRecord
{
    public function getRoom()
    {
return $this->hasOne(Room::className(), ['id' => 'room_id']);
    }
}

调用 $reservation->room,框架将执行此查询:

SELECT * FROM `room` WHERE `id` = reservation_id

记住,当我们调用包含 hasOne()hasMany() 方法的属性时,将执行一个 SQL 查询,并且其响应将被缓存。因此,下次调用该属性时,将不会执行 SQL 查询,并且将释放最后缓存的响应。

这种获取相关数据的方法被称为 延迟加载,意味着数据只有在实际请求时才会被加载。

现在让我们写一个示例来显示关于一个房间的最后预订详情。如果您之前还没有这样做,请使用 Gii 创建一个预订模型类。

首先,我们需要一些数据来工作。在 customer 表中插入此记录:

INSERT INTO `customer` (`id` ,`name` ,`surname` ,`phone_number`) VALUES ( NULL , 'James', 'Foo', '+39-12345678');

reservation 表中插入这些记录:

INSERT INTO `reservation` (`id`, `room_id`, `customer_id`, `price_per_day`, `date_from`, `date_to`, `reservation_date`) VALUES (NULL, '2', '1', '90', '2015-04-01', '2015-05-06', NULL), (NULL, '2', '1', '48', '2019-08-27', '2019-08-31', CURRENT_TIMESTAMP);

打开 basic/models/Room.php 中的房间模型,并在文件底部添加此属性声明:

    public function getLastReservation()
    {
        return $this->hasOne(
          Reservation::className(),
          ['room_id' => 'id']
          )
          ->orderBy('id');
    }

如前所述,hasOne()hasMany() 返回一个 ActiveQuery 实例。我们可以通过添加 orderBy() 方法来获取第一条记录,就像我们之前所做的那样,来添加任何方法以完成关系。

Rooms 控制器中创建一个名为 actionLastReservationByRoomId($room_id) 的新操作,内容如下:

    public function actionLastReservationByRoomId($room_id)
    {
        $room = Room::findOne($room_id);

        // equivalent to
        // SELECT * FROM reservation WHERE room_id = $room_id
        $lastReservation = $room->lastReservation;

        // next times that we will call $room->reservation, no sql query will be executed.

        return $this->render('lastReservationByRoomId', ['room' => $room, 'lastReservation' => $lastReservation]);
    }
    Finally, create the view in basic/views/rooms/lastReservationByRoomId.php with this content:<table class="table">
    <tr>
        <th>Room Id</th>
        <td><?php echo $lastReservation['room_id'] ?></td>
    </tr>
    <tr>
        <th>Customer Id</th>
        <td><?php echo $lastReservation['customer_id'] ?></td>
    </tr>
    <tr>
        <th>Price per day</th>
        <td><?php echo Yii::$app->formatter->asCurrency($lastReservation['price_per_day'], 'EUR') ?></td>
    </tr>
    <tr>
        <th>Date from</th>
        <td><?php echo Yii::$app->formatter->asDate($lastReservation['date_from'], 'php:Y-m-d') ?></td>
    </tr>
    <tr>
        <th>Date to</th>
        <td><?php echo Yii::$app->formatter->asDate($lastReservation['date_to'], 'php:Y-m-d') ?></td>
    </tr>
    <tr>
        <th>Reservation date</th>
        <td><?php echo Yii::$app->formatter->asDate($lastReservation['reservation_date'], 'php:Y-m-d H:i:s') ?></td>
    </tr>
</table>

将您的浏览器指向 http://hostname/basic/web/rooms/last-reservation-by-room-id?room_id=2 以可视化此框架:

处理关系

展示 ID 为 2 的房间最后预订的可视化

只会显示数据库中插入的最后预订。

那么如何在单个表格中显示每个房间的最后预订呢?

在这里,延迟加载方法将会有性能问题,因为对于每个房间,它将执行一个单独的 SQL 查询来获取最后预订的数据。这是视图中的代码片段:

for($roomsList as $room)
{
    // SELECT * FROM reservation WHERE room_id = $room->id
      $lastReservation = $room->lastReservation;
}

为了完成脚本的执行,它将执行与房间数量一样多的相关 SQL 查询,当房间数量增加时,这种解决方案将不再高效。

Yii2 框架提供了解决此类问题的一种数据加载类型,称为预加载。

通过 ActiveQuery 的with()方法应用预加载。此方法的参数可以是单个或多个字符串,或者是一个包含关系名称的单个数组以及可选的回调来定制关系。

当我们获取房间列表时,如果我们对查询应用with()方法,将自动执行第二个 SQL 查询,这将返回每个房间的最后预订列表。

通过这个示例,我们将得到一个房间列表以及每个房间条目的lastReservation关系列表。这样,当我们引用$room->lastReservation时,不会执行其他 SQL 查询:

// SELECT * FROM `room`
// SELECT * FROM `reservation` WHERE `room_id` IN ( room_id list from previous select ) ORDER BY `id` DESC
$rooms = Room::find()
->with('lastReservation')
->all();

// no query will be executed
$lastReservation = $rooms[0]->lastReservation;

让我们写一个完整的示例来获取每个房间的最后预订的完整列表。在basic/controllers/RoomsController.php中,添加一个名为actionLastReservationForEveryRoom()的新操作:

    public function actionLastReservationForEveryRoom()
    {
            $rooms = Room::find()
            ->with('lastReservation')
            ->all();

            return $this->render('lastReservationForEveryRoom', ['rooms' => $rooms]);
    }

这个操作将传递一个名为lastReservationForEveryRoom的房间列表给视图,以及使用预加载加载的lastReservation关系。

basic/views/rooms/lastReservationForEveryRoom.php中创建一个名为lastReservationForEveryRoom.php的视图:

<table class="table">
    <tr>
        <th>Room Id</th>
        <th>Customer Id</th>
        <th>Price per day</th>
        <th>Date from</th>
        <th>Date to</th>
        <th>Reservation date</th>
    </tr>
    <?php foreach($rooms as $room) { ?>
    <?php $lastReservation = $room->lastReservation; ?>
    <tr>
        <td><?php echo $lastReservation['room_id'] ?></td>
        <td><?php echo $lastReservation['customer_id'] ?></td>
        <td><?php echo Yii::$app->formatter->asCurrency($lastReservation['price_per_day'], 'EUR') ?></td>
        <td><?php echo Yii::$app->formatter->asDate($lastReservation['date_from'], 'php:Y-m-d') ?></td>
        <td><?php echo Yii::$app->formatter->asDate($lastReservation['date_to'], 'php:Y-m-d') ?></td>
        <td><?php echo Yii::$app->formatter->asDate($lastReservation['reservation_date'], 'php:Y-m-d H:i:s') ?></td>
    </tr>
    <?php } ?>
</table>

在这个视图中,将为每个房间显示最后预订的数据。由于第一个房间没有预订,将显示一个空行。这是结果:

处理关系

每个房间的最后预订

注意

with()方法有两种变体:joinWith()innerJoinWith(),它们将左连接或内部连接应用于主查询。

例如,这是使用joinWith()的示例:

            $rooms = Room::find()
            ->leftJoinWith('lastReservation')
            ->all();

上述代码片段等同于:

SELECT `room`.* FROM `room` LEFT JOIN `reservation` ON `room`.`id` = `reservation`.`room_id` ORDER BY `id` DESC

SELECT * FROM `reservation` WHERE `room_id` IN ( room_id list from previous sql respone ) ORDER BY `id` DESC

记住,内部连接只要两个表中的列有匹配就会选择两个表的所有行;相反,左连接返回左表(房间)的所有行,以及右表(预订)中匹配的行。当没有匹配时,右侧的结果是 NULL。

有时候,我们需要在表之间有多个级别的关联。例如,我们可能找到一个与房间相关的客户。在这种情况下,从房间开始,我们通过预订,然后从预订到客户。

这里的关系将是:

room -> reservation -> customer

如果我们想从房间对象中找到客户对象,只需输入:

$customer = $room->customer;

通常,我们有多层关系,但在这个情况下只有两层(预订和客户)。

Yii2 允许我们使用 via()viaTable() 方法指定一个连接表。第一个 via() 方法基于模型中现有的关系,并支持两个参数:

  • 关系名称

  • 用于自定义关联关系的 PHP 回调参数

第二种方法 viaTable() 基于直接访问数据库中的物理表,并支持三个参数:

  • 第一个参数是一个关系或表名称

  • 第二个参数是与主模型关联的链接

  • 第三个参数是一个 PHP 回调函数,用于自定义关联关系

示例 - 使用关系连接房间、预订和客户

在本例中,我们将查看如何构建一个同时显示房间、预订和客户列表的单个视图;当用户点击房间记录的 详情 按钮时,预订列表将使用与该房间链接的数据进行过滤。同样,当用户点击预订记录的 详情 按钮时,客户列表将使用与该预订链接的数据进行过滤。

如果没有传递参数(在页面首次调用时发生的情况),则房间、预订或客户列表将包含来自相应表的数据的完整记录。

basic/controllers/RoomsController.php 中开始编写 actionIndexWithRelationships。这是此操作的待办事项列表:

  • 检查哪些详细参数已被传递(room_id 表示必须使用 room_id 过滤的数据填充预订列表,而 reservation_id 表示必须使用 reservation_id 过滤的数据填充客户列表)

  • 填充三个模型:roomSelectedreservationSelectedcustomerSelected 以显示详细信息,并填充三个模型的三个数组:roomsreservationscustomers

这是 actionIndexWithRelationships 的完整内容:

     public function actionIndexWithRelationships()
    {
        // 1\. Check what parameter of detail has been passed
        $room_id = Yii::$app->request->get('room_id', null);
        $reservation_id = Yii::$app->request->get('reservation_id', null);
        $customer_id = Yii::$app->request->get('customer_id', null);

        // 2\. Fill three models: roomSelected, reservationSelected and customerSelected and
        //    Fill three arrays of models: rooms, reservations and customers;
        $roomSelected = null;
        $reservationSelected = null;
        $customerSelected = null;

        if($room_id != null)
        {
            $roomSelected = Room::findOne($room_id);

            $rooms = array($roomSelected);
            $reservations = $roomSelected->reservations;
            $customers = $roomSelected->customers;
        }
        else if($reservation_id != null)
        {
            $reservationSelected = Reservation::findOne($reservation_id);

            $rooms = array($reservationSelected->room);
            $reservations = array($reservationSelected);
            $customers = array($reservationSelected->customer);
        }
        else if($customer_id != null)
        {
            $customerSelected = Customer::findOne($customer_id);

            $rooms = $customerSelected->rooms;
            $reservations = $customerSelected->reservations;
            $customers = array($customerSelected);
        }
        else
        {
            $rooms = Room::find()->all();
            $reservations = Reservation::find()->all();
            $customers = Customer::find()->all();
        }

        return $this->render('indexWithRelationships', ['roomSelected' => $roomSelected, 'reservationSelected' => $reservationSelected, 'customerSelected' => $customerSelected, 'rooms' => $rooms, 'reservations' => $reservations, 'customers' => $customers]);
    }

注意

记得在 RoomsController 文件顶部添加 use 关键字以使用 CustomerReservation 类:

use app\models\Reservation;
use app\models\Customer;

动作体的第二部分需要更多关注,因为在这个特定位置填充了选择模型和列表模型。

$room_id$reservation_id$customer_id 之间只能同时选择一个参数。当选择这三个参数之一时,将使用模型中的关系填充 RoomReservationCustomer 模型的三个数组。为此,模型必须使用之前代码中使用的所有关系。

让我们确保模型中存在所有关系。

basic/models/Room.php中的Room模型必须同时定义getReservations()getCustomers(),这两个都使用via()方法来处理第二级关系:

    public function getReservations()
    {
            return $this->hasMany(Reservation::className(), ['room_id' => 'id']);
    }
public function getCustomers()
    {
            return $this->hasMany(Customer::className(), ['id' => 'customer_id'])->via('reservations');
    }

basic/models/Reservation.php中的Reservation模型必须定义getCustomer()getRoom(),这两个都返回一个相关的单模型:

    public function getRoom()
    {
            return $this->hasOne(Room::className(), ['id' => 'room_id']);
    }

    public function getCustomer()
    {
            return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    }

最后,在basic/models/Customer.php中的Customer模型必须定义getReservations()getRooms(),这两个都使用via()方法来处理第二级关系:

    public function getReservations()
    {
            return $this->hasMany(Reservation::className(), ['customer_id' => 'id']);
    }

    public function getRooms()
    {
            return $this->hasMany(Room::className(), ['id' => 'room_id'])->via('reservations');
    }

现在在basic/view/rooms/indexWithRelationships.php中编写一个视图文件。我们将使用 Bootstrap 提供的 CSS(我们将在接下来的几章中广泛研究)将 HTML 页面分成三个部分(三个表格)。

第一张表将用于房间列表,第二张表用于预订列表,最后一张表用于客户列表:

<?php
use yii\helpers\Url;
?>

<a class="btn btn-danger" href="<?php echo Url::to(['index-with-relationships']) ?>">Reset</a>

<br /><br />
<div class="row">
    <div class="col-md-4">
        <legend>Rooms</legend>
        <table class="table">
            <tr>
                <th>#</th>
                <th>Floor</th>
                <th>Room number</th>
                <th>Price per day</th>
            </tr>
            <?php foreach($rooms as $room) { ?>
            <tr>
                <td><a class="btn btn-primary btn-xs" href="<?php echo Url::to(['index-with-relationships', 'room_id' => $room->id]) ?>">detail</a></td>
                <td><?php echo $room['floor'] ?></td>
                <td><?php echo $room['room_number'] ?></td>
                <td><?php echo Yii::$app->formatter->asCurrency($room['price_per_day'], 'EUR') ?></td>
            </tr>
            <?php } ?>
        </table>

        <?php if($roomSelected != null) { ?>
            <div class="alert alert-info">
                <b>You have selected Room #<?php echo $roomSelected->id ?></b>
            </div>
        <?php } else { ?>
            <i>No room selected</i>
        <?php } ?>
    </div>

    <div class="col-md-4">
        <legend>Reservations</legend>
        <table class="table">
            <tr>
                <th>#</th>
                <th>Price per day</th>
                <th>Date from</th>
                <th>Date to</th>
            </tr>
            <?php foreach($reservations as $reservation) { ?>
            <tr>
                <td><a class="btn btn-primary btn-xs" href="<?php echo Url::to(['index-with-relationships', 'reservation_id' => $reservation->id]) ?>">detail</a></td>
                <td><?php echo Yii::$app->formatter->asCurrency($reservation['price_per_day'], 'EUR') ?></td>
                <td><?php echo Yii::$app->formatter->asDate($reservation['date_from'], 'php:Y-m-d') ?></td>
                <td><?php echo Yii::$app->formatter->asDate($reservation['date_to'], 'php:Y-m-d') ?></td>
            </tr>
            <?php } ?>
        </table>

        <?php if($reservationSelected != null) { ?>
            <div class="alert alert-info">
                <b>You have selected Reservation #<?php echo $reservationSelected->id ?></b>
            </div>
        <?php } else { ?>
            <i>No reservation selected</i>
        <?php } ?>

    </div>
    <div class="col-md-4">
        <legend>Customers</legend>
        <table class="table">
            <tr>
                <th>#</th>
                <th>Name</th>
                <th>Surname</th>
                <th>Phone</th>
            </tr>
            <?php foreach($customers as $customer) { ?>
            <tr>
                <td><a class="btn btn-primary btn-xs" href="<?php echo Url::to(['index-with-relationships', 'customer_id' => $customer->id]) ?>">detail</a></td>
                <td><?php echo $customer['name'] ?></td>
                <td><?php echo $customer['surname'] ?></td>
                <td><?php echo $customer['phone_number'] ?></td>
            </tr>
            <?php } ?>
        </table>

        <?php if($customerSelected != null) { ?>
            <div class="alert alert-info">
                <b>You have selected Customer #<?php echo $customerSelected->id ?></b>
            </div>
        <?php } else { ?>
            <i>No customer selected</i>
        <?php } ?>        
    </div>   
</div>

通过将浏览器指向http://hostname/basic/rooms/index-with-relationships来测试代码。这应该是尝试过滤二楼房间的结果:

示例 – 使用关系连接房间、预订和客户

具有预订和客户之间关系的房间

如何从表单保存模型

现在我们来看看如何从表单保存模型,这可能是一个新的或更新的模型。

你需要遵循的步骤是:

  1. action方法中,创建一个新的模型或获取现有的模型。

  2. action方法中,检查$_POST数组中是否有数据。

  3. 如果$_POST中有数据,将模型属性的attributes属性填充为$_POST中的数据,并调用模型的save()方法;如果save()返回 true,则将用户重定向到另一个页面(例如详情页面)。

从现在起,我们将继续使用框架提供的 widgets 和 helper 类。在这种情况下,HTML 表单将通过yii\widget\ActiveForm类渲染。

我们能写的最简单的表单如下:

<?php
use yii\widgets\ActiveForm;

$form = ActiveForm::begin([
    'id' => 'login-form',
]) ?>
    …
    …
    …
<?php ActiveForm::end() ?>

此代码生成一个带有login-form作为id属性和空内容的表单 HTML 标签;默认情况下,methodaction属性分别是表单生成的 post 和相同动作 URL。其他关于 AJAX 验证和客户端验证的属性可以设置,正如你将在后面看到的。

$form小部件是通过使用静态方法ActiveForm::begin创建的,传递一个包含表单 HTML 标签属性(idactionmethod等)的数组、一个配置参数和一个名为options的键来指定所有我们想要传递给表单的额外选项。最后,当调用静态方法ActiveForm::end()时,表单将被完成。在表单的begin()end()方法之间,我们可以插入所有需要的内容。

特别是,可以使用 ActiveField 小部件来管理表单的输入字段。与模型属性相关的 ActiveField 小部件是通过调用$form对象的field()方法创建的:

$field = $form->field($model, 'attribute');

field()方法返回的对象是一个通用字段,我们可以通过简单地应用其他方法来专门化,以生成所有常见的输入字段:隐藏、文本、密码、文件等。这返回相同的 ActiveField $field对象,因此可以级联应用其他方法。

使用以下方式创建一个文本字段输入:

$textInputField = $field->textInput();

或者可以简单地这样创建:

$textInputField = $form->field($model, 'attribute')->textInput();

这个变量$textInputField再次是 ActiveField(与$field相同的对象),因此我们可以应用所有其他所需的方法来完成我们的输入字段;例如,如果我们需要在输入字段中放置提示,我们可以使用:

$textInputField->hint('Enter value');

或者我们可以简单地使用:

$textInputField = $form->field($model, 'attribute')->textInput()->hint('Enter value');

除了自动考虑模型类rules()方法中定义的属性验证规则外,框架还会额外考虑。例如,如果一个属性是必需的,我们点击它并将其传递给另一个字段而不输入任何内容,将会显示一个错误提示,提醒我们该字段是必需的。

当使用 ActiveField 小部件创建输入字段时,该输入的idname属性将具有以下格式:idmodel-class-name_attribute-namenamemodel-class-name[attribute-name]。这意味着当我们将表单提交给控制器操作时,所有模型属性都将分组在一个与模型类同名的容器数组中传递。

例如,如果$model类是Room,属性是floor,其内容是12,则从$form对象创建一个文本字段:

<?php echo $floorInputField = $form->field($model, 'floor')
->textInput()->hint('Enter value for floor');

这将输出以下 HTML:

<input id="Room_floor" name="Room[floor]" value="12" placeholder="Enter value for floor" />

示例 - 从表单创建和更新房间

只需遵循上一段中的说明,我们将尝试从 HTML 表单创建和更新一个房间。

我们现在更新之前在RoomsController中创建的actionCreate()方法,添加一些代码来实例化一个新的模型对象,检查$_POST数组的内容,如果已设置,则在模型上调用save()

    public function actionCreate()
    {
        // 1\. Create a new Room instance;
        $model = new Room();

        // 2\. Check if $_POST['Room'] contains data;
        if(isset($_POST['Room']))
        {
            $model->attributes = $_POST['Room'];

            // Save model
            if($model->save())
            {
             // If save() success, redirect user to action view.
             return $this->redirect(['view', 'id' => $model->id]);
            }
        }

        return $this->render('create', ['model' => $model]);
    }

要更新basic/views/rooms/create.php中的视图,传递以下参数:

<?php
use yii\widgets\ActiveForm;
use yii\helpers\Html;
?>

<div class="row">

    <div class="col-lg-6">

        <h2>Create a new room</h2>

        <?php $form = ActiveForm::begin(['id' => 'room-form']) ?>

        <?php echo $form->field($model, 'floor')->textInput(); ?>
        <?php echo $form->field($model, 'room_number')->textInput(); ?>
        <?php echo $form->field($model, 'has_conditioner')->checkbox(); ?>
        <?php echo $form->field($model, 'has_tv')->checkbox(); ?>
        <?php echo $form->field($model, 'has_phone')->checkbox(); ?>
        <?php echo $form->field($model, 'available_from')->textInput(); ?>
        <?php echo $form->field($model, 'price_per_day')->textInput(); ?>
        <?php echo $form->field($model, 'description')->textArea(); ?>
        <?php echo Html::submitButton('Save', ['class' => 'btn btn-primary']); ?>
        <?php ActiveForm::end() ?>
    </div>
</div>

默认情况下,ActiveForm::begin()创建一个启用了客户端验证的表单;因此,只有当所有验证规则都满足时,表单才会提交,因为submit按钮是使用yii\helpers\Html渲染的。

注意视图顶部包含use关键字,用于定义HtmlActiveForm类的完整路径:

use yii\widgets\ActiveForm;
use yii\helpers\Html;

将您的浏览器指向http://hostname/basic/rooms/create以显示创建新房间的表单。以下截图显示了您应该显示的内容,并报告了一些特定条件:

示例 - 从表单创建和更新房间

创建新房间的表单

这张截图展示了字段的不同状态:楼层输入有一个红色边框,因为它有错误的内容类型(它必须是整数!),房间号有一个绿色边框以表示它是正确的,而可用从字段有一个红色边框,因为它虽然是必需的,但用户留空了。如果$_POST数据可用,框架提供了一个更简洁的表单来填写属性:

$model->load(Yii::$app->request->post());

如果$_POST[model-class]内容可用,这将填充模型的属性,并且根据这个建议,我们可以将actionCreate内容更改为以下内容:

    public function actionCreate()
    {
        // 1\. Create a new Room instance;
        $model = new Room();

        // 2\. Check if $_POST['Room'] contains data and save model;
        if( $model->load(Yii::$app->request->post()) && ($model->save()) )
        {
            return $this->redirect(['detail', 'id' => $model->id]);
        }

        return $this->render('create', ['model' => $model]);
    }

这非常简洁!同样,我们可以处理更新操作以保存对现有模型的更改。

我们可以通过将其内容放在外部来创建一个可重用的表单。在basic/views/rooms/_form.php中创建一个新文件(第一个下划线表示这是一个可包含在其他视图中的视图),并将create视图中的表单生成代码剪切并粘贴到这个新的_form视图中:

<?php
use yii\widgets\ActiveForm;
use yii\helpers\Html;
?>
<?php $form = ActiveForm::begin(['id' => 'room-form']) ?>

<?php echo $form->field($model, 'floor')->textInput(); ?>
<?php echo $form->field($model, 'room_number')->textInput(); ?>
<?php echo $form->field($model, 'has_conditioner')->checkbox(); ?>
<?php echo $form->field($model, 'has_tv')->checkbox(); ?>
<?php echo $form->field($model, 'has_phone')->checkbox(); ?>
<?php echo $form->field($model, 'available_from')->textInput(); ?>
<?php echo $form->field($model, 'price_per_day')->textInput(); ?>
<?php echo $form->field($model, 'description')->textArea(); ?>

<?php echo Html::submitButton('Create', ['class' => 'btn btn-primary']); ?>

<?php ActiveForm::end() ?>

basic/views/rooms/create.php文件中,而不是放置表单代码,只需在其中放置渲染_form视图的代码:

<?php echo $this->render('_form', ['model' => $model]); ?>

注意

当我们修改create视图时,请记住将$model作为第二个参数传递以渲染_form视图。

我们准备好构建更新流程,以便从表单中更新房间内容。首先,在basic/controllers/RoomsController.php中创建一个名为actionUpdate的操作,传递$id作为参数,该参数用于标识要查找的模型的主键。

在此操作中,我们将放置一些代码来根据id主键获取模型,检查$_POST数组是否包含数据,然后保存模型:

    public function actionUpdate($id)
    {
        // 1\. Create a new Room instance;
        $model = Room::findOne($id);

        // 2\. Check if $_POST['Room'] contains data and save model;
        if( ($model!=null) && $model->load(Yii::$app->request->post()) && ($model->save()) )
        {
            return $this->redirect(['detail', 'id' => $model->id]);
        }

        return $this->render('update', ['model' => $model]);
    }

这基本上等同于create操作的代码。现在,在basic/views/rooms/update.php中创建一个名为update的视图,内容如下:

<div class="row">

    <div class="col-lg-6">

        <h2>Update a room</h2>
        <?php echo $this->render('_form', ['model' => $model]); ?>
    </div>

</div>

从数据库中检查一个现有的房间,并在您的浏览器中输入此 URL 的id值:http://hostname/basic/rooms/update?id=id-found

例如,如果现有房间的id1,请在您的浏览器中输入此 URL:

http://hostname/basic/rooms/update?id=1

这将显示一个基于模型属性内容填充的字段表单。

这个示例是完整的,因为它已经构建了detail视图,该视图显示了模型属性的 内容。创建一个名为actionDetail的操作,传递$id作为参数,该参数用于标识要查找的模型的主键:

    public function actionDetail($id)
    {
        // 1\. Create a new Room instance;
        $model = Room::findOne($id);

        return $this->render('detail', ['model' => $model]);
    }

然后,在basic/views/rooms/detail.php中创建detail视图以显示模型一些属性值的某些状态:

<table class="table">
    <tr>
        <th>ID</th>
        <td><?php echo $model['id'] ?></td>
    </tr>
    <tr>
        <th>Floor</th>
        <td><?php echo $model['floor'] ?></td>
    </tr>
    <tr>
        <th>Room number</th>
        <td><?php echo $model['room_number'] ?></td>
    </tr>
</table>

在成功创建或更新模型后,将显示包含模型一些属性内容的详细视图。

设置 GMT 时区

设置日期/时间管理的默认时区非常重要。

通常,当我们提到日期/时间时,不要注意正在引用哪个时区值。

例如,如果我们住在罗马,想在纽约度过我们的下一个假期,当我们从酒店收到登机日期/时间时,我们必须考虑所指的是哪个时区的时间(本地或远程)。

当我们显示可能被误解的日期/时间值时,始终建议添加时区参考。时区通过与通常为GMT格林威治标准时间)的参考相比的正负小时数来表示。

例如,如果现在是罗马的晚上 9 点(GMT +1),在 GMT 时间将是晚上 8 点(GMT +0),在纽约是下午 3 点(GMT -5),最后在洛杉矶是中午 12 点(GMT -8)。

因此,建立共同的共享时间值是必要的。为此,建议使用 GMT 作为所有值和值操作的时参考。

我们需要在两个环境中配置时区:

  • 在一个应用程序中,设置配置的timeZone属性;这将设置所有关于日期和时间的函数的默认时区

  • 一些数据库,如 MySQL,没有内部时区管理,因此每个值都使用数据库的默认时区或从应用程序到数据库连接期间配置的时区;我们将在连接到数据库时设置默认时区

完成第一步。打开basic/config/web.php,并在config数组中添加timeZone属性,值为GMT,例如在basePath属性之后:

    'timeZone' => 'GMT',

第二步是设置数据库连接的时区,如果数据库(如 MySQL)不提供它。这是通过在on afterOpen事件中添加此代码来全局完成的。打开basic/config/db.php,并将其作为数组中的最后一个属性(通常最后一个属性是charset)附加:

'on afterOpen' => function($event) {
$event->sender->createCommand("SET time_zone = '+00:00'")->execute();
}

这段代码意味着一旦与数据库的连接打开,SQL 查询SET time_zone = +00:00将为我们将要建立的每个数据库连接执行,并且每个与 GMT (+00:00)时区相关的日期/时间字段值和函数都将被考虑。

让我们进行一个测试。创建一个新的控制器,它简单地显示当前的日期/时间和时区,在basic/controllers/TestTimezoneController.php中,操作名为actionCheck()

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;

class TestTimezoneController extends Controller
{
    public function actionCheck()
    {
        $dt = new \DateTime();
        echo 'Current date/time: '.$dt->format('Y-m-d H:i:s');
        echo '<br />';
        echo 'Current timezone: '.$dt->getTimezone()->getName();
        echo '<br />';
    }
}

将您的浏览器指向http://hostname/basic/web/test-timezone/check。这是我浏览器显示的内容:

Current date/time: 2015-05-27 19:53:35
Current timezone: GMT

而且,当地时间(在罗马)是 21:53:35,因为那时罗马处于+02:00 GMT,由于夏令时。

如果我们在basic/config/web.php中的应用配置中注释掉timeZone属性,我们将看到浏览器中的默认服务器时区:

Current date/time: 2015-05-27 21:53:35
Current timezone: Europe/Rome

这确认了我们已更改所有日期/时间函数的默认timezone属性。最后要执行的是对数据库的检查。创建一个名为actionCheckDatabase的新操作,以验证数据库当前(以及每个)连接的默认时区是否为 GMT:

public function actionCheckDatabase()
{
        $result = \Yii::$app->db->createCommand('SELECT NOW()')->queryColumn();

        echo 'Database current date/time: '.$result[0];
}

将您的浏览器指向 http://hostname/basic/web/test-timezone/check-database。这是我的浏览器显示的内容:

Database current date/time: 2015-05-27 20:12:08

当时的本地时间(在罗马)是 22:12:08,因为当时罗马在+02:00 GMT。

请记住,从现在起,数据库中显示的所有日期/时间信息都指的是 GMT 时区,尽管这个规范缺失(如我们在上一个数据库的当前日期/时间中看到的那样)。

小贴士

另一种处理数据库日期/时间列中 GMT 时区的方法是将值存储为时间戳,根据定义,它是一个整数,表示从 GMT(UTC)时区的 1970 年 1 月 1 日 00:00:00 开始经过的秒数;因此,可以立即理解该字段是带有 GMT 时区的日期/时间,但请记住,应用于它的任何数据库函数都将使用数据库的默认时区执行。

使用多个数据库连接

应用程序可能需要多个数据库连接,以便它们可以从不同的来源发送和获取数据。

使用其他数据库源非常简单。唯一要做的就是向主配置中添加一个新的数据库条目并使用 ActiveRecord 支持。所有对记录的操作对开发人员来说都是透明的。

这里有一些配置访问其他数据库的连接字符串(dsn)的示例:

  • MySQL 和 MariaDB: mysql:host=localhost;dbname=mydatabase

  • SQLite: sqlite:/path/to/database/file

  • PostgreSQL: pgsql:host=localhost;port=5432;dbname=mydatabase

  • CUBRID: cubrid:dbname=demodb;host=localhost;port=33000

  • MS SQL Server(通过 sqlsrv 驱动程序):sqlsrv:Server=localhost;Database=mydatabase

  • MS SQL Server(通过 dblib 驱动程序):dblib:host=localhost;dbname=mydatabase

  • MS SQL Server(通过 mssql 驱动程序):mssql:host=localhost;dbname=mydatabase

  • Oracle: oci:dbname=//localhost:1521/mydatabase

示例 - 配置第二个数据库连接以将数据导出到本地 SQLite 数据库

我们现在想向 SQLite 数据库添加一个新的数据库连接。当我们使用数据库时,我们必须确保系统已安装 PDO 驱动程序,否则 PHP 无法处理它。

打开 basic/config/web.php 并内部的 components 属性,然后添加一个名为 dbSqlite 的新属性,具有以下属性:

        'dbSqlite' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'sqlite:'.dirname(__DIR__).'/../db.sqlite',
        ],

此条目将使用名为 db.sqlite 的 DB SQLite,我们可以在 /basic/web 文件夹下的 dirname(__DIR__).'/../web/db.sqlite' 路径中找到它。如果此文件不存在,它将被创建(如果 /basic/web 文件夹有写权限)。

注意

确保 /basic/web 文件夹可写,否则框架将无法创建 db.sqlite 文件。

创建一个新的控制器来处理这个新数据库中的操作。这将放在 /basic/controllers/TestSqliteController.php

在这个新控制器中插入第一个名为 actionCreateRoomTable 的操作,这将创建与 MySQL 中的 Room 表相同的结构在 dbSqlite

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;

class TestSqliteController extends Controller
{
    public function actionCreateRoomTable()
    {
        // Create room table
        $sql = 'CREATE TABLE IF NOT EXISTS room (id int not null, floor int not null, room_number int not null, has_conditioner int not null, has_tv int not null, has_phone int not null, available_from date not null, price_per_day float, description text)';
        \Yii::$app->dbSqlite->createCommand($sql)->execute();
        echo 'Room table created in dbSqlite';
    }
}

注意

注意,在 actionCreateRoomTable 中,数据库实例是从:\Yii::$app->dbSqlite 获取的。

将您的浏览器指向 http://hostname/basic/web/test-sqlite/create-room-table 并在 basic/web 中创建一个 db.sqlite 文件以及其中的 room 表。

如前所述,如果 PDO 驱动程序正确安装,将显示一个空白页面,上面有 Room table created in dbSqlite 文本。

现在我们想将 MySQL 中的房间表克隆到 SQLite 中以备份数据表。我们需要将 MySQL 中的记录保存到 SQLite 中,并验证存储的数据以在表中显示。

创建一个名为 actionBackupRoomTable() 的新动作,执行以下步骤:

  1. 创建一个 room 表(如果不存在)。

  2. dbSqlite(别名 truncate)中删除房间中的所有记录。

  3. 使用 ActiveRecord 从 MySQL 中的房间表中加载所有记录。

  4. 将 MySQL 中的每个记录插入到 SQLite 中。

  5. 渲染视图以显示 SQLite 中的数据(以验证复制是否成功)。

actionBackupRoomTable() 动作的内容是:

    use app\models\Room;

    public function actionBackupRoomTable()
    {
        // Create room table
        $sql = 'CREATE TABLE IF NOT EXISTS room (id int not null, floor int not null, room_number int not null, has_conditioner int not null, has_tv int not null, has_phone int not null, available_from date not null, price_per_day float, description text)';
        \Yii::$app->dbSqlite->createCommand($sql)->execute();

        // Truncate room table in dbSqlite
        $sql = 'DELETE FROM room';
        \Yii::$app->dbSqlite->createCommand($sql)->execute();

        // Load all records from MySQL and insert every single record in dbqlite
        $models = Room::find()->all();

        foreach($models as $m)
        {
            \Yii::$app->dbSqlite->createCommand()->insert('room', $m->attributes)->execute();            
        }

        // Load all records from dbSqlite
        $sql = 'SELECT * FROM room';
        $sqliteModels = \Yii::$app->dbSqlite->createCommand($sql)->queryAll();

        return $this->render('backupRoomTable', ['sqliteModels' => $sqliteModels]);
   }

最后,在 basic/views/test-sqlite/backupRoomTable.php 中创建一个名为 backupRoomTable 的视图,内容如下以显示 dbSqlite 中的数据:

<h2>Rooms from dbSqlite</h2>

<table class="table">
    <tr>
        <th>Floor</th>
        <th>Room number</th>
        <th>Has conditioner</th>
        <th>Has tv</th>
        <th>Has phone</th>
        <th>Available from</th>
        <th>Available from (db format)</th>
        <th>Price per day</th>
        <th>Description</th>
    </tr>
    <?php foreach($sqliteModels as $item) { ?>
    <tr>
        <td><?php echo $item['floor'] ?></td>
        <td><?php echo $item['room_number'] ?></td>
        <td><?php echo Yii::$app->formatter->asBoolean($item['has_conditioner']) ?></td>
        <td><?php echo Yii::$app->formatter->asBoolean($item['has_tv']) ?></td>
        <td><?php echo ($item['has_phone'] == 1)?'Yes':'No' ?></td>
        <td><?php echo Yii::$app->formatter->asDate($item['available_from']) ?></td>
        <td><?php echo Yii::$app->formatter->asDate($item['available_from'], 'php:Y-m-d') ?></td>
        <td><?php echo Yii::$app->formatter->asCurrency($item['price_per_day'], 'EUR') ?></td>
        <td><?php echo $item['description'] ?></td>
    </tr>
    <?php } ?>
</table>

将您的浏览器导航到 http://hostname/basic/web/test-sqlite/backup-room-table,它应该显示与此类似的输出:

示例 – 配置第二个数据库连接以将数据导出到本地 SQLite 数据库

SQLite 数据库中的房间列表

现在,我们可以从 http://hostname/basic/web/db.sqlite 下载 db.sqlite 文件,以保留房间表的备份副本!

摘要

在本章中,您掌握了如何从头配置数据库连接并使用框架的 DAO 支持执行 SQL 查询。接下来,您发现了如何使用 Gii,并了解了它在从数据库表结构创建模型方面的优势。Gii 创建扩展 ActiveRecord 类的模型,通过使用它,您最终学会了如何操作数据。所有示例都附有显示数据的可视化网格,Bootstrap 的存在增强了 Yii2 的图形效果。

我们仔细分析了表关系这一常见主题,必须在模型中管理并在视图中显示。

在本章结束时,您学会了使用 ActiveRecord 操作数据后,编写了一个完整的流程,将数据从 HTML 表单保存到数据库中。最后,您学习了在日期/时间字段中设置 GMT 时区以及在同一应用程序中使用其他数据库源以备份数据库的重要性。

在下一章中,您将学习如何使用和自定义网格小部件以改进数据可视化。

第六章:使用网格进行数据和关系

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

  • 网格的 DataProvider

  • 使用网格

  • 网格中的自定义列:

    • 例如:通过点击客户网格行来显示预订列表
  • GridView 中的过滤器

  • 在网格列中显示和过滤 ActiveRecord 关系数据

  • 总结网格的底部行:

    • 例如:扩展 GridView 来自定义网格的底部行
  • 单页上的多个网格:

    • 例如:在同一个视图中管理预订和房间网格

简介

在上一章中,你学习了如何从数据库中获取数据。现在,是时候使用框架提供的基本控件:GridView 了。我们将首先介绍网格期望的数据输入格式。然后,我们将分析网格的默认实现,并继续查看自定义以显示数据之间的关系。最后,你将学习扩展网格基类以在网格布局中显示所需的所有内容。

网格的 DataProvider

GridView 是 Yii2 提供的用于在网格布局中显示数据的控件。

此控件要求用作输入源的数据是抽象类yii\data\BaseDataProvider的扩展。

为了处理数据源,DataProvider 提供了一些额外的操作来处理分页和排序。

BaseDataProvider有一个名为getModels()的方法,它返回当前页面的项目列表。这意味着我们也可以使用 DataProvider 从源分页数据,并按需显示。

默认情况下,框架有三个核心类扩展了yii\data\BaseDataProvider

  • yii\data\ActiveDataProvider

  • yii\data\ArrayDataProvider

  • yii\data\SqlDataProvider

第一个,ActiveDataProvider,使用 ActiveRecord 的yii\db\Query实例作为数据源。参数数组传递给构造函数,yii\db\Query对象在query属性中填充:

// build an ActiveDataProvider with an empty query and a pagination with 35 items for page
$provider = new \yii\data\ActiveDataProvider([
    'query' => Room::find(),
    'pagination' => [
        'pageSize' => 35,
    ],
]);

// get all rooms in current page
$rooms = $provider->getModels();

ActiveDataProvider是最常用的 DataProvider,因为它直接依赖于 ActiveRecord,与数据库交互的最佳方式。

第二点,ArrayDataProvider使用一个可以排序或分页的项目数组作为数据源。当数据不能用 ActiveRecord 表示时,会使用这个提供者,例如,当它们来自另一个数据源时,如 JSON REST 服务或 RSS 源。

ActiveDataProvider的主要区别在于所有数据应立即传递给构造函数:

// build an ArrayDataProvider with an empty query and a pagination with 40 items for page
$provider = new \yii\data\ArrayDataProvider([
    'allModels' => Room::find()->all(),
    'pagination' => [
        'pageSize' => 40,
    ],
]);

// get all rooms in current page
$rooms = $provider->getModels();

在这个片段中,我们从 ActiveRecord 中获取数据来展示ActiveDataProviderArrayDataProvider之间的区别。对于这个最后的提供者,所有模式都应该传递给构造函数。

因此,如果Room表有 10,000 条记录,使用ActiveDataProvider每次将加载 35 个项目,而通过ArrayDataProvider它们将全部从头开始加载(存在大的性能问题)。

最后一个,SqlDataProvider,使用 SQL 查询作为数据源。如果我们使用此提供程序创建分页,我们需要将totalCount属性传递给构造函数,以通知 DataProvider SQL 查询应返回多少条记录:

// return total items count for this sql query
$itemsCount = \Yii::$app->db->createCommand('SELECT COUNT(*) FROM room')->queryScalar();

// build a SqlDataProvider with a pagination with 10 items for page

$dataProvider = new \yii\data\SqlDataProvider([
    'sql' => 'SELECT * FROM room',
    'totalCount' => $itemsCount,
    'pagination' => [
            'pageSize' => 10,
    ],
]);

// get the user records in the current page
$models = $dataProvider->getModels();

使用网格

现在我们知道了如何获取要传递给 GridView 的数据输入源,让我们看看如何实现它。GridView 的最小实现需要将两个属性传递给构造函数的数组:dataProvidercolumns。第一个参数dataProvider是我们想要用来操作数据的。

第二个参数,columns,表示要显示的表格列,例如:

<?= \yii\grid\GridView::widget([
    'dataProvider' => $dataProvider,
    'columns' => [
      'id',
      'floor',
      'room_number',
       'available_from:datetime',
       'price_per_day:currency',
    ],
]) ?>

此代码将显示一个表格,其中包含从$dataProvider获取的数据和五列:idfloorroom_numberavailable_fromprice_per_day;最后两列首先使用datetime格式化,然后使用currency格式化。冒号用于指定应用于列数据的格式化程序。

注意

表的样式可以通过许多属性进行自定义,并且默认情况下,表格布局使用 Bootstrap 渲染。

网格表中的列可以使用字符串进行标识,但通常它们是根据yii\grid\Column类进行配置的。

网格中的自定义列

如前一段所述,GridView 小部件的columns属性主要填充字符串。

当我们需要应用特定格式,如货币或日期/时间时,我们可以将此规范附加到列名后跟一个冒号和用于格式化的类型,如currencydatetime

但 GridView 列的最一般形式是yii\grid\Column类的一个对象,该类由yii\grid\DataColumn类派生。

通过yii\grid\Column类扩展的 GridView 列使用具有以下键的数组进行渲染:

        [
// can be omitted, as it is the default
'class' => 'yii\grid\DataColumn',

        'attribute',    // name of model attribute
        'format',         // format use to display data
        'header',        // header of column
        'footer',        // footer of column
        'visible',        // flag to set visibility
        'content'         // callback to print data
        ],

还有其他参数,但这些都是最常用的。

示例 - 通过单击客户网格行显示预订列表

现在我们准备创建一个包含每行链接预订列表的客户网格。首先,确保客户和预订表的结构和数据如下:

--
-- Structure of Table `customer`
--

CREATE TABLE IF NOT EXISTS `customer` (
  `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `surname` varchar(50) NOT NULL,
  `phone_number` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
);

--
-- Data Dump of Table `customer`
--

INSERT INTO `customer` (`id`, `name`, `surname`, `phone_number`) VALUES
(1, 'James', 'Foo', '+39-12345678'),
(2, 'Bar', 'Johnson', '+47-98438923');

--
-- Structure of Table `reservation`
--

CREATE TABLE IF NOT EXISTS `reservation` (
  `id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
  `room_id` int(11) NOT NULL,
  `customer_id` int(11) NOT NULL,
  `price_per_day` decimal(20,2) NOT NULL,
  `date_from` date NOT NULL,
  `date_to` date NOT NULL,
  `reservation_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `room_id` (`room_id`),
  KEY `customer_id` (`customer_id`)
);

--
-- Data Dump of table `reservation`
--

INSERT INTO `reservation` (`id`, `room_id`, `customer_id`, `price_per_day`, `date_from`, `date_to`, `reservation_date`) VALUES
(1, 2, 1, 90.00, '2015-04-01', '2015-05-06', '2015-05-24 22:45:37'),
(2, 2, 1, 48.00, '2019-08-27', '2019-08-31', '2015-05-24 22:45:37'),
(3, 1, 2, 105.00, '2015-09-24', '2015-10-06', '2015-06-03 00:21:14');

basic/controllers/CustomersController.php中创建一个名为CustomersController的新控制器,并带有actionGrid操作来在网格视图中显示列表:

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\Customer;
use yii\data\ActiveDataProvider;

class CustomersController extends Controller
{
    public function actionGrid()
    {
        $query = Customer::find();

        $dataProvider = new ActiveDataProvider([
            'query' => $query,
            'pagination' => [
                'pageSize' => 10,
            ],
        ]);

        return $this->render('grid', [ 'dataProvider' => $dataProvider ]);

    }
}

此操作actionGrid简单地创建一个包含所有客户(未过滤)数据的提供程序,并带有每页显示十个项目的分页。最后,渲染网格视图。

这是basic/views/customers/grid.php中网格视图的内容:

<?php
use yii\grid\GridView;
use yii\helpers\Html;
?>

<h2>Customers</h2>

<?= GridView::widget([
    'dataProvider' => $dataProvider,
    'columns' => [
        'id',
        'name',
        'surname',
        'phone_number',

        [
            'header' => 'Reservations',
            'content' => function ($model, $key, $index, $column) {
                return Html::a('Reservations', ['reservations/grid', 'Reservation[customer_id]' => $model->id]);
            }
        ],

        [
            'class' => 'yii\grid\ActionColumn',
            'template' => '{delete}',
            'header' => 'Actions',
        ],        
    ],
]) ?>

最后两列需要特别说明。

倒数第二项“预订”显示一个链接,让您访问所有客户预订的列表。我们将“预订”作为标题,并在content属性中填充从回调函数传递的动态数据,该函数返回指向reservations/index路由的 HTML 链接,并带有指示所选customer_id的参数。

最后一个标题为“操作”的列显示了带有单个操作“删除”的 ActionColumn,用于删除所选记录。

将您的浏览器指向http://hostname/basic/customers/grid,您应该看到以下输出:

示例 – 通过单击客户网格行显示预订列表

使用 GridView 小部件的“客户”网格

注意

GridView 中使用的语言在basic/config/web.php中通过language属性进行配置。此属性对每个核心小部件具有全局影响。

我们可以通过在“预订”链接附近放置一个计数器来完成此示例,以指示每个客户的预订数量。

为此,我们需要在basic/models/Customer.php中的客户模型中添加一个名为getReservationsCount的新关系,它返回与客户关联的预订数量:

    public function getReservationsCount()
    {
      return $this->hasMany(\app\models\Reservation::className(), ['customer_id' => 'id'])->count();
    }

现在我们可以通过以下方式修改倒数第二列:

        [
            'header' => 'Reservations',
            'content' => function ($model, $key, $index, $column) {
                $title = sprintf('Reservations (%d)', $model->reservationsCount);
                return Html::a($title, ['reservations/grid', 'Reservation[customer_id]' => $model->id]);
            }
        ],

如果我们现在刷新浏览器,我们将在“预订”锚链接附近看到该客户的正确预订数量。

此示例表示用户点击“预订”链接时显示的完整预订列表。

basic/controllers/ReservationsController.php中创建名为ReservationsController的新文件,包含一个名为grid的操作和以下内容:

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\Reservation;
use yii\data\ActiveDataProvider;

class ReservationsController extends Controller
{
    public function actionGrid()
    {
        $query = Reservation::find();

        if(isset($_GET['Reservation']))
        {
            $query->andFilterWhere([
                'customer_id' => isset($_GET['Reservation']['customer_id'])?$_GET['Reservation']['customer_id']:null,
            ]);
        }

        $dataProvider = new ActiveDataProvider([
            'query' => $query,
            'pagination' => [
                'pageSize' => 10,
            ],
        ]);

        return $this->render('grid', [ 'dataProvider' => $dataProvider ]);

    }
}

在此控制器中,我们应用了一个andFilterWhere条件来查询$_GET['Reservation']是否已设置。如果条件不为空,andFilterWhere()方法将应用作为参数传递的筛选器。因此,如果$_GET['Reservation']['customer_id']未设置,andFilterWhere()条件参数将具有空值,并且不会附加到任何其他查询条件。

GridView 中的筛选器

GridView 的核心特性是能够通过在标题行下方添加一个额外的行来简化筛选行。

筛选器主要是文本输入框,但通常可以是任何类型的控件,我们可以根据需要自定义它们。

可以通过填写 GridView 小部件属性filterModel为模型类的一个实例来激活筛选器,自动在标题下方创建一个新行,包含可用的文本输入框。

筛选文本输入框的名称属性填充了模型类名,包括字段名。这样,我们将向控制器传递数据,包括在单个数组中的所有内容;一个可以轻松用于填充搜索模型的变量。

注意

只有对于属于ActiveDataProviderrules()方法中至少一个规则的属性,才会创建自动文本输入过滤器;否则,属性属于safe验证器就足够了。

让我们用一个预订网格的例子来创建一个示例。

我们将填写filterModel属性以应用 GridView 的过滤器,例如:

<?= \yii\grid\GridView::widget([
    ...
    'filterModel' => $searchModel,
    ...        
?>

在这里,$searchModel是我们将从ReservationsController的网格操作传递到视图的Reservation模型类的实例。

现在让我们在basic/controllers/ReservationsController.php中的ReservationsController创建actionGrid()

    <?php

public function actionGrid()
    {
        $query = \app\models\Reservation::find();

        $searchModel = new \app\models\Reservation();
        if(isset($_GET['Reservation']))
        {
            $searchModel->load( \Yii::$app->request->get() );

            $query->andFilterWhere([
                'id' => $searchModel->id,
                'customer_id' => $searchModel->customer_id,
                'room_id' => $searchModel->room_id,
                'price_per_day' => $searchModel->price_per_day,
            ]);
        }

        $dataProvider = new \yii\data\ActiveDataProvider([
            'query' => $query,
            'pagination' => [
                'pageSize' => 10,
            ],
        ]);

        return $this->render('grid', [ 'dataProvider' => $dataProvider, 'searchModel' => $searchModel ]);

    }

$searchModel实例在以下行中填充了$_GET['Reservation']的内容:

            $searchModel->load( Yii::$app->request->get() );

然后,$query将更新为非空属性的内容。

注意

记住 ActiveRecord 的load()方法将从模型类名中包含的数组中获取值,并将其作为键应用于传递给第一个函数参数的数组。

浏览到http://hostname/basic/reservations/grid并在房间 ID列过滤器(第二列)中输入2。这应该是输出:

GridView 中的过滤器

在 GridView 小部件中使用过滤器

我们还可以选择自定义渲染过滤器的方式。想象一下,将房间 ID列过滤器作为一个下拉列表而不是输入文本框。

我们只需要填写房间 IDfilter属性为dropDownList。建议使用Html辅助类通过activeDropDownList()方法渲染dropDownListactive前缀代表 ActiveRecord。此dropDownList()方法需要三个参数:模型类、模型类的属性,以及最后是一个键值数组,其中key<option>标签的值属性,value<option>标签的文本。

我们将使用yii\helpers\ArrayHelper创建键值数组,其中键是模型的id属性,值是回调函数的返回值。

这就是basic/views/reservations/grid.php中的文件如何更改:

<?php
$roomsFilterData = yii\helpers\ArrayHelper::map( app\models\Room::find()->all(), 'id', function($model, $defaultValue) {
    return sprintf('Floor: %d - Number: %d', $model->floor, $model->room_number);
});
?>

<?= \yii\grid\GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'columns' => [
        'id',

        [
            'header' => 'Room',
            'filter' => \Html::activeDropDownList($searchModel, 'room_id', $roomsFilterData, ['prompt' => '--- all']),
            'content' => function($model) {
                return $model->room->floor;
            }
        ],

这是预期的输出:

GridView 中的过滤器

带下拉列表过滤器的 GridView

在网格列中显示和过滤 ActiveRecord 关系数据

让我们现在关注 GridView 中的关系数据,这是一个常见的话题,可以很容易地自行解决。

考虑到预订网格有两个关系字段:room_idcustomer_id,分别引用房间和客户表。如果我们想立即显示客户的姓氏或房间号怎么办?

到目前为止,我们的目标是显示关系数据,例如,在 GridView 中显示客户的姓氏而不是customer_id。引用相关数据的字段用relation属性表示。

在预订网格视图中,customer是获取相关客户的关系,而surname是要保留的字段。

因此,要显示客户的姓氏,只需在预订网格视图中插入此列(作为字符串)即可:

    'customer.surname'

这相当于:

    [
        'attribute' => 'customer.surname'
    ]

将显示一个名为surname的列。如果我们想将列名改为Customer,我们使用这个:

    [
        'header' => 'Customer',
        'attribute' => 'customer.surname'
    ]

注意

我们可以使用自定义属性来获取数据,例如,使用getnameAndSurname来获取特定客户的个人详细信息。

Customer模型中插入一个新属性:

public function getNameAndSurname() {
     return $this->name.' '.$this->surname;
}

然后,这将是在 GridView 中的列:

     [
        'header' => 'Customer',
        'attribute' => 'customer.nameAndSurname'
    ]

我们现在想过滤Customer列。由于customer.surname属性不在Reservation模型的rules()方法中,我们需要扩展此类来处理额外属性。

所以,在basic/models/ReservationSearch.php中创建一个名为ReservationSearch的新类,内容如下:

<?php

class ReservationSearch extends app\models\Reservation
{
    public function attributes()
    {
        // add related fields to searchable attributes
        return array_merge(parent::attributes(), ['customer.surname']);
    }

    public function rules()
    {
        // add related rules to searchable attributes
        return array_merge(parent::rules(),[ ['customer.surname', 'safe'] ]);
    }    

}

此扩展仅添加了一个新属性和附加到该属性的新规则。该属性的名称是customer.surname

我们现在必须更改ReservationsController中的actionGrid()操作,以便与允许根据客户姓氏进行过滤的customer表建立连接。

这是basic/controllers/ReservationsController.phpReservationsControlleractionGrid()方法的内容:

    public function actionGrid()
    {
        $query = \app\models\Reservation::find();

        $searchModel = new \app\models\ReservationSearch();
        if(isset($_GET['ReservationSearch']))
        {
            $searchModel->load( \Yii::$app->request->get() );

            $query->joinWith(['customer']);
            $query->andFilterWhere(
                ['LIKE', 'customer.surname', $searchModel->getAttribute('customer.surname')]
            );

            $query->andFilterWhere([
                'id' => $searchModel->id,
                'customer_id' => $searchModel->customer_id,
                'room_id' => $searchModel->room_id,
                'price_per_day' => $searchModel->price_per_day,

            ]);
        }

        $dataProvider = new \yii\data\ActiveDataProvider([
            'query' => $query,
            'pagination' => [
                'pageSize' => 10,
            ],
        ]);

        return $this->render('grid', [ 'dataProvider' => $dataProvider, 'searchModel' => $searchModel ]);

    }

注意

请注意确保$searchModel是从ReservationSearch类实例化的,以及用于获取数据的$_GET参数也是从ReservationSearch而不是Reservation(因为它已更改类)实例化的。

actionGrid()中使用这些代码行来过滤客户姓氏的操作:

            $query->joinWith(['customer']);
            $query->andFilterWhere(
                ['LIKE', 'customer.surname', $searchModel->getAttribute('customer.surname')]
            );

我们进行连接,如果customer.surname属性不为空,则将有一个新的过滤器。浏览到http://hostname/basic/reservations/grid并在Customer列过滤器中输入Fo。你应该看到这个:

在网格列中显示和过滤 ActiveRecord 关系数据

使用关系数据过滤

网格中的汇总页脚行

GridView 的一个特点是它显示汇总或统计数据,通常作为底部行或第一行,以便立即获取数据(而不是滚动到页面底部的网格底部)。

GridView 小部件的列有一个名为footer的属性,用于标识当前分页的最后一行。在此属性中填入的值将被放置在网格的最后一行。

默认情况下,显示页脚是禁用的;要启用页脚,只需将 GridView 的showFooter属性设置为true。然后,我们需要在要显示的列的footer属性中插入数据。

例如,我们想显示房间每天的平均价格。

basic/views/reservations/grid.php网格视图中顶部添加此代码以计算每天的平均价格:

<?php
use yii\grid\GridView;
use yii\helpers\Html;
?>

<h2>Reservations</h2>

<?php 
$sumOfPricesPerDay = 0;
$averagePricePerDay = 0;

if(count($dataProvider->getModels()) > 0)
{
    foreach($dataProvider->getModels() as $m) $sumOfPricesPerDay += $m->price_per_day;
    $averagePricePerDay = $sumOfPricesPerDay / sizeof($dataProvider->getModels());
}  
?>

<?php 
$roomsFilterData = yii\helpers\ArrayHelper::map( app\models\Room::find()->all(), 'id', function($model, $defaultValue) {
    return sprintf('Floor: %d - Number: %d', $model->floor, $model->room_number);
});
?>

<?= app\components\GridViewReservation::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'showFooter' => true,
    'columns' => [
        'id',

        [
            'header' => 'Room',
            'filter' => Html::activeDropDownList($searchModel, 'room_id', $roomsFilterData, ['prompt' => '--- all']),
            'content' => function($model) {
                return $model->room->floor;
            }
        ],

        [
            'header' => 'Customer',
            'attribute' => 'customer.surname',
        ],

        [
            'attribute' => 'price_per_day',
            'footer' => Yii::$app->formatter->asCurrency($resultQueryAveragePricePerDay, 'EUR')
        ],

        'date_from',
        'date_to',

        [
            'class' => 'yii\grid\ActionColumn',
            'template' => '{delete}',
            'header' => 'Actions',
        ],        
    ],
]) ?>

小心!在这个例子中,count是使用当前分页的模型进行的。如果网格由更多页面组成,它将只显示当前页的平均值!

此计数可以考虑到所有记录(包括过滤后的记录),计算基于当前分页的模型以及查询的结果。在 ReservationsControlleractionGrid() 中添加平均计数:

    public function actionGrid()
    {
        $query = \app\models\Reservation::find();

        $searchModel = new \app\models\ReservationSearch();
        if(isset($_GET['ReservationSearch']))
        {
            $searchModel->load( \Yii::$app->request->get() );

            $query->joinWith(['customer']);
            $query->andFilterWhere(
                ['LIKE', 'customer.surname', $searchModel->getAttribute('customer.surname')]
            );

            $query->andFilterWhere([
                'id' => $searchModel->id,
                'customer_id' => $searchModel->customer_id,
                'room_id' => $searchModel->room_id,
                'price_per_day' => $searchModel->price_per_day,

            ]);

        }
        $resultQueryAveragePricePerDay = $query->average('price_per_day');

        $dataProvider = new \yii\data\ActiveDataProvider([
            'query' => $query,
            'pagination' => [
                'pageSize' => 10,
            ],
        ]);

        return $this->render('grid', [ 'dataProvider' => $dataProvider, 'searchModel' => $searchModel, 'resultQueryAveragePricePerDay' => $resultQueryAveragePricePerDay ]);

    }

平均值是从 $query 对象的 average() 方法计算得出的(因此如果已填写,将考虑过滤器)并传递到视图中,因此视图顶部的计算代码不再需要,因为我们已经正确地将其移动到控制器操作中。

然后更改 price_per_day 列的 footer 内容:

        [
            'attribute' => 'price_per_day',
            'footer' => sprintf('Average: %0.2f', $resultQueryAveragePricePerDay)
        ],

现在,平均计数将独立于分页。

示例 - 扩展 GridView 以自定义网格的页脚行

在高度定制的 GridView 中,需要显示默认情况下未处理的 GridView 中的数据位置,或者需要应用特定的更改(例如合并列)。

在这两种情况下,当无法使用 GridView 的属性创建所需的输出时,将需要子类化 GridView 小部件。

GridView 小部件有特定的方法来渲染其不同部分:renderTableBody()renderTableFooter()renderTableHeader()renderTableRow() 等等。

考虑之前的示例。现在,我们还想收集页脚中的前三个列以显示 Average 标签,price_per_day 列中的唯一值,以及最后的四个列留空。

basic/components/GridViewReservation.php 中创建一个新的组件,该组件扩展了 yii\grid\GridView 小部件,并包含以下内容:

<?php

namespace app\components;

use Yii;
use yii\web\Controller;
use yii\grid\GridView;

class GridViewReservation extends GridView
{
    public function renderTableFooter()
    {
        // Search column for 'price_per_day'
        $columnPricePerDay = null;
        foreach($this->columns as $column)
        {
            if(get_class($column) == 'yii\grid\DataColumn')
            {
                if($column->attribute == 'price_per_day') $columnPricePerDay = $column;
            }
        }

        $html = '<tfoot><tr>';
        $html .= '<td colspan="3"><b>Average:</b></td>';
        $html .= $columnPricePerDay->renderFooterCell();
        $html .= '<td colspan="4"><i>this space is intentionally empty</i></td>';
        $html .= '</tr></tfoot>';

        return $html;
    }
}

此组件仅扩展 yii\grid\GridView 并重写 renderTableFooter() 方法以进行所需的定制(主要是合并单元格)。此代码中的唯一逻辑是找到 price_per_day 列,遍历由 $this->columns 给出的列数组,其中 $this 指的是 GridView 对象。

单页上的多个网格

每个 Yii2 小部件都封装了如此多的内容,因此使用多个 GridView 小部件只是一个简单的活动,只需要进行少量更改。

实际上,唯一不能通过 DataProvider 模型类自定义的参数是 pageParamsortParam,它们分别定义了当前页码索引和用于排序网格的参数。

假设,例如,我们有两个填充了两个不同数据提供者的 GridView,$firstDataProvider$secondDataProvider

在控制器中,我们将设置每个 DataProvider 的 pageParamsortParam 参数:

$firstDataProvider->pagination->pageParam = 'first-dp-page';
$firstDataProvider->sort->sortParam = 'first-dp-sort';

$secondDataProvider->pagination->pageParam = 'second-dp-page';
$secondDataProvider->sort->sortParam = 'second-dp-sort';

如果我们在更改页面或排序列时遗漏了这些定义,此操作也将影响同一页面上的其他 GridView,因为我们没有区分两个网格视图参数。

示例:在同一视图中管理预订和房间网格

此示例的目的是在完全独立于彼此的情况下在同一页面上显示预订和房间网格。

basic/controllers/ReservationsController.php 中的 ReservationsController,创建一个名为 actionMultipleGrid() 的新操作,其内容如下:

    public function actionMultipleGrid()
    {
        /**
         * Reservations
         */
        $reservationsQuery = \app\models\Reservation::find();
        $reservationsSearchModel = new \app\models\ReservationSearch();

        if(isset($_GET['ReservationSearch']))
        {
            $reservationsSearchModel->load( \Yii::$app->request->get() );

            $reservationsQuery->joinWith(['customer']);
            $reservationsQuery->andFilterWhere(
                ['LIKE', 'customer.surname', $reservationsSearchModel->getAttribute('customer.surname')]
            );

            $reservationsQuery->andFilterWhere([
                'id' => $reservationsSearchModel->id,
                'customer_id' => $reservationsSearchModel->customer_id,
                'room_id' => $reservationsSearchModel->room_id,
                'price_per_day' => $reservationsSearchModel->price_per_day,

            ]);
        }

        $reservationsDataProvider = new \yii\data\ActiveDataProvider([
            'query' => $reservationsQuery,
            'sort' => [
                'sortParam' => 'reservations-sort-param',
            ],
            'pagination' => [
                'pageSize' => 10,
                'pageParam' => 'reservations-page-param'
            ],
        ]);        

        /**
         * Rooms
         */
        $roomsQuery = \app\models\Room::find();
        $roomsSearchModel = new \app\models\Room();

        if(isset($_GET['Room']))
        {
            $roomsSearchModel->load( \Yii::$app->request->get() );

            $roomsQuery->andFilterWhere([
                'id' => $roomsSearchModel->id,
                'floor' => $roomsSearchModel->floor,
                'room_number' => $roomsSearchModel->room_number,
                'has_conditioner' => $roomsSearchModel->has_conditioner,
                'has_phone' => $roomsSearchModel->has_conditioner,
                'has_tv' => $roomsSearchModel->has_conditioner,
                'available_from' => $roomsSearchModel->has_conditioner,

            ]);
        }

        $roomsDataProvider = new \yii\data\ActiveDataProvider([
            'query' => $roomsQuery,
            'sort' => [
                'sortParam' => 'rooms-sort-param',
            ],
            'pagination' => [
                'pageSize' => 10,
                'pageParam' => 'rooms-page-param'
            ],
        ]);        

        return $this->render('multipleGrid', [
            'reservationsDataProvider' => $reservationsDataProvider, 'reservationsSearchModel' => $reservationsSearchModel,
            'roomsDataProvider' => $roomsDataProvider, 'roomsSearchModel' => $roomsSearchModel,
        ]);

    }

我们已经将预订声明从房间声明中分离出来,以便清楚地区分它们。请注意,确保你为 DataProvider 的任何一个都定义了 sortparampageparam

现在我们创建一个新的视图在 basic/views/reservations/multipleGrid.php

<?php
use yii\grid\GridView;
use yii\helpers\Html;
?>

<h2>Reservations</h2>
<?= GridView::widget([
    'dataProvider' => $reservationsDataProvider,
    'filterModel' => $reservationsSearchModel,
    'columns' => [
        'id',
        'room_id',
        'attribute' => 'customer.surname',
        'price_per_day',
        'date_from',
        'date_to'
    ],
]) ?>

<h2>Rooms</h2>
<?= GridView::widget([
    'dataProvider' => $roomsDataProvider,
    'filterModel' => $roomsSearchModel,
    'columns' => [
        'id',
        'floor',
        'room_number',        
        'has_conditioner:boolean',
        'has_phone:boolean',
        'has_tv:boolean',
        'available_from',
    ],
]) ?>

这两个网格是完全独立的,我们现在可以排序或更改页面,而不会干扰其他网格。

摘要

在本章中,我们介绍了用于显示数据(直接或关联)的 GridView 小部件。在讨论 GridView 时,一个基本话题是 DataProvider,它是一种向 GridView 提供数据的方式。你学习了如何根据可用的来源从 ActiveRecord、数组或 SQL 中获取 DataProvider。

在 GridView 的第一次简单实现之后,你理解了列的定制,并使用模型类的扩展来添加新属性以添加额外功能,从而显示了来自其他表的关系数据。接下来,我们说明了如何通过筛选数据来选择特定的行。

在本章结束之前,你看到了如何通过子类化核心小部件 yii\grid\GridView 来在 GridView 中显示、总结和定制页脚以及更多内容。最后,最后一个话题是使用同一页面上的多个网格,特别关注需要发生的少量更改,以避免它们相互干扰。

在下一章中,你将学习如何使用 CSS、JavaScript、小部件以及框架直接提供的工具(如 Gii)来定制用户界面。

第七章。用户界面工作

在本章中,你将发现 Gii 作为工具是多么强大。它提供了对 CRUD 操作的支持,以及创建控制器及其相应的视图。

在本章中,我们将介绍与用户界面相关的以下主题:

  • 使用 Gii 生成创建、读取、更新和删除 (CRUD) 操作:

    • 例如 - 使用 CRUD 通过 Gii 管理房间、预订和客户
  • 自定义 JavaScript 和 CSS:

    • 例如 - 使用 JavaScript 和 CSS 显示广告列,如果空间不足则消失
  • 使用 AJAX:

    • 例如:从客户下拉列表加载的预订详情
  • 使用 Bootstrap 小部件:

    • 例如 - 使用日期选择器
  • 在同一视图中查看多个模型:

    • 例如 - 同时保存多个客户
  • 在同一视图中保存链接的模型:

    • 例如 - 在同一视图中创建客户和预订

现在是时候学习 Yii2 支持的以自定义网页的 JavaScript 和 CSS 部分了。JavaScript 的一个常见用途是处理 AJAX 调用,即从 jQuery 和 Bootstrap 中管理小部件和复合控件(如依赖性下拉列表)。

最后,我们将使用 jQuery 动态从同一类中创建更多模型,这些模型将被传递到控制器以进行验证和保存。

使用 Gii 生成 CRUD

我们在第五章中介绍了 Gii,开发预订系统,用于生成模型。现在我们想使用 Gii 来创建带有控制器和视图的 CRUD 操作。

在浏览器中输入 http://hostname/basic/web/gii 返回 Gii 欢迎页面。点击 CRUD 部分的 开始 按钮。我们必须填写四个字段:

  • 模型类: 这是与 CRUD 将要构建的表关联的 ActiveRecord 类;这个类应该使用完全限定的命名空间路径提供,例如:app\models\ModelClass

  • 搜索模型类: 这是将要生成的搜索模型类的名称,并从模型类扩展;这个类将提供在搜索记录时使用的有用方法和扩展。这应该使用完全限定的命名空间路径提供,例如:app\models\ModelClassSearch

  • 控制器类: 这是将要生成的控制器类的名称;这个类应该使用完全限定的命名空间路径和 CamelCase 格式提供名称,名称以大写字母开头,例如:app\controller\MyCustomController

  • 视图路径: 这是控制器操作生成的视图将被存储的目录。我们可以使用路径,别名 @app/views,来表示视图文件的基准路径,例如:@app/views/myCustom 表示 MyCustomController 视图的基准路径,默认填充为 @app/views/controller-id

然后,我们可以自定义BaseControllerClass,这是在索引页面中使用的小部件,以启用 I18N 状态和代码模板,但保留默认值是完全可以的。

注意

如果我们检查启用 I18N,那么我们必须注意每个属性标签的翻译,这将在后面的章节中介绍。

示例 - 使用 Gii 通过 CRUD 管理房间、预订和客户

在本例中,我们将创建完整的 CRUD 操作来管理房间、预订和客户。

在前面的章节中,我们处理了 Gii CRUD 操作以创建表单。现在,我们必须为所有三个模型(房间、预订和客户模型类)重复这些说明。为了区分 Gii 创建的文件和前面章节中手动创建的文件,我们将 Gii 后缀添加到控制器类的名称中。

浏览到 Gii 欢迎页面http://hostname/basic/web/gii,在CRUD部分点击开始按钮,并使用以下值填写字段以创建Room模型类的 CRUD 操作:

  • 模型类: app\models\Room

  • 搜索模型类: app\models\RoomSearch

  • 控制器类: app\controllers\RoomsWithGiiController

  • 视图路径: @app/views/rooms-with-gii

然后,为Reservation模型类重复此操作:

  • 模型类: app\models\Reservation

  • 搜索模型类: app\models\ReservationSearch

  • 控制器类: app\controllers\ReservationsWithGiiController

  • 视图路径: @app/views/reservations-with-gii

最后,为Customer模型类重复它们:

  • 模型类: app\models\Customer

  • 搜索模型类: app\models\CustomerSearch

  • 控制器类: app\controllers\CustomersWithGiiController

  • 视图路径: @app/views/customers-with-gii

    注意

    确保视图路径中有斜杠(/)而不是反斜杠(\),因为在模型类、搜索模型类和控制器类中的命名空间路径中不应使用反斜杠。

以下截图显示了填写以生成Room模型类的 CRUD 操作的字段:

示例 - 使用 Gii 通过 CRUD 管理房间、预订和客户

Gii 的 CRUD 生成器

在导航文件夹结构时,你会看到 Gii 在basic/controllers中创建了三个新的文件,分别命名为RoomsWithGiiController.phpReservationsWithGiiController.phpCustomersWithGiiController.php

每个这些文件都包含五个操作:

  • actionCreate(): 此操作用于创建新的模型对象

  • actionView(): 此操作用于查看模型对象的详细信息

  • actionUpdate(): 此操作用于更新现有的模型对象

  • actionDelete(): 此操作用于删除现有的模型对象

  • actionIndex(): 此操作用于使用网格布局显示模型对象的列表

打开 basic/models 文件夹,你会找到三个新的文件:RoomSearch.phpReservationSearch.php(该文件应该已经存在)和 CustomerSearch.php

这些文件基本上都包含一个 search() 方法,该方法返回 ActiveDataProvider,用于在 GridView 中显示数据,并传递一些过滤条件。

最后,打开 basic/views 文件夹,你会找到三个新的文件夹:roomsWithGiireservationsWithGiicustomersWithGii;每个文件夹都包含六个文件:

  • _form.php

  • _search.php

  • create.php

  • index.php

  • update.php

  • view.php

以下划线开头的视图文件在 Yii2 中默认被视为子视图,或者说是由其他视图调用的视图。

前两个文件以下划线开头;实际上,如果我们打开 create.phpupdate.php,我们会注意到,在这些文件的末尾,使用 _form.php 视图调用了 render() 方法。创建和更新视图将使用相同的 _form 视图来显示编辑字段表单。

最后四个文件,create.phpindex.phpupdate.phpview.php 是指向控制器中相同操作的视图。默认情况下,它们每个页面都有一个面包屑和标题。

进行一些测试,例如浏览到 http://hostname/basic/web/rooms-with-gii/indexhttp://hostname/basic/web/rooms-with-gii/index,以查看 Gii 制作的出色作品。

这是 RoomsWithGiiController 的索引动作结果:

示例 – 使用 CRUD 通过 Gii 管理房间、预订和客户

RoomsWithGiiController 索引动作的输出

自定义 JavaScript 和 CSS

如前所述,在本章中,你将了解如何使用前端交互。使用 JavaScript 和 CSS 是自定义前端输出的基础。

与 Yii1 不同,在 Yii1 中,调用 JavaScript 和 CSS 脚本和文件是通过使用 Yii::app() 单例完成的,在新框架版本 Yii2 中,这项任务现在是 yii\web\View 类的一部分。

调用 JavaScript 或 CSS 有两种方式:要么直接传递要执行的代码,要么传递文件路径。

备注

当直接传递要执行的代码时,我们将使用 PHP 提供的 Heredoc 语法来避免处理字符串转义。

registerJs() 函数允许我们使用三个参数执行 JavaScript 代码:

  • 第一个参数是要注册的 JavaScript 代码块

  • 第二个参数是 JavaScript 标签应插入的位置(头部、身体部分的开始、身体部分的结束、在 jQuery load() 方法内或 jQuery document.ready() 方法内,默认为后者)

  • 第三个和最后一个参数是一个键,用于标识 JavaScript 代码块(如果没有提供,则使用第一个参数的内容作为键)

另一方面,registerJsFile() 函数允许我们使用三个参数执行一个 JavaScript 文件:

  • 第一个参数是 JavaScript 文件的路径文件。

  • 第二个参数是脚本标签的 HTML 属性,特别关注 depends 和 position 值,它们不被视为标签属性。

  • 第三个参数是一个标识 JavaScript 代码块的关键字(如果没有提供,则将使用第一个参数的内容作为关键字)。

CSS,类似于 JavaScript,可以通过代码执行或通过传递路径文件执行。

registerCss()函数允许我们使用三个参数来执行 CSS 代码:

  • 第一个是需要注册的 CSS 代码块。

  • 第二个是style标签的 HTML 属性。

  • 第三个和最后一个参数是一个标识 JavaScript 代码块的关键字(如果没有提供,则将使用第一个参数的内容作为关键字)。

registerCssFile()函数允许我们使用三个参数来执行 CSS 文件:

  • 第一个是 CSS 文件的路径文件。

  • 第二个参数是链接标签的 HTML 属性,特别关注 depends 值,它不被视为标签属性。

  • 第三个参数是一个标识 JavaScript 代码块的关键字(如果没有提供,则将使用第一个参数的内容作为关键字)。

通常,JavaScript 或 CSS 文件发布在basic/web文件夹中,该文件夹无限制可访问。

因此,当我们必须使用自定义 JavaScript 或 CSS 文件时,建议将它们放在basic/web文件夹的子文件夹中,可以命名为cssjs

注意

默认情况下,CSS 文件文件夹basic/web/css应该已经存在。但我们仍然需要为 JavaScript 文件创建basic/web/js

在某些情况下,我们可能需要为所有 Web 应用程序页面添加新的 CSS 或 JavaScript 文件。将这些条目放在AppAsset.php文件中最合适,该文件位于basic/assets/AppAsset.php。在其中,我们可以添加 Web 应用程序所需的 CSS 和 JavaScript 条目,如果需要,甚至可以使用依赖项。

示例 - 使用 JavaScript 和 CSS 显示广告列,如果空间不足则消失。

如果需要同时使用 JavaScript 和 CSS 自定义,则此示例是合适的。

想象一下构建为三个垂直列的布局,这是博客系统的典型布局。左侧一列宽 200 像素(通常用于广告),中间一列宽 1000 像素(通常用于内容),右侧一列宽 200 像素(通常再次用于广告)。

如果浏览器宽度至少为 1,400 像素,我们希望显示所有三列(内容和两个广告列)。

如果没有足够的空间来显示所有列,并且浏览器的宽度大小在 1,200 到 1,400 像素之间,则只显示左侧和中间列(只有一个广告列和一个内容列。最后,如果浏览器的宽度大小小于 1,200 像素,则只显示包含内容的中间列)。

此外,我们的目标是确保这些列始终在浏览器中居中。

basic/controllers/ThreeColumnsController.php 中创建一个新的控制器类,以处理渲染视图文件的动作:

<?php
namespace app\controllers;

use Yii;
use yii\web\Controller;

class ThreeColumnsController extends Controller
{
    public function actionIndex()
    {
        return $this->render('index.php');
    }
}

此外,在 basic/views/three-columns 中创建一个新的 view 文件夹,并在其中插入 index.php 文件以存储视图内容。

基本上,这是构建三列布局所需的内容:

<div id="layout">
    <div id="colSx" class="column">
            Content of SX Column
    </div>
    <div id="colCenter" class="column">
            Content of Central Column
    </div>
    <div id="colDx" class="column">
            Content of DX Column
    </div>
</div>

CSS 类 column 将仅用于通过围绕它们添加黑色边框来增强单元格的可见性。

在这一点上,我们将使用 registerCss() 方法在视图文件顶部居中布局并固定列的宽度:

<?php

$this->registerCss( <<< EOT_CSS

    .column
    {
            border:1px solid black;
    }

    #layout
    {
        position:relative;
        margin:0pt auto;
        width:1400px;
    }

    #colSx
    {
        width:200px;
        float:left;
    }

    #colCenter
    {
        width:1000px;
        float:left;
    }

    #colDx
    {
        width:200px;
        float:left;
    }

EOT_CSS
);

?>

将您的浏览器指向 http://hostname/basic/web/three-columns/index,您将看到以下内容:

示例 – 使用 JavaScript 和 CSS 显示在空间不足时消失的广告列

内容宽度分为三列

我们必须通过 JavaScript 处理浏览器调整大小事件,以使用本章开头定义的维度规则来管理列的可视化。

我们将使用 registerJs() 方法,仅传递要执行的代码:

<?php
$this->registerJs( <<< EOT_JS

    function resizeLayout()
    {
        var windowWidth = $(window).width();

        if(windowWidth > 1400)
        {
            $('#colSx').css('display', 'block');
            $('#colCenter').css('display', 'block');
            $('#colDx').css('display', 'block');
            $('#layout').css('width', 1400);
        }
        else if((windowWidth>1200)&&(windowWidth<=1400))
        {
            $('#colSx').css('display', 'block');
            $('#colCenter').css('display', 'block');
            $('#colDx').css('display', 'none');
            $('#layout').css('width', 1200);
        }
        else if(windowWidth<1200)
        {
            $('#colSx').css('display', 'none');
            $('#colCenter').css('display', 'block');
            $('#colDx').css('display', 'none');
            $('#layout').css('width', 1000);
        }

    }

    $(window).resize(function() {
            resizeLayout();
    });

    $(function() {
            resizeLayout();
    });

EOT_JS
);
?>

刷新您的浏览器到 http://hostname/basic/web/three-columns/index 并调整到所需的宽度,列的可视化应根据特定宽度中的可用空间而变化。

使用 AJAX

Yii2 为某些小部件提供了适当的属性以进行 AJAX 调用;然而,有时在这些属性中编写 JavaScript 代码会使代码难以阅读,尤其是当我们处理复杂代码时。

因此,为了进行 AJAX 调用,我们将使用由 registerJs() 执行的外部 JavaScript 代码。

这是使用 GETPOST 方法的 AJAX 类模板:

<?php
$this->registerJs( <<< EOT_JS

     // using GET method
$.get({
  url: url,
  data: data,
  success: success,
  dataType: dataType
});

     // using POST method
$.post({
  url: url,
  data: data,
  success: success,
  dataType: dataType
});

EOT_JS
);
?>

AJAX 调用通常是用户界面事件(如按钮点击、链接等)的效果。因此,大多数情况下,AJAX 调用直接与 jQuery 在 HTML 元素(锚点、按钮等)上的 .on() 事件相关联。因此,记住 Yii2 如何渲染输入字段的 nameid 属性非常重要。

当我们调用 Html::activeTextInput($model, $attribute) 或以相同方式使用 <?= $form->field($model, $attribute)->textInput() ?>

输入文本字段的 nameid 属性将被渲染如下:

  • id : 模型类名通过小写属性名与短横线分隔;例如,如果模型类名是 Room 且属性是 floor,则 id 属性将是 room-floor

  • name:包含属性名的模型类名,例如,如果模型类名是 Reservation 且属性是 price_per_day,则 name 属性将是 Reservation[price_per_day];因此,Reservation 模型拥有的每个字段都将包含在一个单独的数组中

示例 – 从客户的下拉列表中加载的预订详情

在这个例子中,有两个下拉列表和一个详细框。这两个下拉列表分别对应客户和预订;当用户点击客户列表项时,根据他们的选择,预订的下拉列表将被填写。

最后,当用户点击预订列表项时,一个包含所选预订数据的详细框将被填写。

basic/controllers/ReservationsController.php中创建一个新的操作,命名为actionDetailDependentDropdown()

    public function actionDetailDependentDropdown()
    {
        $showDetail = false;

        $model = new Reservation();

        if(isset($_POST['Reservation']))
        {
          $model->load( Yii::$app->request->post() );

          if(isset($_POST['Reservation']['id'])&&($_POST['Reservation']['id']!=null))
            {
               $model = Reservation::findOne($_POST['Reservation']['id']);
               $showDetail = true;
            }
        }

        return $this->render('detailDependentDropdown', [ 'model' => $model, 'showDetail' => $showDetail ]);
    }

在这个操作中,我们将根据Reservation模型数据从表单中获取customer_idid参数,如果它们被填写,这些数据将被用来搜索正确的预订模型并将其传递给视图。

有一个名为$showDetail的标志,如果接收到模型的id属性,它将显示预订详情内容。

ReservationsController中,还有一个在用户更改下拉列表中的客户选择时将通过 AJAX 调用的操作:

    public function actionAjaxDropDownListByCustomerId($customer_id)
    {
        $output = '';

        $items = Reservation::findAll(['customer_id' => $customer_id]);
        foreach($items as $item)
        {
            $content = sprintf('reservation #%s at %s', $item->id, date('Y-m-d H:i:s', strtotime($item->reservation_date)));
            $output .= \yii\helpers\Html::tag('option', $content, ['value' => $item->id]);
        }

        return $output;
    }

这个操作将返回填充有按客户 ID 过滤的预订数据的<option> HTML 标签。

现在让我们看看basic/views/reservations/detailDependentDropdown.php中的视图:

<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
use yii\helpers\ArrayHelper;
use yii\helpers\Url;
use app\models\Customer;
use app\models\Reservation;

$urlReservationsByCustomer = Url::to(['reservations/ajax-drop-down-list-by-customer-id']);
$this->registerJs( <<< EOT_JS

    $(document).on('change', '#reservation-customer_id', function(ev) {

        $('#detail').hide(); 

        var customerId = $(this).val();    

        $.get(
            '{$urlReservationsByCustomer}',
            { 'customer_id' : customerId },
            function(data) {
                data = '<option value="">--- choose</option>'+data;
                $('#reservation-id').html(data);
            }
        )
        ev.preventDefault();
    });

    $(document).on('change', '#reservation-id', function(ev) {
        $(this).parents('form').submit();
        ev.preventDefault();
    });

EOT_JS
);

?>

<div class="customer-form">
    <?php $form = ActiveForm::begin(['enableAjaxValidation' => false, 'enableClientValidation' => false, 'options' => ['data-pjax' => '']]); ?>

    <?php $customers = Customer::find()->all(); ?>
    <?= $form->field($model, 'customer_id')->dropDownList(ArrayHelper::map( $customers, 'id', 'nameAndSurname'), [ 'prompt' => '--- choose' ]) ?>

    <?php $reservations = Reservation::findAll(['customer_id' => $model->customer_id]); ?>
    <?= $form->field($model, 'id')->label('Reservation ID')->dropDownList(ArrayHelper::map( $reservations, 'id', function($temp, $defaultValue) {
      $content = sprintf('reservation #%s at %s', $temp->id, date('Y-m-d H:i:s', strtotime($temp->reservation_date)));
        return $content;
    }), [ 'prompt' => '--- choose' ]); ?>

    <div id="detail">
    <?php if($showDetail) { ?>
        <hr />
        <h2>Reservation Detail:</h2>
        <table>
            <tr>
                <td>Price per day</td>
                <td><?php echo $model->price_per_day ?></td>
            </tr>
        </table>
    <?php } ?>
    </div>

    <?php ActiveForm::end(); ?>

</div>

在视图的顶部,有处理客户和预订下拉列表变化的处理程序。

如果客户下拉列表发生变化,detail div 将被隐藏,一个 AJAX 调用将获取所有按customer_id过滤的预订,并将结果作为内容传递给预订下拉列表。如果预订下拉列表发生变化,将提交一个表单。

在表单声明中,我们首先找到客户下拉列表,然后是预订列表,它使用闭包从ArrayHelper::map()方法获取值。我们可以在Reservation模型中添加一个新属性,通过创建一个以get前缀开始的函数来实现,例如getDescription(),并在其中放入闭包的内容,或者更确切地说:

public function getDescription()
{
$content = sprintf('reservation #%s at %s', $this>id, date('Y-m-d H:i:s', strtotime($this>reservation_date)));
            return $content;
}

或者我们可以使用简短语法从ArrayHelper::map()获取数据,如下所示:

    <?= $form->field($model, 'id')->dropDownList(ArrayHelper::map( $reservations, 'id', 'description'), [ 'prompt' => '--- choose' ]); ?>

最后,如果$showDetail被标记,将显示一个简单的详细框,其中只包含预订的每日价格。

将您的浏览器指向http://hostname/basic/web/reservations/detail-dependent-dropdown

示例 – 从客户下拉列表加载的预订详情

从客户下拉列表动态加载的预订详情

使用 Bootstrap 小部件

Yii2 支持 Bootstrap 作为核心功能。Bootstrap 框架的 CSS 和 JavaScript 文件默认注入到所有页面中,我们甚至可以使用这个功能来仅应用 CSS 类或调用 Bootstrap 提供的自己的 JavaScript 函数。

然而,Yii2 将 Bootstrap 作为小部件嵌入,我们可以像访问任何其他小部件一样访问这个框架的功能。

最常用的有:

类名 描述
yii\bootstrap\Alert 这个类用于渲染一个 Bootstrap 提示组件
yii\bootstrap\Button 这个类用于渲染一个 Bootstrap 按钮
yii\bootstrap\Dropdown 这个类用于渲染一个 Bootstrap 下拉菜单组件
yii\bootstrap\Nav 这个类用于渲染一个 nav HTML 组件
yii\bootstrap\NavBar 这个类用于渲染一个 navbar HTML 组件

例如,yii\bootstrap\Navyii\bootstrap\NavBar 被用于默认的主模板中。

这是从主布局视图(在 basic/views/layouts/main.php)的摘录:

        <?php
            NavBar::begin([
                'brandLabel' => 'My Company',
                'brandUrl' => Yii::$app->homeUrl,
                'options' => [
                    'class' => 'navbar-inverse navbar-fixed-top',
                ],
            ]);
            echo Nav::widget([
                'options' => ['class' => 'navbar-nav navbar-right'],
                'items' => [
                    ['label' => 'Home', 'url' => ['/site/index']],
                    ['label' => 'About', 'url' => ['/site/about']],
                    ['label' => 'Contact', 'url' => ['/site/contact']],
                    Yii::$app->user->isGuest ?
                        ['label' => 'Login', 'url' => ['/site/login']] :
                        ['label' => 'Logout (' . Yii::$app->user->identity->username . ')',
                            'url' => ['/site/logout'],
                            'linkOptions' => ['data-method' => 'post']],
                ],
            ]);
            NavBar::end();
        ?>

示例:使用日期选择器

Yii2 通过 JUI 扩展(yii2-jui)本身也支持许多 jQuery UI 组件。

如果我们在 vendor 文件夹中没有 yii2-jui 扩展,我们可以使用以下命令从 Composer 获取它:

php composer.phar require --prefer-dist yiisoft/yii2-jui

在本例中,我们将讨论两个最常用的组件:datepickerautocomplete。首先让我们看看 datepicker 组件。这个组件可以使用模型属性或通过填写值属性来初始化。以下是一个使用模型实例及其属性之一制作的示例:

echo DatePicker::widget([
    'model' => $model,
    'attribute' => 'from_date',
    //'language' => 'it',
    //'dateFormat' => 'yyyy-MM-dd',
]);

以下是值属性使用的一个示例:

echo DatePicker::widget([
    'name'  => 'from_date',
    'value'  => $value,
    //'language' => 'it',
    //'dateFormat' => 'yyyy-MM-dd',
]);

现在在 basic/controllers/JuiWidgetsController.php 中创建一个名为 JuiWidgetsController 的新控制器:

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\Reservation;

class JuiWidgetsController extends Controller
{
    public function actionDatePicker()
    {
        $reservationUpdated = false;

        $reservation = Reservation::findOne(1);

        if(isset($_POST['Reservation']))
        {
            $reservation->load( Yii::$app->request->post() );

            $reservation->date_from = Yii::$app->formatter->asDate(  date_create_from_format('d/m/Y', $reservation->date_from), 'php:Y-m-d' );
            $reservation->date_to = Yii::$app->formatter->asDate(  date_create_from_format('d/m/Y', $reservation->date_to), 'php:Y-m-d' );

            $reservationUpdated = $reservation->save();
        }

        return $this->render('datePicker', ['reservation' => $reservation, 'reservationUpdated' => $reservationUpdated]);
    }
}

在这个操作中,我们定义了 $reservation 模型,从具有 id 1 的预订数据库表中选取。

当数据通过 POST 发送时,date_fromdate_to 字段将从 d/m/y 格式转换为 y-m-d 格式,以便数据库能够保存数据。然后通过 save() 方法更新模型对象。使用 Bootstrap 小部件,在更新模型后,视图中将显示一个提示框。

basic/views/jui-widgets/datePicker.php 中创建 datePicker 视图:

<?php

use yii\helpers\Html;
use yii\widgets\ActiveForm;
use yii\jui\DatePicker;

?>

<div class="row">
    <div class="col-lg-6">
        <h3>Date Picker from Value<br />(using MM/dd/yyyy format and English language)</h3>
        <?php
            $value = date('Y-m-d');

        echo DatePicker::widget([
            'name'  => 'from_date',
            'value'  => $value,
            'language' => 'en',
            'dateFormat' => 'MM/dd/yyyy',
        ]);
        ?>
    </div>
    <div class="col-lg-6">

        <?php if($reservationUpdated) { ?>
            <?php
            echo yii\bootstrap\Alert::widget([
                'options' => [
                    'class' => 'alert-success',
                ],
                'body' => 'Reservation successfully updated',
            ]);   
            ?>         
        <?php } ?>

        <?php $form = ActiveForm::begin(); ?>

        <h3>Date Picker from Model<br />(using dd/MM/yyyy format and italian language)</h3>

        <br />

        <label>Date from</label>
        <?php
        // First implementation of DatePicker Widget
        echo DatePicker::widget([
            'model'  => $reservation,
            'attribute' => 'date_from',
            'language' => 'it',
            'dateFormat' => 'dd/MM/yyyy',
        ]);
        ?>

        <br />
        <br />

        <?php
        // Second implementation of DatePicker Widget
        echo $form->field($reservation, 'date_to')->widget(\yii\jui\DatePicker::classname(), [
                'language' => 'it',
                'dateFormat' => 'dd/MM/yyyy',
        ]) ?>        

        <?php     
            echo Html::submitButton('Send', ['class' => 'btn btn-primary'])
        ?>

        <?php $form = ActiveForm::end(); ?>

    </div>
</div>

视图分为两列,左列和右列。左列简单地显示一个从值(固定为当前日期)中获取的 DataPicker 示例。右列显示一个提示框,如果 $reservation 模型已被更新,以及接下来的两种组件声明;第一个没有使用 $form,第二个使用 $form,两者输出相同的 HTML 代码。

在任何情况下,DatePicker 日期输出格式都通过 dateFormat 属性设置为 dd/MM/yyyy,语言通过 language 属性设置为意大利语。

将您的浏览器指向 http://hostname/basic/web/jui-widgets/date-picker 以查看以下输出:

示例:使用日期选择器

使用日期选择器

同一个视图中存在多个模型

通常,我们可以在单个视图中找到许多相同或不同类的模型。首先,请记住,Yii2 将所有视图的表单属性封装在同一个容器中,该容器的名称与模型类名相同。因此,当控制器接收数据时,这些数据都将组织在 $_POST 数组的同一个键中,键名与模型类名相同。

如果模型类名是 Customer,则每个表单输入名称属性将为 Customer[attributeA_of_model]。这是通过以下方式构建的:$form->field($model, 'attributeA_of_model')->textInput()

在同一类多个模型的情况下,容器将再次命名为模型类名,但每个模型的每个属性都将插入到一个数组中,例如:

Customer[0][attributeA_of_model_0]
Customer[0][attributeB_of_model_0]
…
…
…
Customer[n][attributeA_of_model_n]
Customer[n][attributeB_of_model_n]

这些是用以下方式构建的:

$form->field($model, '[0]attributeA_of_model')->textInput();
$form->field($model, '[0]attributeB_of_model')->textInput();
…
…
…
$form->field($model, '[n]attributeA_of_model')->textInput();
$form->field($model, '[n]attributeB_of_model')->textInput();

注意

注意,数组键信息被插入到属性名中!

因此,当数据传递给控制器时,$_POST['Customer'] 将是一个由 Customer 模型组成的数组,并且这个数组的每个键,例如,$_POST['Customer'][0]Customer 类的一个模型。

示例 – 同时保存多个客户

现在让我们看看如何一次性保存三个客户。我们将创建三个容器,每个容器对应一个模型类,将包含一些 Customer 模型的字段。

basic/views/customers/createMultipleModels.php 中创建一个视图,其中包含一个输入字段块,该块为控制器传递的每个模型重复:

<?php

use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */
/* @var $model app\models\Room */
/* @var $form yii\widgets\ActiveForm */
?>

<div class="room-form">

    <?php $form = ActiveForm::begin(); ?>

    <div class="model">

      <?php for($k=0;$k<sizeof($models);$k++) { ?>
          <?php $model = $models[$k]; ?>
          <hr />
          <label>Model #<?php echo $k+1 ?></label>
          <?= $form->field($model, "[$k]name")->textInput() ?>
          <?= $form->field($model, "[$k]surname")->textInput() ?>
          <?= $form->field($model, "[$k]phone_number")->textInput() ?>
      <?php } ?>

    </div>

<hr />

    <div class="form-group">
      <?= Html::submitButton('Save', ['class' => 'btn btn-primary']) ?>
    </div>

    <?php ActiveForm::end(); ?>

</div>

对于每个模型,所有字段都将具有 Customer 类相同的验证规则,并且每个模型对象都将单独进行验证。

接下来在 basic/controllers/CustomersController.php 中的客户控制器中创建一个新的操作,命名为 actionCreateMultipleModels。如果设置了 $_POST['Customer'] 内容,并且它们都经过验证并最终重定向到网格操作,它们将一起保存;否则,它将创建三个 Customer 类的模型:

    public function actionCreateMultipleModels()
    {
        $models = [];

        if(isset($_POST['Customer']))
        {
             $validateOK = true;

            foreach($_POST['Customer'] as $postObj)
            {
                $model = new Customer();
                $model->attributes = $postObj;
                $models[] = $model;

                $validateOK = ($validateOK && ($model->validate()));               
            }

            // All models are validated and will be saved
            if($validateOK)
            {
                foreach($models as $model)
                {
                    $model->save();
                }

                // Redirect to grid after save
                return $this->redirect(['grid']);
            }
        }
        else
        {
            for($k=0;$k<3;$k++)
            {
                $models[] = new Customer();
            }    
        }

        return $this->render('createMultipleModels', ['models' => $models]);
    }

在控制器中创建模型可能很有用,因为在这里配置了大量模型和其他验证检查。

浏览到 http://hostname/basic/web/customers/create-multiple-models 以查看完整的页面:

示例 – 同时保存多个客户

在同一视图中保存多个模型

在同一视图中保存链接模型

在同一视图中保存不同类型的模型可能很方便。这种方法允许我们节省时间,并从每个单独的细节导航到最后一个合并所有数据的项。处理相互链接的不同类型的模型与之前看到的方法没有太大区别。唯一需要注意的点是在模型之间的链接(外键),我们必须确保它是有效的。

因此,控制器操作将接收封装在模型类名容器中的 $_POST 数据;如果我们考虑客户和预订模型,例如,$_POST 变量中将有两个数组,$_POST['Customer']$_POST['Reservation'],包含有关客户和预订模型的所有字段。

然后必须一起保存所有数据。在保存数据时使用数据库事务是明智的,因为只有当所有数据都已保存时,操作才能被认为是完成的。

在 Yii2 中使用数据库事务非常简单!数据库事务从在数据库连接对象上调用beginTransaction()开始,并在通过beginTransaction()创建的数据库事务对象上调用commit()rollback()方法时结束。

要开始一个事务:

$dbTransaction = Yii::$app->db->beginTransaction();

提交一个事务,以保存所有数据库活动:

$dbTransaction->commit();

回滚一个事务,以清除所有数据库活动:

$dbTransaction->rollback();

因此,如果客户已保存而预订未保存(任何可能的原因),我们的数据将是部分和不完整的。使用数据库事务,我们将避免这种危险。

示例 – 在同一视图中创建客户和预订

现在,我们希望在同一个视图中一次性创建客户和预订模型。这样,我们将在视图中有一个包含客户模型字段的框和一个包含预订模型字段的框。

basic/views/reservations/createCustomerAndReservation.php中创建一个视图,包含客户和预订模型中的字段:

<?php

use yii\helpers\Html;
use yii\widgets\ActiveForm;
use yii\helpers\ArrayHelper;
use \app\models\Room;
?>

<div class="room-form">

    <?php $form = ActiveForm::begin(); ?>

    <div class="model">

      <?php echo $form->errorSummary([$customer, $reservation]); ?>

      <h2>Customer</h2>        
      <?= $form->field($customer, "name")->textInput() ?>
      <?= $form->field($customer, "surname")->textInput() ?>
      <?= $form->field($customer, "phone_number")->textInput() ?>

      <h2>Reservation</h2>        
      <?= $form->field($reservation, "room_id")->dropDownList(ArrayHelper::map(Room::find()->all(), 'id', function($room, $defaultValue) {
          return sprintf('Room n.%d at floor %d', $room->room_number, $room->floor);
      })); ?>
      <?= $form->field($reservation, "price_per_day")->textInput() ?>
      <?= $form->field($reservation, "date_from")->textInput() ?>
      <?= $form->field($reservation, "date_to")->textInput() ?>

    </div>

    <div class="form-group">
        <?= Html::submitButton('Save customer and room', ['class' => 'btn btn-primary']) ?>
    </div>

    <?php ActiveForm::end(); ?>

</div>

我们在表单中创建了两个区块,用于填写客户和预订的字段。

现在,在basic/controllers/ReservationsController.php中的ReservationsController中创建一个名为actionCreateCustomerAndReservation的新操作:

    public function actionCreateCustomerAndReservation()
    {
        $customer = new \app\models\Customer();
        $reservation = new \app\models\Reservation();

        // It is useful to set fake customer_id to reservation model to avoid validation error (because customer_id is mandatory)
        $reservation->customer_id = 0;

        if(
            $customer->load(Yii::$app->request->post())
            &&
            $reservation->load(Yii::$app->request->post())
            &&
            $customer->validate()
            &&
            $reservation->validate()
        )
        {

            $dbTrans = Yii::$app->db->beginTransaction();

            $customerSaved = $customer->save();

            if($customerSaved)
            {
                $reservation->customer_id = $customer->id;
                $reservationSaved = $reservation->save();

                if($reservationSaved)
                {
                    $dbTrans->commit();
                }
                else {
                    $dbTrans->rollback();
                }                
            }
            else {
                $dbTrans->rollback();
            }
        }

        return $this->render('createCustomerAndReservation', [ 'customer' => $customer, 'reservation' => $reservation ]);
    }

确保你注意以下两点:

  • $reservation->customer_id = 0:使用此代码,我们避免了当$reservation验证时出现的与customer_id要求相关的验证错误

  • 只有当客户模型和预订模型的保存操作完成时,数据库事务才会提交

浏览到http://hostname/basic/web/reservations/create-customer-and-reservation以查看完整页面:

示例 – 在同一视图中创建客户和预订

同时创建客户和预订

摘要

在本章中,我们讨论了用户界面以及 Yii 如何通过其核心功能帮助我们。Yii 提供的第一项重要工具是 Gii,它简化了 CRUD 操作和视图的创建,例如,我们使用 Gii 来管理房间、预订和客户。

接下来,我们看到了如何在布局和视图中嵌入 JavaScript 和 CSS,使用文件内容或内联块。这应用于一个示例,展示了如何根据浏览器的可用宽度更改显示的列数;这通常是网站或显示广告列的 Web 应用的典型任务。

再次谈到 JavaScript,你学习了如何实现直接的 AJAX 调用,以一个示例为例,其中预订详情是从客户的下拉列表中动态加载的。

接下来,我们探讨了 Yii 的核心用户界面库,它是基于 Bootstrap 构建的,我们展示了如何原生地使用主要的 Bootstrap 小部件,以及 DatePicker(可能是最常用的 jQuery UI 小部件)。

最后,我们讨论了同一类和不同类别的多种模型。我们探讨了这两个主题的两个示例:第一个是同时保存多个客户,第二个是在同一视图中创建客户和预订。

在下一章中,我们将解释如何设置登录认证和授权,并从头开始实现这些目标。

第八章. 登录应用

本章将解释如何设置登录认证和授权。登录是保护我们应用的基本步骤,您将学习如何从头开始实现这些目标,使用在互联网上广泛可用的免费 Web 管理扩展。

本章将涵盖以下主题:

  • 创建用户登录:

    • 例如:创建登录表单以访问
  • 配置用户授权

    • 例如:创建一个访问控制过滤器以授权
  • 基于角色的访问控制RBAC

    • 例如:配置 RBAC 为用户设置权限
  • 混合访问控制过滤器ACF)和 RBAC

    • 例如:管理用户角色以访问房间、预订和客户

第一步将是使用数据库表创建对我们的应用的认证访问,并将其与通过扩展IdentityInterface的用户模型关联到 Yii 用户组件。我们将提供一个如何使用它的示例:构建一个登录表单以认证用户。

下一步将是控制用户可以执行哪些操作,使用 ACF 和 RBAC。我们将通过一些使用 ACF 和 RBAC 的示例,并在后者中从头开始构建一个完整的授权管理器。

创建用户登录

应用程序的安全始于用户登录的两个明显区分的阶段:认证和授权。

第一个,认证,是验证用户身份的过程,通常使用用户名和密码,或者电子邮件和密码进行。认证完成时,用户已被识别,并且他们的状态已被保留以供后续请求。

第二个,授权,是验证用户是否有权限执行特定操作的过程。

注意

由于 HTTP 请求是无状态的,我们需要保留登录状态,这意味着它们之间没有数据上下文共享。这个限制通过会话得到解决,主要是文件,其中 Web 服务器存储数据。一个文件名用作会话标识符,并通过 HTML 响应中包含的链接的 URL 参数传递给浏览器。这样,浏览器通过发送会话标识符到 Web 服务器通过 cookie 或请求 URL 中的参数来保持会话活跃,而 Web 服务器知道哪个文件包含会话数据。

可以使用具有相同功能的数据库表代替文件。

Yii2 通过yii\web\User组件实现认证,该组件管理用户认证状态,并包含一个指向代表我们引用的具体对象的identityClass的引用。

一个identityClass类应该实现五个方法:

  • findIdentity(): 此方法使用提供的 ID 参数查找一个身份类实例。它通常在我们需要通过会话保持登录状态时使用。

  • findIdentityByAccessToken(): 此方法使用参数提供的访问令牌查找身份类的实例。它通常在我们需要使用单个密钥进行认证时使用。

  • getId(): 此方法返回身份实例的 ID。

  • getAuthKey(): 此方法返回在登录完成后使用浏览器发送的 cookie(在登录时勾选记住我)时用于验证基于 cookie 的登录的密钥。

  • validateAuthKey(): 此方法验证提供的作为参数传递的authKey是否正确(在基于 cookie 的登录中)。

通常,identityClass类对应于User数据库表的一条记录。因此,通常identityClass类实现IdentityInterface并扩展ActiveRecord

现在是时候实现认证了。首先要做的是配置yii\web\User组件及其identityClass。打开basic/config/web.php文件,如果尚未存在,则将user属性添加到components中:

    'components' => [
        …
        …
        'user' => [
            'identityClass' => 'app\models\User',
        ],
    ],

接下来,我们必须创建一个数据库表,用于存储用户的记录:

CREATE TABLE `user` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `username` varchar(255) NOT NULL,
 `auth_key` varchar(32) NOT NULL,
 `password_hash` varchar(255) NOT NULL,
 `access_token` varchar(100) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

注意

注意,我们没有密码字段,但有password_hash字段。这是因为密码是使用散列方法存储的。在模型中,我们将有一个setPassword()设置器方法,它获取纯文本密码以填充password_hash字段。

最后,让我们更新处理登录状态的basic/models/User类,通过实现IdentityInterface并将其连接到数据库的user表。这是basic/models/User的常见实现:

<?php
namespace app\models;

use Yii;
use yii\base\NotSupportedException;
use yii\db\ActiveRecord;
use yii\web\IdentityInterface;

class User extends ActiveRecord implements IdentityInterface
{
    public static function tableName()
    {
        return 'user';
    }

    public static function findIdentity($id)
    {
        return static::findOne(['id' => $id]);
    }

    public static function findIdentityByAccessToken($token, $type = null)
    {
        return static::findOne(['access_token' => $token]);
    }

    public static function findByUsername($username)
    {
        return static::findOne(['username' => $username]);
    }

    public function getId()
    {
        return $this->getPrimaryKey();
    }

    public function getAuthKey()
    {
        return $this->auth_key;
    }

    public function validateAuthKey($authKey)
    {
        return $this->getAuthKey() === $authKey;
    }

    public function validatePassword($password)
    {
        return Yii::$app->security->validatePassword($password, $this->password_hash);
    }

    public function setPassword($password)
    {
        $this->password_hash = Yii::$app->security->generatePasswordHash($password);
    }

    public function generateAuthKey()
    {
        $this->auth_key = Yii::$app->security->generateRandomString();
    }

}

注意

如果我们的应用程序也使用基于 cookie 的认证,我们还需要填写auth_key字段,因为这将作为 http 响应传递给客户端。当通过覆盖\app\models\User模型中的beforeSave()方法插入新用户时,自动填充auth_key字段是方便的:

    public function beforeSave($insert)
    {
        if (parent::beforeSave($insert)) {
            if ($this->isNewRecord) {
                $this->auth_key = \Yii::$app->security->generateRandomString();
            }
            return true;
        }
        return false;
    }

用户组件提供登录、注销和访问identityClass的方法,并验证用户认证的有效性。

要验证用户是否已正确认证,请使用以下方法:

// whether the current user is a guest (not authenticated)
$isGuest = Yii::$app->user->isGuest;

当用户认证并通过\app\models\User模型实例化时,我们可以通过调用以下方法来完成认证:

// find a user identity with the specified username.
// note that you may want to check the password if needed
$userModel = User::findOne(['username' => $username]);

// logs in the user
Yii::$app->user->login($userModel);

然后,当我们需要访问身份类时:

// access to identity class that it is equivalent to $userModel
$identity = Yii::$app->user->identity;

最后,为了注销用户:

Yii::$app->user->logout();

示例 - 登录表单以访问

在此示例中,我们将创建一个登录表单并完成用户认证。为了进行此操作,需要根据前一段描述创建一个user数据库表。

要添加用户,只需在user表中插入一条新记录,其中foo作为用户名,foopassword作为密码:

INSERT INTO `user` (
`username` ,
`password_hash` ,
)
VALUES (
'foo',
'$2a$12$hL0rmIMjxhLqI.xr7jD1FugNWEgZNh62HuJj5.y34XBUfBWB4cppW'
);

注意

密码使用 bcrypt 方法进行散列,成本值为 12,可以通过快速 Google 搜索在互联网上找到。

然后,在 basic/controllers/MyAuthenticationController.php 中创建一个名为 MyAuthentication 的新控制器,并确保它包含两个操作:actionLoginactionLogout

actionLogin 方法从 $_POST 获取用户名和密码数据,并使用 $error 变量将错误描述传递到视图。如果填写了用户名和密码数据,用户将在数据库表中找到,并验证插入的密码,然后用户将被登录。

最后,actionLogout 简单地将用户从会话中注销,并将浏览器重定向到登录页面:

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;

use app\models\User;

class MyAuthenticationController extends Controller
{
    public function actionLogin()
    {
        $error = null;

        $username = Yii::$app->request->post('username', null);
        $password = Yii::$app->request->post('password', null);

        $user = User::findOne(['username' => $username]);

        if(($username!=null)&&($password!=null))
        {
            if($user != null)
            {
                if($user->validatePassword($password))
                {
                    Yii::$app->user->login($user);
                }
                else {
                    $error = 'Password validation failed!';
                }
            }
            else
            {
                $error = 'User not found';
            }
        }

        return $this->render('login', ['error' => $error]);
    }
    public function actionLogout()
    {
        Yii::$app->user->logout();
        return $this->redirect(['login']);
    }

}

现在,在 basic/views/my-authentication/login.php 中创建包含以下内容的视图。在用户可以登录之前,将显示一个包含要填写的用户名和密码的表单。当用户名和密码与用户数据库表中的条目匹配时,将显示确认消息和注销按钮:

<?php
use \yii\bootstrap\ActiveForm;
use \yii\helpers\Html;
use \yii\bootstrap\Alert;
?>

<?php
if($error != null) {
    echo Alert::widget([ 'options' => [ 'class' => 'alert-danger' ], 'body' => $error ]);    
}
?>

<?php if(Yii::$app->user->isGuest) { ?>

    <?php ActiveForm::begin(); ?>

    <div class="form-group">
    <?php echo Html::label('Username', 'username'); ?>
    <?php echo Html::textInput('username', '', ['class' => 'form-control']); ?>
    </div>

    <div class="form-group">
    <?php echo Html::label('Password', 'password'); ?>
    <?php echo Html::passwordInput('password', '', ['class' => 'form-control']); ?>
    </div>

    <?php echo Html::submitButton('Login', ['class' => 'btn btn-primary']); ?>

    <?php ActiveForm::end(); ?>

<?php } else { ?>

    <h2>You are authenticated!</h2>
    <br /><br />
    <?php echo Html::a('Logout',  ['my-authentication/logout'], ['class' => 'btn btn-warning']); ?>

<?php } ?>

通过将浏览器指向 http://hostname/basic/web/my-authentication/login 并填写 foo 作为用户名和 foopassword 作为密码来测试它,应该会显示:

示例 – 访问的登录表单

访问的登录表单

点击 登录 按钮后,你应该会看到:

示例 – 访问的登录表单

成功认证

此方法不提供字段错误处理,因为我们没有使用模型来创建表单字段。如果我们创建了一个包含用户名和密码字段的表单模型,我们就可以向此模型添加规则验证,并看到输入错误处理(如字段值缺失、字段长度错误等)。幸运的是,Yii2 在 basic/models/LoginForm.php 中提供了一个可用的登录表单模型。

如果我们想要使用此模型,我们会在 MyAuthenticationController 中创建一个名为 actionLoginWithForm 的新操作,该操作通过模型处理登录字段,而不是从 $_POST 参数中获取:

    public function actionLoginWithModel()
    {
        $error = null;

        $model = new \app\models\LoginForm();
        if ($model->load(Yii::$app->request->post())) {
            if(($model->validate())&&($model->user != null))
            {
                Yii::$app->user->login($model->user);
            }
            else
            {
                $error = 'Username/Password error';
            }
        }

        return $this->render('login-with-model', ['model' => $model, 'error' => $error]);
    }

这是 basic/views/my-authentication/login-with-model.php 的内容:

<?php
use \yii\bootstrap\ActiveForm;
use \yii\helpers\Html;
use \yii\bootstrap\Alert;
?>

<?php
if($error != null) {
    echo Alert::widget([ 'options' => [ 'class' => 'alert-danger' ], 'body' => $error ]);    
}
?>
<?php if(Yii::$app->user->isGuest) { ?>

    <?php $form = ActiveForm::begin([
        'id' => 'login-form',
    ]); ?>

    <?= $form->field($model, 'username') ?>

    <?= $form->field($model, 'password')->passwordInput() ?>

    <div class="form-group">
        <?= Html::submitButton('Login', ['class' => 'btn btn-primary', 'name' => 'login-button']) ?>
    </div>

    <?php ActiveForm::end(); ?>

<?php } else { ?>
    <h2>You are authenticated!</h2>
    <br /><br />
    <?php echo Html::a('Logout',  ['my-authentication/logout'], ['class' => 'btn btn-warning']); ?>    
<?php } ?>    

我们可以通过将浏览器指向 http://hostname/basic/web/my-authentication/login-with-model 来查看输出。

如果我们尝试提交未填写所有字段的表单,我们会立即得到错误,因为它们通过表单客户端验证激活:

示例 – 访问的登录表单

使用模型的登录错误

如果标准行为不足以满足我们的需求,我们可以按需自定义 LoginForm 模型类。

配置用户授权

Yii 有两种方法来授权用户:ACF 和 RBAC。

第一个,ACF,用于需要最小和简单访问控制的应用程序。基本上,其行为基于五个参数:

  • allow: 此参数指定这是一个允许还是拒绝规则;可能的值是 allowdeny

  • actions: 此参数指定此规则匹配哪些操作,它们使用字符串数组声明

  • roles:此参数指定此规则匹配哪些用户角色;可能的值是?@,分别表示访客用户和已认证用户。

  • ips:此参数指定此规则匹配哪个客户端 IP 地址;IP 地址可以包含*作为通配符。

  • verbs:此参数指定此规则匹配哪个动词(请求方法)。

默认情况下,如果没有规则匹配,将拒绝访问。

ACF 通过覆盖Controllerbehaviors()方法,并用一些(或所有)前一个参数的内容填充其access属性来实现。

    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'only' => ['login', 'logout', 'signup', 'index'],
                'rules' => [
                    [
                        'allow' => true,
                        'actions' => ['login', 'signup', 'index'],
                        'roles' => ['?'],
                    ],
                    [
                        'allow' => true,
                        'actions' => ['logout'],
                        'roles' => ['@'],
                    ],
                ],
            ],
        ];
    }

在这个例子中,loginlogoutsignupindex操作对访客用户(所有用户)可用,而logout操作仅对已认证用户可用。

ACF 有许多其他参数可以定义,例如controllers,用于定义此规则匹配哪些控制器(如果为空,则表示所有控制器);matchCallback,其值是一个 PHP 可调用函数,用于验证此规则是否可以应用;最后是denyCallback,其值是一个 PHP 可调用函数,当此规则将拒绝访问时使用。

当规则被拒绝时,根据用户的角色,会有两种不同的行为。如果访客被拒绝,拒绝的规则将调用yii\web\User::loginRequired()方法将用户的浏览器重定向到登录页面;如果用户已认证,它将抛出yii\web\ForbiddenHttpException异常。

可以使用前面提到的denyCallback属性以及定义正确的 PHP 可调用函数来自定义此行为。

显然,任何关于已登录用户的具体信息都不会被这种授权方式考虑。实际上,在behaviors()方法的配置中,用户的任何详细信息(例如,role)都不会出现。因此,我们无法更精确地定义用户可以执行或不能执行控制器操作的条件。

ACF 建议,如果我们必须限制对已认证用户的访问,而不需要其他详细信息来允许执行控制器操作,那么这是最佳选择。

但在所有这些情况下,如果仅基于用户是否登录的条件来限制访问就足够了,那么这是最佳方法。在有限访问的 REST API(其中只有已认证用户能够进行调用)中,ACF 可能是最佳解决方案。

示例 - 创建一个 ACF 来授权用户

现在我们来看看如何创建一个 ACF 来授权用户显示或不显示页面内容。

我们有两个操作:actionPrivatePageactionPublicPage。第一个操作仅对已认证用户可用,第二个操作是公开可访问的。

MyAuthenticationController.php中,让我们添加以下内容的behaviors()方法:

    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'only' => ['public-page', 'private-page'],
                'rules' => [
                    [
                        'allow' => true,
                        'actions' => ['public-page'],
                        'roles' => ['?'],
                    ],
                    [
                        'allow' => true,
                        'actions' => ['private-page'],
                        'roles' => ['@'],

                    ],
                ],

                // Callable function when user is denied
                'denyCallback' => function($rule, $data) {
                        $this->redirect(['login']);
                }
            ],
        ];
    }

此方法将 ACF 应用于仅两个操作actionPublicPageactionPrivatePage(仅基于属性值),并限制指定角色为@的私有页面的访问。

然后,我们添加了 denyCallback 属性来指示当用户访问被拒绝时应该如何显示行为。在这种情况下,我们将其设置为用户应重定向到 MyAuthenticationControllerlogin 操作。

RBAC

当我们需要更细粒度的授权控制时,RBAC 是正确的选择。

RBAC 包括两个部分:

  • 第一种方法是构建 RBAC 授权数据

  • 第二种方法是使用授权数据执行进一步的访问控制

我们现在开始构建 RBAC 授权数据。RBAC 可以通过两种方式初始化:通过 PhpManager,实例化 yii\rbac\PhpManager 组件,该组件将在 @app/rbac 文件夹中存储 RBAC 数据,以及通过 DbManager,实例化 yii\rbac\DbManager 组件,该组件将使用四个数据库表来存储其数据。

我们需要在主配置文件中配置 authManager 应用组件,使用其中一个授权管理器,即 yii\rbac\PhpManageryii\rbac\DbManager

以下代码展示了如何在 basic/config/web.php 中使用 yii\rbac\PhpManager 类配置 authManager

return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\PhpManager',
        ],
        // ...
    ],
];

以下代码展示了如何在 basic/config/web.php 中使用 yii\rbac\DbManager 类配置 authManager

return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\DbManager,
        ],
        // ...
    ],
];

这两种方法都基于三个对象:permissionsrolesrulespermissions 方法表示可以控制的操作;roles 是一组权限,目标可以启用或禁用;rules 是在检查权限时执行的额外验证。最后,permissionsroles 可以分配给用户,并由 Yii::$app->user 组件的 IdentityInterface::getId() 值识别。

当访问权限不改变时,我们可以创建一个控制台命令以在权限更改的情况下启动,或者一旦更改权限就启动。然而,我们现在不会讨论这个问题,因为您将在下一章中深入了解控制台命令。

相反,我们将使用一个假操作来仅执行权限、角色和分配设置。

basic/controllers/MyAuthenticationController.php 文件中,添加名为 actionInitializeAuthorizations 的操作:

    public function actionInitializeAuthorizations()
    {
        $auth = Yii::$app->authManager;

        // Reset all
        $auth->removeAll();

        // add "createReservation" permission
        $permCreateReservation = $auth->createPermission('createReservation');
        $permCreateReservation->description = 'Create a reservation';
        $auth->add($permCreateReservation);

        // add "updatePost" permission
        $permUpdateReservation = $auth->createPermission('updateReservation');
        $permUpdateReservation->description = 'Update reservation';
        $auth->add($permUpdateReservation);

        // add "operator" role and give this role the "createReservation" permission
        $roleOperator = $auth->createRole('operator');
        $auth->add($roleOperator);
        $auth->addChild($roleOperator, $permCreateReservation);

        // add "admin" role and give this role the "updateReservation" permission
        // as well as the permissions of the "operator" role
        $roleAdmin = $auth->createRole('admin');
        $auth->add($roleAdmin);
        $auth->addChild($roleAdmin, $permUpdateReservation);
        $auth->addChild($roleAdmin, $roleOperator);

        // Assign roles to users. 1 and 2 are IDs returned by IdentityInterface::getId()
        // usually implemented in your User model.
        $auth->assign($roleOperator, 2);
        $auth->assign($roleAdmin, 1);
    }

注意

在从浏览器调用此操作之前,请确保 basic/rbac 文件夹已存在并且可写。

为了从开始执行此操作,创建了两个权限和两个角色,然后将 createReservation 权限作为子项添加到操作员角色中,将 updateReservation 权限作为子项添加到管理员角色中,并一起添加到操作员角色中。

如果我们检查具有 roleOperator 角色的用户对 createReservation 权限的检查,它将成功确认。同样,如果检查具有 adminOperator 的用户,也会发生这种情况。但是,当我们检查具有 roleOperator 角色的用户对 updateReservation 权限的检查时,它将被拒绝,因为该权限未分配给该特定角色。

注意

权限和角色名称可以无限制地选择,因为它们在检查权限时用作参数。

现在,让我们将浏览器指向 http://hostname/basic/my-authentication/initialize-authorizations 以启动权限创建。

通过此操作在 basic/rbac 文件夹中创建的文件内容仅仅是数组。这是 items.php 文件的内容:

<?php
return [
    'createReservation' => [
        'type' => 2,
        'description' => 'Create a reservation',
    ],
    'updateReservation' => [
        'type' => 2,
        'description' => 'Update reservation',
    ],
    'operator' => [
        'type' => 1,
        'children' => [
            'createReservation',
        ],
    ],
    'admin' => [
        'type' => 1,
        'children' => [
            'updateReservation',
            'operator',
        ],
    ],
];

这是 assignments.php 的内容:

<?php
return [
    2 => [
        'operator',
    ],
    1 => [
        'admin',
    ],
];

最后,为了检查用户授权,只需调用 yii\web\User::can() 方法:

if (\Yii::$app->user->can()) {
    // create reservation permission is enabled to current user
}

示例 - 配置 RBAC 以设置用户权限

在此示例中,我们将从头开始创建一个基于 RBAC 的用户权限管理系统。我们将在 basic/controllers/AuthorizationManagerController.php 中创建一个名为 AuthorizationManagerController 的新控制器,该控制器将显示数据库中的所有用户和所有可用的权限和角色。此示例基于前几段中已使用的用户数据库表。

让我们再次看看它的结构:

CREATE TABLE `user` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `username` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
 `auth_key` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
 `password_hash` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
 `access_token` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
 PRIMARY KEY (`id`)
)

我们将截断数据库表并插入这些记录,五个条目,用于后续示例:

TRUNCATE user;

INSERT INTO `user` (`id`, `username`, `auth_key`, `password_hash`, `access_token`) VALUES
(1, 'foo', '', '$2a$12$hL0rmIMjxhLqI.xr7jD1FugNWEgZNh62HuJj5.y34XBUfBWB4cppW', NULL),
(2, 'userA', '', '$2a$12$hL0rmIMjxhLqI.xr7jD1FugNWEgZNh62HuJj5.y34XBUfBWB4cppW', NULL),
(3, 'userB', '', '$2a$12$hL0rmIMjxhLqI.xr7jD1FugNWEgZNh62HuJj5.y34XBUfBWB4cppW', NULL),
(4, 'userC', '', '$2a$12$hL0rmIMjxhLqI.xr7jD1FugNWEgZNh62HuJj5.y34XBUfBWB4cppW', NULL),
(5, 'admin', '', '$2a$12$hL0rmIMjxhLqI.xr7jD1FugNWEgZNh62HuJj5.y34XBUfBWB4cppW', NULL);

现在我们有了可以工作的数据,我们可以编写代码。

在此控制器中要创建的第一个方法是 initializeAuthorizations(),它必须初始化系统中的所有可用授权:

    <?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use yii\filters\AccessControl;
use app\models\User;
use app\models\LoginForm;

class MyAuthenticationController extends Controller
{

public function initializeAuthorizations()
    {
        $auth = Yii::$app->authManager;

        $permissions = [
            'createReservation' => array('desc' => 'Create a reservation'),
            'updateReservation' => array('desc' => 'Update reservation'),
            'deleteReservation' => array('desc' => 'Delete reservation'),

            'createRoom' => array('desc' => 'Create a room'),
            'updateRoom' => array('desc' => 'Update room'),
            'deleteRoom' => array('desc' => 'Delete room'),

            'createCustomer' => array('desc' => 'Create a customer'),
            'updateCustomer' => array('desc' => 'Update customer'),
            'deleteCustomer' => array('desc' => 'Delete customer'),
        ];

        $roles = [
            'operator' => array('createReservation', 'createRoom', 'createCustomer'),
        ];

        // Add all permissions
        foreach($permissions as $keyP=>$valueP)
        {
            $p = $auth->createPermission($keyP);
            $p->description = $valueP['desc'];
            $auth->add($p);

            // add "operator" role and give this role the "createReservation" permission
            $r = $auth->createRole('role_'.$keyP);
            $r->description = $valueP['desc'];
            $auth->add($r);
            if( false == $auth->hasChild($r, $p)) $auth->addChild($r, $p);
        }

        // Add all roles
        foreach($roles as $keyR=>$valueR)
        {
            $r = $auth->createRole($keyR);
            $r->description = $keyR;
            $auth->add($r);

            foreach($valueR as $permissionName)
            {
             if( false == $auth->hasChild($r, $auth->getPermission($permissionName))) $auth->addChild($r, $auth->getPermission($permissionName));
            }

        }

        // Add all permissions to admin role
        $r = $auth->createRole('admin');
        $r->description = 'admin';
        $auth->add($r);
        foreach($permissions as $keyP=>$valueP)
        {
            if( false == $auth->hasChild($r, $auth->getPermission($permissionName))) $auth->addChild($r, $auth->getPermission($keyP));
        }
    }
}

在此方法顶部,我们创建了一个权限和角色列表,然后我们将它们分配给 Yii 授权组件。请注意,在第一次调用此方法后,您需要通过在每个 addChild() 插入尝试上调用 hasChild 方法来检查是否已存在任何子项。

注意

我们为每个权限创建了一个角色,因为 assign()revoke() 方法将角色而不是权限作为第一个参数,因此我们需要为每个权限复制一个角色。

接下来,我们可以创建 actionIndex(),它启动之前的初始化授权,获取所有用户并将所有分配给每个用户的权限填充到一个数组中。这是 actionIndex() 方法的具体内容:

    public function actionIndex()
    {
        $auth = Yii::$app->authManager;

        // Initialize authorizations
        $this->initializeAuthorizations();

        // Get all users        
        $users = User::find()->all();

        // Initialize data
        $rolesAvailable = $auth->getRoles();
        $rolesNamesByUser = [];

        // For each user, fill $rolesNames with name of roles assigned to user
        foreach($users as $user)
        {
            $rolesNames = [];

            $roles = $auth->getRolesByUser($user->id);
            foreach($roles as $r)
            {
                $rolesNames[] = $r->name;
            }

            $rolesNamesByUser[$user->id] = $rolesNames;
        }

        return $this->render('index', ['users' => $users, 'rolesAvailable' => $rolesAvailable, 'rolesNamesByUser' => $rolesNamesByUser]);
    }

按照索引操作视图的内容 basic/views/authorization-manager/index.php

<?php
use yii\helpers\Html;
?>

<table class="table">
    <tr>
        <td>User</td>
        <?php foreach($rolesAvailable as $r) { ?>
            <td><?php echo $r->description ?></td>
        <?php } ?>
    </tr>

    <?php foreach($users as $u) { ?>
        <tr>
            <td><?php echo $u->username ?></td>

            <?php foreach($rolesAvailable as $r) { ?>
                <td align="center">
                <?php if(in_array($r->name, $rolesNamesByUser[$u->id])) { ?>
                  <?php echo Html::a('Yes', ['remove-role', 'userId' => $u->id, 'roleName' => $r->name]); ?>
                <?php } else { ?>
                    <?php echo Html::a('No', ['add-role', 'userId' => $u->id, 'roleName' => $r->name]); ?>
                <?php } ?>
                </td>
            <?php } ?>
        </tr>
    <?php } ?>

</table>

这个循环遍历 $rolesAvailable 数组中的每个用户的角色内容。要查看此输出,请将浏览器指向 http://hostname/basic/web/authorization-manager/index

示例 - 配置 RBAC 以设置用户权限

用户/权限表

每个权限状态都是一个链接,指向添加角色或删除角色的操作(根据当前状态而定)。

现在我们必须创建最后两个操作:向用户添加角色和撤销角色:

    public function actionAddRole($userId, $roleName)
    {
        $auth = Yii::$app->authManager;

        $auth->assign($auth->getRole($roleName), $userId);

        return $this->redirect(['index']);
    }

    public function actionRemoveRole($userId, $roleName)
    {
        $auth = Yii::$app->authManager;

        $auth->revoke($auth->getRole($roleName), $userId);

        return $this->redirect(['index']);
    }

混合 ACF 和 RBAC

ACF 包含一个名为 role 的属性,通常用 ? 填充以表示对所有用户开放访问,用 @ 表示仅对认证用户开放访问。但还有一个第三种选项,它将内容引用到 RBAC 系统的角色名称。

因此,对于每个控制器来说,只需要通过指定可以访问控制器内部动作的角色来覆盖 behaviors() 方法,然后将用户关联到该角色,以便允许或拒绝访问。

示例 - 管理用户角色以访问房间、预订和客户

在这个例子中,我们将向你展示如何使用 ACF 和 RBAC 管理控制器动作的访问。

我们将使用 foo 用户来模拟 RoomsController 的认证用户。首先要做的事情是在 basic/controller/RoomsController.php 中扩展 behaviors() 方法,添加以下内容:

Use yii\filters\AccessControl;

    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'rules' => [
                    [
                        'allow' => true,
                        'actions' => ['create'],
                        'roles' => ['operator'],
                    ],
                    [
                        'allow' => true,
                        'actions' => ['index'],
                    ],                    
                ],

            ],
        ];
    }

通过这段代码,我们将保证只有具有 operator 角色的用户才能访问 create 动作,而 index 动作的访问权限给予所有用户,其他所有动作则拒绝所有人。

因此,如果我们尝试浏览到 http://hostname/basic/web/rooms/create,我们应该看到一个包含禁止错误的错误页面。这是因为我们正在尝试访问一个权限不足的页面。

现在,我们可以通过访问 http://hostname/basic/web/my-authentication/login 并输入 foo 作为用户名和 foopassword 作为密码来执行认证,因为我们已经在上一章的数据库中创建了一个具有这些凭证的用户。我们应该看到一个成功登录的页面。

最后要做的就是将 operator 角色分配给 foo 用户。我们可以使用刚刚创建的授权管理器 http://hostname/basic/web/authorization-manager/index。现在,点击指向 foo 用户和 operator 角色的单元格。这样,我们就已经将 operator 角色分配给了 foo 用户。

最后,我们可以在 http://hostname/basic/web/rooms/create 刷新房间创建页面。现在我们可以看到房间控制器的创建动作页面。

摘要

在本章中,你学习了如何将用户认证和授权应用到应用中。第一步是为应用程序创建一个认证访问。为此,我们创建了一个数据库表来管理用户,并通过一个扩展 IdentityInterface 的用户模型将其关联到 Yii 用户组件。

本章的第一个例子是构建一个登录表单以验证用户。下一步是控制用户可以执行或不执行哪些动作,这在授权阶段也是如此。正如你所看到的,Yii 提供了两种解决方案:ACF 和 RBAC。我们配置了一个控制器使用 ACF,然后你看到了 RBAC 是如何作为一个更强大的工具,以更细粒度地管理用户授权。最后,我们完全自己构建了一个授权管理器。

在下一章中,我们将介绍如何安装和使用高级模板,以及在同一上下文中拥有多个应用。

第九章. 前端向所有人显示房间

本章将涵盖使用模板在同一上下文中拥有多个应用的主题。

Yii 确实允许你拥有一个高级安装,能够包含多个 Yii 应用实例。因此,项目中的每个文件夹实际上都是一个全新的 Yii 应用。

我们将看到如何安装和配置项目,在它们之间共享数据,并最终自定义 URL 以使其对搜索引擎友好。

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

  • 使用高级模板来分割前端和后端

  • 使用 init 配置应用

    • 示例 - 创建面向公开访问的前端
  • 在应用之间共享 ActiveRecord 模型

    • 示例 - 在前端网站上显示可用房间
  • 在高级模板中自定义 URL

    • 示例 - 在同一域名中使用高级模板
  • 如何在共享托管中使用高级模板

使用高级模板来分割前端和后端

到目前为止,我们看到了只有单一入口点的简单应用。然而,单一入口点对于更通用的应用来说是不够的。实际上,在高级网络应用中,我们不仅仅有一个入口点,通常有三个:前端、后端以及用作每个入口点共享区域的公共区域。

前端入口点是一个公开访问,对所有用户无限制。

另一方面,后端入口点是仅对具有管理内容管理角色的认证用户开放的受限访问。

最后,公共入口点用于在入口点之间共享数据。

想象一个预订系统,其中前端是显示房间可用性和价格的网站,而后端是管理员区域,操作员可以在此管理房间。

同样,前端和后端的另一个例子可能是一个包含公开可见新闻的前端区域和记者可以插入新闻的后端区域的报纸网站。

现在我们已经了解了前端和后端之间的区别以及它们的目的,我们将创建一个高级 Yii 应用。

安装 Yii 应用高级模板的步骤与安装基本模板的步骤类似。

注意

在这一点上,强烈建议拥有对主机的控制台访问权限,我们可以在此放置文件。

在网络托管中定位网站文档根目录。从它开始,我们将启动命令,在名为 yiiadv 的新子文件夹中创建高级应用,其中 yiiadv 代表使用高级模板的 Yii 安装。

我们将使用 Composer 安装 Yii 高级模板,因为这是最推荐的方式。如果我们还没有将 Composer 作为全局应用安装,现在我们可以在 yiiadv 文件夹中安装它。

以下是从文档根目录开始安装 Yii 高级模板的说明:

$ curl -sS https://getcomposer.org/installer | php
$ php composer.phar global require "fxp/composer-asset-plugin:~1.0.0"
$ php composer.phar create-project --prefer-dist yiisoft/yii2-app-advanced yiiadv

通过打开yiiadv子文件夹,我们可以看到除了基本模板之外的一些新文件夹,如下所示:

  • backend:这个文件夹是项目后端应用的入口点

  • common:这个文件夹是包含项目其他应用通用数据的应用的入口点

  • console:这个文件夹是项目控制台应用的入口点

  • frontend:这个文件夹是项目前端应用的入口点

这种结构是开发 Web 应用的经验结果。后端和前端入口点已经被讨论过;通用入口点是一个放置所有其他应用共享数据(通用模型、组件等)的区域。

备注

项目中的每个应用(后端、前端、common 和控制台)在 Web 应用中被视为一个单独的命名空间。因此,当我们提到前端中的RoomsController时,完整的类命名空间将是frontend/controllers/RoomsController

这个安装仍然是原始的,需要使用init命令进行初始化。然而,如果我们尝试打开这些应用中的任何一个,我们可以识别出相同的基模板结构,包括assetsconfigcontrollersmodelsruntimeviewsweb子文件夹。因此,一个基本的模板应用可以被认为是高级模板中唯一的独特应用。

最后,在高级模板属性中,每个应用的起始点始终在web/index.php。例如,对于前端应用,起始点是frontend/web/index.php

使用 init 配置应用

除了有多种配置之外,在高级应用中我们还可以有多个入口点。

在高级 Web 应用中,实际上在开发阶段我们也有不同的方法。我们通常有两个环境:开发和生产。在第一个环境中,我们使用假用户、数据等进行测试,而在第二个环境中,我们必须确保项目的正常运行。

因此,我们将根据我们将工作的环境拥有不同的配置文件和参数集。

实际上,我们可能希望使用开发数据库而不是生产数据库来测试应用,或者只在特定环境中可用的特定参数。

事实上,init命令提供了这种能力,可以为不同的环境切换不同的配置和参数。基本上,有两个环境:开发和生产。

备注

需要进行一次初始化以确保项目能够运行。

init命令可以在交互模式和静默模式下启动。

在交互模式下,从yiiadv文件夹开始:

$ php init

在非交互式(静默)模式下:

$ php init --env=Development --overwrite=All

在这两种模式中,如果我们想覆盖所有当前的配置文件,我们只需要指定目标环境。

此命令将简单地复制所选环境(根据所选环境的类型)在相应应用文件夹中的内容,从根目录开始具有相同的名称。

例如,打开environments/dev/backend文件夹。我们将看到两个文件夹:configweb,包含前两个配置文件以及其他文件index.phpindex-test.php。这些文件将从项目根目录开始覆盖backend文件夹中的相应文件。

因此,如果我们使用init参数启动前面的命令,environments/devbackendcommonconsolefrontend文件夹)中的文件夹内容将被复制到项目根目录开始的backendcommonconsolefrontend文件夹中。

此外,使用此命令,还可以完成其他操作,例如使某些文件夹可写或将特定值应用于配置属性。然而,init命令主要用于切换不同的配置和index.php文件。

从项目的任何应用(后端、前端、通用和控制台)开始,配置值和参数是从任何应用的index.php文件顶部(后端、前端、通用或控制台)读取的,读取顺序如下:

  • common/config/main.php

  • common/config/main-local.php

  • config/main.php

  • config/main-local.php

这意味着config参数最初是从common/config/main.php读取的,然后是从common/config/main-local.php读取的,接着是从application config/main.php读取的,最后是从application config/main-local.php读取的。在读取其他配置文件的过程中,同名属性将被覆盖。

因此,如果相同的配置属性在所有四个配置文件中都有声明,其值将与config/main-local.php相同,这是最后被读取的配置文件。

由于我们本地有最后一次机会通过-local版本的文件应用对特定配置属性的差异,因此环境子文件夹的内容将仅关于特定文件的-local版本。例如,如果我们打开environments/dev/backend/config path,我们将看到只有main-local.phpparams-local.php,实际上这是index.php将按顺序读取的最后两个文件名。

因此,如果我们更改environments/dev/backend/config/main-local.php中的数据库连接参数,然后使用dev目标环境应用init,此文件将覆盖backend/config/main-local.php。这是backend/web/index.php在其引导过程中最后读取的配置文件(如果我们浏览/backend/web/index.php)。

现在我们已经在 dev 环境中执行了 init 命令,我们可以将浏览器指向 http://hostname/yiiadv/frontend/web,并且我们应该看到基本模板相同的祝贺页面。

同样,后端入口点也是可用的,指向 http://hostname/yiiadv/backend/web,默认情况下会显示登录表单(这是因为这是一个受限区域)。

注意

如果我们想在项目中添加一个新的应用程序,只需将前端或后端文件夹的内容复制到项目中的另一个新文件夹中即可。

示例 - 创建公共访问的前端

正如我们所看到的,前端应用程序是一个指向 http://hostname/yiiadv/frontend/web 的可访问浏览器。

然而,在前端访问中首先要设置的是 URL 友好性定制;这是因为我们的公共网站在搜索引擎中的良好定位非常重要。

正如我们在基本模板中所做的那样,我们也可以在高级模板中渲染漂亮的 URL,遵循以下两个步骤:

  1. yiiadv/frontend/web 中创建 .htaccess 文件。

  2. yiiadv/frontend/config/main.php 中添加 urlManager 组件。

在步骤 1 中,只需在 yiiadv/frontend/web/.htaccess 中创建一个包含以下内容的文件即可:

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

此代码将使网络服务器 URL 重写工作,将所有请求重写到 yiiadv/frontend/web/index.php 文件。

而,在步骤 2 中,我们必须在 yiiadv/frontend/config/main.php 中添加 urlManager 属性:

         'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
        ],

现在,我们可以刷新网络浏览器到 http://hostname/yiiadv/frontend/web 并导航到顶部的 URL 链接,我们可以看到,例如,该 URL 是漂亮的格式。

我们可以将 frontend 文件夹视为一个 Yii 独立应用程序,并且我们可以创建控制器、视图、模型等。

在应用程序之间共享 ActiveRecord 模型

虽然主 Yii 项目中的每个文件夹都可以被视为一个 Yii 独立应用程序,拥有自己的控制器、模型、视图等,但传统上认为所有共享数据都位于 common 文件夹中。

因此,每个可以用于其他 Yii 应用程序共享模型(如 UserRoomReservationCustomer)都应该插入到 common/models 下的 common\models 命名空间中。

从我的观点来看,当一个应用程序需要使用 common/models 中的 ActiveRecord 时,我更倾向于指向其命名空间中的扩展版本,这样我们就有机会再次为该应用程序的模型添加自定义方法或属性。

例如,考虑我们在 common/models 中的 Room 模型:

<?php
namespace common\models;
class Room extends ActiveRecord
{
….
….
}

在后端应用程序中,我们将从公共命名空间创建 Room 类的空扩展:

<?php
namespace backend\models;
class Room extends \common\models\Room
{
}

这样,我们就有可能根据需要向特定应用程序(命名空间)添加自定义方法或属性。

因此,当后端命名空间中的每个控制器、视图或模型需要引用 Room ActiveRecord 时,都将指向 \backend\models\Room

示例 – 在前端网站上显示可用房间

这个示例将强调在开发阶段基本应用和高级应用之间存在的少数差异。

首先要检查数据库配置是否正确,因为我们刚刚初始化了一个高级应用。

注意

生成服务器上的数据库配置可以在 common/config/main.php 中找到,而开发服务器上的数据库配置位于 common/config/main-local.php,它会覆盖 common/config/main.php 中的配置。

打开 common/config/main.php 并将 db 属性添加到配置数组中:

        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=yii_db',
            'username' => 'my_username',
            'password' => 'my_password',
            'charset' => 'utf8',   
        ],

根据我们的配置参数更改数据库属性(hostusernamepassword)。

注意

记得注释掉 common/config/main-local.php 中的数据库配置,以避免覆盖配置。

这样,我们将能够完全访问之前创建的数据库和表,以及房间数据。

现在,我们准备好创建:

  1. Room 模型。

  2. Rooms 控制器。

  3. Rooms 控制器的索引动作视图。

第一步需要使用 Gii。默认情况下,Gii 在前端应用程序中启用基本配置(仅从本地主机)。

我们将覆盖此配置,以便从任何地方使用 Gii。因此,在前端本地配置(frontend/config/main-local.php)中,以下行:

    $config['bootstrap'][] = 'gii';
    $config['modules']['gii'] = 'yii\gii\Module';

将它们替换为以下内容:

    $config['bootstrap'][] = 'gii';
    $config['modules']['gii'] = [
            'class' => 'yii\gii\Module',
            'allowedIPs' => ['*']
    ];    

现在,我们终于可以从任何地方访问 Gii。使用浏览器,访问 http://hostname/yiiadv/frontend/web/gii;应该会显示一个欢迎页面。

前往 模型生成器 并在第一个字段 表名 中填写 room,这是我们正在创建的模型的名称,就像我们在前面的章节中所做的那样。

由于我们正在使用高级模板,模型文件(如 Gii 创建的其他对象)将创建在 frontend 命名空间中,或者更确切地说,在 frontend/models 中。

因此,有必要更改 模型生成器 的第一个字段 命名空间,从 app/models 切换到 common/models,这是公共数据共享区域:

示例 – 在前端网站上显示可用房间

高级模板中的 Gii 模型生成器

common/models 中,应该有一个包含 Room 表模型的 Room.php 文件。

第二步是创建控制器以及用于显示房间列表的控制器动作。

让我们在 frontend/controllers/RoomsController.php 下创建控制器,内容如下:

<?php
namespace frontend\controllers;

use Yii;
use yii\web\Controller;
use yii\data\ActiveDataProvider;
use common\models\Room;

class RoomsController extends Controller
{
    public function actionIndex()
    {
        $dataProvider = new ActiveDataProvider([
            'query' => Room::find(),
            'pagination' => [
                'pageSize' => 20,
            ],
        ]);

        return $this->render('index', [
            'dataProvider' => $dataProvider,
        ]);
    }
}

确保顶部的命名空间声明是 frontend\controllers,因为每个网络项目中的应用程序都有自己的命名空间(在这种情况下,frontend)。

注意

我们永远不应该直接继承 yii\web\Controller,相反,我们应该为每个应用程序创建一个自定义控制器,例如,frontend\controllers\BaseController,然后从我们将创建的每个控制器中继承它。

最后,第三步是在 frontend/views/rooms/index.php 中创建索引操作的视图内容:

<div class="row">
<?php foreach($dataProvider->getModels() as $model) { ?>
    <div class="col-md-3" style="border:1px solid gray; margin-right:10px; padding:20px;">
        <h2>Room #<?= $model->id ?></h2>
        Floor: <?= $model->floor ?>
        <br />
        Number: <?= $model->room_number; ?>
    </div>
<?php } ?>
</div>

这将产生以下输出,其中包含数据库中的可用数据:

示例 - 在前端网站上显示可用房间

前端房间可用性

在高级模板中自定义 URL

当在同一个项目中处理多个应用程序时,你可能需要从一个应用程序访问另一个应用程序,例如,从后端到前端链接。这是因为我们希望在插入后端数据后在前端显示公共页面渲染。

urlManager 属性通过引用定义它的应用程序进行自定义。然而,我们可以添加特定的属性来引用相应的应用程序。

因此,在 common/config/main.php 中,我们可以添加这两个属性:

        'urlManagerFrontend' => [
            'class' => 'yii\web\urlManager',
            'baseUrl' => '/yiiadv/frontend/web',
            'enablePrettyUrl' => true,
            'showScriptName' => false,
        ],      

        'urlManagerBackend' => [
            'class' => 'yii\web\urlManager',
            'baseUrl' => '/yiiadv/backend/web',
            'enablePrettyUrl' => true,
            'showScriptName' => false,
        ],

例如,我们可以从任何地方获取前端 URL。只需编写此代码 echo Yii::$app->urlManagerFrontend->createUrl(...) 就可以从前端创建一个 URL。

注意

在每个具有 urlManager 配置中 enablePrettyUrl 属性的应用程序的 web 文件夹中放置 .htaccess 文件是必要的。

Yii 还提供了方便的应用程序路径别名,除了基本模板的默认别名之外:

  • @common: 这是公共目录

  • @frontend: 这是前端 Web 应用程序目录

  • @backend: 这是后端 Web 应用程序目录

  • @console: 这是控制台目录

示例 - 在同一域名中使用高级模板

我们已经看到,高级模板在同一个 Web 应用程序中创建了比我们可以通过 /frontend/backend 或任何其他应用程序名称前缀在 URL 中访问到的更多应用程序。然而,对于前端来说,所有 URL 都包含 /frontend 前缀并不建议。

我们希望前端有这种 URL 格式:http://hostname/yiiadv/;而后端有这种格式:http://hostname/yiiadv/admin(我们可以选择我们想要的名称)。

所有请求都必须在 /yiiadv 文件夹级别上进行管理。因此,我们将在 /yiiadv 文件夹中添加一个 .htaccess 文件,它将转发到正确的路由。

这里是一个必须执行的操作列表:

  1. /yiiadv 中配置 .htaccess 以处理所有请求。

  2. 配置后端应用程序以自定义其 baseUrl

  3. 配置前端应用程序以自定义其 baseUrl

显然,步骤 2 和 3 必须为任何其他应用程序重复,对于这些应用程序,我们想要操作基本 URL。

对于步骤 1,让我们在 /yiiadv 文件夹中放置以下内容的 .htaccess 文件:

RewriteEngine on
# For Backend
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} ^/yiiadv/admin
RewriteRule ^admin(/.+)?$ /yiiadv/backend/web/$1 [L,PT]
# For Frontend
RewriteCond %{REQUEST_URI} !index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /yiiadv/frontend/web/$1

因此,在.htaccessBackend块中,我们捕获了/yiiadv/admin的请求并将它们重定向到yiiadv/backend/web/基本 URL。

对于第 2 步,当我们在后端配置中也进行这些更改时,后端请求捕获完成,我们在backend/config/main.php中添加了request属性:

        'request' => [
            // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
            'cookieValidationKey' => '2OofX7Q9e-EQLSK5BEk70_07fUXkka8y',
            'baseUrl' => '/yiiadv/admin',     
        ],

现在,将浏览器指向http://hostname/yiiadv/admin,如果我们一切操作正确,我们最终应该能够看到登录页面。

注意

确保在backend/config/main-local.php配置数组中有一个request属性;我们需要注释掉它,否则它将覆盖我们刚刚更改的backend/config/main.php文件中的request

最后,就像我们对后端请求所做的那样,在第 3 步中,我们需要在配置文件frontend/config/main.php下更改前端请求的request属性:

        'request' => [
            // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
            'cookieValidationKey' => 'ear8GcRjBGXQgKVwfEpbApyj7Fb0UKXk',
            'baseUrl' => '/yiiadv',     
        ],

现在,将浏览器指向http://hostname/yiiadv,如果我们一切操作正确,我们应该看到前端的成功页面。

作为本例的最后一部分,如果我们想将前端指向http://hostname URL,后端指向http://hostname/admin URL,我们必须在文档根目录中放置一个包含以下内容的.htaccess文件:

RewriteEngine on
# For Backend
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} ^/admin
RewriteRule ^admin(/.+)?$ /yiiadv/backend/web/$1 [L,PT]
# For Frontend
RewriteCond %{REQUEST_URI} !index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /yiiadv/frontend/web/$1

然后,我们必须在frontend/config/main.php中更改前端配置的request属性:

        'request' => [
            // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
            'cookieValidationKey' => 'ear8GcRjBGXQgKVwfEpbApyj7Fb0UKXk',
            'baseUrl' => '',     
        ],

最后,更改backend/config/main.php中后端配置的request属性:

        'request' => [
            // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
            'cookieValidationKey' => '2OofX7Q9e-EQLSK5BEk70_07fUXkka8y',
            'baseUrl' => '/admin',     
        ],

这样,现在前端可以通过将浏览器指向http://hostname来访问,后端可以通过将浏览器指向http://hostname/admin来访问。

如何在共享托管中使用高级模板

在我看来,几乎所有的应用程序都应该使用高级模板,因为它从一开始就提供了正确的项目结构,以便立即处理每个 Web 项目中出现的 frontend 和 backend。

然而,我们也看到高级模板需要控制台访问来执行安装和初始化命令。因此,如果我们有一个没有这种能力的远程托管,使用高级模板安装和 Yii 可能会很困难。

如果我们无法将控制台功能添加到远程托管,我们有两种可能性:

  • 在我们可以安装我们想要和需要的东西的本地环境中创建项目;只需在本地安装一个 WAMP 或 LAMP 发行版(基于托管机的操作系统),然后启动 composer 命令安装 Yii

  • 启动init命令以初始化项目(它可以在生产模式下从开始初始化,这样就不需要其他更改)

因此,项目已准备好上传到远程托管。请记住,项目环境处于生产模式,但以这种方式,如果我们想从开发模式切换到生产模式,我们不需要手动更改配置。

摘要

在本章中,我们学习了如何使用 Yii 来构建一个基于前端和后端应用的现代网络项目。我们发现了基本模板和高级模板之间的差异,并安装了我们的第一个基于高级模板的高级项目。

然后,我们使用了 init 命令来自定义开发或生产环境,以便使应用程序运行。然后,我们编写了一个示例,用于在前端房间列表中显示,类似于我们在之前的基本模板中所做的。

最后,我们自定义了 URL,使其在高级模板中看起来也很美观,以便在无需 URL 应用前缀的情况下引用前端和后端。我们还学习了如何在没有控制台访问权限的共享托管环境中使用高级模板。

在下一章中,我们将解释如何编写一个多语言应用程序,适应并渲染不同语言的应用程序,而无需更改源代码。

第十章。本地化应用

本章解释了如何编写多语言应用。本地化,也称为国际化(I18N),负责确保软件应用可以在不同的语言中适应和渲染,而无需更改源代码。这在用户说不同语言的 Web 应用中尤为重要。

Yii 提供了强大的工具来处理这项任务,可以选择文件或数据库方法(根据应用的复杂性)。我们将涵盖以下主题:

  • 设置默认语言

  • 基于文件的翻译

    • 示例 - 使用基于文件的翻译为整个网站
  • 占位符格式化

  • 基于数据库的翻译

    • 示例 - 使用数据库翻译房间描述

设置默认语言

Yii 应用使用两种语言:源语言和目标语言。

源语言指定编写源代码所使用的语言;默认设置是 en-US,建议不要更改此值,因为英语是软件开发中最常用和最知名的语言。另一方面,目标语言用于向最终用户显示内容,我们将专门讨论这个方面。

可以使用配置文件中的 language 属性设置此语言:

return [
    // set target language to be Italian
    'language' => 'it-IT',

      ....
      ....
];

或者,你可以使用以下代码:

// change target language to Italian
\Yii::$app->language = 'it-IT';

现在,让我们看看如何在实践中处理应用本地化。

基于文件的翻译

这是将文本消息从一种语言翻译到另一种语言的最简单方法。基本上,每个语言都有一个或多个包含关键词文本表示的文件;我们将把这些关键词放入源代码中,框架将用文本替换它们。

关键词-文本翻译的成对组合按类别分组,这些类别代表存储它们的文件名。这些成对组合是数组键值对,其中键表示关键词,值表示文本翻译。

默认情况下,包含特定语言翻译的路径文件夹位于 @app/messages/<language>/<category>.php。因此,如果我们正在为 app 类别和 en-US 语言编写翻译,例如,翻译文件的完整路径将在 @app/messages/en-US/app.php

在源代码中,使用接受四个参数的 Yii::t() 静态方法激活翻译,但只需要前两个参数;第一个是类别,第二个是要翻译的消息。

现在,我们想要举一个例子,在这个例子中我们将用两种语言:英语和意大利语来编写一个经典的 Hello World!。然而,将其翻译成任何其他语言也同样简单。

在之前的基本模板项目上,在 basic/controllers/FileTranslatorController.php 中编写一个新的控制器 FileTranslatorController,内容如下:

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;

class FileTranslatorController extends Controller
{
    public function actionIndex()
    {
        \Yii::$app->language = 'en-US';
        $englishText = \Yii::t('app', 'Hello World!');

        \Yii::$app->language = 'it-IT';
        $italianText = \Yii::t('app', 'Hello World!');

        return $this->render('index', ['englishText' => $englishText, 'italianText' => $italianText]);
    }
}

actionIndex()中的前两行源代码将设置应用程序语言为en-US,然后它们将basic/messages/en-US/app.php文件中Hello World!键的内容存储在$englishText变量中。

同样,actionIndex()中的最后两行源代码将设置应用程序语言为it-IT,然后它们将basic/messages/it-IT/app.php文件中Hello World!键的内容存储在$italianText变量中。

basic/views/file-translator/index.php中的视图内容如下所示:

<b>Display Hello World! in two language: English and Italian</b>

<br /><br />

In English:
<?= $englishText ?>

<br /><br />
In Italian:
<?= $italianText ?>

现在,我们需要定义英语和意大利语翻译的文件语言。

如果basic/messages中不存在messages文件夹,我们将创建它;然后,创建两个新的文件夹,分别命名为en-USit-IT。在每个文件夹中,添加一个名为app.php的新文件。

对于包含英文翻译的文件basic/messages/en-US/app.php,让我们写下:

<?php

return [
    'Hello World!' => 'Hello world!',
];

?>

而对于basic/messages/it-IT/app.php中的意大利语翻译,让我们写下:

<?php

return [
    'Hello World!' => 'Ciao Mondo!',
];

?>

您可以浏览到http://hostname/basic/file-translator/index来查看输出。

示例 - 使用基于文件的翻译为整个网站

将翻译应用到整个网站是繁琐的,而且最重要的是,你可能会错过一些翻译。Yii 提供了一个强大的工具,可以自动为所有我们想要的语种生成消息的 PHP 文件。

注意

这个强大的工具是一个名为message的控制台命令;因此,我们需要控制台访问权限。

此命令需要两个步骤:

  1. 创建一个配置文件,我们将在这里指定languages属性,即我们想要在项目中支持的语言,以及messagePath属性,或者说,翻译消息的存储位置。

  2. 启动message命令。

对于第一步,前往控制台,在项目的根目录下,即yii文件所在的位置。

如果我们正在处理基本模板,我们将启动以下命令:

$ ./yii message/config config/i18n.php

第一个参数message/config是在message控制器上调用的config动作,第二个参数是我们想要保存配置的文件路径(在这种情况下,config/i18n.php,但我们可以写任何内容)。

如果我们正在处理高级模板,我们将启动以下命令:

./yii message/config common/config/i18n.php

唯一的区别是,在最后一个命令中,我们指定了消息命令翻译的配置文件位于common/config而不是config文件夹中。

现在,如果我们打开config/i18n.php,我们应该看到message命令的默认配置文件,其外观应如下所示:

<?php

return [
    // string, required, root directory of all source files
    'sourcePath' => __DIR__ . DIRECTORY_SEPARATOR . '..',
    // array, required, list of language codes that the extracted messages
    // should be translated to. For example, ['zh-CN', 'de'].
    'languages' => ['de'],
    // string, the name of the function for translating messages.
    // Defaults to 'Yii::t'. This is used as a mark to find the messages to be
    // translated. You may use a string for single function name or an array for
    // multiple function names.
    'translator' => 'Yii::t',
    // boolean, whether to sort messages by keys when merging new messages
    // with the existing ones. Defaults to false, which means the new (untranslated)
    // messages will be separated from the old (translated) ones.
    'sort' => false,
    // boolean, whether to remove messages that no longer appear in the source code.
    // Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks.
    'removeUnused' => false,
    // array, list of patterns that specify which files/directories should NOT be processed.
    // If empty or not set, all files/directories will be processed.
    // A path matches a pattern if it contains the pattern string at its end. For example,
    // '/a/b' will match all files and directories ending with '/a/b';
    // the '*.svn' will match all files and directories whose name ends with '.svn'.
    // and the '.svn' will match all files and directories named exactly '.svn'.
    // Note, the '/' characters in a pattern matches both '/' and '\'.
    // See helpers/FileHelper::findFiles() description for more details on pattern matching rules.
    'only' => ['*.php'],
    // array, list of patterns that specify which files (not directories) should be processed.
    // If empty or not set, all files will be processed.
    // Please refer to "except" for details about the patterns.
    // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
    'except' => [
        '.svn',
        '.git',
        '.gitignore',
        '.gitkeep',
        '.hgignore',
        '.hgkeep',
        '/messages',
    ],

    // 'php' output format is for saving messages to php files.
    'format' => 'php',
    // Root directory containing message translations.
    'messagePath' => __DIR__,
    // boolean, whether the message file should be overwritten with the merged messages
    'overwrite' => true,

    /*
    // 'db' output format is for saving messages to database.
    'format' => 'db',
    // Connection component to use. Optional.
    'db' => 'db',
    // Custom source message table. Optional.
    // 'sourceMessageTable' => '{{%source_message}}',
    // Custom name for translation message table. Optional.
    // 'messageTable' => '{{%message}}',
    */

    /*
    // 'po' output format is for saving messages to gettext po files.
    'format' => 'po',
    // Root directory containing message translations.
    'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
    // Name of the file that will be used for translations.
    'catalog' => 'messages',
    // boolean, whether the message file should be overwritten with the merged messages
    'overwrite' => true,
    */
];

配置非常易于阅读,因此我们只需解释其主要属性:languagesmessagePathexcept

languages属性定义了在 Web 项目中支持哪些语言。例如,我们可以写:

'languages' => ['en', 'it', 'fr'],

前面的命令支持并自动生成英语、意大利语和法语的消息。

messagePath 属性定义了自动生成的消息应该保存的位置。建议指向 messages 文件夹(如果不存在,必须创建);这样我们可以在基本模板中写入以下内容:

'messagePath' =>  __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'messages',

在这里,__DIR__ 指的是 config 文件夹,而在基本模板中,它是 basic/config 文件夹。

一旦我们启动了 message 命令,它将查找所有包含 .php 文件的文件夹和子文件夹,如 only 属性中所示(只有 .php 文件将被处理)。

因此,在项目的根文件夹中,有一些文件夹,如 vendor,与我们无关。

因此,我们将 /vendor 值添加到 except 属性中,以指示 message 命令不会查看此文件夹,这样:

    'except' => [
        '.svn',
        '.git',
        '.gitignore',
        '.gitkeep',
        '.hgignore',
        '.hgkeep',
        '/messages',
        '/vendor'
    ],

对于步骤 2,我们现在将尝试启动以下命令:

$ ./yii message config/i18n.php

它将在 sourcePath 属性指定的文件夹和子文件夹中的所有文件中找到 Yii::t 标记,考虑 except 属性以排除我们不想查找的文件和文件夹。

翻译后的消息将(如果不存在)创建在 messagePath 文件夹中,在我们的例子中,是从项目的根文件夹开始的 messages 文件夹。

如果所有搜索的文件中都没有 Yii::t 标记,则相对语言子文件夹将为空。

例如,打开 basic/controller/SiteController.php 中的 SiteController 并按以下方式更改 actionIndex 内容:

    public function actionIndex()
    {
        $message = \Yii::t('app', 'this message must be translated!');

        return $this->render('index');
    }

现在,重新启动 message 命令:

$ ./yii message config/i18n.php

然后,检查 basic/messages/en 文件夹。我们将找到一个包含 this message must be translated 键的 app.php 文件,我们必须填写值以指定翻译。

占位符格式

Yii:t 方法不仅限于用其他语言的翻译替换字符串,它还处理源字符串的特定格式化以支持许多类型的泛化。

首先,Yii:t() 支持以下两种格式的占位符:

  • {nameOfPlaceholder} 格式的字符串

  • 整数在 {0} 格式,这种类型的占位符是从零开始的

要替换占位符的值数组作为 Yii:t() 方法的第三个参数传递。

例如,我们想要通过添加自定义名字到文本来显示一个只包含 Hello World, I'm ... 的页面。

创建 basic/controllers/FileTranslatorController.php

    public function actionHelloWorldWithName($name='')
    {
        $text = \Yii::t('app', 'Hello World! I\'m {name}', ['name' => $name]);

        return $this->render('helloWorldWithName', ['text' => $text]);        
    }

现在,在 basic/views/file-translator/helloWorldWithName.php 中创建视图,只需使用以下命令:

<?= $text ?>

它将显示从控制器传递的 $text 值。

通过将浏览器指向 http://hostname/basic/web/file-translator/hello-world-with-name 并传递 ?name= 参数来测试它,否则文本末尾将没有名字。

可以使用我们刚刚看到的 message 命令来准备翻译:

$ ./yii message config/i18n.php

这将自动在 basic/messages 子文件夹中创建一个新的标记 Hello World! I\'m {name}

可以使用两个其他属性ParameterTypeParameterStyle来专门化占位符,在PlaceholderName后添加逗号。因此,指定占位符的完整形式如下:

{PlaceholderName, ParameterType, ParameterStyle}

在这里,ParameterType可以是:

  • number:参数样式可以是整数、货币、百分比或自定义模式(例如,000)

  • date:参数样式可以是短格式、中格式、长格式、完整格式或自定义模式(例如,dd/mm/yyyy)

  • time:参数样式可以是短格式、中格式、长格式、完整格式或自定义模式(例如,hh:mm)

  • spellout:没有参数样式

  • ordinal:没有参数样式

  • duration:没有参数样式

最常用的消息格式可能是plural,这允许我们根据传递的参数数量指定不同的键字符串。

以下代码作为示例:

// if $n = 0, it shows "There are no books!"
// if $n = 1, it shows "There is one book!"
// if $n = 4, it shows "There are 4 books!"

echo \Yii::t('app', 'There {n, plural, =0{are no books} =1{is one book} other{are # books}}!', ['n' => $n]);

在这里,=0代表当$n0时显示的消息,=1代表当$n1时显示的消息,而other代表当$n不是01时显示的消息。

基于数据库的翻译

Yii 还支持将数据库作为消息翻译的存储选项。

如果我们在基本模板中工作,必须在config/web.php文件中显式配置,如果我们在高级模板中工作,则必须在common/config/main.php中配置。

接下来,我们需要添加两个额外的数据库表来管理消息源和消息翻译。

首先根据 Yii 官方文档中的建议创建数据库表,文档地址为www.yiiframework.com/doc-2.0/yii-i18n-dbmessagesource.html

CREATE TABLE source_message (
    id INTEGER PRIMARY KEY AUTO_INCREMENT,
    category VARCHAR(32),
    message TEXT
);

CREATE TABLE message (
    id INTEGER,
    language VARCHAR(16),
    translation TEXT,
    PRIMARY KEY (id, language),
    CONSTRAINT fk_message_source_message FOREIGN KEY (id)
        REFERENCES source_message (id) ON DELETE CASCADE ON UPDATE RESTRICT
);

注意

可以在配置文件中自定义表名。

source_message表将存储所有用源语言编写的消息;message表将存储所有翻译;这两个表通过id字段连接在一起。

在下一个示例中,让我们为每个表插入一条记录:

INSERT INTO `source_message` (`id`, `category`, `message`) VALUES
(1, 'app', 'Hello World from Database!');

INSERT INTO `message` (`id`, `language`, `translation`) VALUES
(1, 'it', 'Ciao Mondo dal Database!');

现在,是时候对配置进行一些更改了。我们需要在config/web.php配置文件的components部分插入i18n属性(基于基本模板):

'components' => [
    // ...
    'i18n' => [
        'translations' => [
            'app' => [
                    'class' => 'yii\i18n\DbMessageSource',
                    //'messageTable' => 'message,
                    //'sourceMessageTable' => 'source_message,

            ],
        ],
    ],
],

此组件,i18n,默认使用yii\i18n\PhpMessageSource类,并已用于基于文件的翻译。

现在,我们想显示意大利语的消息。在basic/controllers/FileTranslatorController.php中创建一个新的操作actionHelloWorldFromDatabase(),内容如下:

     public function actionHelloWorldFromDatabase()
    {
        \Yii::$app->language = 'it';
        $text = \Yii::t('app', 'Hello World from Database!');

        return $this->render('helloWorldFromDatabase', ['text' => $text]);        
    }

basic/views/file-translator/helloWorldFromDatabase视图将显示$text内容:

<?= $text ?>

通过将浏览器指向http://hostname/basic/web/file-translator/hello-world-from-database来测试它。如果一切正常,我们应该看到Ciao Mondo dal Database!,这是Hello World from Database!的意大利语版本。

示例 - 使用数据库翻译房间描述

本例将向您展示如何使用数据库作为存储选项来翻译房间的描述。我们将为messagesource_message数据库表创建模型,因为我们打算使用 ActiveRecord 来管理所有控制翻译的表的记录。

首先,我们将使用 Gii 创建messagesource_message数据库表的模型。在基本模板中,将浏览器指向http://hostname/basic/web/gii,然后转到模型生成器。Gii 将在basic/models文件夹中创建MessageSourceMessage模型。

接下来,我们想要创建一个包含原始语言和所有其他翻译的描述的表单。

为此目的,我们将在basic/views/rooms/indexWithTranslatedDescriptions.php中创建一个视图,如下所示:

<?php
use yii\helpers\Url;
use yii\widgets\ActiveForm;
?>

<div class="row">
    <div class="col-md-4">
        <legend>Rooms with translated descriptions</legend>

        <?php $form = ActiveForm::begin([]); ?>
        <table class="table">
            <tr>
                <th>#</th>
                <th>Floor</th>
                <th>Room number</th>
                <th>Description - English</th>
                <th>Description - Italian</th>
                <th>Description - French</th>
            </tr>
            <?php for($k=0;$k<count($rooms);$k++) : ?>
                <?php $room = $rooms[$k]; ?>
                <input type="hidden" name="Room[<?= $k ?>][id]" value="<?= $room->id ?>" />
                <tr>
                    <td><?php echo $k+1 ?></td>
                    <td><?php echo $room->floor ?></td>
                    <td><?php echo $room->room_number ?></td>
                    <td><input type="text" name="Room[<?= $k ?>][description][en]" value="<?= $room->description ?>" /></td>
                    <td><input type="text" name="Room[<?= $k ?>][description][it]" value="<?= Yii::$app->i18n->translate('app', $room->description, [], 'it') ?>" /></td>
                    <td><input type="text" name="Room[<?= $k ?>][description][fr]" value="<?= Yii::$app->i18n->translate('app', $room->description, [], 'fr') ?>" /></td>
                </tr>
            <?php endfor; ?>
        </table>
        <br />
        <input type="submit" class="btn btn-primary" value="Submit descriptions" />
        <?php ActiveForm::end(); ?>
    </div>
</div>

我们将使用Yii::$app->i18n->translate方法检查其他语言的翻译,该方法接受:

  • 分类

  • 待翻译的消息

  • 消息参数

  • 语言

现在是时候在basic/controllers/RoomsController.php中添加actionIndexWithTranslatedDescriptions()了:

    public function actionIndexWithTranslatedDescriptions()
    {
        if(isset($_POST['Room']))
        {
            $roomsInput = $_POST['Room'];
            foreach($roomsInput as $item)
            {
                $sourceMessage = \app\models\SourceMessage::findOne(['message' => $item['description']]);

                // If null, I need to create source message
                if($sourceMessage == null)
                {
                    $sourceMessage = new \app\models\SourceMessage();
                }
                $sourceMessage->category = 'app';
                $sourceMessage->message = $item['description']['en'];
                $sourceMessage->save();

                $otherLanguages = ['it', 'fr'];

                foreach($otherLanguages as $otherLang)
                {
                    $message = \app\models\Message::findOne(['id' => $sourceMessage->id, 'language' => $otherLang]);
                    if($message == null)
                    {
                        $message = new \app\models\Message();
                    }
                    $message->id = $sourceMessage->id;
                    $message->language = $otherLang;
                    $message->translation = $item['description'][$otherLang];
                    $message->save();
                }

                // Room to update
                $roomToUpdate = \app\models\Room::findOne($item['id']);
                $roomToUpdate->description = $item['description']['en'];
                $roomToUpdate->save();
            }
        }

        $rooms = Room::find()
        ->all();

        return $this->render('indexWithTranslatedDescriptions', ['rooms' => $rooms]);
    }

注意

如果我们无法访问 URL,请检查此控制器behaviors()方法返回的access属性,以确保此操作被允许。

在此代码之上,我们将检查$_POST数组是否已填充;在这种情况下,我们将从视图中传递的描述中获取$sourceMessage对象。接下来,我们可以为任何我们想要的语种创建或更新消息模型。最后,我们还将保存房间对象,最终更改其描述字段。

使用此解决方案,每次我们想要更改描述时,由于文本已更改,都会创建一个新的记录。

摘要

在本章中,我们看到了如何在我们的应用程序中配置多种语言。我们发现处理国际化有两种存储选项:文件和数据库。对于小型项目建议使用文件,对于大型项目建议使用数据库。

我们已经发现如何通过控制台中的'message'命令从整个网站中抓取占位符,以及如何创建包含格式化信息的占位符。

最后,我们已经将数据库配置为翻译的存储目标,并创建了一个完整的示例来处理不同语言的房间描述。

在下一章中,我们将学习如何使用 Yii 2 的新集成管理创建 RESTful 网络服务。

第十一章:创建用于移动应用程序的 API

在本章中,你将学习如何使用 Yii 2 的新集成管理创建 RESTful Web 服务。

你将学习如何创建一个新的应用程序来管理api环境,以及如何使用框架提供的默认基类创建控制器。

然后,我们将介绍认证方法,并教你如何自定义响应输出格式。我们还将讨论:

  • 在高级模板中配置 REST 应用程序

  • 创建控制器:

    • 例如:创建一个控制器来管理房间
  • 认证:

    • 例如:使用认证来获取客户列表
  • 新的控制器操作:

    • 例如:获取预订的房间列表
  • 自定义认证和响应

    • 例如:接收数据中的状态响应节点
  • 其他形式的导出 – RSS:

    • 例如:创建包含可用房间列表的 RSS

在高级模板中配置 REST 应用程序

在使用高级模板之前,建议配置 RESTful Web 服务,因为,正如你在前面的章节中看到的,这种配置允许你轻松地在同一项目中添加新的应用程序。

Yii 提供了许多内置功能来创建 RESTful Web 服务,它减少了实现所需的代码,并且总是以模型、控制器和动作的结构化方式实现。

它的主要功能包括:

  • yii\rest\ActiveController中的默认操作(indexviewcreateupdatedeleteoptions),这是建议覆盖的基本控制器

  • 可从输入中选择响应格式

  • 自定义认证和授权

  • 缓存和速率限制

Yii 在创建 RESTful Web 服务方面应用了成熟的知识,例如如何在响应输出中呈现元数据。因此,我们尽可能遵循框架指南是明智的;这样,我们将编写易于管理的 REST API。

使用高级模板的第一件事是在同一项目中创建一个新的应用程序,例如将其重命名为api。Yii 没有内置创建新应用程序的功能,但只需几个步骤就可以完成这项任务。

从我们项目的根目录开始,我们将创建,以及其他应用程序(commonbackendfrontendconsole),一个名为api的新文件夹,以下命令:

$ mkdir api

现在,进入api并创建这五个子文件夹:

$ mkdir config
$ mkdir web
$ mkdir controllers
$ mkdir runtime

我们必须只为前两个文件夹创建文件,其他文件夹暂时保持为空。

注意

另一个可能的解决方案是从其他应用程序(如frontendbackend)复制完整内容到新的应用程序目标文件夹,然后清除无用的内容。

config文件夹中,我们必须创建两个文件:main.phpparams.php。第二个文件params.php将暂时为空,因为我们还没有任何参数要存储在其中,例如:

<?php
return [
];

api/config/main.php的内容将变为:

<?php
$params = array_merge(
    require(__DIR__ . '/../../common/config/params.php'),
    require(__DIR__ . '/../../common/config/params-local.php'),
    require(__DIR__ . '/params.php')
);

return [
    'id' => 'app-api',
    'basePath' => dirname(__DIR__),
    'controllerNamespace' => 'api\controllers',
    'bootstrap' => ['log'],
    'modules' => [],

    'components' => [

        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
        ],        

        'user' => [
            'identityClass' => '\common\models\User',
            'enableSession' => false,
            'loginUrl' => null
        ],

        'log' => [
            'traceLevel' => YII_DEBUG ? 3 : 0,
            'targets' => [
                [
                    'class' => 'yii\log\FileTarget',
                    'levels' => ['error', 'warning'],
                ],
            ],
        ],

    ],
    'params' => $params,
];

然后,我们在 web 文件夹中创建一个 index.php 文件,内容如下:

<?php
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');

require(__DIR__ . '/../../vendor/autoload.php');
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
require(__DIR__ . '/../../common/config/bootstrap.php');

$config = yii\helpers\ArrayHelper::merge(
    require(__DIR__ . '/../../common/config/main.php'),
    require(__DIR__ . '/../../common/config/main-local.php'),
    require(__DIR__ . '/../config/main.php')
);

$application = new yii\web\Application($config);
$application->run();

仍然在 web 文件夹中,我们将创建 .htaccess 文件来处理漂亮的 URL:

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

最后,我们必须在 common/config/bootstrap 中添加一个关于 api 应用程序的新别名:

Yii::setAlias('api', dirname(dirname(__DIR__)) . '/api');

我们的工作完成了,因为我们最终从头开始创建了一个全新的应用程序。

注意

请确保将 runtime 文件夹设置为可写,因为框架将在此处写入运行时数据,例如日志文件。

创建控制器

当我们创建新的 RESTful 网络服务控制器时,Yii 提供了两个基类:\yii\rest\Controller\yii\rest\ActiveController,我们可以扩展它们。

这两个类都包含以下有用的公共特性,按执行顺序排列:

  1. 根据请求所需输出的响应(内容协商器)。

  2. HTTP 方法验证。

  3. 认证。

  4. 速率限制。

第二类 \yii\rest\ActiveController 通过 ActiveRecord 添加更多功能,例如处理用户授权和一组已存在的操作:indexviewcreateupdatedeleteoptions

我们将看到 Yii 通过正文和 HTTP 头提供了所有必要的详细信息来获取响应状态和内容。

让我们创建一个控制器来扩展 \yii\rest\Controller 或者更确切地说,不使用 ActiveRecord。在 api/controllers/TestRestController.php 中创建一个新的控制器:

<?php
namespace api\controllers;

use yii\rest\Controller;

class TestRestController extends Controller
{
    private function dataList()
    {
        return [
            [ 'id' => 1, 'name' => 'Albert', 'surname' => 'Einstein' ],
            [ 'id' => 2, 'name' => 'Enzo', 'surname' => 'Ferrari' ],
            [ 'id' => 4, 'name' => 'Mario', 'surname' => 'Bros' ]
        ];
    }

    public function actionIndex()
    {
            return $this->dataList();
    }
}

在前面的代码中,我们有一个 dataList 方法,它返回一个对象数组,还有一个 actionIndex 方法,它为 TestRestController 提供 index 动作并返回该列表。

注意

许多示例可以使用网络浏览器执行(通过使用 GET 动词请求)。然而,通常我们需要一个特定的工具来测试 RESTful 网络服务,例如 Postman,它是 Chrome 浏览器的一个优秀扩展,或者对于高级用户来说,可以使用 curl 命令。

\yii\rest\Controller 的第一个特性是根据请求动态安排响应输出格式,这被称为 内容协商

的确,我们可以尝试在我们的浏览器中通过 http://hostname/yiiadv/api/web/test-rest/index 启动此请求,或者通过使用 GET 动词和将 Accept HTTP 头设置为 application/xml 的特定工具,或者使用 curl,如下所示:

$ curl -H "Accept: application/xml" http://hostname/yiiadv/api/web/test-rest/index
<?xml version="1.0" encoding="UTF-8"?>
<response><item><id>1</id><name>Albert</name><surname>Einstein</surname></item><item><id>2</id><name>Enzo</name><surname>Ferrari</surname></item><item><id>4</id><name>Mario</name><surname>Bros</surname></item></response>

在这些情况下,我们将根据 XML 数据获得响应:

创建控制器

XML 数据响应到 test-rest/index

然而,如果我们将 Accept 头更改为 application/json,我们将根据 JSON 数据获得响应:

$ curl -H "Accept: application/json" http://hostname/yiiadv/api/web/test-rest/index

[{"id":1,"name":"Albert","surname":"Einstein"},{"id":2,"name":"Enzo","surname":"Ferrari"},{"id":4,"name":"Mario","surname":"Bros"}]

在这些情况下,我们将根据 JSON 数据获得响应:

创建控制器

JSON 数据响应到 test-rest/index

根据客户端发送的 Accept 头,相同的数据将以不同的方式呈现。

第二个特性,HTTP 方法验证,允许您指定资源可用的动词。动词在behaviors()方法中定义,必须扩展以修改此设置:

    public function behaviors()
    {
        $behaviors = parent::behaviors();
        $behaviors['verbs'] = [
                'class' => \yii\filters\VerbFilter::className(),
                'actions' => [
                    'index'  => ['get'],
                ],
        ];
        return $behaviors;
    }

在这种情况下,我们只将 GET 动词设置为index操作,因为behaviors['verbs']actions属性的键是操作,值是一个包含支持的 HTTP 方法的数组。

如果我们使用 GET 动词(作为浏览器请求)启动http://hostname/yiiadv/api/web/test-rest/index,我们将继续显示结果。然而,如果我们将 HTTP 方法更改为 POST 动词,例如,我们将得到异常错误:

创建控制器

使用错误的动词引发的异常错误

这是因为只有 GET 动词支持index操作。

在下一节中,我们将解释第三个和第四个特性,即身份验证和速率限制。

示例 - 创建一个用于管理房间的控制器

在这个例子中,我们将应用上一章中处理的概念,在这种情况下,使用\yii\rest\ActiveController作为基类而不是\yii\rest\Controller,因为我们打算使用 ActiveRecord 类来操作数据。

api/controllers/RoomsController.php中创建一个新的控制器:

<?php
namespace api\controllers;

use yii\rest\ActiveController;

class RoomsController extends ActiveController
{
    public $modelClass = 'common\models\Room';
}

此控制器隐式包含以下操作:

  • actionIndex返回模型列表,只能通过 GET 和 HEAD HTTP 方法访问

  • actionView返回关于模型的详细信息,通过传递id参数,只能通过 GET 和 HEAD HTTP 方法访问

  • actionCreate用于创建新模型,只能通过 POST HTTP 方法访问

  • actionUpdate用于更新现有模型,只能通过 PUT 和 PATCH HTTP 方法访问

  • actionDelete用于删除现有模型,只能通过 DELETE HTTP 方法访问

  • 返回允许的 HTTP 方法的actionOptions

现在,让我们尝试启动所有这些方法。

使用 GET 方法在http://hostname/yiiadv/api/web/rooms上启动actionIndex

[
{
    "id": 1,
    "floor": 1,
    "room_number": 101,
    "has_conditioner": 1,
    "has_tv": 0,
    "has_phone": 1,
    "available_from": "2015-05-20",
    "price_per_day": "120.00",
    "description": "description 1"

},

    {
        "id": 2,
        "floor": 2,
        "room_number": 202,
        "has_conditioner": 0,
        "has_tv": 1,
        "has_phone": 1,
        "available_from": "2015-05-30",
        "price_per_day": "118.00",
        "description": "description 2"
    }
]

我们将以 JSON 对象的数组形式以及 HTTP 头部、成功状态码和分页详情获取数据库中的所有记录:

X-Pagination-Current-Page: 1
X-Pagination-Page-Count: 1
X-Pagination-Per-Page: 20
X-Pagination-Total-Count: 2

如果我们使用 HEAD HTTP 方法启动相同的 URL,我们只会得到没有主体的 HTTP 头部响应,因此我们只会得到分页信息。

最后,如果我们使用不支持 HTTP 方法(例如 PUT 方法)启动相同的 URL,我们将得到两个重要的 HTTP 头部:

  • 状态码头设置为405 方法不允许

  • Allow头设置为GET, HEAD

状态码头表明不支持该方法,而Allow头返回对该操作支持的 HTTP 方法列表。

现在,使用 GET 方法在http://hostname/yiiadv/api/web/rooms/view?id=1上启动actionView

{
  "id": 1,
  "floor": 1,
  "room_number": 101,
  "has_conditioner": 1,
  "has_tv": 0,
  "has_phone": 1,
  "available_from": "2015-05-20",
  "price_per_day": "120.00",
  "description": "description 1"
}

如果我们尝试使用 GET 方法启动一个不存在的 ID,例如http://hostname/yiiadv/api/web/rooms/view?id=100,我们将得到以下响应体:

{
  "name": "Not Found",
  "message": "Object not found: 100",
  "code": 0,
  "status": 404,
  "type": "yii\\\\web\\\\NotFoundHttpException"
}

HTTP 状态码 标头将被设置为 404 Not Found 以指定请求的项目(id=100)不存在。仅使用 HEAD HTTP 方法,我们将从设置为 404 的 HTTP 状态码 获取信息。CreateUpdate 动作要求客户端发送要创建或更新的对象的正文内容。

默认情况下,Yii 只识别 application/x-www-form-urlencodedmultipart/form-data 输入格式。为了启用 JSON 输入格式,我们需要在 api/config/main.php 文件中配置请求应用程序组件的 parsers 属性:

'request' => [
    'parsers' => [
        'application/json' => 'yii\web\JsonParser',
    ]
]

在配置 JSON 输入解析器后,我们可以使用 POST HTTP 方法调用 http://hostname/yiiadv/api/web/rooms/create 来创建一个新的房间,并传递例如以下 JSON:

    {
        "floor": 99,
        "room_number": 999,
        "has_conditioner": 1,
        "has_tv": 1,
        "has_phone": 1,
        "available_from": "2015-12-30",
        "price_per_day": "48.00",
        "description": "description room 999"
    }

如果没有发生错误,我们将得到:

201 Created as HTTP Header Status Code
Object just created as body content

如果缺少一些必需的字段并且存在验证错误,我们将得到:

422 Data Validation Failed
An array of field-message to indicate which validation errors occurred

对于更新操作,也需要做同样的事情,在这种情况下,然而,我们将调用 http://hostname/yiiadv/api/web/rooms/update 并使用 PUT 或 PATCH HTTP 方法传递 id URL 参数。在这种情况下,只有 HTTP 标头状态码 200 OK 才是成功的响应,并且更新对象将作为正文内容返回。

最后,通过调用 http://hostname/yiiadv/api/web/rooms/delete 并传递 id URL 参数,使用 DELETE HTTP 方法来使用 actionDelete。成功的执行将返回 204 No Content 作为 HTTP 状态码;否则,它将是 404 Not Found

认证

有三种认证方式:

  • HTTP 基本认证 (HttpBasicAuth 类): 此方法使用 WWW-Authenticate HTTP 标头为每个请求发送用户名和密码

  • 查询参数 (QueryParamAuth 类): 此方法使用作为 API URL 查询参数传递的访问令牌

  • OAuth 2 (HttpBearerAuth 类): 此方法使用由消费者从授权服务器获取的访问令牌,并通过 HTTP 承载令牌发送到 API 服务器

Yii 支持所有提到的方法,但我们也可以轻松地创建一个新的方法。

要启用认证,请按照以下步骤操作:

  1. 在配置中配置用户应用程序组件,将 enableSession 设置为 false 以确保用户认证状态在请求之间不持久化使用会话。接下来,将 loginUrl 设置为 null 以显示 HTTP 403 错误而不是将其重定向到登录页面。

  2. 指定我们想要使用的认证方法,在 API 控制器类中配置 authenticator 行为。

  3. 在用户身份类中实现 yii\web\IdentityInterface::findIdentityByAccessToken()

    注意

    第一步确保 REST 请求确实是无状态的,但如果您需要持久化或存储会话数据,则可以跳过此步骤。

第一步可以在 api/config/main.php 中配置:

    'components' => [
            ...
        'user' => [
            'identityClass' => 'common\models\User',
            'enableSession' => false,
            'loginUrl' => null
        ],
];

第二步要求我们扩展 behaviors() 控制器方法,指定一个单独的认证器:

public function behaviors()
{
    $behaviors = parent::behaviors();
    $behaviors['authenticator'] = [
        'class' => yii\filters\auth\HttpBasicAuth::className(),
    ];
    return $behaviors;
}

或者我们可以通过指定多个认证器来完成这个操作:

public function behaviors()
{
    $behaviors = parent::behaviors();
    $behaviors['authenticator'] = [
        'class' => yii\filters\auth\CompositeAuth::className(),
        'authMethods' => [
            yii\filters\auth\HttpBasicAuth::className(),
            yii\filters\auth\HttpBearerAuth::className(),
            yii\filters\auth\QueryParamAuth::className(),
        ],
    ];
    return $behaviors;
}

最后,步骤 3 需要实现配置文件中指定的 identityClassfindIdentityByAccessToken()

在一个简单场景中,访问令牌可以存储在 User 表的列中,然后检索:

    public static function findIdentityByAccessToken($token, $type = null)
    {
        return static::findOne(['access_token' => $token]);
    }

在配置的末尾,每个请求都会尝试在相同控制器的 beforeAction() 方法中认证用户。

现在,让我们看看第一种认证方法,HTTPBasicAuth。此方法要求我们将 auth 属性设置为可调用的 PHP 函数;如果没有设置,则将使用用户名作为传递给 \yii\web\User::loginByAccessToken() 方法的访问令牌。

HttpBasicAuth 认证的基本实现是:

public function behaviors()
{
    $behaviors = parent::behaviors();
    $behaviors['authenticator'] = [
            'class' => yii\filters\auth\HttpBasicAuth::className(),
           'auth' => function($username, $password) {
            // return null or identity interface
    // For example search by username and password
    return \common\models\User::findOne(['username' => $username, 'password' => $password);
           }

           /*
           'auth' => [$this, 'httpBasicAuthHandler'],
           */
    ];
    return $behaviors;
}

public function httpBasicAuthHandler($username, $password)
{
    // For example search by username and password
    return \common\models\User::findOne(['username' => $username, 'password' => $password]);
}

存储在 auth 属性中的可调用 PHP 函数可以表示为一个内联函数,或者作为一个数组,其中第一个值是对象,第二个是要调用的函数名,通过传递 $username$password 参数。

检查 PHP 通过 phpinfo() 的运行情况。如果您显示 CGI/FCGI,那么您需要在 .htaccess 中添加 SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0 以使用 HTTP Auth 从 PHP。

第二种认证方法是查询参数,通过使用 QueryParamAuth 类。使用此方法,必须将名为 access-token 的查询参数传递到 URL。然后,它将调用 \yii\web\user::loginByAccessToken() 方法,将 access-token 作为第一个参数传递。此函数将返回 IdentityInterfacenull

可以使用 tokenParam 在认证声明中更改 URL 参数的名称:

public function behaviors()
{
    $behaviors = parent::behaviors();
    $behaviors['authenticator'] = [
            'class' => yii\filters\auth\QueryParamAuth::className(),
           'tokenParam' => 'myAccessToken'
    ];
    return $behaviors;
}

使用此配置,URL 必须是 http://hostname/url?myAccessToken=...

最后一种认证方法是 OAuth 2,它需要一个授权服务器,从该服务器我们将获取用于传递给 REST API 服务器的载体令牌,这与 QueryParamAuth 类似。

示例 - 使用认证获取客户列表

在这个例子中,我们将同时使用两种方法进行认证:HTTPBasicAuthQueryParamAuth。当使用 QueryParamAuth 并带有访问令牌时,我们首先调用一个公开可访问的操作来获取用户将传递给所有其他操作的访问令牌作为查询 URL 参数。

我们将首先从 Customer 数据库表创建一个新的模型,并将其放入 common/models 文件夹中。然后,我们将使用 foo 作为用户名,$2a$12$xzGZB29iqBHva4sEYbJeT.pq9g1/VdjoD0S67ciDB30EWSCE18sW6 作为密码(这相当于散列后的文本)在 User 数据库表中创建一个新的用户。

api/controllers/CustomersController.php 中创建一个新的控制器,该控制器只扩展 behaviors() 方法以实现 HTTPBasicAuthQueryParamAuth

<?php
namespace api\controllers;

use yii\rest\ActiveController;
use yii\filters\auth\CompositeAuth;
use yii\filters\auth\HttpBasicAuth;
use yii\filters\auth\QueryParamAuth;

class CustomersController extends ActiveController
{
  public $modelClass = 'common\models\Customer';

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

    $behaviors['authenticator'] = [
      'class' => CompositeAuth::className(),
      'authMethods' => [
        [
          'class' => HttpBasicAuth::className(),
          'auth' => function($username, $password)
          {
            $out = null;
            $user = \common\models\User::findByUsername($username);
            if($user!=null)
            {
              if($user->validatePassword($password)) $out = $user;
            }
            return $out;
          }
        ],
        [
           'class' => QueryParamAuth::className(),
        ]
      ]
    ];

   return $behaviors;
  }
}

HTTPBasicAuth中,我们通过检查$username并在配置数组中实现auth属性来验证密码。如果用户名和密码匹配,它将返回找到的用户或否则返回 null。

相反,QueryParamAuth不需要除类以外的任何属性,因为我们将以access-token作为查询参数名称。尽管如此,为了完成这个任务,我们需要一个操作,该操作在传递用户名和密码后返回相关用户的访问令牌。

为了这个目的,我们将添加actionAccessTokenByUser()方法,该方法查找带有$username$password参数的用户。如果用户已经存在,其access_token属性将使用随机字符串更新,因此每次调用此操作时,access_token都会更改,上一个将被取消:

    public function actionAccessTokenByUser($username, $passwordHash)
    {
        $accessToken = null;

        $user = \common\models\User::findOne(['username' => $username, 'password_hash' => $passwordHash]);
        if($user!=null)
        {
            $user->access_token = Yii::$app->security->generateRandomString();
            $user->save();
            $accessToken = $user->access_token;
        }        
        return [ 'access-token' => $accessToken ];
    }

最后,为了测试HTTPBasicAuth,我们需要通过调用http://hostname/yiiadv/api/web/customers/index URL 来传递 WWW-Authentication 头。

如果我们想使用QueryParamAuth,我们需要:

  • 通过传递用户名和散列密码从http://hostname/yiiadv/api/web/customers/access-token-by-user获取access-token

  • 通过传递从上一个请求中接收到的访问令牌属性值,调用http://hostname/yiiadv/api/web/customers/index?access-token

QueryParamAuth调用IdentityInterfaces(用户模式)的findIdentityByAccessToken()函数。因此,请检查该方法是否已实现,如果没有,按照以下方式实现它:

public static function findIdentityByAccessToken($token, $type = null)
    {
    return User::findOne(['access_token' => $token]);
    }

注意,这种使用访问令牌的方式允许同时只使用相同的凭据使用 REST API。这是因为每次调用access-token-by-user时,都会创建一个新的access-token。因此,应该创建用户和access-token之间的一对多关系,以便为多个客户端提供使用相同的用户名/密码凭据的访问。

新的控制器操作

向 REST API 控制器添加新操作非常简单。我们只需要记住在 Web 控制器中的三个区别:

  • 新操作的动词设置

  • 验证新操作的设置

  • 新操作的输出

前两个步骤在控制器的behaviors()方法中配置:

    public function behaviors()
    {
        $behaviors = parent::behaviors();
        $behaviors['verbs'] = [
                'class' => \yii\filters\VerbFilter::className(),
                'actions' => [
                    'myCustomAction'  => ['get', 'head'],
                ],
        ];

        $behaviors['authenticator'] = [
        'except' => 'myCustomAction',
            'class' => HttpBasicAuth::className(),
        ];

        return $behaviors;
    }

public function actionMyCustomAction()
{
    …
    …

}

behaviors()方法的第一个部分,我们只会将gethead HTTP 方法设置为调用myCustomAction操作。如果我们尝试使用其他 HTTP 方法调用此操作,我们将得到一个不支持异常。

behaviors()方法的最后部分,我们将设置myCustomAction没有认证,因为它在except属性中。

新操作的第三个区别,输出,表明我们有不同的方式来返回数据。我们可以使用:

  • 从头开始创建单个对象的关键值对数组

  • ActiveRecord 实例用于创建单个对象

  • ActiveRecord 数组用于创建对象列表

  • 数据提供者

在最后这种情况中,框架将自动输出分页信息和链接到其他页面(如果有的话)。

示例 - 获取预订的房间列表

在这个例子中,我们需要在 common/models 文件夹中使用 Gii 创建一个 Reservation 模型。

然后,我们在 api/controllers/ReservationsController.php 中创建一个新的控制器:

<?php
namespace api\controllers;

use Yii;
use yii\rest\ActiveController;
use yii\filters\auth\CompositeAuth;
use yii\filters\auth\HttpBasicAuth;
use yii\filters\auth\QueryParamAuth;

class ReservationsController extends ActiveController
{
    public $modelClass = 'common\models\Reservation';

    public function actionIndexWithRooms()
    {
        $reservations = \common\models\Reservation::find()->all();

        $outData = [];
        foreach($reservations as $r)
        {
            $outData[] = array_merge($r->attributes, ['room' => $r->room->attributes]);
        }
        return $outData;        
    }

}

现在,让我们调用 http://hostname/yiiadv/api/web/reservations/index-with-rooms,我们将显示一个预订列表,其中每个预订的 room 属性都与相关的房间对象内容一起展开。

注意

请注意确保 room 关系已在 Reservation 模型中存在。如果没有,我们必须将此关系添加到 Reservation 模型中:

    public function getRoom()
    {
        return $this->hasOne(Room::className(), ['id' => 'room_id']);
    }

然而,这个解决方案效率不高,因为我们总是获取所有行,如果行数太多,这可能会对我们来说成本过高。为了解决这个问题,我们可以使用从找到的一组数据创建的数据提供程序,或者更好的是,使用 Yii 自动提供的更简单的解决方案。

事实上,Yii 提供了一些简单的方法来显示关系和过滤返回字段。例如,可能有一些我们不希望显示的字段,如密码、私人数据等。

模型有这些方法:

  • fields(): 默认情况下,扩展 yii\base\Model::fields() 的类返回所有模型属性作为字段,而扩展 yii\db\ActiveRecord::fields() 的类仅返回已从数据库中填充的属性

  • extraFields(): 默认情况下,扩展 yii\base\Model::extraFields() 的类返回空值,而扩展 yii\db\ActiveRecord::extraFields() 的类返回已从数据库中填充的关系的名称

第一种方法,fields(),是一个键值数组,其中键是返回字段的名称。如果返回的内容与键具有相同的名称,则值可以为空;也可以是一个表示从哪个属性获取返回值的字符串,或者是一个可调用的 PHP 函数来操作返回值。

第二种方法,extraFields(),是一个字符串数组,其值是在模型类中定义的关系。

最后,为了动态过滤请求的字段,我们将 fields 参数附加到请求的 URL 上,并将 expand 参数用于从模型获取关系列表。

因此,如果我们调用 http://hostname/yiiadv/api/web/reservations/index?expand=room,我们将得到相同的结果,但我们还将拥有仅对该页面必要的分页和加载的模型。

然而,对我们来说,分发不带特殊参数的 URL 会更方便,例如 expandfields,例如,为了避免使用这些 API 的开发者之间的混淆。

我们可以使用 actionIndexWithRooms 作为 actionIndex 的包装器,并以此方式包含展开参数:

    public function actionIndexWithRooms()
    {
            $_GET['expand'] = 'room';
            return $this->runAction('index');
    }

使用这个解决方案,http://hostname/yiiadv/api/web/reservations/index-with-rooms URL 只是 http://hostname/yiiadv/api/web/reservations/index?expand=room 的包装器,但这阻止了开发者记住需要传递哪些参数到 URL 以获取响应中的必要节点。

自定义身份验证和响应

Yii 允许我们快速为我们的应用程序创建自定义的身份验证方法。这很有用,因为在某些情况下,之前提到的身份验证方法是不够的。

通过扩展 yii\filters\auth\AuthMethod 类来创建自定义身份验证模型,该类实现了 yii\filters\auth\AuthInterface,需要重写 authenticate ($user, $request, 和 $response) 方法:

<?php

namespace api\components;

use yii\filters\auth\AuthMethod;
use Yii;

class CustomAuthMethod extends AuthMethod {

    public function authenticate($user, $request, $response) {
    …
    …
    …
}
…
…
…
}

尽管 REST API 应该是无状态的,或者说不应该保存会话数据,但在会话期间跨请求存储一些信息或首选项可能是必要的。

因此,如果我们需要支持会话,我们可以通过在 beforeAction() 事件中调用的 authenticate() 方法来启动会话。想法是使用 QueryParamAuth,使用 access-token 作为会话 ID 来标识当前会话。

为了这个目的,我们将在 api\components 中创建一个新的文件夹来存储自定义的 SessionAuth 方法。

这是 api/components/SessionAuth.php 文件的内容,其中查询 URL 参数被命名为 sid

<?php

namespace api\components;

use yii\filters\auth\AuthMethod;
use Yii;

class SessionAuth extends AuthMethod {
  public $tokenParam = 'sid';

  public function authenticate($user, $request, $response) {
    $accessToken = $request->get($this->tokenParam);

    if (is_string($accessToken)) {

       Yii::$app->session->id = $accessToken;

       $identity = isset(Yii::$app->session['loggedUser'])?Yii::$app->session['loggedUser']:null;

          if ($identity !== null) {
             return $identity;
          }
    }
    if ($accessToken !== null) {
        $this -> handleFailure($response);
    }
    return null;
  }

}

同时,创建一个用于启动会话的操作也是必要的;否则,用户将不会被存储在会话中。

因此,在 api/controllers/UsersController.php 中创建一个新的控制器 UsersController 来处理登录:

<?php
namespace api\controllers;

use Yii;
use yii\rest\ActiveController;
use yii\filters\auth\CompositeAuth;
use yii\filters\auth\HttpBasicAuth;
use yii\filters\auth\QueryParamAuth;
use api\components\SessionAuth;
use common\models\User;

class UsersController extends ActiveController
{
    public $modelClass = 'common\models\User';

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

        $behaviors['authenticator'] = [
                'except' => ['login'],
                'class' => SessionAuth::className(),
        ];

        return $behaviors;
    }    

    public function actionLogin($username, $passwordHash)
    {
        $dataOut = null;

        $user = User::findOne(['username' => $username, 'password_hash' => $passwordHash]);
        if($user != null)
        {
            $session = Yii::$app->session;
            $session->open();

            $session['loggedUser'] = $user;

            $sid = $session->id;

            $dataOut = ['sid' => $sid];        
        }

        return $dataOut;
    }
}

如前所述,在 behaviors() 方法中,除了 login 之外,这个控制器的所有操作都将对 SessionAuth 组件进行身份验证,该组件主要检查用户是否成功执行了登录操作。

我们现在调用 http://hostname/yiiadv/api/web/users/login?username=&passwordHash= 并填写 usernamepasswordHash 字段。它返回会话 ID 以访问会话数据。此外,会话中的 loggedUser 属性会填充用户模型数据。

现在,我们可以像典型的 Web 应用程序一样在请求之间存储共享信息。

现在,让我们看看如何在 RESTful Web 服务中自定义响应。首先,当我们需要添加额外的信息时,例如在客户端显示的显式错误消息或操作状态码,这个操作可能是必要的。

自定义响应必须扩展 \yii\web\Response 并重写 send() 方法,如下所示:

<?php
namespace api\components;

use yii\rest\ActiveController;
use Yii;
use yii\web\Response;

class ApiResponse extends \yii\web\Response
{

    public function send()
    {
      ..
  ..
  ..
  }
}

这个 send() 方法操作对象属性中存储的数据,主要在 $this->data 变量中。

这种定制,我们将在下一个示例中详细看到,是不完整的,因为 send() 方法应该实现从 \yii\web\Response 版本所做的所有数据操作。我们必须记住,Yii 根据客户端传递的 Accept HTTP 头返回数据,以及许多其他便利的功能。

可以通过在从 send() 函数返回之前调用 parent::send() 来简单地保持这种行为,如下所示:

    public function send()
    {
        ..
  ..
        parent::send();
 }

因为,如前所述,send() 使用 $this->data 变量作为发送数据的容器。

示例 - 数据接收中的状态响应节点

现在,让我们将前一章中看到的概念应用于向响应中添加额外数据。当我们需要向客户端返回有关操作状态和额外数据(如详细错误消息)的信息时,这种做法很有用。

本例的目的是返回包含两个属性的响应:

  • 包含三个属性的 status 属性:response_code,表示操作状态的整数值;response_message,表示 response_code 的字符串值;以及 response_extra,表示自定义文本字符串。

  • 包含预期输出数据的 data 属性

我们将使用一个包含所有整数代码及其文本表示的类作为响应代码,因为整数值将用于填充 response_code 属性,而字符串表示将用于填充 response_message 属性。

api/components/ApiResponseCode.php 中创建一个新的类文件,内容如下:

<?php
namespace api\components;

class ApiResponseCode
{
    const ERR_OK = 0;
    const ERR_LOGIN_REQUIRED = 1;
    const ERR_METHOD_NOT_FOUND = 2;
    const ERR_NOT_FOUND = 3;
    const ERR_NOT_SAVED = 4;
    const ERR_DUPLICATE = 5;
    const ERR_INPUT_DATA_FORMAT = 6;

    public static function responsesExtras()
    {
        return [
            ApiResponseCode::ERR_OK => '',
            ApiResponseCode::ERR_LOGIN_REQUIRED => 'Login required to use this interface',
            ApiResponseCode::ERR_METHOD_NOT_FOUND => 'Interface not found',
            ApiResponseCode::ERR_NOT_FOUND => 'Record not found',
            ApiResponseCode::ERR_NOT_SAVED => 'Error in saving',
            ApiResponseCode::ERR_DUPLICATE => 'Duplicated record',
            ApiResponseCode::ERR_INPUT_DATA_FORMAT => 'Input data format incompatible',
        ];        
    }

    public static function responseExtraFromCode($rc)
    {
        $al = ApiResponseCode::responsesExtras();
        return (isset($al[$rc]))?$al[$rc]:null;
    }     

    public static function responseMessages()
    {
        return [
            ApiResponseCode::ERR_OK => 'OK',
            ApiResponseCode::ERR_LOGIN_REQUIRED => 'ERR_LOGIN_REQUIRED',
            ApiResponseCode::ERR_METHOD_NOT_FOUND => 'ERR_METHOD_NOT_FOUND',
            ApiResponseCode::ERR_NOT_FOUND => 'ERR_NOT_FOUND',
            ApiResponseCode::ERR_NOT_SAVED => 'ERR_NOT_SAVED',
            ApiResponseCode::ERR_DUPLICATE => 'ERR_DUPLICATED',
            ApiResponseCode::ERR_INPUT_DATA_FORMAT => 'ERR_INPUT_DATA_FORMAT',
        ];        
    }

    public static function responseMessageFromCode($rc)
    {
        $al = ApiResponseCode::responseMessages();
        return (isset($al[$rc]))?$al[$rc]:null;
    }            
}

在此组件中,我们定义了一个表示所有可以发送给客户端的响应代码的常量列表。对于每个响应代码,responseMessage() 静态方法将返回一个相对文本表示。然后,responseExtras() 方法还将返回一个额外的文本消息数组,如果没有传递特定的文本 extra,它将填充 response_extra 属性。

最后,我们必须在 api/components/ApiResponse.php 中编写一个扩展 \yii\web\Response 的组件,命名为 ApiResponse。在这个组件中,我们将定义三个自定义属性:statusResponseCodestatusResponseMessagestatusResponseExtra,我们将用 status 属性中的内容来填充它们。

这样,我们将有一个基于 $code 参数的便利方法 fillStatusResponse(),它将自动填充 statusResponseExtrastatusResponseMessage 属性。

此组件的核心是重写的 send() 方法,它将默认返回 status,其中包含 ERR_OK 作为响应消息和 0 作为响应代码,如果没有客户端错误(如认证、未找到等)。除非开发者更改 statusResponseCodestatusResponseExtrastatusResponseMessage 的值,或者手动或自动调用其属性 fillStatusResponse(),否则情况如此。

否则,如果存在一些客户端错误,我们将支持 未认证未找到 错误。

这是 api/components/ApiResponse.php 文件的内容:

<?php
namespace api\components;

use Yii;
use yii\web\Response;

class ApiResponse extends Response
{
    public $statusResponseCode;
    public $statusResponseMessage;
    public $statusResponseExtra;

    /**
     * Set response code and extra from code.
     *
     * Response extra will be filled based on $extraData value
     * If $extraData is null, response extra will be value from ApiResponseCode::responseExtraFromCode($code)
     * If $extraData is string, response extra will be filled with this value
     */
    public function fillStatusResponse($code, $extraData=null)
    {
        $responseExtra = ApiResponseCode::responseExtraFromCode($code);
        $responseMessage = ApiResponseCode::responseMessageFromCode($code);

        if($extraData == null)
        {
            $statusResponseExtra = $responseExtra;
        }
        else
        {
            $statusResponseExtra = $extraData;
        }

        $this->statusResponseCode = $code;
        $this->statusResponseMessage = $responseMessage;
        $this->statusResponseExtra = $statusResponseExtra;
    }

    /**
     * Override send() method.
     *
     * $this->data member contains data released to client.
     */
    public function send()
    {
        $responseMessage = ApiResponseCode::responseMessageFromCode($this->statusResponseCode);

        if($this->isClientError)
        {
           $dataOut = $this->data;

           if($this->statusCode == 401) {   // Not authorized
             $dataOut = null;

             $this->fillStatusResponse(ApiResponseCode::ERR_LOGIN_REQUIRED);
            }
            else if($this->statusCode == 404) {  // Non found
                $dataOut = null;

                $this->fillStatusResponse(ApiResponseCode::ERR_METHOD_NOT_FOUND);
            }            

            $this->data = ['status' => ['response_code' => $this->statusResponseCode, 'response_message' => $this->statusResponseMessage, 'response_extra' => $this->statusResponseExtra ], 'data' => $dataOut ];

        }
        else
        {
            $this->data = ['status' => ['response_code' => $this->statusResponseCode, 'response_message' => $responseMessage, 'response_extra' => $this->statusResponseExtra ], 'data' => $this->data ];
        }

        parent::send();
    }

    public function init()
    {
        parent::init();

        $this->statusResponseCode = ApiResponseCode::ERR_OK;
    }

}

最后,我们必须通过添加 response 属性作为组件来更改配置文件 api/config/main.php,以指示使用自定义响应类:

        'response' => [

            'format' => yii\web\Response::FORMAT_JSON,
            'charset' => 'UTF-8',
            'class' => '\api\components\ApiResponse',

        ],

让我们尝试一下。尝试调用不存在的 URL http://hostname/yiiadv/api/web/reservations/index-inexistent

这将是输出,正确地返回空数据和带有错误说明的状态:

示例 – 数据接收中的状态响应节点

调用不存在的 URL 后的错误响应

然后,尝试调用需要认证的 URL:http://hostname/yiiadv/api/web/customers/index,这已经在之前的段落中实现了。

这将是输出,正确地返回空数据和带有错误说明的状态:

示例 – 数据接收中的状态响应节点

调用带认证的 URL 时的错误响应

最后,我们尝试调用返回数据的 URL:http://hostname/yiiadv/api/web/rooms/index,这已经在之前的段落中实现了。

这将是输出,正确地返回填充的数据和成功状态:

示例 – 数据接收中的状态响应节点

成功输出的响应

其他形式的导出 – RSS

Yii 允许我们创建自定义格式响应以输出数据。响应格式可以根据客户端发送的 Accept HTTP 头部值进行更改,或者通过程序化方式完成。当 Yii 收到请求时,它会根据 Accept HTTP 头部值搜索可用的响应格式化程序,并最终调用找到的响应格式化程序的 format ($response) 方法。

因此,创建自定义响应有三个步骤:

  1. 实现 yii\web\ResponseFormatterInterface 接口。

  2. 在配置文件中添加新的自定义格式化响应属性。

  3. 扩展控制器的 behaviors() 方法以处理特定的 Accept HTTP 头部值。

第一步要求我们实现 yii\web\ResponseFormatterInterface 接口并扩展其方法 format ($response)。要格式化的数据存储在 $response->data 属性中,客户端的响应必须在 $response->content 属性中填写:

<?php
namespace api\components;

use yii\web\ResponseFormatterInterface;

class RssResponseFormatter implements ResponseFormatterInterface
{
    public function format($response)
    {
        $response->getHeaders()->set('Content-Type', 'application/rss+xml; charset=UTF-8');
        if ($response->data !== null) {
            $response->content = "<rss></rss>";
        }
    }
}

第二步要求我们添加对自定义响应格式化程序的引用。为此,我们将使用 responseformatters 属性,它是一个数组,键是格式名称,数组值是创建格式化对象对应的配置:

         'response' => [
            'formatters' => [

                'rss' => [
                    'format' => 'raw',
                    'charset' => 'UTF-8',
                    'class' => '\api\components\RssResponseFormatter',
                ],                

            ]

        ],             

第三步要求我们扩展控制器的 behaviors() 方法,以便处理特定的 Accept HTTP 头部值,并根据 Accept HTTP 头部值指示框架使用哪个响应格式化程序,例如:

    public function behaviors()
    {
        $behaviors = parent::behaviors();
        $behaviors['contentNegotiator']['formats']['application/rss+xml'] = 'rss';
        return $behaviors;
    }    

当客户端发送一个将 Accept HTTP 头设置为 application/rss+xml 的请求时,这个控制器将使用 rss 格式化器(从配置文件中读取)来准备响应。如果我们指定一个配置文件中不存在的格式化器,我们将得到 InvalidConfigException

示例 - 使用可用房间列表创建 RSS

现在,让我们看看如何为可用房间创建 RSS 响应格式化器。

首先,我们必须在 api/components/RssResponseFormatter.php 中创建完整的响应格式化器组件:

<?php
namespace api\components;

use yii\web\ResponseFormatterInterface;

class RssResponseFormatter implements ResponseFormatterInterface
{
    public function format($response)
    {
        $response->getHeaders()->set('Content-Type', 'application/rss+xml; charset=UTF-8');
        if ($response->data !== null) {
            $rssOut = '<?xml version="1.0" encoding="UTF-8"?>';
            $rssOut .= '<rss>';
            $rssOut .= '<channel>';
            foreach($response->data as $d)
            {
                $rssOut .= '<item>';    
                $rssOut .= sprintf('<title>Room #%d at floor %d</title>', $d['id'], $d['floor']);
                $rssOut .= '</item>';
            }
            $rssOut .= '</channel>';
            $rssOut .= '</rss>';

            $response->content = $rssOut;;
        }
    }
}

RSS 响应格式化器必须实现 format ($response) 方法以正确实现 yii\web\ResponseFormatterInterface。当 format ($response) 方法被调用时,它将设置 Content-Type HTTP 头为 application/rss+xml,使用从 $response->data 属性准备好的数据,并填充 $response->content 属性,这是客户端接收到的最终内容。

然后,我们必须更改 api/config/main.php 文件,以添加具有对新响应格式化器支持的 response 属性:

        'response' => [
            'formatters' => [

                'rss' => [
                    'format' => 'raw',
                    'charset' => 'UTF-8',
                    'class' => '\api\components\RssResponseFormatter',
                ],                

            ]
        ],

formatter 属性是一个响应格式化器的数组,其中键是格式名称,值是创建格式化对象对应的配置。

在这个例子中,我们配置了一个名为 rss 的新格式化器,它代表 \api\components\RssResponseFormatter 组件。

最后,我们必须在控制器中配置 behaviors() 方法来处理具有 application/rss+xml 值的 Accept HTTP 头。

打开 api/controllers/RoomsController.php 文件,并将扩展添加到 behaviors() 方法中:

    public function behaviors()
    {
        $behaviors = parent::behaviors();
        $behaviors['contentNegotiator']['formats']['application/rss+xml'] = 'rss';
        return $behaviors;
    }    

从从 parent::behaviors() 继承的 $behaviors 基础配置开始,contentNegotiator 属性包含对 Accept HTTP 头值 formats 的引用。数组键是支持的 Accept HTTP 头值,值是对应的响应格式化器。

如果我们尝试执行以下请求:

GET /yiiadv/api/web/rooms/index HTTP/1.1
Host: hostname
Accept: application/rss+xml

我们应该显示以下响应:

示例 - 使用可用房间列表创建 RSS

RSS 响应输出

我们还可以以编程方式使用响应格式化器。只需在配置文件中将 Yii::$app->response 应用程序组件的格式设置为配置的响应格式化器即可。

例如,我们可以在 RoomsController 中添加一个名为 actionIndexRss 的新操作,它将使用以下方式通过 RssResponseFormatter 输出数据:

    public function actionIndexRss()
    {
        \Yii::$app->response->format = 'rss';

        $provider = new \yii\data\ActiveDataProvider([
            'query' => \common\models\Room::find(),
            'pagination' => [
                'pageSize' => 20,
            ],
        ]);

        return $provider;
    }

摘要

在本章中,我们通过使用 Yii 提供的强大工具创建了 api,以便在移动应用中使用。我们采用了创建新应用程序的方法来分发 RESTful 网络服务,而不是混合网络和 api 控制器。为此,在章节开头,我们使用高级模板配置了一个新的 REST 应用程序。

在配置好 RESTful 网络服务环境之后,我们发现 Yii 默认提供了两种api控制器,然后我们创建了带有自定义数据和来自 ActiveRecord 的数据的控制器。

接着,我们了解了框架提供的 RESTful 网络服务的默认认证方法,并学习了如何使用它们。

最后,我们关注了如何自定义响应输出格式,以创建可用数据的 RSS 版本为例。

在下一章中,你将学习如何编写控制台应用程序,并了解网络应用程序和控制台应用程序之间的区别。

第十二章:创建控制台应用程序以自动化周期性任务

在本章中,我们将学习如何编写控制台应用程序,并会发现 Web 应用程序和控制台应用程序之间的主要区别。

然后,我们将创建我们的第一个控制台控制器,使用一个实际示例来说明如何更新数据库表。

在最后几段中,我们将看到如何设置输出颜色和文本格式,以及如何实现一个完整的周期性任务,例如使用每日预订发送电子邮件。在本章中,我们将涵盖以下主题:

  • 与控制台应用程序交互

  • 创建控制台控制器

    • 示例 - 为已过期的预订设置警报标志
  • 格式化控制台输出

  • 实施和执行 cron 作业

    • 示例 - 发送包含当日新预订的电子邮件

与控制台应用程序交互

控制台是高级模板默认安装的第三个应用程序。

此应用程序配置为通过控制台访问启动命令,并且它具有与之前章节中看到的相同的应用程序结构。因此,在本节中,我们需要对主机进行控制台访问。

与到目前为止使用的 Web 和 API 应用程序相比,有一些区别。

控制器的public属性实际上在命令行上作为option可见。需要扩展控制器的option()方法以使这些属性可用。此外,根据特定操作,操作参数作为命令行参数传递。

最后,控制台控制器操作可以返回一个退出代码,这是一个数字,其中 0 表示一切正常,这是控制台应用程序开发的最佳实践。

下面是一个从 shell 开始使用控制台应用程序的典型用法:

yii <route> [--option1=value1 --option2=value2 ... argument1 argument2 ...]

上述代码的元素解释如下:

  • route:这表示要调用的controller/action路径

  • option:这表示控制器针对该特定操作的public属性的可访问性;我们只能访问控制器options()方法返回的公共属性

  • argument:这表示要传递给控制器操作的参数

    注意

    总有一个始终可用的选项,appconfig,用于指示必须使用哪个配置文件路径。如果没有设置,将采用默认配置文件。

Yii 提供了一套核心控制台应用程序,我们可以通过调用help控制器(作为一个 Web 应用程序,默认操作将是index)来访问它们,以便显示有关可用控制台控制器列表或单个控制器或操作控制器详细信息的所有内容。

让我们考虑一个示例;打开命令行(在这种情况下,Linux shell)并从项目根目录输入以下内容:

$ ./yii help

这将显示类似于以下内容的输出(部分显示):

This is Yii version 2.0.4.

The following commands are available:

- asset                         Allows you to combine and compress your JavaScript and CSS files.
 asset/compress (default)    Combines and compresses the asset files according to the given configuration.
 asset/template              Creates template of configuration file for [[actionCompress]].

- cache                         Allows you to flush cache.
 cache/flush                 Flushes given cache components.
 cache/flush-all             Flushes all caches registered in the system.
 cache/flush-schema          Clears DB schema cache for a given connection component.
 cache/index (default)       Lists the caches that can be flushed.
…
…

在这里,第一级分组表示控制器名称(右侧有相对描述),第二级包括相关控制器的操作。当我们传递控制器名称时,我们将需要更详细的信息来帮助它:

$ ./yii help message

要显示控制器描述和操作列表,我们还可以通过输入完整路由(控制器/操作)来获取有关完整路由的帮助:

$ ./yii help message/config

这返回了一个包含操作描述、用法和可用选项的输出:

DESCRIPTION

Creates a configuration file for the "extract" command.

The generated configuration file contains detailed instructions on
how to customize it to fit for your needs. After customization,
you may use this configuration file with the "extract" command.

USAGE

yii message/config <filePath> [...options...]

- filePath (required): string
 output file name or alias.

OPTIONS

--appconfig: string
 custom application configuration file path.
 If not set, default application configuration is used.

--color: boolean, 0 or 1
 whether to enable ANSI color in the output.
 If not set, ANSI color will only be enabled for terminals that support it.

--interactive: boolean, 0 or 1 (defaults to 1)
 whether to run the command interactively.

创建控制台控制器

控制台控制器与之前创建的 Web 控制器完全相同。它扩展了 \yii\console\Controller 基类,并可以返回一个整数值,表示操作的响应状态(0 表示操作成功执行),也称为 退出码

只有当控制器的 public 属性名称由接受 actionID 作为参数的 options() 方法返回时,它们才能作为选项提供;因此,响应可以根据 actionID 进行定制。

options() 方法的响应是一个表示控制器公共属性名的文本字符串数组。

从我们之前在 yiiadv 文件夹中安装的高级模板应用程序开始,让我们在 console/controllers/MyExampleController.php 中创建一个名为 MyExampleController 的新控制台控制器,其内容如下:

<?php

namespace console\controllers;

use \yii\console\Controller;

/**
 * This is an example controller
 */
class MyExampleController extends Controller
{
    public $option1;
    public $option2;

    public function options($action)
    {
        return ['option1'];
    }

    /**
     * Simply return a welcome text
     */
    public function actionTest($param1)
    {
        echo 'this is my first controller using console application';
        echo "\n";
        echo "You have passed param1 with value: ".$param1;
        echo "\n";
        echo "Value of option1 is: ".$this->option1;
        echo "\n";

        // equivalent to return 0;
        return Controller::EXIT_CODE_NORMAL;
    }

}

?>

此控制器包含两个公共属性,但只有 option1 可以从控制台使用,因为它是由 options() 方法返回的。我们将显示以下命令的结果:

$ ./yii help my-example

前面的命令将返回以下输出:

DESCRIPTION

This is an example controller

SUB-COMMANDS

- my-example/test  Simply return a welcome text

To see the detailed information about individual sub-commands, enter:

 yii help <sub-command>

如果我们需要有关 test 操作的其他详细信息,我们可以通过指定完整路由来启动前面的命令:

$ ./yii help my-example/test

现在,尝试使用路由 my-example/test 启动命令,不添加任何参数:

$ ./yii my-example/test

我们将收到一个关于缺少 param1 的错误。以下是正确的语法:

$ ./yii my-example/test "this is value for param1"

前面的命令将返回以下输出,没有 option1 的值:

this is my first controller using console application
You have passed param1 with value: this is value for param1
Value of option1 is:.

我们也可以通过在命令后附加 --option1 来传递值 option1,如下所示:

$ ./yii my-example/test "this is value for param1" --option1="this is value for option1"

前面的命令将返回完整的输出,如下所示:

this is my first controller using console application
You have passed param1 with value: this is value for param1
Value of option1 is: this is value for option1

示例 – 为已过期的预订设置警报标志

现在,让我们通过一个示例来说明如何使用控制台命令来执行维护操作。

在控制台控制器中,我们可以访问项目中所有可用的模型、组件和扩展,以及我们在 Web 应用程序中所做的。因此,我们将以与 Web 应用程序相同的方式操作数据。

从前几章中使用的预订数据库表开始,我们将添加一个新的布尔字段,命名为 expired,用于设置哪些预订已超出结束日期。

这是存储在 MySQL 服务器中的 reservation 表的结构:

CREATE TABLE `reservation` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `room_id` int(11) NOT NULL,
 `customer_id` int(11) NOT NULL,
 `price_per_day` decimal(20,2) NOT NULL,
 `date_from` date NOT NULL,
 `date_to` date NOT NULL,
 `reservation_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
 `expired` int(1) NOT NULL DEFAULT '0',
 PRIMARY KEY (`id`)
 )

现在,让我们插入一些记录以进行模拟。如果今天是 date_to value 之后,我们将更新 expired 字段的值为 1;否则,它将是 0

这些是要插入到 reservation 数据库表中的记录:

INSERT INTO `reservation` (`id`, `room_id`, `customer_id`, `price_per_day`, `date_from`, `date_to`, `reservation_date`, `expired`) VALUES
(1, 2, 1, 90.00, '2015-02-10', '2015-05-23', '2015-05-24 22:45:37', 0),
(2, 2, 1, 48.00, '2019-08-27', '2019-08-31', '2015-05-24 22:45:37', 0),
(3, 1, 2, 105.00, '2015-09-24', '2015-10-06', '2015-06-03 00:21:14', 0),
(4, 1, 2, 150.00, '2015-06-22', '2015-06-28', '2015-06-21 22:24:25', 0),
(5, 1, 2, 150.00, '2015-07-22', '2015-08-28', '2015-06-21 22:24:34', 0);

注意

确保用户存在于用户数据库表中

现在,在 console/controllers/ReservationsController.php 中创建一个新的控制台控制器,内容如下:

<?php

namespace console\controllers;

use \yii\console\Controller;

/**
 * Manage reservations
 */
class ReservationsController extends Controller
{
    /**
     * Update 'expired' field of reservations
     */
    public function actionUpdateExpired()
    {
        $models = \common\models\Reservation::find()->all();

        foreach($models as $m)
        {
            echo sprintf('Check reservation #%d - date_to = %s - status : %s', $m->id, $m->date_to, (strtotime($m->date_to)<=time())?'OK':'Expired');
            echo "\n";
            // Set expired field. I'll for every model because if we could have changed 'date_to' value.
            $m->expired = (strtotime($m->date_to)<=time())?0:1;
            $m->save();
        }
        // equivalent to return 0;
        return Controller::EXIT_CODE_NORMAL;
    }
}
?>

actionUpdateExpired 中,我们向控制台显示每个模型的一些数据,例如 iddate_tostatus。然后,我们将根据 date_to 值为每个模型设置 expired 字段的值。

最后,我们将运行以下命令:

$ ./yii reservations/update-expired

这将返回以下输出:

Check reservation #1 - date_to = 2015-05-23 - status : OK
Check reservation #2 - date_to = 2019-08-31 - status : Expired
Check reservation #3 - date_to = 2015-10-06 - status : Expired
Check reservation #4 - date_to = 2015-06-28 - status : OK
Check reservation #5 - date_to = 2015-08-28 - status : OK

格式化来自控制台的输出

基础类控制台控制器 yii\console\Controller 支持显示彩色和格式化输出的方法。

有两种标准方法来显示输出,具体如下:

  • stdout:这会将字符串打印到 STDOUT

  • strerr:这会将字符串打印到 STDERR

这两种方法都支持更多参数:第一个是要显示的文本字符串,另一个包括可以传递的格式化选项,以生成美观的输出。

有颜色和打字格式的格式化选项;这些是由 \yii\helpers\Console 中的常量定义的;例如,BG_CYAN 用于青色背景,BG_RED 用于红色背景,UNDERLINE 用于下划线文本。

让我们使用以下代码看看一个例子:

$this->stdout("Hello?\n", Console::BOLD);

这将显示 Hello?(带有回车符),字体加粗。有时,可能不会显示任何效果,因为我们的终端不支持颜色。

在这种情况下,控制台控制器的一个方法将帮助我们验证我们的终端功能:isColorEnabled() 返回一个布尔值,指示终端是否支持 ANSI 颜色。

两种方法 stroutstrerr 都应用于整个文本字符串,并且作为第一个参数传递。如果我们只想将一些功能应用于文本的某个部分,我们必须使用返回 ANSI 格式化字符串的 ansiFormat 方法。

让我们举一个例子。创建一个控制器来检查控制台是否支持 ANSI,并尝试打印彩色文本,如果这个功能被支持。

然后,在 console/controllers/ColorController.php 中创建一个名为 ColorController 的新控制器,内容如下:

<?php

namespace console\controllers;

use \yii\console\Controller;
use \yii\helpers\Console;

/**
 * Colors dedicated controller
 */
class ColorController extends Controller
{
    /**
     * Simply return a welcome text
     */
    public function actionIsClientEnabled()
    {
        if($this->isColorEnabled())
        {
            $this->stdout('OK, terminal supports colors!');
        }
        else
        {
            $this->stdout('NOT OK, terminal does not support colors!'); 

        }

        $this->stdOut("\n");        

        // equivalent to return 0;
        return Controller::EXIT_CODE_NORMAL;
    }

    public function actionPrintColouredText()
    {
        $colouredText = $this->ansiFormat('This text is coloured', Console::FG_RED);
        $normalText ="This text is normal";

        $this->stdout(sprintf("%s - %s\n", $normalText, $colouredText));
    }

}

?>

我们调用 launch 来检查客户端是否支持 ANSI 颜色:

$ ./yii color/is-client-enabled

并且为了显示彩色文本(如果客户端支持的话):

$ ./yii color/print-coloured-text

\yii\helpers\ 下的 Console 类包含许多其他有用的方法来格式化文本和输出,例如 confirm()prompt() 从用户获取输入,或者 progress 创建进度条以显示执行状态。

实现和执行 cron 作业

控制台应用程序的主要用途在于使用 cron 作业(在 Linux 或 Unix 机器上)执行周期性任务。

我们可以使用控制台应用程序发送大量电子邮件以执行系统维护或检查应用程序的特定状态。

在下一个示例中,我们将看到如何发送包含当日预订摘要的电子邮件。

示例 - 发送包含当日新预订的电子邮件

此示例说明了如何发送包含新每日预订摘要的电子邮件。

首先,让我们在console/config/main.php中配置mailer组件,如果尚未配置。

只需传递几个参数给组件:

    'components' => [
    ..
    ..

        'mailer' => [
            'class' => 'yii\swiftmailer\Mailer',
            'viewPath' => '@common/mail',
            // send all mails to a file by default. You have to set
            // 'useFileTransport' to false and configure a transport
            // for the mailer to send real emails.
            'useFileTransport' => true,
        ],
..
..
    ],
];

class参数表示处理组件的类,viewPath参数表示电子邮件或电子邮件模板的视图存储位置;最后一个参数useFileTransport表示电子邮件发送方法。

现在,在ReservationsController中,在console/controllers/ReservationsController.php下添加方法actionReservationsOfTheDay,该方法发送每日预订的内容:

    public function actionReservationsOfTheDay($currentDate=null)
    {
        if($currentDate == null) $currentDate = date('Y-m-d');
        $models = \common\models\Reservation::find()->where('DATE(reservation_date) = "'.$currentDate.'"')->all(); 
        \Yii::$app->mailer->compose(['html' => 'reservationsOfTheDay-html', 'text' => 'reservationsOfTheDay-text'], ['models' => $models, 'currentDate' => $currentDate])
            ->setFrom('myemail@example.com')
            ->setTo('administrator@example.com')
            ->setSubject('Reservations of the day: '.$currentDate)
            ->send();

    }

注意

建议将from电子邮件参数,例如,放在params.php文件中,该文件包含整个应用程序中可用的所有全局参数。

此方法简单地从输入中获取currentDate参数,以便我们可以根据需要更改评估日期;动作体找到输入日期的预订并将它们传递给htmltext格式的电子邮件视图reservationsOfTheDay

现在,我们必须在common/mail中创建电子邮件格式的內容,创建两个文件:reservationsOfTheDay-html.phpreservationsOfTheDay-text.php

这是 HTML 版本的內容:

There are <?= count($models) ?> reservations for the date <?= $currentDate ?>

<br /><br />

<?php if(count($models)>0) { ?>
    <b>This is a summary:</b>

    <br />

    <table>
        <tr>
            <td>Reservation #</td>
            <td>Room</td>
            <td>Customer</td>
            <td>Price per day</td>
            <td>Date from</td>
            <td>Date to</td>
        </tr>

        <?php foreach($models as $m) { ?>
        <tr>
            <td><?= $m->id ?></td>
            <td><?= $m->room->floor.' '.$m->room->number ?></td>
            <td><?= $m->customer->surname.' '.$m->customer->name ?></td>
            <td><?= $m->price_per_day ?></td>
            <td><?= $m->date_from ?></td>
            <td><?= $m->date_to ?></td>
        </tr>    
        <?php } ?>

    </table>
<?php } else { ?>
    <i>There is no summary for current date</i>
<?php } ?>    

这是文本格式的对应内容(对于 HTML 电子邮件客户端不是必需的):

There are <?= count($models) ?> reservations for the date <?= $currentDate ?>

<?php if(count($models)>0) { ?>
    This is a summary

    <?php foreach($models as $m) { ?>
        Reservation #: <?= $m->id ?> - Room: <?= $m->room->floor.' '.$m->room->number ?> - Customer: <?= $m->customer->surname.' '.$m->customer->name ?> - Price per day: <?= $m->price_per_day ?> - Date from: <?= $m->date_from ?> - Date to: <?= $m->date_to ?>
    <?php } ?>
<?php } else { ?>
    There is no summary for the current date
<?php } ?>

可以通过启动以下命令来执行:

$ ./yii reservations/reservations-of-the-day

我们还可以调用传递日期参数来更改要检查的日期,例如,检查2015-08-05的预订:

$ ./yii reservations/reservations-of-the-day "2015-08-05"

最后,根据操作系统,将此命令附加到周期性任务调度程序,例如 Linux 或 Unix 环境中的 cron。

摘要

在本章中,我们讨论了与 Yii 的高级模板一起安装的第三种默认应用程序,即控制台应用程序。

我们已经看到了控制台应用程序和 Web 应用程序之间的主要区别,我们学习了如何创建我们的第一个控制台控制器,处理传递给动作的选项和参数。然后,我们通过一个具体的示例应用了控制台应用程序,例如对预订表进行维护操作,以更新预订的状态为已过期。

然后,我们关注了控制台应用程序如何使用颜色和文本格式化功能生成漂亮的输出。

最后,我们掌握了如何使用控制台控制器动作创建一个完整的周期性任务,以发送包含当日预订的每日总结电子邮件。

在最后一章,我们将看到我们发展的最终阶段,在这个阶段我们必须使代码可重用,但尤其是可维护的。

第十三章。最终重构

这是我们的开发阶段的最后阶段。现在我们已经编写了所有的工作代码,我们必须使其可重用,但更重要的是,可维护。本章将帮助你通过小部件和其他组件重用代码。我们将看到一些如何使用它们的实际示例。然后,我们将处理文档,这是应用开发的一个重要方面,它允许每个人快速了解项目的结构和构建方式。

对于文档,我们将使用框架提供的两个最重要的工具来构建 API 和指南参考,通过一个真实世界的示例。我们将涵盖以下主题:

  • 创建小部件

    • 示例 - 创建带有轮播的小部件
  • 创建组件

    • 示例 - 创建一个创建 MySQL 数据库备份并发送电子邮件给管理员的组件
  • 创建模块

  • 生成 API 文档

    • 示例 - 使用 API 文档生成应用的文档

创建小部件

小部件是一个可重用的客户端代码(包含 JavaScript、CSS 和 HTML),它包含最少的逻辑,并封装在yii\base\Widget对象中,我们可以轻松地将其插入并应用于任何视图。

构建小部件需要你扩展yii\base\Widget的两个方法:

  • init()方法初始化对象

  • run()方法执行对象

为了实例化一个小部件,只需调用接受一个参数或更好的是一个包含其公共属性值的数组的静态widget()方法。

以下是一个示例:

    MyWidget::widget(['prop1' => 'value of prop1', …])

这将返回一个包含小部件输出的字符串,传递其prop1公共属性的value of prop1值。

如果我们需要在部件的执行中插入额外的代码(例如,在 ActiveForm 部件中),我们可以使用begin()end()方法以更复杂的方式实例化部件。

第一种方法,begin(),接受一个带有配置数组的函数参数,并将其传递给小部件,然后它将返回小部件对象。

当调用第二种方法end()时,这两个方法之间的代码将被显示,并且同时,end()方法直接回显小部件run()方法的输出:

    $widget = MyWidget::begin(['prop1' => 'value of prop1', …]);

    ..
    .. I can use $widget object here  ..
    ..

     MyWidget::end();

对于任何其他视图,在run()方法中,我们可以通过render()方法引用视图文件,以显示小部件输出。

例如,一个部件可能是一个实时日期/时间时钟。为此目的,我们将基于一个包含由 JavaScript 代码更新的日期/时间字符串的块来构建一个时钟。我们可以向部件构造函数传递一些值,例如边框框的颜色。

为了创建一个实例,让我们从基本的模板应用开始(但这显然也适用于高级模板应用)。在项目的根目录中创建一个名为components的新文件夹(如果不存在),位于controllersmodelsviews等同一级别,它将包含我们想要构建的所有小部件。

然后,在这个文件夹中,我们将创建一个名为 ClockWidget.php 的新文件,完整路径为 basic/components/ClockWidget.php

<?php

namespace app\components;

use yii\base\Widget;

class ClockWidget extends Widget
{

    public function init()
    {
        \yii\web\JqueryAsset::register($this->getView());
    }

    public function run()
    {
        return $this->render('clock');
    }

}

init() 方法中,我们还引用了 jQuery 资产,请求框架加载 jQuery 插件,因为我们需要在视图文件中使用它。

run() 方法中,我们渲染了 clock 视图,其内容将在下一行讨论。

因此,在 basic/components/views 下创建一个新的文件夹,并在其中创建一个名为 clock.php 的新文件,其代码如下:

<?php

$this->registerJs( <<< EOT_JS

    function ClockWidget_refresh_datetime()
    {
        var dateTimeString = new Date().toString();
        $('#ClockWidget_realtime_clock').html(dateTimeString);
    }

    setInterval(ClockWidget_refresh_datetime,1000);

    ClockWidget_refresh_datetime();
EOT_JS
);

?>

<div style="border:1px solid black;padding:5px;width:200px;text-align:center">
    <span id="ClockWidget_realtime_clock"></span>
</div>

这段代码简单地显示一个包含实时日期和时间值的字符串的框,每秒更新一次。

最后,我们可以使用以下代码在任何视图中使用我们的组件:

<?= \app\components\ClockWidget::widget(); ?>

示例 - 创建带有轮播的组件

在这个例子中,我们将创建一个包含一些房间的轮播组件(我们可以通过将它们传递给组件的公共属性来选择显示哪一个)。再次,我们将使用基本的模板应用程序;然而,所有这些同样适用于高级模板应用程序。

对于这个例子,我们将创建一个新的控制器来使用其视图作为组件容器。

那么,让我们在 basic/controller/TestCarouselController.php 下创建这个名为 TestCarouselController 的新控制器。从这里,我们将传递 models 属性,它包含最多三个房间的列表:

<?php

namespace app\controllers;

use yii\web\Controller;
use app\models\Room;

class TestCarouselController extends Controller
{
    public function actionIndex()
    {
        $models = Room::find()->limit(3)->all();

        return $this->render('index', ['models' => $models]);
    }
}

接下来,我们在 basic/views/test-carousel/index.php 下创建视图,组件输出如下:

This is a carousel widget with some rooms:
<?= \app\components\CarouselWidget\CarouselWidget::widget(['models' => $models, 'options' => ['style' => 'border:1px solid black;text-align:center;padding:5px;']]); ?>

这将构建组件填充及其公共属性 modelsoptions

现在是创建我们的组件的时候了。为了尽可能地将组件与其他代码隔离,我们在 basic/components 文件夹下创建一个特定的组件文件夹,在 CarouselWidget 子文件夹中,我们将创建一个名为 CarouselWidget.php 的组件文件。

这个组件包含一个公共属性 models,它包含从容器视图传递过来的房间模型。在 \yii\bootstrap\Carousel 中将这些模型作为此类数组传递是必要的:

items => [
['content' => '...', 'caption' => '...'],
['content' => '...', 'caption' => '...'],
['content' => '...', 'caption' => '...'],
...
];

这样,在 init() 方法中,我们将根据 Bootstrap Yii2 组件的期望创建模型的内部表示。

最后,在 run() 方法中,我们将输出现在位于 basic/components/CarouselWidget/views 视图文件夹中的视图。这是组件内容;请记住,它存储在 basic/components/CarouselWidget 下的 CarouselWidget.php 中:

<?php

namespace app\components\CarouselWidget;

use yii\base\Widget;

class CarouselWidget extends Widget
{
    public $carouselId = 'carouselWidget_0';
    public $options = [];
    public $models = [];

    private $carouselItemsContent;

    public function init()
    {
        // It is not necessary because yii bootstrap Carousel widget will load it automatically
        // \yii\jui\JuiAsset::register($this->getView());

        $this->carouselItemsContent = [];
        foreach($this->models as $model)
        {
            $caption = sprintf('<h1>Room #%d</h1>', $model->id);
            $content = sprintf('This is room #%d at floor %d with %0.2f€ price per day', $model->id, $model->floor, $model->price_per_day);
            $itemContent = ['content' => $content, 'caption' => $caption];
            $this->carouselItemsContent[] = $itemContent;
        }

    }

    public function run()
    {
        return $this->render('carousel', ['carouselItemsContent' => $this->carouselItemsContent]);
    }

}

run() 方法中调用的组件视图将存储在 basic/components/CarouselWidget/views 下的 carousel.php 文件中:

<?php $styleOption = isset($this->context->options['style'])?$this->context->options['style']:''; ?>
<div id="<?php echo $this->context->id ?>" style="<?php echo $styleOption ?>">
    <?php
    echo \yii\bootstrap\Carousel::widget([
        'id' => $this->context->carouselId,
        'items' => $carouselItemsContent

    ]);
    ?>

</div>

浏览到 http://hostname/basic/web/test-carousel/index,我们将看到轮播组件(仅包含文本,但我们也可以在其中插入一些图片)。

创建组件

组件是一个可重用的对象,应该只包含逻辑,并且可以从应用程序的任何位置调用。在组件中,我们放置所有在应用程序的多个位置可用的函数。

技术上,一个组件扩展了yii\base\Component,实现了属性、事件和行为功能。我们可以有两种类型的组件:组件和应用组件。它们之间的唯一区别是第二个组件还需要在应用的配置文件中的components属性中进行配置,并且它作为属性从Yii::$app对象中可用。应用组件的例子有dbuser等等。

通常,组件存储在项目根目录开始的components文件夹中。

让我们看看如何创建一个简单的自定义组件:

namespace app\components;

use Yii;
use yii\base\Component;

class MyComponent extends Component
{
..
..
}

我们可以按以下方式实例化此组件:

$myCmp = new \app\components\MyComponent();

然后,我们将有一个新的MyComponent对象实例。

如果我们想将此组件渲染为应用组件并通过Yii::$app->myComponent访问它,我们必须更新配置文件web.php中的basic/config

'components' => [
    ..
    ..
        'myComponent' => [
            'class' => '\app\components\MyComponent'
        ],
]

在这个阶段,我们可以使用以下方式调用myComponent

Yii:$app->myComponent

注意

记住,应用组件是相同对象的单个共享实例。

我们可以通过覆盖组件的init()方法来在组件实例化时进行自定义初始化。

组件的一个具体例子(或根据我们的需求,应用组件)可能是向应用短信网关发送短信。

组件可以是:

namespace app\components;

use Yii;
use yii\base\Component;

class SmsGateway extends Component
{
    public function send($to, $text)
    {
        ..
        ..
        ..
    }
}

此示例适合将此组件用作应用组件:

'components' => [
    ..
    ..
        'smsgw' => [
            'class' => '\app\components\SmsGateway
        ],
]

这可以直接从以下位置使用:

Yii:$app->smsgw->send('+3913456789', 'hello world!');

应用组件的另一个常见例子可能是向移动设备发送推送通知的对象,它是以与之前短信网关对象相同的方式制作的。

示例 - 创建一个组件,该组件备份 MySQL 数据库并向管理员发送电子邮件

此示例将展示一个关于创建主数据库备份副本和管理员完成时接收的警报消息的常见任务。

将使用命令行 MySQL 工具进行备份。

维护操作应在控制台环境中执行,因为它们可以被安排(每天、每周、每周两天等),并且如果此操作耗时超过最大时间,可能会导致 Web 服务器超时(通常,如果操作未完成,Web 服务器将在 30 秒后返回超时错误)。因此,我们将首先在之前安装的高级模板中创建一个控制台控制器。

记住,高级模板的项目根文件夹是yiiadv

yiiadv/common/componentsMaintenance.php中创建一个新的组件,内容如下:

<?php
namespace common\components;

use Yii;
use yii\base\Component;

class Maintenance extends Component
{
    public function launchBackup($database, $username, $password, $pathDestSqlFile)
    {
        $cmd = sprintf('mysqldump -u %s -p%s %s > %s', $username, $password, $database, $pathDestSqlFile);
        $outputLines = [];
        exec($cmd, $outputLines, $exitCode);

        return ['cmd' => $cmd, 'exitCode' => $exitCode, 'outputLines' => $outputLines];        
    }
}
?>

launchBackup()方法将通过传递用户名、密码、数据库和 SQL 命令输出要存储的目标文件路径来启动mysqldump(应该在系统中安装)。

然后,它将返回一个包含这些值的数组:命令、命令的退出代码以及可能的输出文本。现在让我们创建将用于启动命令的控制台控制器。我们也可以从网络控制器启动它,例如在点击按钮后。

让我们在 yiiadv/console/controllers 下的 MaintenanceController.php 中创建控制台控制器:

<?php

namespace console\controllers;

use \yii\console\Controller;
use \yii\helpers\Console;
use \common\components\Maintenance;

class MaintenanceController extends Controller
{
    public function actionBackupDatabase()
    {
        $tmpfname = tempnam(sys_get_temp_dir(), 'FOO');
        $obj = new Maintenance();
        $ret = $obj->launchBackup('username', 'password', 'database_name', $tmpfname);

        if($ret['exitCode'] == 0)
        {
            $this->stdOut("OK\n");        
            $this->stdOut(sprintf("Backup successfully stored in: %s\n", $tmpfname));        
        }
        else
        {
            $this->stdOut("ERR\n");
        }

        // equivalent to return 0;
        return $ret['exitCode'];
    }

}

?>

让我们做一些考虑:

  • 我们可以通过避免创建对象实例来将维护组件的 launchBackup() 方法设置为静态;然而,如果我们保持它为非静态,我们也可以将其用作应用程序组件。否则,如果我们将方法标记为静态,然后在调用对象中的静态方法 launchBackup() 时将其用作应用程序组件,我们将收到 PHP 的警告。

  • 我们可以将文件创建移动到 launchBackup() 方法内部,因为在这种情况下它是一个临时文件,但通常我们可以使用特定的文件路径。

  • 如果我们将数据库信息存储在参数文件中,我们可以避免传递数据库信息,并从 Yii 参数中获取它。

一个更完整的操作是备份并发送电子邮件给管理员,包含备份结果,如果需要,还可以包含备份文件:

    public function actionBackupDatabaseAndSendEmail()
    {
        $tmpfname = tempnam(sys_get_temp_dir(), 'FOO'); // good
        $obj = new Maintenance();
        $ret = $obj->launchBackup('username', 'password', 'database_name', $tmpfname);

        $emailAttachment = null;
        if($ret['exitCode'] == 0)
        {
            $this->stdOut("OK\n");        
            $this->stdOut(sprintf("Backup successfully stored in: %s\n", $tmpfname));

            $textEmail = 'Backup database successful! Find it in attachment';
            $emailAttachment = $tmpfname;
        }
        else
        {
            $this->stdOut("ERR\n");

            $textEmail = 'Error in backup database! Check it!';
        }

        $emailMsg = Yii::$app->mailer->compose()
            ->setFrom('from@example.com')
            ->setTo('to@example.com')
            ->setSubject('Backup database')
            ->setTextBody($textEmail);

        if($emailAttachment!=null) $emailMsg->attach($emailAttachment, ['fileName' => 'backup_db.sql']);
        $emailMsg->send();            

        // equivalent to return 0;
        return $ret['exitCode'];
    }

创建模块

模块实际上是在主应用内部的一个应用。实际上,它被组织成一个名为模块基本路径的目录。在目录内,有包含其控制器、模型、视图和其他代码的文件夹,就像在一个应用中一样。

按照模块的典型结构:

myCustomModule/
    Module.php                   the module class file
    controllers/                 containing controller class files
        DefaultController.php    the default controller class file
    models/                      containing model class files
    views/                       containing controller view and layout files
        layouts/                 containing layout view files
        default/                 containing view files for DefaultController
            index.php            the index view file

当访问模块时,会实例化 module 类文件,它用于在代码中共享数据组件,例如应用程序实例。

module 类文件具有以下特点:

  • 默认命名为 Module.php

  • 在代码执行过程中仅实例化一次

  • 它位于模块基本路径的直接下方

  • 它继承自 yii\base\Module

让我们看看 myCustomModule 模块(在 app\modules\myCustomModule 命名空间下)的模块类文件示例:

namespace app\modules\myCustomModule;

class Module extends \yii\base\Module
{
    public function init()
    {
        parent::init();

        $this->params['foo'] = 'bar';
        // ...  other initialization code ...
    }
}

作为标准应用,模块可以基于具有与标准应用相同内容的配置文件有自己的配置:

<?php
return [
    'components' => [
        // list of component configurations
    ],
    'params' => [
        // list of parameters
    ],
  ..
  ..
  ..
];

我们在模块的 init() 方法中加载这个:

public function init()
{
    parent::init();
    // initialize the module with the configuration loaded from config.php
    \Yii::configure($this, require(__DIR__ . '/config.php'));
}

然后,我们以与普通应用相同的方式创建和使用控制器、模型和视图。

注意

我们总是需要在每个文件的最顶部指定正确的命名空间。

最后,要在应用中使用模块,我们只需在应用的模块属性中列出模块。以下代码在应用配置中使用论坛模块:

[
    'modules' => [
        'myCustomModule' => [
            'class' => 'app\modules\myCustomModule\Module',
            // ... other configurations for the module ...
        ],
    ],
]

生成 API 文档

文档无疑是应用最重要的方面之一,因为它提供了关于其流程和结构的信息。不幸的是,由于时间不足,它经常被省略。

Yii 为我们提供了一个强大的工具来自动生成漂亮的文档。基本上,它使用应用程序中存在的所有文档注释,那些以/**开头而不是经典的/*

因此,我们有这样的优势,即代码中的注释被用来生成完整的文档。

在这些注释中,有一些关键字可以根据上下文使用——文件、类或函数/方法。

在文件的情况下,最常见的关键字放在顶部的是:

  • @link url,其中url是链接到文件的参考 URL

  • @copyright text,其中text是版权内容

  • @license url,其中url是许可证内容的参考

在类的情况下,最常见的关键字放在顶部的是:

  • @author name,其中name是作者名称

  • @since version,其中version是包含此类的项目版本

在函数/方法的情况下,最常见的关键字放在顶部的是:

  • @param type name,其中type是参数的类型,name是作为函数参数传递的参数名称

  • @return type,其中type是返回的类型

  • @throws class,其中class是抛出的异常类

除了 API 文档外,Yii 还提供创建.md格式(典型的 GitHub)的漂亮指南文件的工具。通过在互联网上搜索,很容易找到有关格式化.md文件的信息。

示例 - 使用 API 文档生成应用程序和服务的文档

现在我们来看看哪些命令能自动从 Yii 应用程序生成文档。

文档有两种类型:

  • API 文档,它是项目中每个.php文件的参考,由指向单个文件、类或函数的 doc 注释完成

  • 指南,这是一个相当好的应用程序手册,使用 Yii 渲染的.md文件创建的.html文件

第一步是安装api-doc,如果尚未安装。

指向项目根目录并启动此命令:

$ php composer.phar require --prefer-dist yiisoft/yii2-apidoc

这将安装yii2-apidoc扩展。

注意

如果此命令没有正确完成,请按以下方式启动 Composer 更新:

$ php composer.phar update

现在我们可以从项目根目录启动命令以生成 API 文档:

$ vendor/bin/apidoc api ./ ../app-doc

参数如下:

  • 第一个参数api标识要执行的命令

  • 第二个参数./标识要扫描的源文件路径

  • 第三个参数../app-doc标识创建的文档的目标文件夹

启动命令后,在浏览器中转到../app-doc文件夹将显示框架创建的 API 文档。

当我们在源文件中做出任何更改时,必须重新启动命令以更新 API 文档。第二种文档是指南,是一组由.md文件生成的.html文件。

因此,我们需要从项目根文件夹创建一个文件夹,例如名为guide的文件夹,我们将把所有想要从命令guide转换为.html格式漂亮文件的.md文件放入其中。

现在我们准备启动命令来创建我们的指南,这与之前制作的 API 命令完全相同:

$ vendor/bin/apidoc guide ./guide ../app-doc

此命令将把./guide文件夹中所有的.md文件转换为.html文件,并将它们存储在../app-doc文件夹中(与 API 文档文件一起)。

让我们举一个具体的例子。从基本模板项目开始,在basic/controllers中的TestDocController.php创建一个名为TestDocController的新控制器:

<?php

/**
 * This file contains a controller to demonstrate api documentation tool.  
 *
 * @link http://www.example.com/
 * @copyright Copyright (c) 2015
 * @license http://www.example.com/license/
 */

namespace app\controllers;

use Yii;
use yii\web\Controller;

/**
 * This is a controller class to demonstrate api documentation tool.  
 *
 * @author Fabrizio Caldarelli
 * @since 1.0
 */
class TestDocController extends Controller
{
    /**
     * Make sum of the operands
     *
     * @param float $a first operand
     * @param float $b second operand
     * @return float sum of parameters
     * @author  
     */
    public function makeSum(float $a, float $b)
    {
        return $a+$b;
    }
}

现在在主机上打开一个 shell 控制台,从项目根文件夹启动生成 API 文档的命令:

$  vendor/bin/apidoc api ./ ../app-doc

这将为从根文件夹(./)开始的 所有文件创建文档,并将 HTML 结果文件存储在../app-doc

现在,在你的浏览器中,访问http://hostname/app-doc,我们将显示 API 文档索引页面。在侧菜单中搜索TestDocController.php并点击它。这应该是输出:

示例 – 使用 API 文档生成应用和服务的文档

TestDocController API 文档

现在,我们想展示第二种文档类型——指南文档。

从项目根文件夹创建一个名为app-guide的文件夹。在其中,放置一个名为test-doc-controller.md的新文件,内容如下:

## TestDoc Controller

This is the guide for TestDoc Controller.

## Functionalities

It is provided makeSum function, that makes a sum of two values passed as parameter

$a = 10;

$b = 20;

$c = $this->makeSum(float $a, float $b) // $c = 30;

前往托管的 shell 控制台,从项目根文件夹启动生成指南文档的命令:

$  vendor/bin/apidoc guide ./app-guide ../app-doc

这将为./app-guide文件夹中的所有.md文件创建指南文档,并将.html结果存储在../app-doc

在你的浏览器中访问http://hostname/app-doc/guide-test-doc-controller.html,你应该看到以下屏幕:

示例 – 使用 API 文档生成应用和服务的文档

TestDocController 指南文档

摘要

在本章的最后,你学习了如何使用小部件和组件制作可重用且易于维护的代码。谈到可重用视图代码(HTML、JavaScript 和 CSS),我们介绍了小部件,定义了它们并为项目带来的好处。接下来,你学习了如何构建和使用它们,最后,我们通过从头开始构建一个新的小部件进行了实际示例。谈到可重用逻辑代码,我们发现了其组件,区分了组件和应用组件,并通过为现实生活中的问题构建有用的组件进行了一些实际示例。

然后我们掌握了文档生成器,特别是 API 和指南文档。你学习了如何启动和使用 Yii 提供的工具。最后,我们构建了一个控制器类,通过一个实际示例来解释如何为该控制器构建 API 参考和指南参考。

posted @ 2025-09-09 11:30  绝不原创的飞龙  阅读(10)  评论(0)    收藏  举报