PHP YII Web 应用开发(一)
原文:
zh.annas-archive.org/md5/6008a5c78f9d1deb914065f1c36d5b5a译者:飞龙
前言
本书是使用 Yii Web 应用程序开发框架开发真实应用程序的逐步教程。本书试图模拟一个被要求构建在线应用程序的软件开发团队的环境,涵盖软件开发生命周期的每个方面,从构思到生产部署,构建项目任务管理应用程序。
在对 Yii 框架进行简要的一般介绍,并通过标志性的“Hello World”示例之后,剩下的章节以与真实项目中软件开发迭代相同的方式进行了分解。我们首先创建一个具有有效,经过测试的与数据库连接的工作应用程序。
然后我们开始定义我们的主要数据库实体和领域对象模型,并熟悉 Yii 的对象关系映射(ORM)层Active Record。我们学习如何依靠 Yii 的代码生成工具自动构建针对我们新创建的模型的创建/读取/更新/删除(CRUD)功能。我们还关注 Yii 的表单验证和提交模型的工作原理。到第五章 管理问题 结束时,您将拥有一个可以管理项目和项目中的问题(任务)的工作应用程序。
然后我们转向用户管理的话题。我们了解 Yii 内置的身份验证模型,以帮助应用程序的登录和注销功能。我们深入研究授权模型,首先利用 Yii 的简单访问控制模型,然后实施 Yii 提供的更复杂的基于角色的访问控制(RBAC)框架。
到第七章 用户访问控制 结束时,任务管理应用程序的所有基础都已就位。接下来的几章将开始专注于额外的用户功能,用户体验和设计。我们添加用户评论功能,介绍了一种可重用的内容小部件架构方法。我们添加了一个 RSS 网络订阅,并演示了在 Yii 应用程序中集成其他第三方工具和框架有多么容易。我们利用 Yii 的主题结构来帮助简化和设计应用程序,然后介绍 Yii 的国际化(I18N)功能,以便应用程序可以在不进行工程更改的情况下适应各种语言和地区。
在最后一章中,我们将把重点转向准备应用程序进行生产部署。我们介绍了优化性能和提高安全性的方法,以准备应用程序投入生产环境。
本书涵盖内容
第一章,遇见 Yii,为您提供了 Yii 的简要历史,介绍了模型视图控制器(MVC)应用程序架构,并向您介绍了典型的请求生命周期,从最终用户通过应用程序,最终作为响应返回给最终用户。
第二章,入门,致力于下载和安装框架,创建一个新的 Yii 应用程序外壳,并介绍 Gii,Yii 强大灵活的代码生成工具。
第三章,TrackStar 应用程序,介绍了 TrackStar 应用程序。这是一个在线项目管理和问题跟踪应用程序,您将在接下来的章节中构建。在这里,您将学习如何将 Yii 应用程序连接到底层数据库。您还将学习如何从命令行运行交互式 shell。本章的最后部分侧重于在 Yii 应用程序中提供单元测试和功能测试的概述,并提供了在 Yii 中编写单元测试的具体示例。
第四章,“项目 CRUD”,帮助您开始与数据库交互,开始向基于数据库的 Yii 应用程序 TrackStar 添加功能。您将学习如何使用 Yii 迁移进行数据库变更管理,我们使用 Gii 工具创建模型类,并使用模型类构建创建、读取、更新和删除(CRUD)功能。本章还向读者介绍了配置和执行表单字段验证。
第五章,“管理问题”,解释了如何向 TrackStar 应用程序添加其他相关的数据库表,并介绍了 Yii 中的关联 Active Record。本章还涵盖了使用控制器过滤器来利用应用程序生命周期,以提供前操作和后操作处理。我们介绍了官方 Yii 扩展库 Zii,并使用 Zii 小部件来增强 TrackStar 应用程序。
第六章,“用户管理和身份验证”,解释了如何在 Yii 中对用户进行身份验证。在为 TrackStar 应用程序添加管理用户功能的同时,读者学习如何利用 Yii 中的“行为”来实现对 Yii 组件之间共享通用代码和功能的极其灵活的方法。本章还详细介绍了 Yii 的身份验证模型。
第七章,“用户访问控制”,专门介绍了 Yii 的授权模型。首先,我们介绍了简单的访问控制功能,它允许您轻松地为基于多个参数的控制器操作配置访问规则。然后,我们看看在 Yii 中如何实现基于角色的访问控制(RBAC),它允许更健壮的授权模型,完全基于角色、操作和任务的分层模型进行完整的访问控制。将基于角色的访问控制实现到 TrackStar 应用程序中还向读者介绍了在 Yii 中使用控制台命令。
第八章,“添加用户评论”,帮助演示了如何实现允许用户在 TrackStar 应用程序中对项目和问题进行评论的功能;我们介绍了如何配置和使用统计查询关系,如何创建高度可重用的用户界面组件称为“小部件”,以及如何在 Yii 中定义和使用命名范围。
第九章,“添加 RSS Web Feed”,演示了在 Yii 应用程序中使用其他第三方框架和库有多么容易,并展示了如何使用 Yii 的 URL 管理功能来自定义应用程序的 URL 格式和结构。
第十章,“使其看起来不错”,帮助您更多地了解 Yii 中的视图,以及如何使用布局来管理跨应用程序页面共享的标记和内容。我们还介绍了主题化,展示了如何轻松地为 Yii 应用程序赋予全新的外观,而无需修改任何基础工程。然后,我们将介绍 Yii 中的国际化(i18n)和本地化(l10n),因为语言翻译被添加到我们的 TrackStar 应用程序中。
第十一章,“使用 Yii 模块”,解释了如何通过使用 Yii 模块向 TrackStar 网站添加管理功能。模块提供了一种非常灵活的方法来开发和管理应用程序中较大的、独立的部分。
第十二章,“生产就绪”,帮助我们准备我们的 TrackStar 应用程序投入生产。您将了解 Yii 的日志框架、缓存技术和错误处理方法,以帮助使您的 Yii 应用程序达到生产就绪状态。
本书所需软件
本书需要以下软件:
-
Yii 框架版本 1.1.12
-
PHP 5.1 或更高版本(建议使用 5.3 或 5.4)
-
MySQL 5.1 或更高版本
-
能够运行 PHP 5.1 的 Web 服务器;本书中提供的示例是在 Apache HTTP 服务器上构建和测试的,在这些服务器上 Yii 已经在 Windows 和 Linux 环境中进行了彻底测试
-
Zend 框架版本 1.1.12 或更高版本(仅需要第九章中的内容,添加 RSS Web Feed,以及此库的下载和配置,这在本章中有介绍)
这本书适合谁
如果您是具有面向对象编程知识的 PHP 程序员,并且希望快速开发现代、复杂的 Web 应用程序,那么这本书适合您。阅读本书不需要对 Yii 有任何先前的了解。
约定
在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的含义解释。
文本中的代码词如下所示:“我们可以通过使用include指令来包含其他上下文。”
代码块设置如下:
'components'=>array(
'db'=>array(
'connectionString' => 'mysql:host=localhost;dbname=trackstar',
'emulatePrepare' => true,
'username' => '[YOUR-USERNAME]',
'password' => '[YOUR-PASSWORD]',
'charset' => 'utf8',
),
),
当我们希望引起您对代码块的特定部分的注意时,相关行或项会以粗体显示:
'components'=>array(
'db'=>array(
**'connectionString' => 'mysql:host=localhost;dbname=trackstar',
'emulatePrepare' => true,**
'username' => '[YOUR-USERNAME]',
'password' => '[YOUR-PASSWORD]',
'charset' => 'utf8',
),
),
任何命令行输入或输出都以以下方式编写:
**$ yiic migrate create <name>**
**%cd /WebRoot/trackstar/protected/tests**
新术语和重要单词以粗体显示。例如,屏幕上看到的单词,如菜单或对话框中的单词,会以这样的方式出现在文本中:“点击下一步按钮会将您移动到下一个屏幕”。
注意
警告或重要说明会以这样的方式出现在框中。
提示
提示和技巧会以这样的方式出现。
第一章:见识 Yii
网络开发框架通过立即提供核心基础和所需的基础设施,帮助您快速将白板上的想法转化为功能齐全的生产就绪代码,从而帮助您快速启动应用程序。有了今天网络应用程序所需的所有常见功能,并且有可用的框架选项来满足这些期望,几乎没有理由从头开始编写下一个网络应用程序。现代、灵活、可扩展的框架几乎和编程语言本身一样是今天网络开发人员的必备工具。当两者特别互补时,结果是一个非常强大的工具包——比如 Java 和 Spring、Ruby 和 Rails、C#和.NET 以及 PHP 和 Yii。
Yii 是创始人薛强的心血结晶,他于 2008 年 1 月 1 日开始开发这个开源框架。薛强在开始这个项目之前,曾经多年开发和维护 PRADO 框架。从 PRADO 项目中积累的多年经验和用户反馈,巩固了对一个更易于扩展、更高效的基于 PHP5 的框架的需求,以满足应用开发人员日益增长的需求。Yii 的初始 alpha 版本于 2008 年 10 月正式发布,与其他基于 PHP 的框架相比,其极其出色的性能指标立即引起了极大的关注。2008 年 12 月 3 日,Yii 1.0 正式发布,截至 2012 年 10 月 1 日,最新的生产就绪版本为 1.1.12。它拥有一个不断壮大的开发团队,并且每天都在 PHP 开发人员中不断增加其知名度。
Yii这个名字是Yes, it is的缩写,发音为Yee或(ji:)。Yii 是一个高性能、基于组件的、用 PHP5 编写的 Web 应用程序框架。这个名字也代表了最常用来描述它的形容词,比如易用、高效和可扩展。让我们快速看一下 Yii 的每个特点。逐个进行。
易用
要运行基于 Yii 1.x 的 Web 应用程序,您只需要核心框架文件和支持 PHP 5.1.0 或更高版本的 Web 服务器。要使用 Yii 进行开发,您只需要了解 PHP 和面向对象编程。您不需要学习任何新的配置或模板语言。构建 Yii 应用程序主要涉及编写和维护自己的自定义 PHP 类,其中一些将扩展自核心 Yii 框架组件类。
Yii 吸收了许多其他知名 Web 编程框架和应用程序的优秀理念和工作。因此,如果您从其他网络开发框架转向 Yii,您很可能会发现它很熟悉并且易于操作。
Yii 还秉承着“约定优于配置”的理念,这有助于其易用性。这意味着 Yii 对于几乎所有用于配置应用程序的方面都有合理的默认值。遵循规定的约定,您可以编写更少的代码,并花费更少的时间开发应用程序。但是,Yii 并不强迫您。它允许您自定义所有默认值,并且很容易覆盖所有这些约定。我们将在本章和整本书中介绍一些这些默认值和约定。
高效
Yii 是一个高性能的基于组件的框架,可用于开发任何规模的 Web 应用程序。它鼓励在 Web 编程中最大程度地重用代码,并且可以显著加速开发过程。正如之前提到的,如果您遵循 Yii 的内置约定,您可以几乎不需要手动配置就能让应用程序立即运行起来。
Yii 还设计为帮助您进行DRY开发。DRY代表不要重复自己,这是敏捷应用程序开发的一个关键概念。所有 Yii 应用程序都是使用模型-视图-控制器(MVC)架构构建的。Yii 通过提供一个地方来保存您的 MVC 代码的每一部分来强制执行这种开发模式。这最小化了重复,并有助于促进代码重用和易于维护。您需要编写的代码越少,将应用程序推向市场所需的时间就越少。应用程序越容易维护,它在市场上的时间就越长。
当然,这个框架不仅使用高效,而且速度非常快,性能优化。Yii 从一开始就考虑了性能优化,并且结果是 PHP 框架中最高效的之一。因此,Yii 增加到其上的应用程序的任何额外开销都是极其微不足道的。
可扩展
Yii 经过精心设计,几乎可以扩展和定制其代码的每一部分,以满足任何项目需求。事实上,很难不利用 Yii 的可扩展性,因为开发 Yii 应用程序的主要活动之一就是扩展核心框架类。如果您想将扩展的代码转化为其他开发人员有用的工具,Yii 提供了易于遵循的步骤和指南,帮助您创建这样的第三方扩展。这使您能够为 Yii 不断增长的功能列表做出贡献,并积极参与扩展 Yii 本身。
值得注意的是,这种易用性、卓越性能和深度的可扩展性并不是以牺牲其功能为代价的。Yii 充满了功能,帮助您满足当今 Web 应用程序所面临的高要求。支持 AJAX 的小部件,RESTful 和 SOAP Web 服务集成,MVC 架构的强制执行,DAO 和关系 ActiveRecord 数据库层,复杂的缓存,分层基于角色的访问控制,主题,国际化(I18N)和本地化(L10N)只是 Yii 冰山一角。从 1.1 版本开始,核心框架现在打包了一个官方扩展库,称为Zii。这些扩展由核心框架团队成员开发和维护,并继续扩展 Yii 的核心功能集。并且随着一个庞大的用户社区,他们也通过编写 Yii 扩展来贡献,Yii 应用程序的整体功能集每天都在增长。在 Yii 框架网站上可以找到可用的用户贡献的扩展列表,网址为www.yiiframework.com/extensions。还有一个非官方的扩展库,其中包含了一些很棒的扩展,网址为yiiext.github.com/,这真正展示了社区的力量和这个框架的可扩展性。
MVC 架构
正如前面提到的,Yii 是一个 MVC 框架,并为每个模型、视图和控制器代码的每个部分提供了明确的目录结构。在我们开始构建第一个 Yii 应用程序之前,我们需要定义一些关键术语,并了解 Yii 如何实现和强制执行这种 MVC 架构。
模型
通常在 MVC 架构中,模型负责维护状态,并应该封装适用于定义此状态的数据的业务规则。在 Yii 中,模型是框架类CModel或其子类的任何实例。模型类通常由可以具有单独标签(用于显示目的的用户友好内容)的数据属性组成,并且可以根据模型中定义的一组规则进行验证。构成模型类属性的数据可以来自数据库表的一行,也可以来自用户输入表单中的字段。
Yii 实现了两种模型,即表单模型(CFormModel类)和活动记录(CActiveRecord类)。它们都是从同一个基类CModel继承而来。CFormModel类表示收集 HTML 表单输入的数据模型。它封装了所有表单字段验证的逻辑,以及可能需要应用于表单字段数据的任何其他业务逻辑。然后它可以将这些数据存储在内存中,或者借助活动记录模型将数据存储在数据库中。
活动记录(AR)是一种设计模式,用于以面向对象的方式抽象数据库访问。Yii 中的每个 AR 对象都是CActiveRecord或其子类的实例,它包装数据库表或视图中的单行数据,封装了所有与数据库访问相关的逻辑和细节,并包含了大部分需要应用于该数据的业务逻辑。表行中每个列的数据字段值都表示为活动记录对象的属性。稍后将更详细地介绍活动记录。
视图
通常视图负责呈现用户界面,通常基于模型中的数据。Yii 中的视图是一个包含用户界面相关元素的 PHP 脚本,通常使用 HTML 构建,但也可以包含 PHP 语句。通常,视图中的任何 PHP 语句都非常简单,是条件或循环语句,或者引用其他 Yii UI 相关元素,如 HTML 助手类方法或预构建小部件。更复杂的逻辑应该与视图分离,并适当放置在模型中(如果直接处理数据)或控制器中(用于更一般的业务逻辑)。
控制器
控制器是我们路由请求的主要指挥官,负责接收用户输入,与模型交互,并指示视图更新和适当显示。Yii 中的控制器是CController或其子类的实例。当控制器运行时,它执行请求的操作,然后与必要的模型交互,并呈现适当的视图。一个操作,简单来说,就是一个以action开头的控制器类方法。
将这些连接在一起:Yii 请求路由
在 MVC 实现中,Web 请求通常具有以下生命周期:
-
浏览器向托管 MVC 应用程序的服务器发送请求
-
调用控制器操作来处理请求
-
控制器与模型交互
-
控制器调用视图
-
视图呈现数据(通常为 HTML)并将其返回给浏览器显示
Yii 的 MVC 实现也不例外。在 Yii 应用程序中,来自浏览器的传入请求首先由路由器接收。路由器分析请求以决定应将其发送到应用程序的何处进行进一步处理。在大多数情况下,路由器会识别控制器类中的特定操作方法,将请求传递给该方法。这个操作方法将查看传入的请求数据,可能与模型交互,并执行其他所需的业务逻辑。最终,这个操作方法将准备好响应数据并发送给视图。视图将格式化这些数据以符合所需的布局和设计,并返回给浏览器显示。
博客发布示例
为了更好地理解所有这些,让我们看一个虚构的例子。假设我们使用 Yii 构建了一个新的博客网站http://yourblog.com。这个网站与大多数典型的博客网站类似。主页显示最近发布的博客文章列表。每篇博客文章的名称都是超链接,可以将用户带到显示完整文章的页面。以下图表说明了 Yii 如何处理从点击这些假想博客文章链接发送的传入请求:
该图表跟踪了用户点击链接时发出的请求:http://yourblog.com/post/show/id/99
首先,请求被发送到路由器。路由器解析请求,决定将其发送到何处。URL 的结构对路由器将做出的决定至关重要。默认情况下,Yii 识别以下格式的 URL:
http://hostname/index.php?r=ControllerID/ActionID
r查询字符串变量指的是 Yii 路由器分析的路由。它将解析此路由以确定适当的控制器和操作方法,以进一步处理请求。现在您可能立即注意到我们上面的示例 URL 不遵循此默认格式。这只是一个非常简单的配置应用程序以识别以下格式的 URL:
http://hostname/ControllerID/ActionID
我们将继续使用这种简化的格式来进行示例。URL 中的ControllerID名称指的是控制器的名称。默认情况下,这是控制器类名称的第一部分,直到单词Controller。例如,如果您的控制器类名称是TestController,ControllerID名称将是test。ActionID类似地指的是由控制器定义的操作的名称。如果操作是在控制器内定义的简单方法,那么它将是方法名称中跟在单词action后面的任何内容。例如,如果您的操作方法名为actionCreate(),ActionID名称就是create。
注意
如果省略了ActionID,控制器将采取默认操作,按照约定是控制器中称为actionIndex()的方法。如果还省略了ControllerID,应用程序将使用默认控制器。Yii 默认控制器称为SiteController。
回到示例,路由器将分析 URLhttp://yourblog.com/post/show/id/99,并将 URL 路径的第一部分post作为ControllerID,将第二部分show作为ActionID。这将转换为将请求路由到PostController类中的actionShow()方法。URL 的最后部分,id/99部分,是一个名称/值查询字符串参数,在处理过程中将可用于该方法。在这个示例中,数字99代表所选博客文章的唯一内部 ID。
在我们虚构的博客应用程序中,actionShow()方法处理特定博客文章条目的请求。它使用查询字符串变量id来确定请求的特定文章。它要求模型检索有关博客文章条目编号 99 的信息。模型 AR 类与数据库交互以检索所请求的数据。在从模型检索数据后,我们的控制器通过使其可用于视图来进一步准备数据以供显示。视图然后负责处理数据布局,并向浏览器提供响应以供用户显示。
这种 MVC 架构允许我们将数据呈现与数据操作、验证和其他应用程序业务逻辑分开。这使得开发人员非常容易改变应用程序的各个方面而不影响 UI,UI 设计人员也可以自由地进行更改而不影响模型或业务逻辑。这种分离还使得非常容易提供同一模型代码的多个呈现方式。例如,您可以使用驱动http://yourblog.com的 HTML 布局的相同模型代码来驱动 RIA 呈现、移动应用程序、Web 服务或命令行界面。最终,遵循这些约定并分离功能将导致一个更容易扩展和维护的应用程序。
Yii 做了很多工作来帮助您执行这种分离,不仅仅提供一些命名约定和代码放置建议。它有助于处理所有需要将所有部分粘合在一起的低级"胶水"代码。这使您能够在不必自己编写所有细节的情况下获得严格的 MVC 设计应用程序的好处。让我们来看看其中一些低级细节。
对象关系映射和 Active Record
在很大程度上,我们构建的 Web 应用程序将其数据存储在关系数据库中。我们在上一个示例中使用的博客帖子应用程序将博客帖子内容存储在数据库表中。然而,Web 应用程序需要将持久数据库存储中的数据映射到定义域对象的内存类属性中。对象关系映射(ORM)库提供了将数据库表映射到域对象类的功能。
处理 ORM 的大部分代码都是关于描述数据库中的字段如何对应到我们内存对象的属性,并且编写起来是乏味和重复的。幸运的是,Yii 通过提供 Active Record(AR)模式的 ORM 层来拯救我们,使我们免受这种重复和乏味。
Active Record
如前所述,AR 是一种用于以面向对象的方式抽象数据库访问的设计模式。它将表映射到类,行映射到对象,列映射到类属性。换句话说,每个 Active Record 类的实例代表数据库表中的一行。然而,AR 类不仅仅是一组属性,这些属性映射到数据库表中的列。它还包含应用于该数据的必要业务逻辑。最终结果是一个定义了如何写入和从数据库中读取的类。
通过依赖约定并坚持合理的默认设置,Yii 对 AR 的实现将节省开发人员大量时间,否则可能会花在配置上,或者编写创建、读取、更新和删除数据所需的乏味重复的 SQL 语句上。它还允许开发人员以面向对象的方式访问存储在数据库中的数据。为了说明这一点,让我们再次以我们虚构的博客示例为例。以下是一些使用 AR 操作特定博客帖子的示例代码,其内部 ID(也用作表的主键)为99。它首先通过使用主键检索帖子。然后更改标题并更新数据库以保存更改:
$post=Post::model()->findByPk(99);
$post->title='Some new title';
$post->save();
Active Record 完全解除了我们编写任何 SQL 代码或以其他方式处理底层数据库的乏味。
实际上,Yii 中的 Active Record 甚至做得更多。它与 Yii 框架的许多其他方面无缝集成。有许多"活动"HTML 助手输入表单字段直接与它们各自的 AR 类属性相关联。通过这种方式,AR 直接提取输入表单字段的值到模型中。它还支持复杂的自动数据验证,如果验证失败,Yii 视图类可以轻松地将验证错误显示给最终用户。我们将在本书中多次重新访问 AR 并提供具体示例。
视图和控制器
视图和控制器非常密切相关。控制器使数据可供视图显示,视图生成页面触发事件,将数据发送到控制器。
在 Yii 中,视图文件属于呈现它的控制器类。通过这种方式,我们可以在视图脚本中简单地引用$this来访问控制器实例。这种实现方式使视图和控制器非常密切。
当涉及到 Yii 控制器时,故事远不止调用模型和渲染视图那么简单。控制器可以管理服务,以提供对请求的复杂预处理和后处理,实现基本的访问控制规则以限制对某些操作的访问,管理应用程序范围的布局和嵌套布局文件的渲染,管理数据的分页,以及许多其他幕后服务。再次感谢 Yii,让我们不必为这些混乱的细节而费心。
Yii 有很多内容。探索它所有美丽的最佳方式就是开始使用它。现在我们已经掌握了一些非常基本的想法和术语,我们有很好的条件来做到这一点。
总结
在本章中,我们在很高的层次上介绍了 Yii PHP Web 应用程序框架。我们还涵盖了 Yii 所采用的许多软件设计概念。如果你对这次初步讨论的抽象性有些困惑,不要担心。一旦我们深入具体的例子,一切都会变得清晰起来。但总结一下,我们具体涵盖了:
-
应用程序开发框架的重要性和实用性
-
Yii 是什么,以及使 Yii 变得非常强大和有用的特点。
-
MVC 应用程序架构以及在 Yii 中实现此架构
-
典型的 Yii Web 请求生命周期和 URL 结构
-
Yii 中的对象关系映射和 Active Record
在下一章中,我们将通过简单的 Yii 安装过程,并开始构建一个工作应用程序,以更好地阐述所有这些想法。
第二章:入门
通过简单地使用 Yii,我们很快就能发现 Yii 的真正乐趣和好处。在本章中,我们将看到在一个示例 Yii 应用程序中,前一章介绍的概念是如何体现的。遵循 Yii 的约定优于配置的理念,我们将按照标准约定开始编写一个 Yii 中的“Hello, World!”程序。
在本章中,我们将涵盖:
-
Yii 框架安装
-
创建一个新的应用程序
-
创建控制器和视图
-
向视图文件添加动态内容
-
Yii 请求路由和链接页面
我们的第一步是安装框架。现在让我们来做吧。
安装 Yii
在安装 Yii 之前,您必须将应用程序开发环境配置为支持 PHP 5.1.0 或更高版本的 Web 服务器。Yii 已经在 Windows 和 Linux 操作系统上的 Apache HTTP 服务器上进行了彻底测试。它也可以在支持 PHP 5 的其他 Web 服务器和平台上运行。我们假设读者以前已经参与过 PHP 开发,并且可以访问或者知道如何设置这样的环境。我们将把 Web 服务器和 PHP 本身的安装留给读者自己去练习。
注意
一些流行的安装包包括
基本的 Yii 安装几乎是微不足道的。实际上只有两个必要的步骤:
-
从
www.yiiframework.com/download/下载 Yii 框架。 -
将下载的文件解压到可通过 Web 访问的目录。在下载框架时,可以选择几个版本的 Yii。在本书的目的中,我们将使用 1.1.12 版本,这是写作时的最新稳定版本。虽然大多数示例代码应该适用于任何 1.1.x 版本的 Yii,但如果您使用不同版本可能会有一些细微差异。如果您正在跟随示例,请尝试使用 1.1.12 版本。
在下载了框架文件并将其解压到可通过 Web 访问的目录后,列出其内容。您应该看到以下高级目录和文件:
-
CHANGELOG -
LICENSE -
README -
UPGRADE -
demos/ -
framework/ -
requirements/
现在我们已经在可通过 Web 访问的目录中解压了我们的框架,建议您验证服务器是否满足使用 Yii 的所有要求,以确保安装成功。幸运的是,这样做非常容易。Yii 带有一个简单的要求检查工具。要使用该工具并让其验证您的安装要求,只需将浏览器指向所下载文件中的requirements/目录下的index.php入口脚本。例如,假设包含所有框架文件的目录的名称只是叫做yii,那么访问要求检查器的 URL 可能如下所示:
http://localhost/yii/requirements/index.php
以下屏幕截图显示了我们配置的结果:
使用要求检查器本身并不是安装的要求。但建议使用它来确保正确安装。正如您所看到的,我们在详细部分的结果并非全部都是通过状态,有些显示警告结果。当然,您的配置很可能与我们的略有不同,因此您的结果也可能略有不同。这没关系。并不是所有详细部分的检查都必须通过,但是必须在结论部分收到以下消息:您的服务器配置满足 Yii 的最低要求。
提示
Yii 框架文件不需要被放置在公开访问的 web 目录中,建议不要这样做。我们在这里这样做只是为了快速利用浏览器中的要求检查器。Yii 应用程序有一个入口脚本,通常是唯一需要放置在 web 根目录中的文件(web 根目录指的是包含index.php入口脚本的目录)。其他 PHP 脚本,包括所有的 Yii 框架文件,应该受到保护,以避免安全问题。只需在入口脚本中引用包含 Yii 框架文件的目录,并将这些文件放在 web 根目录之外。
安装数据库
在本书中,我们将使用数据库来支持许多示例和我们将要编写的应用程序。为了正确地跟随本书,建议你安装一个数据库服务器。虽然你可以使用 Yii 支持的任何数据库,如果你想使用 Yii 内置的数据库抽象层和工具,就像我们将要使用的那样,你需要使用框架支持的数据库。截至 1.1 版本,支持的数据库有:
-
MySQL 4.1 或更高版本
-
PostgresSQL 7.3 或更高版本
-
SQLite 2 和 3
-
Microsoft SQL Server 2000 或更高版本
-
Oracle
提示
虽然你可以使用任何受支持的数据库服务器来跟随本书中的所有示例,但我们将在所有示例中使用 MySQL(具体来说是 5.1)作为我们的数据库服务器。建议你也使用 MySQL,版本为 5 或更高,以确保所提供的示例可以正常工作而无需进行调整。在本章中,我们的简单的“Hello, World!”应用程序不需要数据库。
现在我们已经安装了框架并验证了我们已满足最低要求,让我们继续创建一个全新的 Yii web 应用程序。
创建一个新应用程序
为了创建一个新的应用程序,我们将使用一个随框架捆绑的强大工具,称为yiic。这是一个命令行工具,你可以用它快速引导一个全新的 Yii 应用程序。使用这个工具并不是强制的,但它可以节省时间,并保证应用程序有一个正确的目录和文件结构。
要使用这个工具,打开你的命令行,并导航到你的文件系统中你想要创建应用程序目录结构的地方。为了这个演示应用程序的目的,我们假设以下情况:
-
YiiRoot是你安装 Yii 框架文件的目录的名称 -
WebRoot被配置为你的 web 服务器的文档根目录
从命令行中,切换到你的WebRoot目录并执行yiic命令:
% cd WebRoot
% YiiRoot/framework/yiic webapp helloworld
Create a Web application under '/Webroot/helloworld'? [Yes|No]
Yes
mkdir /WebRoot/helloworld
mkdir /WebRoot/helloworld/assets
mkdir /WebRoot/helloworld/css
generate css/bg.gif
generate css/form.css
generate css/main.css
Your application has been created successfully under /Webroot/helloworld.
注意
yiic命令可能不会按预期工作,特别是如果你尝试在 Windows 环境中使用它。yiic文件是一个可执行文件,使用你的命令行版本的 PHP 来运行。它调用yiic.php脚本。你可能需要在前面使用php来完全限定,如$ php yiic或$ php yiic.php。你可能还需要指定要使用的 PHP 可执行文件,比如C:\PHP5\php.exe yiic.php。还有yiic.bat文件,它执行yiic.php文件,可能更适合 Windows 用户。你可能需要确保你的 PHP 可执行文件位置在你的%PATH%变量中是可访问的。请尝试这些变化,找到适合你计算机配置的解决方案。我将继续简单地称这个命令为yiic。
yiic webapp命令用于创建一个全新的 Yii web 应用程序。它只需要一个参数来指定应用程序应该被创建的目录的绝对或相对路径。结果是生成所有必要的目录和文件,用于提供默认 Yii web 应用程序的框架。
让我们列出我们的新应用程序的内容,看看为我们创建了什么:
assets/ images/ index.php themes/
css/ index-test.php protected/
以下是这些高级项目的描述,这些项目是自动创建的:
-
index.php: Web 应用程序入口脚本文件 -
index-test.php: 用于加载测试配置的入口脚本文件 -
assets/: 包含发布的资源文件 -
css/: 包含 CSS 文件 -
images/: 包含图像文件 -
themes/: 包含应用程序主题 -
protected/: 包含受保护的(非公开的)应用程序文件
通过一条简单的命令行命令的执行,我们已经创建了所有所需的目录结构和文件,以立即利用 Yii 的合理默认配置。这些目录和文件,以及它们包含的子目录和文件,乍一看可能有点令人生畏。然而,我们在开始时可以忽略大部分内容。重要的是要注意,所有这些目录和文件实际上都是一个工作的 Web 应用程序。yiic命令已经填充了应用程序足够的代码,以建立一个简单的首页,一个典型的联系我们页面,以提供一个 Web 表单的示例,以及一个登录页面,以演示 Yii 中的基本授权和认证。如果您的 Web 服务器支持 GD2 图形库扩展,您还将在联系我们表单上看到一个 CAPTCHA 小部件,并且应用程序将对该表单字段进行相应的验证。
只要您的 Web 服务器正在运行,您就应该能够打开浏览器并导航到http://localhost/helloworld/index.php。在这里,您将看到一个我的 Web 应用程序首页,以及友好的问候语欢迎来到我的 Web 应用程序,接着是一些有用的下一步信息。以下截图显示了这个示例首页:
注意
您需要确保assets/和protected/runtime/目录对您的 Web 服务器进程是可写的,否则您可能会看到一个错误而不是工作应用程序。
您会注意到页面顶部有一个可用的应用程序导航栏。从左到右依次是主页、关于、联系和登录。点击并探索。点击关于链接提供了一个静态页面的简单示例。联系链接将带您到之前提到的联系我们表单,以及表单中的 CAPTCHA 输入字段。(再次强调,只有在您的 PHP 配置中有gd图形扩展时,您才会看到 CAPTCHA 字段。)
登录链接将带您到显示登录表单的页面。这是一个带有表单验证的工作代码,以及用户名和密码的验证和认证。使用demo/demo或admin/admin作为用户名/密码组合将使您登录到网站。试试看!您可以尝试一个将失败的登录(除了 demo/demo 或 admin/admin 之外的任何组合),并查看错误验证消息的显示。成功登录后,页眉中的登录链接将更改为注销链接(用户名),其中用户名是 demo 或 admin,具体取决于您用于登录的用户名。令人惊讶的是,所有这些都可以在不编写任何代码的情况下完成。
"你好,世界!"
一旦我们通过一个简单的示例走过,所有这些生成的代码将开始变得更加清晰。为了尝试这个新系统,让我们构建在本章开头承诺的“你好,世界!”程序。在 Yii 中,“你好,世界!”程序将是一个向我们的浏览器发送这条非常重要消息的简单 Web 页面应用程序。
如在第一章中讨论的那样,遇见 Yii,Yii 是一个模型-视图-控制器框架。一个典型的 Yii web 应用程序接收用户的传入请求,处理该请求中的信息以创建一个控制器,然后调用该控制器中的一个动作。控制器可以调用特定的视图来渲染并返回响应给用户。如果涉及数据,控制器还可以与模型交互,处理数据的所有CRUD(创建,读取,更新,删除)操作。在我们简单的“你好,世界!”应用程序中,我们只需要控制器和视图的代码。我们不涉及任何数据,因此不需要模型。让我们通过创建我们的控制器来开始我们的示例。
创建控制器
以前,我们使用yiic webapp命令来帮助我们生成一个新的 Yii web 应用程序。为了为我们的“你好,世界!”应用程序创建一个新的控制器,我们将使用 Yii 提供的另一个实用工具。这个工具叫做 Gii。Gii是一个高度可定制和可扩展的基于 Web 的代码生成平台。
配置 Gii
在使用 Gii 之前,我们必须在应用程序中对其进行配置。我们在位于protected/config/main.php的主应用程序配置文件中进行配置。要配置 Gii,打开此文件并取消注释gii模块。我们的自动生成的代码已经添加了gii配置,但它被注释掉了。因此,我们只需要取消注释,然后还要添加我们自己的密码,如下面的代码片段所示:
return array(
'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..',
'name'=>'My Web Application',
// preloading 'log' component
'preload'=>array('log'),
// autoloading model and component classes
'import'=>array(
'application.models.*',
'application.components.*',
),
**'modules'=>array(**
**// uncomment the following to enable the Gii tool**
**/***
**'gii'=>array(**
**'class'=>'system.gii.GiiModule',**
**'password'=>'Enter Your Password Here',**
**// If removed, Gii defaults to localhost only. Edit carefully to taste.**
**'ipFilters'=>array('127.0.0.1','::1'),**
**),**
***/**
**),**
取消注释后,Gii 被配置为一个应用程序模块。我们将在本书后面详细介绍 Yii 模块。此时的重要事情是确保将其添加到配置文件中,并提供您的密码。有了这个配置,通过http://localhost/helloworld/index.php?r=gii导航到工具。
注意
实际上,您可以将密码值指定为false,然后模块将不需要密码。由于 ipFilters 属性被指定为仅允许访问本地主机,因此在本地开发环境中将密码设置为false是安全的。
好的,在成功输入密码后(除非您指定不使用密码),您将看到列出 Gii 主要功能的菜单页面:
Gii 在左侧菜单中列出了几个代码生成选项。我们想要创建一个新的控制器,所以点击控制器生成器菜单项。
这样做将带我们到一个表单,允许我们填写相关细节以创建一个新的 Yii 控制器类。在下面的屏幕截图中,我们已经填写了控制器 ID值为message,并且我们添加了一个我们称之为hello的Action ID值。下面的屏幕截图还反映了我们已经点击了预览按钮。这显示了将与我们的控制器类一起生成的所有文件:
我们可以看到,除了我们的MessageController类之外,Gii 还将为我们指定的每个 Action ID 创建一个视图文件。您可能还记得第一章中提到的,如果message是控制器 ID,我们对应的类文件名为MessageController。同样,如果我们提供了hello的Action ID值,我们期望在控制器中有一个名为actionHello的方法。
您还可以单击预览选项中提供的链接,以查看将为每个文件生成的代码。继续并查看它们。一旦您对即将生成的内容感到满意,请点击生成按钮。您应该收到一条消息,告诉您控制器已成功创建,并附有立即尝试的链接。如果您收到错误消息,请确保controllers/和views/目录对您的 Web 服务器进程是可写的。
单击立即尝试链接实际上会将我们带到一个404 页面未找到错误页面。原因是我们在创建新控制器时没有指定默认的 actionID index。我们决定将我们的称为hello。为了使请求路由到我们的actionHello()方法,我们只需要将 actionID 添加到 URL 中。如下截图所示:
现在它显示了调用MessageController::actionHello()方法的结果。
这很棒。在 Gii 的帮助下,我们生成了一个名为MessageController.php的新控制器 PHP 文件,并将其正确放置在默认控制器目录protected/controllers/下。生成的MessageController类扩展了一个名为Controller的应用基类,位于protected/components/Controller.php中,而这个类又扩展了基础框架类CController。由于我们指定了 actionID hello,因此在MessageController中还创建了一个名为actionHello()的简单操作。Gii 还假定,像大多数由控制器定义的操作一样,此操作将需要呈现一个视图。因此,它添加了呈现同名视图文件hello.php的代码到此方法中,并将其放置在默认目录protected/views/message/中,用于与此控制器相关的视图文件。以下是为MessageController类生成的未注释部分代码:
<?php
class MessageController extends Controller
{
public function actionHello()
{
$this->render('hello');
}
正如我们所看到的,由于我们在使用 Gii 创建此控制器时没有指定'index'作为 actionID 之一,因此没有actionIndex()方法。正如在第一章中讨论的那样,按照约定,指定控制器 ID 为消息,但未指定操作的请求将被路由到actionIndex()方法进行进一步处理。这就是为什么我们最初看到 404 错误的原因,因为请求没有指定 actionID。
让我们花点时间来修复这个问题。正如我们所提到的,Yii 更青睐于约定而不是配置,并且几乎所有内容都有合理的默认值。同时,几乎所有内容也是可配置的,默认控制器操作也不例外。通过在我们的MessageController顶部添加一行简单的代码,我们可以将actionHello()方法定义为默认操作。在MessageController类的顶部添加以下行:
<?php
class MessageController extends Controller
{
**public $defaultAction = 'hello';**
提示
下载示例代码
您可以从您在www.PacktPub.com购买的所有 Packt 图书的帐户中下载示例代码文件。如果您在其他地方购买了本书,您可以访问www.PacktPub.com/support注册并直接通过电子邮件接收文件。
尝试通过导航到http://localhost/helloworld/index.php?r=message来测试。您应该仍然看到显示hello action页面,不再看到错误页面。
最后一步
要将其转换为“Hello, World!”应用程序,我们只需要自定义我们的hello.php视图以显示“Hello, World!”。这样做很简单。编辑文件protected/views/message/hello.php,使其只包含以下代码:
<?php
<h1>Hello World!</h1>
保存它,并在浏览器中再次查看:http://localhost/helloworld/index.php?r=message。
现在它显示了我们的介绍性问候,如下截图所示:
我们的简单应用程序只需极少的代码就可以运行。我们只需在hello.php视图文件中添加了一行 HTML。
注意
您可能想知道所有其他 HTML 是如何/在哪里生成的。我们基本的hello.php视图文件只包含一个带有<h1>标签的单行。当我们在控制器中调用render()时,也会应用布局视图文件。现在不需要太担心这一点,因为我们将在以后更详细地介绍布局。但是如果您感兴趣,您可以查看protected/views/layouts/目录,看看已经定义的布局文件,并帮助您了解其他 HTML 的定义位置。
审查我们的请求路由
让我们回顾一下 Yii 如何在这个示例应用程序的上下文中分析我们的请求:
-
通过将浏览器指向 URL
http://localhost/helloworld/index.php?r=message(或者您可以使用等效的 URLhttp://localhost/helloworld/index.php?r=message/hello)来导航到“Hello, World!”页面。 -
Yii 分析 URL。r(路由)查询字符串变量表示 controllerID 是
message。这告诉 Yii 将请求路由到 message 控制器类,它在protected/controllers/MessageController.php中找到。 -
Yii 还发现指定的 actionID 是
hello。(或者如果没有指定 actionID,它会路由到控制器的默认动作。)因此,在MessageController中调用actionHello()方法。 -
actionHello()方法呈现位于protected/views/message/hello.php的hello.php视图文件。我们修改了这个视图文件,只是简单地显示我们的问候语,然后返回给浏览器。
这一切都是非常轻松地组合在一起的。通过遵循 Yii 的默认约定,整个应用程序请求路由已经无缝地为我们拼接在一起。当然,Yii 给了我们每一个机会来覆盖这个默认的工作流程,但是你越是遵循约定,你就会花费越少的时间在调整配置代码上。
添加动态内容
向我们的视图模板添加动态内容的最简单方法是将 PHP 代码嵌入到模板本身中。视图文件由我们的简单应用程序呈现为 HTML,这些文件中的任何基本文本都会被传递而不会被更改。但是,任何在<?php和?>之间的内容都会被解释和执行为 PHP 代码。这是 PHP 代码嵌入 HTML 文件的典型方式,可能对您来说很熟悉。
添加日期和时间
为了给我们的页面增添动态内容,让我们显示日期和时间:
- 再次打开 hello 视图,并在问候文本下面添加以下行:
<h3><?php echo date("D M j G:i:s T Y"); ?></h3>
- 保存和查看:
http://localhost/helloworld/index.php?r=message/hello。
哎呀!我们已经在我们的应用程序中添加了动态内容。每次刷新页面,我们都可以看到显示的内容在变化。
诚然,这并不是非常令人兴奋,但它确实向您展示了如何将简单的 PHP 代码嵌入到我们的视图模板中。
添加日期和时间的另一种方法
尽管这种直接将 PHP 代码嵌入视图文件的方法允许任意数量或复杂度的 PHP 代码,但强烈建议这些语句不要改变数据模型,并保持简单、面向显示的语句。这将有助于将我们的业务逻辑与我们的呈现代码分开,这是使用 MVC 架构的好处之一。
将数据创建移到控制器
让我们将创建时间的逻辑移回到控制器,并且让视图什么都不做,只是显示时间。我们将时间的确定放到控制器中的actionHello()方法中,并在一个名为$time的实例变量中设置值。
首先让我们修改控制器动作。目前我们在MessageController中的动作actionHello(),只是通过执行以下代码来调用渲染我们的 hello 视图:
$this->render('hello');
在我们渲染视图之前,让我们添加调用来确定时间,然后将其存储在一个名为$theTime的局部变量中。然后我们通过添加第二个参数来修改我们对render()的调用,其中包括这个变量:
$theTime = date("D M j G:i:s T Y");
$this->render('hello',array('time'=>$theTime));
当调用render()并带有包含数组数据的第二个参数时,它将把数组的值提取到 PHP 变量中,并使这些变量可用于视图脚本。数组中的键将是可用于我们视图文件的变量的名称。因此,在这个例子中,我们的数组键'time',其值是$theTime,将被提取到一个名为$time的变量中,并在视图中可用。这是一种从控制器传递数据到视图的方法。
注意
这假设您正在使用 Yii 的默认视图渲染器。正如之前多次提到的,Yii 允许您自定义几乎所有内容,如果您愿意,您可以指定不同的视图渲染实现。其他视图渲染可能不会以完全相同的方式行事。
现在让我们修改视图,使用这个$time变量而不是直接调用日期函数本身:
- 再次打开 HelloWorld 视图文件,并用以下内容替换我们之前添加的用于输出时间的行:
<h3><?php echo $time; ?></h3>
- 再次保存并查看结果:
http://localhost/helloworld/index.php?r=message/hello
我们再次看到时间显示与之前完全相同,因此两种方法的最终结果没有任何不同。
我们已经演示了向视图模板文件添加 PHP 生成内容的两种方法。第一种方法将数据创建逻辑直接放入视图文件本身。第二种方法将这个逻辑放在控制器类中,并通过使用变量将信息传递给视图文件。最终结果是相同的;时间显示在我们渲染的 HTML 中,但第二种方法在保持数据获取和处理(即业务逻辑)与我们的呈现代码分离方面迈出了一小步。这种分离正是模型-视图-控制器架构努力提供的,Yii 的显式目录结构和合理的默认值使其易于实现。
你有在关注吗?
在第一章中提到过,视图和控制器确实是非常相似的。在视图文件中,$this指的是渲染视图的Controller类。
在前面的例子中,我们通过在 render 方法中使用第二个参数,明确地从控制器向视图文件提供了时间。这第二个参数明确地设置了立即可用于视图文件的变量。但是还有另一种方法可以尝试一下。
通过在MessageController上定义一个公共类属性,而不是一个局部作用域的变量,其值是当前日期时间,来修改前面的例子。然后通过$this访问这个类属性,在视图文件中显示时间。
注意
可下载的代码库中包含了这个“自己动手”的练习的解决方案。
链接页面
典型的 Web 应用程序中有多个页面供用户体验,我们简单的应用程序也不例外。让我们添加另一个页面,显示来自世界的响应,“再见,Yii 开发者!”并从我们的“Hello, World!”页面链接到这个页面,反之亦然。
通常,在 Yii web 应用程序中,每个渲染的 HTML 页面都对应一个单独的视图(尽管这并不总是必须的)。因此,我们将创建一个新视图,并使用一个单独的操作方法来渲染这个视图。在添加新页面时,我们还需要考虑是否使用单独的控制器。由于我们的 Hello 和 Goodbye 页面是相关的并且非常相似,目前没有必要将应用程序逻辑委托给单独的控制器类。
链接到新页面
让我们的新页面的 URL 形式为http://localhost/helloworld/index.php?r=message/goodbye。
遵循 Yii 的约定,这个决定定义了我们的操作方法的名称,我们需要在控制器中使用,以及我们的视图的名称。因此,打开MessageController并在我们的actionHello()操作的下面添加一个actionGoodbye()方法:
class MessageController extends Controller
{
...
public function actionGoodbye()
{
$this->render('goodbye');
}
...
}
接下来,我们需要在/protected/views/message/目录中创建我们的视图文件。这应该被称为goodbye.php,因为它应该与我们选择的 actionID 相同。
注意
请记住,这只是一个推荐的约定。视图不一定必须与操作具有相同的名称。视图文件名只需与render()的第一个参数匹配即可。
在该目录中创建一个空文件,并添加一行:
<h1>Goodbye, Yii developer!</h1>
再次保存和查看http://localhost/helloworld/index.php?r=message/goodbye将显示再见消息。
现在我们需要添加链接来连接这两个页面。要在 Hello 页面上添加到 Goodbye 页面的链接,我们可以直接在hello.php视图文件中添加<a>标签,并硬编码 URL 结构如下:
<a href="/helloworld/index.php?r=message/goodbye">Goodbye!</a>
这样做可以,但它将视图代码实现紧密耦合到特定的 URL 结构,这可能在某个时候发生变化。如果 URL 结构发生变化,这些链接将变得无效。
注意
还记得在第一章 遇见 Yii中,我们通过博客发布应用程序示例吗?我们使用的 URL 格式与 Yii 默认格式不同,更符合 SEO,即:
http://yourhostname/controllerID/actionID
将 Yii Web 应用程序配置为使用这种“路径”格式而不是我们在此示例中使用的查询字符串格式是一件简单的事情。能够轻松更改 URL 格式对 Web 应用程序非常重要。只要我们避免在整个应用程序中硬编码它们,更改它们将保持简单,只需更改应用程序配置文件即可。
从 Yii CHtml 获得一点帮助
幸运的是,Yii 在这里提供了帮助。Yii 带有许多可以在视图模板中使用的辅助方法。这些方法存在于静态 HTML 辅助框架类CHtml中。在这种情况下,我们想要使用的是“link”辅助方法,它接受一个controllerID/actionID对,并根据应用程序配置的 URL 结构为您创建适当的超链接。由于所有这些辅助方法都是静态的,我们可以直接调用它们,而无需创建CHtml类的显式实例。使用这个链接助手,我们可以在我们的hello.php视图中在我们输出时间的下面添加一个链接,如下所示:
<p><?php echo CHtml::link('Goodbye'array('message/goodbye')); ?></p>
保存并查看“Hello, World!”页面:http://localhost/helloworld/index.php?r=message/hello
您应该看到超链接,并单击它应该将您带到再见页面。调用link方法的第一个参数是将显示在超链接中的文本。第二个参数是一个包含我们的controllerID/actionID对值的数组。
我们可以采用相同的方法在我们的 Goodbye 视图中放置一个相互链接:
<h1>Goodbye, Yii developer!</h1>
<p><?php echo CHtml::link('Hello',array('message/hello')); ?></p>
保存并查看再见页面:
http://localhost/helloworld/index.php?r=message/goodbye
现在,您应该看到从再见页面返回到“Hello, World!”页面的活动链接。
所以我们现在知道了在我们的简单应用程序中链接网页的几种方法。一种方法是直接在视图文件中添加 HTML <a>标签,并硬编码 URL 结构。另一种更常用的方法是利用 Yii 的CHtml辅助类来帮助构建基于controllerID /actionID对的 URL,以便结果格式始终符合应用程序配置。通过这种方式,我们可以轻松地在整个应用程序中更改 URL 格式,而无需返回更改每个视图文件,这些文件恰好具有内部链接。
我们简单的“Hello, World!”应用程序真正受益于 Yii 的约定优于配置的理念。通过应用某些默认行为并遵循推荐的约定,这个简单应用程序的构建和整个请求路由过程都以非常简单和方便的方式完成了。
总结
在本章中,我们构建了一个极其简单的应用程序,以涵盖许多主题。首先我们安装了框架。然后我们使用yiic控制台命令来引导创建一个新的 Yii 应用程序。然后我们介绍了一个非常强大的代码生成工具叫做 Gii。我们使用它在我们的简单应用程序中创建了一个新的控制器。
一旦我们的应用程序就位,我们就可以亲自看到 Yii 如何处理请求和路由到控制器和动作。然后,我们继续创建和显示非常简单的动态内容。最后,我们看了一下如何在 Yii 应用程序中链接页面。
虽然这个非常简单的应用程序为我们提供了具体的例子,帮助我们更好地理解 Yii 框架的使用,但它过于简单,无法展示 Yii 在简化实际应用程序构建方面的能力。为了证明这一点,我们需要构建一个真实的 Web 应用程序。我们将会这样做。在下一章中,我们将向您介绍项目任务和问题跟踪应用程序,我们将在本书的其余部分中构建该应用程序。
第三章:TrackStar 应用程序
我们可以继续不断向我们简单的“Hello, World!”应用程序添加 Yii 的功能示例,但这并不会真正帮助理解框架在真实应用程序的上下文中。为了做到这一点,我们需要朝着更接近 Web 开发人员实际需要构建的应用程序类型的方向发展。这正是我们将在本书的其余部分中要做的事情。
在本章中,我们将介绍名为 TrackStar 的项目任务跟踪应用程序。世界上有许多其他项目管理和问题跟踪应用程序,我们的基本功能与许多这些应用程序并无不同。那么,为什么要构建它呢?事实证明,这种基于用户的应用程序具有许多对许多 Web 应用程序都是常见的功能。这将使我们能够实现两个主要目标:
-
展示 Yii 作为我们构建有用功能和征服真实世界 Web 应用程序挑战的不可思议的实用性和功能集
-
提供现实世界的示例和方法,这些方法将立即适用于您的下一个 Web 应用程序项目
介绍 TrackStar
TrackStar 是一个软件开发生命周期(SDLC)问题管理应用程序。它的主要目标是帮助跟踪在构建软件应用程序过程中出现的许多问题。它是一个基于用户的应用程序,允许创建用户帐户并在用户经过身份验证和授权后访问应用程序功能。它允许用户添加和管理项目。
项目可以与其关联的用户(通常是项目上工作的团队成员)以及问题相关联。项目问题将是开发任务和应用程序错误等事物。问题可以分配给项目的成员,并且将具有尚未开始,已开始和已完成等状态。通过这种方式,跟踪工具可以准确描述项目的情况,包括已完成的工作,当前正在进行的工作以及尚未开始的工作。
创建用户故事
简单的用户故事是识别应用程序必要功能功能的好方法。用户故事以最简单的形式陈述用户可以使用软件做什么。它们应该从简单开始,并随着您深入了解每个功能周围的细节而变得更加复杂。我们的目标是从足够的复杂性开始,以便我们可以开始。如果有必要,我们将稍后添加更多细节。
我们简要介绍了在这个应用程序中扮演重要角色的三个主要实体,即用户,项目和问题。这些是我们的主要领域对象,在这个应用程序中非常重要。所以让我们从它们开始。
用户
TrackStar 是一个基于用户的 Web 应用程序。在高层次上,用户可以处于两种用户状态中的一种。
-
匿名
-
经过身份验证
匿名用户是应用程序的任何未经过登录过程认证的用户。匿名用户只能访问注册新帐户或登录。所有其他功能将受限于经过身份验证的用户。
经过身份验证的用户是通过登录过程提供有效身份验证凭据的用户。换句话说,经过身份验证的用户是已登录的用户。经过身份验证的用户将可以访问应用程序的主要功能功能,如创建和管理项目以及项目问题。
项目
管理项目是 TrackStar 应用程序的主要目的。项目代表一个由应用程序的一个或多个用户实现的一般高层目标。项目通常被分解为更细粒度的任务或问题,这些任务或问题代表需要采取的更小步骤以实现整体目标。
举个例子,让我们以本书中将要做的事情为例,即构建一个项目和问题跟踪管理应用程序。不幸的是,我们无法使用尚未创建的应用程序来帮助我们跟踪其自身的开发,但如果可以的话,我们可能会创建一个名为“构建 TrackStar 项目/问题管理工具”的项目。该项目将被细分为更详细的项目问题,例如“创建登录界面”,“为问题设计数据库模式”等等。
经过身份验证的用户可以创建新项目。账户内项目的创建者将在该项目中拥有称为项目所有者的特殊角色。项目所有者有权编辑和删除这些项目,以及向项目添加新成员。除项目所有者之外与项目相关的其他用户简称为项目成员。项目成员将有添加新问题以及编辑现有问题的权限。
问题
项目问题将被分类为三个类别之一:
-
特性:代表要添加到应用程序中的实际功能的项目。例如,登录功能的实施。
-
任务:代表需要完成的工作,但不是软件的实际功能。例如,设置构建和集成服务器。
-
错误:代表应用程序行为不如预期工作的项目。例如,账户注册表格未验证输入电子邮件地址的格式。
问题可以处于以下三种状态之一:
-
尚未开始
-
已开始
-
已完成
项目成员可以向项目添加新问题,以及编辑和删除它们。他们可以将问题分配给自己或其他项目成员。
目前,这些三个主要实体的信息足够让我们继续前进。我们可以详细了解“账户注册具体包括什么?”或者“如何向项目添加新任务?”但我们已经概述了足够的规格以开始这些基本功能。随着实施的进行,我们将确定更详细的细节。
但在我们开始之前,我们应该记下一些基本的导航和应用程序工作流程。这将帮助每个人更好地理解我们正在构建的应用程序的一般布局和流程。
导航和页面流程
总是很好地概述应用程序中的主要页面,并查看它们如何配合。这将帮助我们快速确定一些需要的 Yii 控制器、操作和视图,以及帮助设定每个人对我们在开发初期将要构建的期望。
以下图表显示了基本的应用程序流程,从登录到项目详情列表:
当用户首次进入应用程序时,他们必须先登录并进行身份验证,然后才能继续使用任何功能。成功登录后,他们将看到他们当前项目的列表,以及创建新项目的功能。选择特定项目将带他们进入项目详情页面。项目详情页面将展示按类型列出的问题列表。还可以添加新问题,以及编辑列出的任何问题。
这都是非常基本的功能,但这个图表为我们提供了关于应用程序如何组合在一起的更多信息,并且让我们更好地开始确定我们需要的模型、视图和控制器。它还允许与他人分享一些可视化的东西,以便每个参与者对我们正在努力实现的目标有相同的理解。根据我的经验,几乎每个人在首次思考新应用程序时都更喜欢图片而不是书面规格。
数据关系
我们在开始朝着这些规格构建之前,仍然需要更多地考虑我们将要处理的数据。如果我们从系统中挑选出所有的主要名词,我们可能会得到一个相当不错的领域对象列表,通过使用活动记录,我们想要建模的数据也会得到延伸。我们之前概述的用户故事确定了以下内容:
-
一个用户
-
一个项目
-
一个问题
基于这一点以及用户故事和应用程序工作流程图中提供的其他细节,我们在以下图表中展示了对必要数据模型的第一次尝试:
这是一个非常基本的对象模型,概述了我们的主要数据实体、它们各自的属性以及它们之间的一些关系。在项目和用户对象之间的线的两侧的 1..*和 0..*表示它们之间存在多对多的关系。一个用户可以与零个或多个项目相关联,一个项目可以有一个或多个用户。同样地,我们表示了一个项目可以有零个或多个与之相关的问题,而一个问题只属于一个特定的项目。此外,一个用户可以是许多问题的所有者(或请求者),但一个问题只有一个所有者(也只有一个请求者)。
在这个阶段,我们尽可能地保持属性的简单。用户需要用户名和密码才能通过登录界面。项目只有一个名称属性。
根据我们目前所知的信息,问题具有最多的相关信息。正如在之前定义的用户故事中简要讨论的,问题将具有一个类型属性,用于区分一般类别(错误、功能或任务)。它们还将具有一个状态属性,用于指示正在处理的问题的进展。将有一个已登录的用户最初创建问题;这是请求者。一旦系统中的用户被分配来处理问题,他们将成为问题的所有者。我们还定义了描述属性,以允许输入问题的一些描述性文本。
请注意,我们还没有明确讨论模式或数据库。事实上,直到我们仔细考虑从数据角度真正需要什么,我们才会知道用来存储这些数据的正确工具。文件系统上的平面文件是否和关系数据库一样有效?我们是否需要持久化数据?
在这个早期规划阶段,这些问题的答案并不总是必要的。更好的是,更专注于我们想要的功能以及支持这些功能所需的数据类型。在与其他项目利益相关者讨论这些想法之后,我们可以转向明确的技术实施细节,以确保我们走在正确的道路上。其他项目利益相关者包括所有参与这个开发项目的人。这可能包括客户,如果你为别人构建应用程序,以及其他开发团队成员、产品/项目经理等等。从“团队”中获得一些反馈来帮助验证方法和所做的任何假设总是一个好主意。
在我们的情况下,确实没有其他人参与这个开发工作。因此,我们可以快速得出一些结论来回答我们与数据相关的问题,并继续我们的应用程序开发。
由于这是一个基于 Web 的应用程序,并且考虑到我们需要存储、检索和操作的信息的性质,我们可以得出结论,最好将数据持久化在这个应用程序中。此外,基于我们想要捕获和管理的数据类型之间存在的关系,存储这些数据的一个良好方法是使用关系数据库。基于其易用性、优秀的价格点、在 PHP 应用程序开发人员中的普遍受欢迎程度以及与 Yii 框架的兼容性,我们将使用 MySQL 作为特定的数据库服务器。
现在我们已经了解了我们将要开始构建的内容以及我们将如何开始构建它的足够信息,让我们开始吧。
创建新应用程序
首先,让我们先创建初始的 Yii Web 应用程序。我们已经在第二章中看到了这是多么容易实现,入门。就像我们在那里所做的那样,我们将假设以下内容:
-
YiiRoot是您安装 Yii 的目录 -
WebRoot被配置为您的 Web 服务器的文档根目录(即http://localhost/解析到的位置)
因此,从命令行,切换到您的WebRoot目录并执行以下操作:
**% YiiRoot/framework/yiic webapp trackstar**
**Create a Web application under '/Webroot/trackstar'? [Yes|No] Yes**
这为我们提供了我们的骨架目录结构和开箱即用的工作应用程序。您应该能够通过导航到http://localhost/trackstar/index.php?r=site/index来查看这个新应用程序的主页。
注意
因为我们的默认控制器是 SiteController,该控制器中的默认操作是actionIndex(),所以我们也可以在不指定路由的情况下导航到相同的页面。
连接到数据库
现在我们的骨架应用程序已经运行起来了,让我们开始着手正确地连接到数据库。事实上,骨架应用程序已自动配置为使用数据库。使用yiic工具的副产品是,我们的新应用程序配置为使用 SQLite 数据库。如果您在protected/config/main.php中的主应用程序配置文件中查看,您将在文件的中间位置看到以下声明:
'db'=>array('connectionString' => 'sqlite:'.dirname(__FILE__).'/../data/testdrive.db',
),
您还可以验证protected/data/testdrive.db的存在,这是配置要使用的 SQLite 数据库。
由于我们已经决定使用 MySQL,我们需要进行一些配置更改。但是,在我们改变配置以使用 MySQL 数据库服务器之前,让我们简要讨论一下 Yii 和数据库更一般的情况。
Yii 和数据库
Yii 为数据库编程提供了很好的支持。 Yii 的数据访问对象(DAO)是建立在PHP 数据对象(PDO)扩展(php.net/pdo)之上的。这是一个数据库抽象层,使应用程序能够通过一个与数据库无关的接口与数据库交互。所有支持的数据库管理系统(DBMS)都封装在一个统一的接口后面。这样,代码可以保持与数据库无关,使用 Yii DAO 开发的应用程序可以轻松地切换到使用不同的 DBMS,而无需进行修改。
要与支持的 DBMS 建立连接,您可以简单地创建一个新的CDbConnection实例:
$connection=new CDbConnection($dsn,$username,$password);
这里$dsn变量的格式取决于所使用的特定 PDO 数据库驱动程序。一些常见的格式包括:
-
SQLite:
sqlite:/path/to/dbfile -
MySQL:
mysql:host=localhost;dbname=testdb -
PostgreSQL:
pgsql:host=localhost;port=5432;dbname=testdb -
SQL Server:
mssql:host=localhost;dbname=testdb -
Oracle:
oci:dbname=//localhost:1521/testdb
CDbConnection还继承自CApplicationComponent,这使它可以被配置为应用程序组件。这意味着我们可以将其添加到应用程序的 components 属性中,并在主配置文件中自定义类和属性值。这是我们首选的方法,接下来我们将详细介绍。
将 db 连接添加为应用程序组件
让我们快速回顾一下。当我们创建初始应用程序时,我们指定了应用程序类型为 Web 应用程序。记住我们在命令行上指定了webapp。这样做指定了每个请求创建的应用程序单例类的类型为CWebApplication。这个 Yii 应用程序单例是所有请求处理运行的执行上下文。它的主要任务是解析用户请求并将其路由到适当的控制器进行进一步处理。这在第一章中使用的图表中表示为 Yii 应用程序路由器,Meet Yii,当我们介绍请求路由时。它还作为保存应用程序级配置值的中心位置。
要自定义我们的应用程序配置,通常我们会提供一个配置文件来初始化应用程序实例创建时的属性值。主应用程序配置文件位于/protected/config/main.php。这是一个包含键值对数组的 PHP 文件。每个键代表应用程序实例的属性名称,每个值是相应属性的初始值。如果您打开这个文件,您会看到已经为我们配置了几个设置。
向配置中添加应用程序组件很容易。打开文件(/protected/config/main.php)并找到组件属性。
我们可以看到已经有条目指定了log和user应用程序组件。这些将在后续章节中介绍。我们还可以看到(正如我们之前注意到的),还有一个db组件,配置为使用 SQLite 连接到位于protected/data/testdrive.db的 SQLite 数据库。还有一个被注释掉的部分,定义了这个db组件使用 MySQL 数据库。我们所需要做的就是删除 SQLite db组件定义,取消注释定义 MySQL 组件的部分,然后进行相应的更改以匹配您的数据库名称、用户名和密码,以便进行连接。以下代码显示了这个更改:
// application components
'components'=>array(
…
//comment out or remove the reference to the sqlite db
/*
'db'=>array(
'connectionString' => 'sqlite:'.dirname(__FILE__).'/../data/testdrive.db',
),
*/
// uncomment the following to use a MySQL database
'db'=>array(
'connectionString' => 'mysql:host=localhost;dbname=trackstar',
'emulatePrepare' => true,
'username' => '[your-db-username]',
'password' => '[your-db-password]',
'charset' => 'utf8',
),
这假设已经创建了一个名为trackstar的 MySQL 数据库,并且可以使用 localhost 连接。根据您的环境,您可能需要指定127.0.0.1而不是localhost作为 localhost 的 IP。将其作为应用程序组件的一个巨大好处是,现在在我们的应用程序的任何地方,我们可以简单地将数据库连接引用为主 Yii 应用程序Yii::app()->db的属性。同样,我们可以将其用作config文件中定义的任何其他组件的引用。
注意
当charset属性设置为'utf8'时,它设置了数据库连接使用的字符集。这个属性只用于 MySQL 和 PostgreSQL 数据库。它将默认为 null,这意味着它将使用默认字符集。我们在这里设置它是为了确保我们的 PHP 应用程序正确支持utf8 unicode 字符。
emulatePrepare => true配置将 PDO 属性(PDO::ATTR_EMULATE_PREPARES)设置为true,如果您使用的是 PHP 5.1.3 或更高版本,则建议这样做。这是在 PHP 5.1.3 中添加的,当使用时,会导致使用 PDO 本机查询解析器而不是 MySQL 客户端中的本机准备语句 API。MySQL 客户端中的本机准备语句无法利用查询缓存,因此已知会导致性能不佳。PDO 本机查询解析器可以使用查询缓存,因此建议在可用时使用此选项(PHP 5.1.3 或更高版本)。
因此,我们已指定了一个名为trackstar的 MySQL 数据库,以及连接到该数据库所需的用户名和密码。我们没有向您展示如何在 MySQL 中创建这样的数据库。我们假设您了解如何设置 MySQL 数据库以及如何使用它。如果您不确定如何创建名为trackstar的新数据库,并为连接配置用户名和密码,请参考您特定的数据库文档。
测试数据库连接
在继续之前,我们应该确保我们的数据库连接实际上是有效的。我们可以通过几种方式来做到这一点。我们将看两种方法。在第一种方法中,我们将使用yiic命令行工具启动应用程序的交互式 shell,并确保在尝试引用应用程序db组件时没有错误。然后我们将提供第二种方法,介绍 Yii 中使用 PHPUnit 进行单元测试。
使用交互式 shell
我们将从使用 Yii 交互式 shell 开始进行简单测试。您可能还记得,我们使用webapp命令以及yiic命令行实用程序来创建我们的新应用程序。与此实用程序一起使用的另一个命令是shell。这允许您直接从命令行在 Yii 应用程序的上下文中运行 PHP 命令。
要启动 shell,请导航到应用程序的根目录,即包含index.php入口脚本Webroot/trackstar/的目录。然后运行yiic实用程序,将shell作为命令传递(参考以下截图)。
这将启动 shell,并允许您在**>>**提示之后直接输入命令。
我们要做的是测试我们的连接,确保我们的数据库连接应用程序组件是可访问的。我们可以简单地echo出连接字符串,并验证它是否返回我们在配置中设置的内容。因此,从 shell 提示符中输入以下内容:
**>> echo Yii::app()->db->connectionString;**
它应该回显类似于以下内容:
mysql:host=localhost;dbname=trackstar
这表明db应用程序组件已正确配置并可供我们的应用程序使用。
自动化测试-单元和功能测试
收集反馈对应用程序开发至关重要;来自应用程序用户和其他项目利益相关者的反馈,来自开发团队成员的反馈,以及来自软件本身的直接反馈。以一种允许软件在出现故障时告诉您的方式开发软件,可以将与集成和部署应用程序相关的恐惧转化为无聊。您可以赋予软件这种反馈机制的方法是编写自动化单元和功能测试,然后重复并经常执行它们。
单元和功能测试
单元测试是为了向开发人员提供代码是否正确执行的验证。功能测试是为了向开发人员以及其他项目利益相关者提供应用程序是否以正确方式执行的验证。
单元测试
单元测试是专注于软件应用程序中最小单元的测试。在面向对象的应用程序中,比如 Yii web 应用程序,最小的单元是构成类接口的公共方法。单元测试应该专注于一个单一的类,不需要其他类或对象来运行。它们的目的是验证单元代码是否按预期工作。
功能测试
功能测试专注于测试应用程序的端到端功能功能。这些测试存在于比单元测试更高的级别,并且通常需要多个类或对象来运行。它们的目的是验证应用程序的特定功能是否按预期工作。
测试的好处
编写单元测试和功能测试有许多好处。首先,它们是提供文档的好方法。单元测试可以快速告诉代码块存在的确切原因。同样,功能测试记录了应用程序中实现的功能。如果您坚持编写这些测试,那么随着应用程序的发展,文档将自然而然地不断发展。
它们还是一种宝贵的反馈机制,不断向开发人员和其他项目利益相关者保证代码和应用程序按预期工作。每次对代码进行更改时都运行测试,并立即获得反馈,告诉您是否无意中更改了系统的预期行为。然后您可以立即解决这些问题。这确实增加了开发人员对应用程序的信心,并转化为更少的错误和更成功的项目。
这种即时反馈也有助于促进变革和改进代码的设计。如果一套测试能够立即提供反馈,告诉开发人员所做的更改是否改变了应用程序的行为,开发人员更有可能对现有代码进行改进。单元测试和功能测试套件提供的信心使开发人员能够编写更好的软件,发布更稳定的应用程序,并交付高质量的产品。
Yii 中的测试
从 1.1 版本开始,Yii 与 PHPUnit (www.phpunit.de/)和 Selenium Remote Control (seleniumhq.org/projects/remote-control/)测试框架紧密集成。您可以使用任何可用的测试框架测试 Yii PHP 代码。但是,Yii 与前述两个框架的紧密集成使事情变得更加容易。使事情变得容易是我们的主要目标之一。
当我们使用yiic webapp控制台命令创建新的 Web 应用程序时,我们注意到许多文件和目录会自动为我们创建。其中与编写和执行自动化测试相关的是以下内容:
| 文件/目录 | 包含/存储 |
|---|---|
trackstar/ | 包含文件/目录列出的所有文件 |
protected/ | 受保护的应用程序文件 |
tests/ | 应用程序的测试 |
fixtures/ | 数据库固定装置 |
functional/ | 功能测试 |
unit/ | 单元测试 |
report/ | 覆盖率报告 |
bootstrap.php | 在测试开始时执行的脚本 |
phpunit.xml | PHPUnit 配置文件 |
WebTestCase.php | 用于基于 Web 的功能测试的基类 |
您可以将测试文件放入三个主要目录,即fixtures、functional和unit。report目录用于存储生成的代码覆盖率报告。
注意
必须安装 PHP 扩展 XDebug 才能生成报告。有关此安装的详细信息,请参阅xdebug.org/docs/install。此示例不需要此扩展。
单元测试
在 Yii 中,单元测试是以扩展自框架类CTestCase的 PHP 类编写的。约定规定它的名称应为AbcTest,其中Abc被要测试的类的名称替换。例如,如果我们要测试第二章中的“Hello, World!”应用程序中的MessageController类,我们将命名测试类为MessageControllerTest。这个类保存在protected/tests/unit/目录下的名为MessageControllerTest.php的文件中。
测试类主要有一组名为testXyz的测试方法,其中Xyz通常与您编写测试的方法名称相同。
继续使用MessageController示例,如果我们正在测试actionHelloworld()方法,我们将在MessageControllerTest类中命名相应的测试方法为testActionHelloworld()。
安装 PHPUnit
从 1.1 版本开始,Yii 与 PHPUnit(www.phpunit.de/)测试框架紧密集成。
为了跟随这个示例,您需要安装 PHPUnit。这应该使用 Pear Installer 完成。(有关 Pear 的更多信息,请参阅pear.php.net/。)请访问以下网址,了解如何根据您的环境配置安装 PHPUnit 的更多信息:
github.com/sebastianbergmann/phpunit/
注意
本书的范围当然不包括具体介绍 PHPUnit 的测试功能。建议您花些时间阅读文档,了解术语和编写基本单元测试的感觉:github.com/sebastianbergmann/phpunit/
测试连接
假设您已成功安装了 PHPUnit,我们可以在protected/tests/unit/下为我们的数据库连接添加一个测试。让我们在这个目录下创建一个名为DbTest.php的简单数据库连接性测试文件。添加以下内容的新文件:
<?php
class DbTest extends CTestCase
{
public function testConnection()
{
$this->assertTrue(true);
}
}
在这里,我们添加了一个相当琐碎的测试。assertTrue()方法是 PHPUnit 的一部分,它是一个断言,如果传递给它的参数为true,则会通过,如果为false,则会失败。在这种情况下,它正在测试true是否为true。因此,这个测试肯定会通过。我们这样做是为了确保我们的新应用程序按预期工作,用于 PHPUnit 测试。转到 tests 文件夹并执行这个新测试:
**%cd /WebRoot/trackstar/protected/tests**
**%phpunit unit/DbTest.php**
…
Time: 0 seconds, Memory: 10.00Mb
OK (1 test, 1 assertion)
注意
如果由于某种原因此测试在您的系统上失败,您可能需要更改protected/tests/bootstrap.php,以便变量$yiit正确指向您的/YiiRoot/yiit.php文件。
确信我们的测试框架在新创建的 TrackStar 应用程序中按预期工作,我们可以使用它来为db连接编写测试。
将testConnection()测试方法中的assertEquals(true)语句更改为:
$this->assertNotNull(Yii::app()->db->connectionString);
然后重新运行测试:
**%phpunit unit/DbTest.php**
…
Time: 0 seconds, Memory: 10.00Mb
OK (1 test, 1 assertion)
如您所记得的,由于我们将数据库连接配置为名为db的应用程序组件,Yii::app()->db应返回CDbConnection类的实例。如果应用程序未能建立数据库连接,此测试将返回错误。由于测试仍然通过,我们可以放心地继续,确保数据库连接已正确设置。
总结
本章介绍了任务跟踪应用程序 TrackStar,我们将在本书的其余部分中开发。我们讨论了应用程序是什么以及它的功能,并以非正式用户故事的形式提供了一些高级需求。然后,我们确定了一些需要创建的主要领域对象,以及解决一些需要存储和管理的数据。
然后,我们迈出了构建 TrackStar 应用程序的第一步。我们创建了一个新的应用程序,其中包含从自动生成的代码中“免费”获得的所有工作功能。我们还配置了我们的应用程序连接到 MySQL 数据库,并演示了测试该连接的两种方法。一种方法演示了 Yii 与 PHPUnit 的集成以及如何为 Yii 应用程序编写自动化测试。
在下一章中,我们将最终开始深入研究更复杂的功能。我们将开始进行一些实际的编码,以实现在应用程序中管理项目实体所需的功能。
第四章:项目 CRUD
现在我们已经有了一个基本的应用程序,并配置好与我们的数据库通信,我们可以开始着手一些我们应用程序的真正功能。我们知道"项目"是我们应用程序中最基本的组件之一。用户在 TrackStar 应用程序中不能做任何有用的事情,而不是首先创建或选择一个现有的项目,然后在其中添加任务和其他问题。因此,我们首先要把注意力转向将一些项目功能加入应用程序。
功能规划
在本章的努力结束时,我们的应用程序应该允许用户创建新项目,从现有项目列表中选择,更新/编辑现有项目,并删除现有项目。
为了实现这个目标,我们应该确定更加细粒度的任务来关注。下面的列表确定了我们在本章内的任务清单:
-
设计数据库架构以支持项目
-
构建架构中标识的必要表和所有其他数据库对象
-
创建 Yii AR 模型类,以便应用程序可以轻松地与创建的数据库表进行交互
-
创建 Yii 控制器类,用于包含以下功能:
-
创建新项目
-
检索现有项目列表以显示
-
更新与现有项目相关的数据
-
删除现有项目
-
创建 Yii 视图文件和表示层逻辑,将:
-
展示表单以允许创建新项目
-
显示所有现有项目的列表
-
显示表单以允许用户编辑现有项目
-
在项目列表中添加删除按钮,以允许删除项目
这绝对足够让我们开始了。
创建项目表
回到第三章 TrackStar 应用程序,我们谈到了代表项目的基本数据,并且我们决定我们将使用 MySQL 关系数据库来构建这个应用程序的持久层。现在我们需要设计和构建将持久化我们项目数据的表。
我们知道项目需要有名称和描述。我们还将在每个表上保留一些基本的表审计信息,通过跟踪记录创建和更新的时间,以及谁创建和更新记录。
基于这些属性,项目表将如下所示:
CREATE TABLE tbl_`project` (
`id` INTEGER NOT NULL auto_increment,
`name` varchar(255) NOT NULL,
`description` text NOT NULL,
`create_time` DATETIME default NULL,
`create_user_id` INTEGER default NULL,
`update_time` DATETIME default NULL,
`update_user_id` INTEGER default NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB
;
现在,在我们直接使用我们喜爱的 MySQL 数据库编辑器来创建这个表之前,我们需要讨论一下我们如何使用 Yii 来管理我们的数据库架构的变化,因为我们构建我们的 TrackStar 应用程序时会发生变化。
Yii 数据库迁移
我们知道跟踪应用程序源代码的版本更改是一个好习惯。当您在构建我们的 TrackStar 应用程序时,使用版本控制软件如 SVN 或 GIT 来帮助管理我们在代码库中所做的所有更改是明智的。如果我们的代码库更改与数据库更改不同步,很可能我们整个应用程序都会崩溃。因此,管理我们将在数据库中进行的结构更改也是非常重要的。
Yii 在这方面帮助了我们。Yii 提供了一个数据库迁移工具,用于跟踪数据库迁移历史,并允许我们应用新的迁移,以及回滚现有的迁移,以便我们将数据库结构恢复到先前的状态。
Yii 迁移实用程序是一个控制台命令,我们使用yiic命令行工具。作为控制台命令,它使用一个特定于控制台命令的配置文件,默认情况下是protected/config/console.php。我们需要在这个文件中正确配置我们的数据库组件。就像我们在main.php配置文件中所做的那样,我们需要定义我们的db组件来使用我们的 MySQL 数据库。如果你打开protected/config/console.php配置文件,你会看到它已经定义了一个 MySQL 配置,但是被注释掉了。让我们删除 SQLite 配置并取消注释 MySQL 配置,根据你的数据库设置更改用户名和密码:
'components'=>array(
'db'=>array(
'connectionString' => 'mysql:host=localhost;dbname=trackstar',
'emulatePrepare' => true,
'username' => '[YOUR-USERNAME]',
'password' => '[YOUR-PASSWORD]',
'charset' => 'utf8',
),
),
现在我们已经完成了配置更改,可以继续创建迁移。为此,我们使用yiic命令行实用工具和migrate命令。创建迁移的一般形式如下:
**$ yiic migrate create <name>**
在这里,必需的name参数允许我们指定我们正在进行的数据库更改的简要描述。name参数用作迁移文件名和 PHP 类名的一部分。因此,它应该只包含字母、数字或下划线字符。Yii 接受输入的名称参数,并附加一个 UTC 时间戳(格式为yymmdd_hhmmss),并在其后加上字母m以用作文件名和 PHP 类名。让我们继续为我们的项目表创建一个新的迁移,这个命名约定将更加清晰。从命令行中,导航到应用程序的protected/目录,然后发出使用名称create_project_table创建新迁移的命令:
这将创建文件/Webroot/trackstar/protected/migrations/m121108_195611_create_project_table.php,内容如下:
class m121108_195611_create_project_table extends CDbMigration
{
public function up()
{
}
public function down()
{
echo "m121108_195611_create_project_table does not support migration down.\n";
return false;
}
/*
// Use safeUp/safeDown to do migration with transaction
public function safeUp()
{
}
public function safeDown()
{
}
*/
}
当然,我们将不得不对这个文件进行一些更改,以便它创建我们的新表。我们实现up()方法来应用我们所需的数据库更改,并实现down()方法来撤消这些更改,这将允许我们恢复到数据库结构的先前版本。safeUp()和safeDown()方法类似,但它们将在数据库事务中执行更改,以便将整个迁移作为原子单元以一种全有或全无的方式执行。在这种情况下,我们要应用的更改是创建一个新表,我们可以通过删除表来撤消这些更改。这些更改如下:
public function up()
{
$this->createTable('tbl_project', array(
'id' => 'pk',
'name' => 'string NOT NULL',
'description' => 'text NOT NULL',
'create_time' => 'datetime DEFAULT NULL',
'create_user_id' => 'int(11) DEFAULT NULL',
'update_time' => 'datetime DEFAULT NULL',
'update_user_id' => 'int(11) DEFAULT NULL',
), 'ENGINE=InnoDB');
}
public function down()
{
$this->dropTable('tbl_project');
}
保存更改后,我们可以执行迁移。在protected/目录中,执行以下迁移:
使用不带参数的迁移命令将导致对尚未应用的每个迁移执行迁移(即执行up()方法)。而且,由于这是我们第一次运行迁移,Yii 将自动为我们创建一个新的迁移历史表tbl_migration。Yii 使用此表来跟踪已经应用的迁移。如果我们在迁移命令的命令行参数中指定down,则将通过运行该迁移的down()方法来撤消最后应用的迁移。
现在我们已经应用了迁移,我们的新的tbl_project表已经被创建并准备好供我们使用。
注意
在开发我们的 TrackStar 应用程序时,我们将在整本书中使用 Yii 迁移,因此在使用它们时我们将继续学习更多关于它们的知识。有关 Yii 迁移的更详细信息,请参见:
www.yiiframework.com/doc/guide/1.1/en/database.migration
命名约定
您可能已经注意到我们将数据库表以及所有列名都定义为小写。在整个开发过程中,我们将使用小写来表示所有表名和列名。这主要是因为不同的 DBMS 以不同的方式处理大小写敏感性。例如,PostgreSQL默认情况下将列名视为不区分大小写,如果列包含混合大小写字母,则必须在查询条件中引用列。使用小写将有助于消除这个问题。
您可能还注意到我们在命名项目表时使用了tbl_前缀。从 1.1.0 版本开始,Yii 提供了对表前缀的集成支持。表前缀是一个字符串,它被添加到表的名称之前。它经常用于共享托管环境,其中多个应用程序共享一个单一的数据库,并使用不同的表前缀来区分彼此;一种数据库对象的名称空间。例如,一个应用程序可以使用tbl_作为前缀,而另一个应用程序可以使用yii_。此外,一些数据库管理员使用这个作为一个命名约定,以前缀数据库对象的标识符,以确定它们是什么类型的实体或者将要使用。他们使用前缀来帮助将对象组织成相似的组。使用表前缀是一种偏好,当然不是必需的。
为了充分利用 Yii 中集成的表前缀支持,必须适当地设置CDbConnection::tablePrefix属性为所需的表前缀。然后,在整个应用程序中使用的 SQL 语句中,可以使用{{TableName}}来引用表名,其中TableName是表的名称,但不包括前缀。例如,如果我们要进行这个配置更改,我们可以使用以下代码来查询所有项目:
$sql='SELECT * FROM {{project}}';
$projects=Yii::app()->db->createCommand($sql)->queryAll();
但这有点超前。让我们暂时保持配置不变,等到我们稍后在应用程序开发中进行数据库查询时再回顾这个话题。
创建 AR 模型类
现在我们已经创建了tbl_project表,我们需要创建 Yii 模型类,以便我们可以轻松地管理该表中的数据。我们在第一章 遇见 Yii中介绍了 Yii 的 ORM 层,Active Record(AR)。现在我们将在这个应用程序的上下文中看到一个具体的例子。
配置 Gii
回到第二章 入门,当我们构建我们简单的“Hello, World!” Yii 应用程序时,我们介绍了代码生成工具Gii。如果您还记得,在我们开始使用 Gii 之前,我们必须为其配置我们的应用程序。我们需要在我们的新 TrackStar 应用程序中再次这样做。作为提醒,要配置 Gii 的使用,打开protected/config/main.php,并定义 Gii 模块如下:
return array(
…
…
'modules'=>array(
'gii'=>array(
'class'=>'system.gii.GiiModule',
'password'=>false,
// If removed, Gii defaults to localhost only. Edit carefully to taste.
'ipFilters'=>array('127.0.0.1','::1'),
),
…
),
这将 Gii 配置为一个应用程序模块。我们将在本书的后面详细介绍 Yii 模块。此时重要的是确保将其添加到配置文件中,并提供您的密码(或者在开发环境中将密码设置为false,以避免被提示登录屏幕)。现在,通过转到http://localhost/trackstar/index.php?r=gii来导航到该工具。
使用 Gii 创建我们的 Project AR 类
Gii 的主菜单页面如下所示:
由于我们想要为我们的tbl_project表创建一个新的模型类,模型生成器选项似乎是正确的选择。点击该链接会带我们到以下页面:
表前缀字段主要用于帮助 Gii 确定我们正在生成的 AR 类的命名方式。如果您使用前缀,可以在此处添加。这样,它在命名新类时就不会使用该前缀。在我们的情况下,我们使用tbl_前缀,所以我们应该在这里指定。指定此值将意味着我们新生成的 AR 类将被命名为Project,而不是Tbl_project。
接下来的两个字段要求我们的表名和我们想要生成的类文件的名称。在表名字段中输入我们的表名tbl_project,并观察模型类名称自动填充。模型类名称的约定是表的名称,减去前缀,并以大写字母开头。因此,它将假定我们的模型类名称为 Project,但您当然可以自定义。
接下来的几个字段允许进一步定制。基类字段用于指定我们的模型类将继承的类。这将需要是CActiveRecord或其子类。模型路径字段让我们指定在应用程序目录结构中输出新文件的位置。默认值是protected/models/(别名application.models)。构建关系复选框允许您决定是否让 Gii 通过使用在 MySQL 数据库表之间定义的关系来自动定义 AR 对象之间的关系。它默认为选中状态。最后一个字段允许我们指定基于哪个模板进行代码生成。我们可以自定义默认模板以满足可能适用于所有这类类文件的任何特定需求。目前,这些字段的默认值完全满足我们的需求。
点击预览按钮继续。这将导致以下表格显示在页面底部:
此链接允许您预览将要生成的代码。在点击生成之前,点击models/Project.php链接。以下截图显示了这个预览的样子:
它提供了一个可滚动的弹出窗口,以便我们可以预览将要生成的文件。
好的,关闭这个弹出窗口,然后点击生成按钮。假设一切顺利,您应该看到页面底部显示类似以下截图的内容:
提示
在尝试生成新模型类之前,请确保 Gii 尝试创建新文件的路径protected/models/(或者如果您更改了位置,则是模型路径表单字段中指定的任何目录路径)可被您的 Web 服务器进程写入,否则您将收到写入权限错误。
Gii 已为我们创建了一个新的 Yii 活动记录模型类,并按照我们的指示命名为Project.php。它还将其放在了默认的 Yii 模型类位置protected/models/中,这是我们指示的位置。这个类是我们的tbl_project数据库表的包装类。tbl_project表中的所有列都可以作为Project AR 类的属性访问。
为项目启用 CRUD 操作
现在我们有了一个新的 AR 模型类,但接下来呢?在 MVC 架构中,通常我们需要一个控制器和一个视图来配合我们的模型,以完成整个架构。在我们的情况下,我们需要能够在应用程序中管理我们的项目。我们需要能够创建新项目,检索现有项目的信息,更新现有项目的信息,并删除现有项目。我们需要添加一个控制器类,该类将处理我们的模型类上的 CRUD(创建、读取、更新、删除)操作,以及一个视图文件,以提供 GUI,允许用户在浏览器中执行这些操作。我们可以采取的一种方法是打开我们喜欢的代码编辑器,并创建一个新的控制器和视图类。但是,幸运的是,我们不必这样做。
为项目创建 CRUD 脚手架
再次,Gii 工具将帮助我们摆脱编写常见、繁琐且耗时的代码。CRUD 操作在为应用程序创建的数据库表上是如此常见,Yii 的开发人员决定为我们提供这个功能。如果您来自其他框架,您可能会知道这个术语脚手架。让我们看看如何在 Yii 中利用这一点。
返回到位于http://localhost/trackstar/index.php?r=gii的主 Gii 菜单,并选择Crud Generator链接。您将看到以下屏幕:
在这里,我们看到两个输入表单字段。第一个要求我们指定针对哪个模型类生成所有 CRUD 操作。在我们的情况下,这是我们之前创建的Project AR 类。因此,我们将在此字段中输入Project。在这样做时,我们注意到控制器 ID字段自动填充了名称project。这是 Yii 的命名约定。当然,您可以更改为其他名称,但我们暂时将坚持使用默认值。我们还将使用默认的基础控制器类Controller,这是在我们最初创建应用程序时为我们创建的,以及默认的代码模板文件来生成类文件。
填写了所有这些字段后,点击预览按钮会在页面底部显示以下表格:
我们可以看到将生成相当多的文件。列表顶部是一个新的ProjectController控制器类,将包含所有 CRUD 操作方法。列表的其余部分代表还将创建的许多单独的视图文件。每个操作都有一个单独的视图文件,还有一个将提供搜索项目记录功能的视图文件。当然,您可以通过更改表中相应生成列中的复选框来选择不生成其中的一些文件。但是,对于我们的目的,我们希望 Gii 为我们创建所有这些文件。
请点击生成按钮。您应该在页面底部看到以下成功消息:
注意
您可能需要确保根应用程序目录下的/protected/controllers和/protected/views都可以被 Web 服务器进程写入。否则,您将收到权限错误,而不是这个成功的结果。
现在,我们可以点击立即尝试链接,测试我们的新功能。
这样做会带您到一个项目列表页面。这是显示系统中当前所有项目的页面。在我们的情况下,我们还没有创建任何项目,所以页面会显示未找到结果的消息。让我们通过创建一个新项目来改变这种情况。
创建新项目
在项目列表页面(http://localhost/trackstar/index.php?r=project)的右侧有一个小的导航区域。单击创建项目链接。您会发现这实际上将我们带到登录页面,而不是一个创建新项目的表单。原因是 Gii 生成的代码应用了一个规则,规定只有经过适当身份验证的用户(即已登录的用户)才能创建新项目。任何尝试访问创建新项目功能的匿名用户都将被重定向到登录页面。我们稍后会详细介绍身份验证和授权。现在,继续使用用户名demo和密码demo登录。
成功登录后,应将您重定向到以下 URL:
http://localhost/trackstar/index.php?r=project/create
此页面显示了一个用于添加新项目的输入表单,如下面的屏幕截图所示:
让我们快速填写这个表单来创建一个新项目。表单指示有两个必填字段,名称和描述。Gii 代码生成器足够聪明,知道我们在数据库表中定义了tbl_project.name和tbl_project.description列为NOT NULL,这应该在创建新项目时转换为必填表单字段。很酷,对吧?
因此,我们至少需要填写这两个字段。给它起名字,测试项目,并将描述设置为测试项目描述。单击创建按钮将把表单数据发送回服务器,并尝试添加一个新的项目记录。如果有任何验证错误,将显示一个简单的错误消息,突出显示每个错误的字段。成功保存将重定向到新创建项目的特定列表。我们的成功了,我们被重定向到页面http://localhost/trackstar/index.php?r=project/view&id=1,如下面的屏幕截图所示:
正如我们之前简要提到的,我们注意到我们的新项目创建表单中,名称和描述字段都被标记为必填项。这是因为我们在数据库表中定义了名称和描述列不允许为空值。让我们看看 Yii 中这些必填字段是如何工作的。
表单字段验证
在 Yii 中的表单中使用 AR 模型类时,围绕表单字段设置验证规则非常简单。这是通过在 AR 模型类中的rules()方法中定义的数组中指定值来完成的。
如果您查看Project模型类中的代码(/protected/models/Project.php),您会发现rules()公共函数已经为我们定义好了,并且其中已经有一些规则了:
/**
* @return array validation rules for model attributes.
*/
public function rules()
{
// NOTE: you should only define rules for those attributes that
// will receive user inputs.
return array(
array('name, description', 'required'),
array('create_user_id, update_user_id', 'numerical', 'integerOnly'=>true),
array('name', 'length', 'max'=>255),
array('create_time, update_time', 'safe'),
// The following rule is used by search().
// Please remove those attributes that should not be searched.
array('id, name, description, create_time, create_user_id, update_time, update_user_id', 'safe', 'on'=>'search'),
);
}
rules()方法返回一个规则数组。每个规则的一般格式如下:
Array('Attribute List', 'Validator', 'on'=>'Scenario List', …additional options);
Attribute List是一个逗号分隔的类属性名称字符串,根据Validator进行验证。Validator指定应强制执行什么样的规则。on参数指定应用规则的情景列表。例如,如果我们指定验证应在insert情景上下文中应用,这将表示规则应仅在插入新记录时应用。
如果没有定义特定的情景,验证规则将在模型数据进行验证时的所有情景中应用。
注意
从 Yii 的 1.1.11 版本开始,您还可以指定一个except参数,它允许您排除某些情景的验证。语法与on参数相同。
最后,您还可以指定额外的选项作为name=>value对,用于初始化验证器的属性。这些额外的选项将根据指定的验证器的属性而有所不同。
验证器可以是模型类中的一个方法,也可以是一个单独的验证器类。如果定义为模型类方法,它必须具有以下签名:
/**
* @param string the name of the attribute to be validated
* @param array options specified in the validation rule
*/
public function validatorName($attribute,$params)
{
...
}
如果使用一个单独的类来定义验证器,那个类必须继承自CValidator。
实际上有三种指定验证器的方法:
-
在模型类本身中指定一个方法名
-
指定一个验证器类型的单独类(即一个继承自
CValidator的类) -
在 Yii 框架中指定现有验证器类的预定义别名
Yii 为您提供了许多预定义的验证器类,并提供了别名来定义规则时引用这些类。截至 Yii 版本 1.1.12,预定义的验证器类别名的完整列表如下:
-
boolean:
CBooleanValidator的别名,验证包含true或false的属性 -
captcha:
CCaptchaValidator的别名,验证属性值是否与 CAPTCHA 中显示的验证码相同 -
compare:
CCompareValidator的别名,比较两个属性并验证它们是否相等 -
email:
CEmailValidator的别名,验证属性值是否为有效的电子邮件地址 -
date:
CDateValidator的别名,验证属性值是否为有效的日期、时间或日期时间值 -
default:
CDefaultValueValidator的别名,为指定的属性分配默认值 -
exist:
CExistValidator的别名,验证属性值是否与数据库中指定表列中的值相匹配 -
file:
CFileValidator的别名,验证包含已上传文件名称的属性值 -
filter:
CFilterValidator的别名,使用指定的过滤器转换属性值 -
in:
CRangeValidator的别名,验证数据是否在预定范围内的值,或者存在于指定的值列表中 -
length:
CStringValidator的别名,验证属性值的长度是否在指定范围内 -
match:
CRegularExpressionValidator的别名,使用正则表达式验证属性值 -
numerical:
CNumberValidator的别名,验证属性值是否为有效数字 -
required:
CRequiredValidator的别名,验证属性值是否为空 -
type:
CTypeValidator的别名,验证属性值是否为特定数据类型 -
unique:
CUniqueValidator的别名,验证属性值是否唯一,并与数据库表列进行比较 -
url:
CUrlValidator的别名,验证属性值是否为有效的 URL
我们看到在我们的rules()函数中,有一个规则定义了名称和描述属性,并使用了 Yii 别名required来指定验证器:
array('name, description', 'required'),
这个验证规则的声明负责在新项目表单的名称和描述字段旁边显示小红色星号。这表示这个字段现在是必填的。如果我们回到新项目创建表单(http://localhost/trackstar/index.php?r=project/create)并尝试提交表单而没有指定名称或描述,我们将得到一个精美格式的错误消息,告诉我们不能提交带有这些字段空值的表单,如下面的截图所示:
注意
正如我们之前提到的,Gii 代码生成工具将根据底层表中列的定义自动向 AR 类添加验证规则。我们看到了Name和Description列定义为NOT NULL约束,并且有相关的必填验证器定义。另一个例子是,具有长度限制的列,比如我们的名字列被定义为varchar(255),将自动应用字符限制规则。通过再次查看我们在Project AR 类中的rules()方法,我们注意到 Gii 根据其列定义为我们自动创建了规则array('name', 'length', 'max'=>255)。有关验证器的更多信息,请参见www.yiiframework.com/doc/guide/1.1/en/form.model#declaring-validation-rules。
阅读项目
当我们成功保存一个新项目后被带到项目详细信息页面时,我们实际上已经看到了这个过程http://localhost/trackstar/index.php?r=project/view&id=1。该页面展示了 CRUD 中的R。然而,要查看整个列表,我们可以点击右侧列中的List Project链接。这将带我们回到起点,只是现在我们在项目列表中有了我们新创建的项目。因此,我们有能力检索应用程序中所有项目的列表,以及查看每个项目的详细信息。
更新和删除项目
通过点击列表中任何项目的小项目ID链接,可以导航回项目详细信息页面。让我们为我们新创建的项目,即我们的情况下的ID: 1,做这个操作。点击此链接将带我们到该项目的项目详细信息页面。该页面在其右侧列中显示了一些操作功能,如下截图所示:
我们可以看到Update Project和Delete Project链接,分别为我们提供了 CRUD 操作中的U和D。我们将留给您来验证这些链接是否按预期工作。
注意
删除功能仅限于管理员用户;也就是说,您必须使用admin/admin的用户名/密码组合登录。因此,如果您正在验证删除功能并收到 403 错误,请确保您以管理员身份登录。这将在后面更详细地讨论,并且我们将在后面的章节中详细介绍身份验证和授权。
在管理模式下管理项目
我们在上一个截图中未涵盖的最后一个链接是Manage Project链接。请点击此链接。这很可能会导致授权错误,如下截图所示:
出现此错误的原因是该功能调用了 Yii 中的简单访问控制功能,并且只限制了admin用户的访问。如果您回忆起,当我们登录应用程序以创建新项目时,我们使用demo/demo作为我们的用户名/密码组合。这个demo用户没有权限访问此管理员页面。由 Gii 生成的代码限制了对此功能的访问。
在这个上下文中,管理员简单地指的是使用admin/admin用户名/密码组合登录的人。请点击主导航栏中的注销(演示)来退出应用程序。然后再次登录,但这次使用管理员凭据。成功以admin身份登录后,您会注意到顶部导航栏的注销链接变成了注销(管理员)。然后返回到特定的项目列表页面,例如http://localhost/trackstar/index.php?r=project/view&id=1,再次尝试管理项目链接。您现在应该看到以下截图中显示的内容:
我们现在看到的是我们项目列表页面的高度互动版本。它显示了所有项目在一个互动数据表中。每一行都有内联链接,可以查看、更新和删除每个项目。点击任何列标题链接都会按照该列数值对项目列表进行排序。第二行的小输入框允许您通过关键词在各个列数值中搜索这个项目列表。高级搜索链接会显示一个完整的搜索表单,提供了指定多个搜索条件来提交一个搜索的能力。以下截图显示了这个高级搜索表单:
我们基本上实现了这个迭代中设定的所有功能,而且几乎没有编写任何代码。事实上,借助 Gii 的帮助,我们不仅创建了所有的 CRUD 功能,还实现了我们没有预期到达的基本项目搜索功能。虽然非常基础,但我们已经拥有了一个完全功能的应用程序,具有特定于项目任务跟踪应用程序的功能,并且几乎没有付出太多的努力。
当然,我们的 TrackStar 应用程序还有很多工作要完成。所有这些脚手架代码并不打算完全取代应用程序开发。它为我们提供了一个很好的起点和基础,可以继续构建我们的应用程序。当我们通过项目功能应该如何工作的所有细节和微妙之处时,我们可以依靠这个自动生成的代码以快速的速度推动事情向前发展。
摘要
尽管在本章中我们没有做太多编码,但我们取得了很大的成就。我们创建了一个新的数据库表,这使我们能够看到 Yii Active Record(AR)的实际运行情况。我们使用 Gii 工具首先创建了一个 AR 模型类来包装我们的tbl_project数据库表。然后我们演示了如何使用 Gii 代码生成工具在 Web 应用程序中生成实际的 CRUD 功能。这个神奇的工具快速地创建了我们需要的功能,甚至进一步提供了一个管理仪表板,让我们可以根据不同的条件搜索和排序我们的项目。我们还演示了如何实现模型数据验证以及这如何转化为 Yii 中表单字段验证。
在下一章中,我们将在已学到的基础上继续深入研究 Yii 中的 Active Record,同时在我们的数据模型中引入相关实体。
第五章:管理问题
在上一章中,我们提供了围绕项目实体的基本功能。项目是 TrackStar 应用程序的基础。然而,单独的项目并不是非常有用。项目是我们希望这个应用程序管理的问题的基本容器。由于管理项目问题是这个应用程序的主要目的,我们希望开始添加一些基本的问题管理功能。
功能规划
我们已经有了创建和列出项目的能力,但没有办法管理与项目相关的问题。在本章结束时,我们希望应用程序能够在项目问题或任务上公开所有 CRUD 操作。 (我们倾向于交替使用问题和任务这两个术语,但在我们的数据模型中,任务实际上只是问题的一种类型。)我们还希望限制对问题的所有 CRUD 操作都在特定项目的上下文中进行。也就是说,问题属于项目。用户必须在能够对项目的问题执行任何 CRUD 操作之前,选择了一个现有的项目来工作。
为了实现前面提到的目标,我们需要:
-
设计数据库模式并构建支持项目问题的对象
-
创建 Yii 模型类,使应用程序能够轻松地与我们创建的数据库表进行交互
-
创建控制器类,其中将包含允许我们进行以下操作的功能:
-
创建新问题
-
从数据库中检索项目中现有问题的列表
-
更新/编辑现有问题
-
删除现有问题
-
为这些(上述)操作创建视图以渲染用户界面
这个列表足以让我们开始。让我们开始做必要的数据库更改。
设计模式
回到第三章, TrackStar 应用程序,我们提出了一些关于问题实体的初始想法。我们建议它有一个名称,一个类型,一个所有者,一个请求者,一个状态和一个描述。我们还提到当我们创建tbl_project表时,我们将向每个创建的表添加基本的审计历史信息,以跟踪更新表的日期、时间和用户。然而,类型、所有者、请求者和状态本身也是它们自己的实体。为了保持我们的模型灵活和可扩展,我们将分别对其中一些进行建模。所有者和请求者都是系统的用户,因此将被放在一个名为tbl_user的单独表中。我们已经在tbl_project表中介绍了用户的概念,因为我们添加了create_user_id和update_user_id列来跟踪最初创建项目的用户的标识符,以及负责最后更新项目详细信息的用户。尽管我们尚未正式介绍该表,但这些字段旨在成为user表的外键。tbl_issue表中的owner_id和requestor_id列也将是关联回这个tbl_user表的外键。
我们可以以相同的方式对类型和状态属性进行建模。然而,直到我们的需求要求模型中的这种额外复杂性,我们可以保持简单。tbl_issue表上的type和status列将保持整数值,可以映射到命名类型和状态。然而,我们将这些建模为我们为问题实体创建的 AR 模型类中的基本类常量(const)值,而不是通过使用单独的表来使我们的模型复杂化。如果所有这些都有点模糊,不要担心;在接下来的章节中会更清晰。
定义一些关系
由于我们引入了tbl_user表,我们需要回去定义用户和项目之间的关系。在第三章中,TrackStar 应用程序,我们指定用户(我们称之为项目成员)将与零个或多个项目关联。我们还提到项目也可以有许多(一个或多个)用户。由于项目可以有许多用户,并且这些用户可以与许多项目关联,我们将其称为项目和用户之间的多对多关系。在关系数据库中建模多对多关系的最简单方法是使用关联表(也称为分配表)。因此,我们还需要将这个表添加到我们的模型中。
下图概述了用户、项目和问题之间的基本实体关系。项目可以有零到多个用户。用户需要与至少一个项目关联,但可以与多个项目关联。问题属于一个且仅属于一个项目,而项目可以有零到多个问题。最后,一个问题被分配给(或由)一个单一用户。
构建对象及其关系
我们需要创建三个新表,即tbl_issue、tbl_user和我们的关联表tbl_project_user_assignment。您可能还记得我们在第四章介绍了 Yii 数据库迁移。由于我们现在准备对数据库结构进行更改,我们将使用 Yii 迁移来更好地管理这些更改的应用。
由于我们要同时向数据库中添加这些内容,我们将在一个迁移中完成。从命令行,切换到protected/目录,并输入以下命令:
**$ ./yiic migrate create create_issue_user_and_assignment_tables**
这将导致一个新的迁移文件被添加到protected/migrations/目录中。
打开这个新创建的文件,并按照以下方式实现 safeUp()和 safeDown()方法:
// Use safeUp/safeDown to do migration with transaction
public function safeUp()
{
//create the issue table
$this->createTable('tbl_issue', array(
'id' => 'pk',
'name' => 'string NOT NULL',
'description' => 'text',
'project_id' => 'int(11) DEFAULT NULL',
'type_id' => 'int(11) DEFAULT NULL',
'status_id' => 'int(11) DEFAULT NULL',
'owner_id' => 'int(11) DEFAULT NULL',
'requester_id' => 'int(11) DEFAULT NULL',
'create_time' => 'datetime DEFAULT NULL',
'create_user_id' => 'int(11) DEFAULT NULL',
'update_time' => 'datetime DEFAULT NULL',
'update_user_id' => 'int(11) DEFAULT NULL',
), 'ENGINE=InnoDB');
//create the user table
$this->createTable('tbl_user', array(
'id' => 'pk',
'username' => 'string NOT NULL',
'email' => 'string NOT NULL',
'password' => 'string NOT NULL',
'last_login_time' => 'datetime DEFAULT NULL',
'create_time' => 'datetime DEFAULT NULL',
'create_user_id' => 'int(11) DEFAULT NULL',
'update_time' => 'datetime DEFAULT NULL',
'update_user_id' => 'int(11) DEFAULT NULL',
), 'ENGINE=InnoDB');
//create the assignment table that allows for many-to-many
//relationship between projects and users
$this->createTable('tbl_project_user_assignment', array(
'project_id' => 'int(11) NOT NULL',
'user_id' => 'int(11) NOT NULL',
'PRIMARY KEY (`project_id`,`user_id`)',
), 'ENGINE=InnoDB');
//foreign key relationships
//the tbl_issue.project_id is a reference to tbl_project.id
$this->addForeignKey("fk_issue_project", "tbl_issue", "project_id", "tbl_project", "id", "CASCADE", "RESTRICT");
//the tbl_issue.owner_id is a reference to tbl_user.id
$this->addForeignKey("fk_issue_owner", "tbl_issue", "owner_id", "tbl_user", "id", "CASCADE", "RESTRICT");
//the tbl_issue.requester_id is a reference to tbl_user.id
$this->addForeignKey("fk_issue_requester", "tbl_issue", "requester_id", "tbl_user", "id", "CASCADE", "RESTRICT");
//the tbl_project_user_assignment.project_id is a reference to tbl_project.id
$this->addForeignKey("fk_project_user", "tbl_project_user_assignment", "project_id", "tbl_project", "id", "CASCADE", "RESTRICT");
//the tbl_project_user_assignment.user_id is a reference to tbl_user.id
$this->addForeignKey("fk_user_project", "tbl_project_user_assignment", "user_id", "tbl_user", "id", "CASCADE", "RESTRICT");
}
public function safeDown()
{
$this->truncateTable('tbl_project_user_assignment');
$this->truncateTable('tbl_issue');
$this->truncateTable('tbl_user');
$this->dropTable('tbl_project_user_assignment');
$this->dropTable('tbl_issue');
$this->dropTable('tbl_user');
}
在这里,我们实现了safeUp()和safeDown()方法,而不是标准的up()和down()方法。这样做可以在数据库事务中运行这些语句,以便它们作为单个单元被提交或回滚。
注意
实际上,由于我们正在使用 MySQL,这些create table和drop table语句不会在单个事务中运行。某些 MySQL 语句会导致隐式提交,因此在这种情况下使用safeUp()和safeDown()方法并没有太多用处。我们将保留这一点,以帮助用户了解为什么 Yii 迁移提供safeUp()和safeDown()方法。有关更多详细信息,请参见dev.mysql.com/doc/refman/5.5/en/implicit-commit.html。
现在我们可以从命令行运行迁移:
这个迁移已经创建了我们需要的数据库对象。现在我们可以把注意力转向创建我们的活动记录模型类。
创建活动记录模型类
现在我们已经创建了这些表,我们需要创建 Yii 模型 AR 类,以便我们可以在应用程序中轻松地与这些表交互。在上一章创建Project模型类时,我们使用了 Gii 代码生成工具。我们会在这里提醒您这些步骤,但不会给您所有的截图。请参考第四章,项目 CRUD,以获取使用 Gii 工具创建活动记录类的更详细步骤。
创建 Issue 模型类
通过http://localhost/trackstar/index.php?r=gii导航到 Gii 工具,然后选择Model Generator链接。将表前缀保留为tbl_。在Table Name字段中填写tbl_issue,这将自动填充Model Class字段为Issue。还要确保Build Relations复选框被选中。这将确保我们的关系在新的模型类中自动创建。
填写表单后,点击Preview按钮,获取一个弹出窗口的链接,显示即将生成的所有代码。然后点击Generate按钮,实际在/protected/models/目录中创建新的Issue.php模型类文件。
创建用户模型类
这在这一点上可能已经变得老生常谈了,所以我们将把User AR 类的创建留给您作为一个练习。在下一章中,当我们深入研究用户认证和授权时,这个特定的类将变得更加重要。
您可能会问,“tbl_project_user_assignment表的 AR 类呢?”。虽然可以为这个表创建一个 AR 类,但这并不是必要的。AR 模型为我们的应用程序提供了一个对象关系映射(ORM)层,帮助我们更轻松地处理领域对象。然而,ProjectUserAssignment不是我们应用程序的领域对象。它只是一个在关系数据库中的构造,帮助我们建模和管理项目和用户之间的多对多关系。为处理这个表的管理而维护一个单独的 AR 类是我们可以暂时避免的额外复杂性。我们可以直接使用 Yii 的 DAO 来管理这个表的插入、更新和删除。
创建问题的 CRUD 操作
现在我们已经有了问题的 AR 类,我们可以开始构建必要的功能来管理我们的项目问题。我们将再次依靠 Gii 代码生成工具来帮助我们创建这些功能的基础。我们在上一章节详细介绍了项目的这一点。我将再次提醒您 Issues 的基本步骤:
-
导航到 Gii 生成器菜单
http://localhost/trackstar/index.php?r=gii,然后选择Crud Generator链接。 -
使用Issue作为Model Class字段的值填写表单。这将自动填充Controller ID为Issue。Base Controller Class和Code Template字段可以保留它们预定义的默认值。
-
点击Preview按钮,获取 Gii 工具建议创建的所有文件列表。以下截屏显示了这些文件的列表:
-
您可以点击每个单独的链接预览要生成的代码。一旦满意,点击Generate按钮来创建所有这些文件。您应该收到以下成功消息:
使用问题 CRUD 操作
让我们试一试。要么点击前面截屏中显示的try it now链接,要么直接导航到http://localhost/trackstar/index.php?r=issue。您应该看到类似于以下截屏的内容:
使用问题 CRUD 操作
创建一个新问题
由于我们还没有添加任何新问题,所以没有要列出的问题。让我们改变这种情况,创建一个新问题。点击Create Issue链接。(如果这将您带到登录页面,请使用demo/demo或admin/admin登录。成功登录后,您将被正确重定向。)现在您应该看到一个类似于以下截屏的新问题输入表单:
创建一个新问题
当查看这个输入表单时,我们可以看到它在数据库表中的每一列都有一个输入字段,就像在数据库表中定义的那样。然而,正如我们从设计模式和建立表格时所知道的那样,其中一些字段不是直接的输入字段,而是代表与其他实体的关系。例如,与其在这个表单上有一个类型自由文本输入字段,我们应该使用一个下拉输入表单字段,其中填充了允许的问题类型的选择。类似的论点也适用于状态字段。所有者和请求者字段也应该是下拉菜单,显示被分配到处理问题所在项目的用户的名称选择。此外,由于所有问题管理都应该在特定项目的上下文中进行,项目字段根本不应该是这个表单的一部分。最后,创建时间、创建用户、更新时间和更新用户字段都是应该在表单提交后计算和确定的值,不应该供用户直接操作。
看起来我们已经确定了一些我们想要在这个初始输入表单上做出的更正。正如我们在上一章中提到的,Gii 工具生成的自动生成的 CRUD“脚手架”代码只是一个起点。很少有情况下它本身就足以满足应用程序的所有特定功能需求。
添加下拉字段
我们将从为问题类型添加一个下拉菜单开始。问题只有三种类型,即错误、功能和任务。当创建一个新问题时,我们希望看到的是一个下拉式输入类型表单字段,其中包含这三个选择。我们将通过Issue模型类本身提供其可用类型的列表来实现这一点。由于我们没有创建一个单独的数据库表来保存我们的问题类型,我们将这些直接添加为Issue活动记录模型类的类常量。
在Issue模型类的顶部添加以下三个常量定义:
const TYPE_BUG=0;
const TYPE_FEATURE=1;
const TYPE_TASK=2;
现在在这个类中添加一个新的方法Issue::getTypeOptions(),它将根据这些定义的常量返回一个数组:
/**
* Retrieves a list of issue types
* @return array an array of available issue types.
*/
public function getTypeOptions()
{
return array(
self::TYPE_BUG=>'Bug',
self::TYPE_FEATURE=>'Feature',
self::TYPE_TASK=>'Task',
);
}
现在我们有了一种方法来检索可用的问题类型列表,但我们仍然没有一个下拉字段在输入表单中显示这些值,我们可以从中选择。让我们现在添加它。
添加问题类型下拉
打开包含新问题创建表单的文件protected/views/issue/_form.php,找到与表单上的类型字段对应的行:
<div class="row">
<?php echo $form->labelEx($model,'type_id'); ?>
<?php echo $form->textField($model,'type_id'); ?>
<?php echo $form->error($model,'type_id'); ?>
</div>
这些行需要一点澄清。为了理解这一点,我们需要参考_form.php文件顶部的一些代码,如下所示:
<?php $form=$this->beginWidget('CActiveForm', array(
'id'=>'issue-form',
'enableAjaxValidation'=>false,
)); ?>
这是使用 Yii 中的CActiveForm小部件定义$form变量。小部件将在以后更详细地介绍。现在,我们可以通过更好地理解CActiveForm来理解这段代码。CActiveForm可以被认为是一个帮助类,它提供了一组方法来帮助我们创建与数据模型类相关联的表单。在这种情况下,它被用来基于我们的Issue模型类创建一个输入表单。
为了充分理解视图文件中的变量,让我们也回顾一下渲染视图文件的控制器代码。正如之前讨论过的,从控制器传递数据到视图的一种方式是通过显式声明一个数组,其中的键将是视图文件中可用变量的名称。由于这是一个新问题的创建操作,渲染表单的控制器方法是IssueController::actionCreate()。该方法如下所示:
/**
* Creates a new model.
* If creation is successful, the browser will be redirected to the 'view'
* page.
*/
public function actionCreate()
{
$model=new Issue;
// Uncomment the following line if AJAX validation is needed
// $this->performAjaxValidation($model);
if(isset($_POST['Issue']))
{
$model->attributes=$_POST['Issue'];
if($model->save())
$this->redirect(array('view','id'=>$model->id));
}
$this->render('create',array(
'model'=>$model,
));
}
在这里,我们看到当视图被渲染时,它会传递一个Issue模型类的实例,这个实例将在视图中作为一个名为$model的变量可用。
现在让我们回到负责在表单上渲染Type字段的代码。第一行是:
$form->labelEx($model,'type_id');
这一行使用CActiveForm::labelEx()方法为问题模型属性type_id渲染 HTML 标签。它接受模型类的实例和我们想要生成标签的相应模型属性。模型类Issue:: attributeLabels()方法将被用于确定标签。如果我们查看下面列出的方法,我们会看到属性type_id被映射为标签'Type',这正是我们在表单字段中看到的标签。
public function attributeLabels()
{
return array(
'id' => 'ID',
'name' => 'Name',
'description' => 'Description',
'project_id' => 'Project',
**'type_id' => 'Type',**
'status_id' => 'Status',
'owner_id' => 'Owner',
'requester_id' => 'Requester',
'create_time' => 'Create Time',
'create_user_id' => 'Create User',
'update_time' => 'Update Time',
'update_user_id' => 'Update User',
);
}
使用labelEx()方法也是我们的必填字段旁边出现小红星号的原因。当属性是必填时,labelEx()方法将添加一个额外的CSS类名(CHtml::requiredCss,默认为'required')和星号(使用CHtml::afterRequiredLabel,默认为' <span class="required">*</span>')。
接下来的一行,<?php echo $form->textField($model,'type_id'); ?>,使用CActiveForm::textField()方法为我们的Issue模型属性type_id渲染文本输入字段。在模型类Issue::rules()方法中定义的任何验证规则都将被应用为此输入表单的表单验证规则。
最后一行<?php echo $form->error($model,'type_id'); ?>使用CActiveForm::error()方法在提交时渲染与type_id属性相关的任何验证错误。
您可以尝试使用类型字段进行验证。在我们的 MySQL 模式定义中,type_id列被定义为整数类型,因此,Gii 在Issue::rules()方法中生成了一个验证规则来强制执行这一点。
public function rules()
{
// NOTE: you should only define rules for those attributes that
// will receive user inputs.
return array(
array('name', 'required'),
**array('project_id, type_id, status_id, owner_id, requester_id, create_user_id, update_user_id', 'numerical', 'integerOnly'=>true),**
因此,如果我们尝试在Type表单字段中提交字符串值,我们将在字段下方立即收到内联错误,如下面的屏幕截图所示:
现在我们更好地理解了我们所拥有的东西,我们更有能力对其进行更改。我们需要做的是将这个字段从自由格式的文本输入字段更改为下拉式输入类型。也许不足为奇的是,CActiveForm类有一个dropDownList()方法,可以为模型属性生成一个下拉列表。让我们用以下内容替换调用$form->textField的行(在文件/protected/views/issue/_form.php中):
<?php echo $form->dropDownList($model,'type_id', $model->getTypeOptions()); ?>
这仍然将早期的模型作为第一个参数,将模型属性作为第二个参数。第三个参数指定下拉选择列表。这应该是一个value=>display对的数组。我们已经在Issue.php模型类中创建了我们的getTypeOptions()方法来返回这种格式的数组,所以我们可以直接使用它。
注意
应该注意的是,Yii 框架基类使用了 PHP _get "魔术"函数。这允许我们在子类中编写诸如getTypeOptions()之类的方法,并使用->typeOptions语法将这些方法作为类属性引用。因此,当请求问题类型选项数组$model->typeOptions时,我们也可以使用等效的语法。
保存您的工作,再次查看我们的问题输入表单。您应该看到一个漂亮的问题类型选择下拉菜单,取代了自由格式文本字段,如下面的屏幕截图所示:
添加状态下拉菜单:自己动手
我们将采用相同的方法处理问题状态。正如在第三章中提到的TrackStar 应用程序,当我们介绍应用程序时,问题可以处于以下三种状态之一:
-
尚未开始
-
开始
-
完成
我们将在Issue模型类中创建三个类常量来表示状态值。然后我们将创建一个新方法,Issue::getStatusOptions(),来返回一个可用的问题状态数组。最后,我们将修改_form.php文件,以渲染状态选项的下拉菜单,而不是状态的自由格式文本输入字段。
我们将把状态下拉菜单的实现留给你。你可以按照我们为类型所采取的方法来操作。在你做出这些改变后,表单应该看起来和下面的截图类似:
我们还应该注意,当我们把这些从自由格式文本输入字段改为下拉菜单字段时,最好也在我们的rules()方法中添加一个范围验证,以确保提交的值在下拉菜单允许的值范围内。在上一章中,我们看到了 Yii 框架提供的所有验证器列表。CRangeValidator属性,使用别名in,是定义这个验证规则的一个很好的选择。因此,我们可以定义这样一个规则:
array('type_id', 'in', 'range'=>self::getAllowedTypeRange()),
然后我们添加一个方法来返回我们允许的数值类型值的数组:
public static function getAllowedTypeRange()
{
return array(
self::TYPE_BUG,
self::TYPE_FEATURE,
self::TYPE_TASK,
);
}
同样的方法也用于我们的status_id。我们也将把这个留给你来实现。
修复所有者和请求者字段
我们在问题创建表单中注意到的另一个问题是,所有者和请求者字段也是自由格式的文本输入字段。然而,我们知道这些在问题表中是整数值,它们保存了对tbl_user表的id列的外键标识符。因此,我们还需要为这些字段添加下拉菜单。我们不会采取和类型和状态属性相同的方法,因为问题的所有者和请求者需要从tbl_user表中获取。而且,由于系统中并非每个用户都与问题所在的项目相关联,这些问题不能用从整个tbl_user表中获取的数据填充下拉菜单。我们需要将列表限制为仅包括与该项目相关联的用户。
这也带来了另一件我们需要解决的事情。正如本章开头的功能规划部分所提到的,我们需要在特定项目的上下文中管理我们的问题。也就是说,在创建新问题之前,应该选择一个特定的项目。目前,我们的应用程序没有强制执行这个工作流程。
让我们逐一解决这些变化。首先,我们将修改应用程序,以强制在使用与该项目相关的任何功能来管理相关问题之前,必须确定一个有效的项目。一旦选择了一个项目,我们将确保我们的所有者和请求者下拉选择仅限于与该项目相关联的用户。
强制项目上下文
在允许访问管理问题之前,我们希望确保存在一个有效的项目上下文。为了做到这一点,我们将实现一个叫做过滤器的东西。在 Yii 中,过滤器是一段配置为在控制器动作执行之前或之后执行的代码。一个常见的例子是,如果我们想要确保用户在执行控制器动作方法之前已经登录,我们可以编写一个简单的访问过滤器来检查这个要求。另一个例子是,如果我们想在动作执行后执行一些额外的日志记录或其他审计逻辑,我们可以编写一个简单的审计过滤器来提供这种动作后处理。
在这种情况下,我们希望确保在创建新问题之前已经选择了一个有效的项目。因此,我们将在我们的IssueController类中添加一个项目过滤器来实现这一点。
定义过滤器
过滤器可以被定义为控制器类方法,也可以是一个单独的类。使用简单方法的方法时,方法名必须以 filter 开头,并具有特定的签名。例如,如果我们要创建一个名为 someMethodName 的过滤器方法,我们的完整过滤器方法将如下所示:
public function filterSomeMethodName($filterChain)
{
...
}
另一种方法是编写一个单独的类来执行过滤逻辑。使用单独类的方法时,该类必须扩展 CFilter,然后根据逻辑应该在操作调用之前还是之后,重写至少一个 preFilter() 或 postFilter() 方法。
添加一个过滤器
因此,让我们向我们的 IssueController 类添加一个过滤器,以处理对有效项目的检查。我们将采用类方法的方法。
打开 protected/controllers/IssueController.php 并在类的底部添加以下方法:
public function filterProjectContext($filterChain)
{
$filterChain->run();
}
好的,我们现在已经定义了一个过滤器。但是它还没有做太多事情。它只是执行 $filterChain->run(),这会继续过滤过程并允许被该方法过滤的操作方法的执行。这带来了另一个问题。我们如何定义哪些操作方法应该使用这个过滤器?
指定被过滤的操作
我们的控制器类的 Yii 框架基类是 CController。它有一个需要被重写以指定需要应用过滤器的操作的 filters() 方法。实际上,这个方法已经在我们的 IssueController.php 类中被重写。当我们使用 Gii 工具自动生成这个类时,它已经为我们完成了。它已经添加了一个简单的 accessControl 过滤器,该过滤器在 CController 基类中定义,用于处理一些基本授权,以确保用户有足够的权限执行某些操作。如果您尚未登录并单击 创建问题 链接,您将被引导到登录页面进行身份验证,然后才能创建新问题。访问控制过滤器负责此操作。在下一章节中,当我们专注于用户身份验证和授权时,我们将更详细地介绍它。
目前,我们只需要将我们的新过滤器添加到这个配置数组中。要指定我们的新过滤器应该应用于创建操作,通过添加下面的代码来修改 IssueController::filters() 方法:
/**
* @return array action filters
*/
public function filters()
{
return array(
'accessControl', // perform access control for CRUD operations
**'projectContext + create', //check to ensure valid project context**
);
}
filters() 方法应该返回一个过滤器配置的数组。之前的代码返回了一个配置,指定了应该将定义为类内方法的 projectContext 过滤器应用于 actionCreate() 方法。配置语法允许使用 "+" 和 "-" 符号来指定是否应该应用过滤器。例如,如果我们决定希望该过滤器应用于除 actionUpdate() 和 actionView() 之外的所有操作,我们可以指定:
return array(
'projectContext - update, view' ,
);
您不应该同时指定加号和减号运算符。对于任何给定的过滤器配置,只需要一个。加号运算符表示“仅将过滤器应用于以下操作”。减号运算符表示“将过滤器应用于除以下操作之外的所有操作”。如果配置中既没有“+”也没有“-”,则该过滤器将应用于所有操作。
目前,我们将这个限制在只有创建操作。因此,如之前定义的 + create 配置,我们的过滤器方法将在任何用户尝试创建新问题时被调用。
添加过滤逻辑
好的,现在我们已经定义了一个过滤器,并且已经配置它在尝试的 actionCreate() 方法调用时被调用。但是,它仍然没有执行必要的逻辑。由于我们希望在尝试操作之前确保项目上下文,我们需要在调用 $filterChain->run() 之前将逻辑放在过滤器方法中。
我们将在控制器类本身中添加一个项目属性。然后,我们将在我们的 URL 中使用一个查询字符串参数来指示项目标识符。我们的预操作过滤器将检查现有的项目属性是否为空;如果是,它将使用查询字符串参数来尝试根据主键标识符选择项目。如果成功,操作将执行;如果失败,将抛出异常。以下是执行所有这些操作所需的相关代码:
class IssueController extends CController
{
....
/**
* @var private property containing the associated Project model instance.
*/
private $_project = null;
/**
* Protected method to load the associated Project model class
* @param integer projectId the primary identifier of the associated Project
* @return object the Project data model based on the primary key
*/
protected function loadProject($projectId) {
//if the project property is null, create it based on input id
if($this->_project===null)
{
$this->_project=Project::model()->findByPk($projectId);
if($this->_project===null)
{
throw new CHttpException(404,'The requested project does not exist.');
}
}
return $this->_project;
}
/**
* In-class defined filter method, configured for use in the above filters()
* method. It is called before the actionCreate() action method is run in
* order to ensure a proper project context
*/
public function filterProjectContext($filterChain)
{
//set the project identifier based on GET input request variables if(isset($_GET['pid']))
$this->loadProject($_GET['pid']);
else
throw new CHttpException(403,'Must specify a project before performing this action.');
//complete the running of other filters and execute the requested action
$filterChain->run();
}
...
}
有了这个设置,如果您现在尝试通过在问题列表页面上的创建问题链接上点击来创建一个新问题,您应该会看到一个“错误 403”错误消息,同时显示我们之前指定的错误文本。
这很好。它表明我们已经正确实现了防止在没有识别到项目时创建新问题的代码。要快速解决这个错误,只需简单地在用于创建新问题的 URL 中添加一个pid查询字符串参数。让我们这样做,这样我们就可以为过滤器提供一个有效的项目标识符,并继续到创建新问题的表单。
添加项目 ID
回到第四章项目 CRUD,在测试和实施项目的 CRUD 操作时,我们向应用程序添加了几个新项目。因此,您可能仍然在开发数据库中拥有一个有效的项目。如果没有,只需使用应用程序再次创建一个新项目。完成后,请注意所创建的项目 ID,因为我们需要将此 ID 添加到新问题的 URL 中。
我们需要修改的链接位于问题列表页面的视图文件/protected/views/issue/index.php中。在该文件的顶部,您会看到我们的菜单项中定义了创建新问题的链接的位置。这在以下突出显示的代码中指定:
$this->menu=array(
**array('label'=>'Create Issue', 'url'=>array('create')),**
array('label'=>'Manage Issue', 'url'=>array('admin')),
);
要向这个链接添加一个查询字符串参数,我们只需在url参数的定义数组中追加一个name=>value对。我们为过滤器添加的代码期望查询字符串参数是pid(项目 ID)。另外,由于我们在这个例子中使用的是第一个(项目 ID = 1)项目,我们将修改创建问题链接如下:
array('label'=>'Create Issue', 'url'=>array('create', 'pid'=>1)),
现在当您查看问题列表页面时,您会看到创建问题超链接打开了一个在末尾附加了查询字符串参数的 URL:
http://localhost/trackstar/index.php?r=issue/create&pid=1
这个查询字符串参数允许过滤器正确设置项目上下文。所以这一次当您点击链接时,不会再出现 403 错误页面,而是会显示创建新问题的表单。
注意
有关在 Yii 中使用过滤器的更多详细信息,请参阅www.yiiframework.com/doc/guide/1.1/en/basics.controller#filter。
修改项目详细信息页面
将项目 ID 添加到“创建新问题”链接的 URL 是确保我们的过滤器按预期工作的一个很好的第一步。然而,我们现在已经将链接硬编码为始终将新问题与项目 ID = 1 关联起来。这当然不是我们想要的。我们想要做的是让创建新问题的菜单选项成为项目详细信息页面的一部分。这样,一旦您从项目列表页面选择了一个项目,特定的项目上下文将被知晓,我们可以动态地将项目 ID 附加到创建新问题的链接上。让我们做出这个改变。
打开项目详细信息视图文件/protected/views/project/view.php。在这个文件的顶部,您会注意到包含在$this->menu数组中的菜单项。我们需要在已定义的菜单链接列表的末尾添加另一个链接以创建新问题:
$this->menu=array(
array('label'=>'List Project', 'url'=>array('index')),
array('label'=>'Create Project', 'url'=>array('create')),
array('label'=>'Update Project', 'url'=>array('update', 'id'=>$model->id)),
array('label'=>'Delete Project', 'url'=>'#', 'linkOptions'=>array('submit'=>array('delete','id'=>$model->id),'confirm'=>'Are you sure you want to delete this item?')),
array('label'=>'Manage Project', 'url'=>array('admin')),
**array('label'=>'Create Issue', 'url'=>array('issue/create', 'pid'=>$model->id)),**
);
我们所做的是将菜单选项移动到列出特定项目详细信息的页面上创建新问题。我们使用了类似于之前的链接,但这次我们必须指定完整的controllerID/actionID对(issue/create)。而不是将项目 ID 硬编码为 1,我们在视图文件中使用了$model变量,这是特定项目的 AR 类。通过这种方式,无论我们选择哪个项目,这个变量都将始终反映该项目的正确项目id属性。
有了这个设置,我们还可以删除另一个链接,我们在protected/views/issue/index.php视图文件中将项目 ID 硬编码为1。
现在我们在创建新问题时已经正确设置了项目上下文,我们可以将项目字段作为用户输入表单字段删除。打开新问题表单的视图文件/protected/views/issue/_form.php。删除与项目输入字段相关的以下行:
<div class="row">
<?php echo $form->labelEx($model,'project_id'); ?>
<?php echo $form->textField($model,'project_id'); ?>
<?php echo $form->error($model,'project_id'); ?>
</div>
然而,由于project_id属性不会随表单一起提交,我们需要根据我们刚刚实现的过滤器设置的值来设置project_id参数。由于我们已经知道关联的项目 ID,让我们明确地将Issue::project_id设置为我们先前实现的过滤器创建的项目实例的id属性的值。因此,根据以下突出显示的代码修改IssueController::actionCreate()方法:
public function actionCreate()
{
$model=new Issue;
**$model->project_id = $this->_project->id;**
现在当我们提交表单时,问题活动记录实例的project_id属性将被正确设置。即使我们还没有设置我们的所有者和请求者下拉框,我们也可以提交表单,新问题将被创建并正确设置项目 ID。
返回到所有者和请求者下拉框
最后,我们可以回到我们原来要做的事情,即将所有者和请求者字段更改为该项目的有效成员的下拉选择。为了正确地做到这一点,我们需要将一些用户与项目关联起来。由于用户管理是即将到来的章节的重点,我们将通过直接使用直接 SQL 将这些关联手动添加到数据库中来完成这一点。让我们使用以下 SQL 添加两个测试用户:
INSERT INTO tbl_user (email, username, password) VALUES ('test1@notanaddress.com','User One', MD5('test1')), ('test2@notanaddress.com','User Two', MD5('test2'));
注意
我们在这里使用单向MD5哈希算法,因为它易于使用,并且在 MySQL 和 PHP 的 5.x 版本中广泛可用。然而,现在已经知道MD5作为单向哈希算法在安全方面是“破碎的”,不建议在生产环境中使用这个哈希算法。请考虑在您的真实生产应用程序中使用Bcrypt。以下是一些提供有关Bcrypt更多信息的网址:
php.net/manual/en/function.crypt.php
当您在trackstar数据库上运行时,它将在我们的系统中创建两个具有 ID 1 和 2 的新用户。让我们也手动将这两个用户分配给项目#1,使用以下 SQL:
INSERT INTO tbl_project_user_assignment (project_id, user_id)
VALUES (1,1), (1,2);
在运行前面的 SQL 语句之后,我们已经将两个有效成员分配给项目#1。
Yii 中关系型 Active Record 的一个很棒的特性是能够直接从问题$model实例中访问问题所属项目的有效成员。当我们使用 Gii 工具最初创建我们的问题模型类时,我们确保选中了构建关系复选框。这指示 Gii 查看底层数据库并定义相关的关系。这可以在/protected/models/Issue.php中的relations()方法中看到。由于我们在添加适当的关系到数据库后创建了这个类,该方法应该看起来像以下内容:
/**
* @return array relational rules.
*/
public function relations()
{
//NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'requester' => array(self::BELONGS_TO, 'User', 'requester_id'),
'owner' => array(self::BELONGS_TO, 'User', 'owner_id'),
'project' => array(self::BELONGS_TO, 'Project', 'project_id'),
);
}
前面代码片段中的//NOTE注释表明您可能具有略有不同的类属性名称,或者希望略有不同,并鼓励您根据需要进行调整。这个数组配置定义了模型实例上的属性,这些属性本身是其他 AR 实例。有了这些关系,我们可以非常容易地访问相关的 AR 实例。例如,假设我们想要访问与问题相关联的项目。我们可以使用以下语法来实现:
//create the model instance by primary key:
$issue = Issue::model()->findByPk(1);
//access the associated Project AR instance
$project = $issue->project;
由于我们在数据库中定义其他表和关系之前创建了我们的Project模型类,因此尚未定义关系。但是现在我们已经定义了一些关系,我们需要将这些添加到Project::relations()方法中。打开项目 AR 类/protected/models/Project.php,并用以下内容替换整个relations()方法:
/**
* @return array relational rules.
*/
public function relations()
{
return array(
'issues' => array(self::HAS_MANY, 'Issue', 'project_id'),
'users' => array(self::MANY_MANY, 'User', 'tbl_project_user_assignment(project_id, user_id)'),
);
}
有了这些,我们可以很容易地使用非常简单的语法访问与项目相关的所有问题和/或用户。例如:
//instantiate the Project model instance by primary key:
$project = Project::model()->findByPk(1);
//get an array of all associated Issue AR instances
$allProjectIssues = $project->issues;
//get an array of all associated User AR instance
$allUsers = $project->users;
//get the User AR instance representing the owner of
//the first issue associated with this project
$ownerOfFirstIssue = $project->issues[0]->owner;
通常情况下,我们需要编写复杂的 SQL 连接语句来访问这样的相关数据。在 Yii 中使用关系 AR 可以避免这种复杂性和单调性。我们现在可以以非常优雅和简洁的面向对象方式访问这些关系,这样非常容易阅读和理解。
生成用于填充下拉框的数据
我们将采用与状态和类型下拉数据相似的方法来实现有效的用户下拉。我们将在我们的Project模型类中添加一个getUserOptions()方法。
打开文件/protected/models/Project.php,并在类的底部添加以下方法:
/**
* @return array of valid users for this project, indexed by user IDs
*/
public function getUserOptions()
{
$usersArray = CHtml::listData($this->users, 'id', 'username');
return $usersArray;
}
在这里,我们使用 Yii 的CHtml辅助类来帮助我们从与项目相关的每个用户创建一个id=>username对的数组。记住,在项目类中relations()方法中定义的users属性映射到用户 AR 实例的数组。CHtml::listData()方法可以接受这个列表,并产生一个适合CActiveForm::dropDownList()的有效数组格式。
现在我们的getUserOptions()方法返回我们需要的数据,我们应该实现下拉框以显示返回的数据。我们已经使用过滤器从$_GET请求中设置了关联的项目 ID,并且我们在IssueController::actionCreate()方法的开头使用了这个值来设置新问题实例的project_id属性。所以现在,通过 Yii 关系 AR 功能的美妙力量,我们可以轻松地使用关联的Project模型来填充我们的用户下拉框。以下是我们在问题表单中需要做的更改:
打开包含输入表单元素的视图文件/protected/views/issue/_form.php,找到owner_id和requester_id的两个文本输入字段表单元素定义,并用以下代码替换它:
<?php echo $form->textField($model,'owner_id'); ?>
with this:
<?php echo $form->dropDownList($model,'owner_id', $model->project->getUserOptions()); ?>
and also replace this line:
<?php echo $form->textField($model,'requester_id'); ?>
with this:
<?php echo $form->dropDownList($model,'requester_id', $model->project->getUserOptions()); ?>
现在,如果我们再次查看我们的问题创建表单,我们会看到所有者和请求者两个下拉框字段已经很好地填充了。
进行最后一次更改
由于我们已经打开了创建问题表单视图文件,让我们快速进行最后一次更改。我们在每个表上都有用于基本历史和审计目的的创建时间和用户以及最后更新时间和用户字段,不应该暴露给用户。稍后我们将更改应用程序逻辑,以在插入和更新时自动填充这些字段。现在,让我们只是将它们从表单中移除。
从/protected/views/issue/_form.php中完全删除以下行:
<div class="row">
<?php echo $form->labelEx($model,'create_time'); ?>
<?php echo $form->textField($model,'create_time'); ?>
<?php echo $form->error($model,'create_time'); ?>
</div>
<div class="row">
<?php echo $form->labelEx($model,'create_user_id'); ?>
<?php echo $form->textField($model,'create_user_id'); ?>
<?php echo $form->error($model,'create_user_id'); ?>
</div>
<div class="row">
<?php echo $form->labelEx($model,'update_time'); ?>
<?php echo $form->textField($model,'update_time'); ?>
<?php echo $form->error($model,'update_time'); ?>
</div>
<div class="row">
<?php echo $form->labelEx($model,'update_user_id'); ?>
<?php echo $form->textField($model,'update_user_id'); ?>
<?php echo $form->error($model,'update_user_id'); ?>
</div>
以下截图显示了我们的新问题创建表单经过所有这些更改后的样子:
CRUD 的其余部分
本章的目标是实现问题的所有 CRUD 操作。我们已经完成了创建功能,但我们仍需要完成问题的读取、更新和删除。幸运的是,通过使用 Gii CRUD 生成功能,大部分基础已经搭好了。但是,由于我们希望在项目的上下文中管理所有问题,我们需要对访问这些功能的方式进行一些调整。
列出问题
尽管IssueController类中有actionIndex()方法,用于显示数据库中所有问题的列表,但我们目前编写的功能并不需要这个功能。我们不想要一个单独的独立页面列出数据库中的所有问题,而是只想列出与特定项目相关联的问题。因此,我们将修改应用程序,以在项目详细信息页面上显示问题列表。由于我们正在利用 Yii 中的关联 AR 模型,所以对于这个改变来说将会非常容易。
修改项目控制器
首先让我们修改ProjectController类中的actionView()方法。因为我们想在同一个页面上显示与特定项目相关联的问题列表,我们可以在项目详细信息页面上做到这一点。actionView()方法是显示项目详细信息的方法。
将该方法修改为:
/**
* Displays a particular model.
* @param integer $id the ID of the model to be displayed
*/
public function actionView($id)
{
$issueDataProvider=new CActiveDataProvider('Issue', array(
'criteria'=>array(
'condition'=>'project_id=:projectId',
'params'=>array(':projectId'=>$this->loadModel($id)->id),
),
'pagination'=>array(
'pageSize'=>1,
),
));
$this->render('view',array(
'model'=>$this->loadModel($id),
'issueDataProvider'=>$issueDataProvider,
));
}
在这里,我们使用CActiveDataProvider框架类来使用CActiveRecord对象提供数据。它将使用关联的 AR 模型类以一种非常容易与内置的框架列表组件CListView一起使用的方式从数据库中检索数据。我们将使用这个组件在视图文件中显示我们的问题列表。我们使用了 criteria 属性来指定条件,它应该只检索与正在显示的项目相关联的问题。我们还使用了 pagination 属性来限制问题列表每页只显示一个问题。我们将这个值设置得很低,这样我们只需添加另一个问题就可以快速演示分页功能。我们很快会演示这一点。
我们做的最后一件事是将这个数据提供程序添加到render()调用中定义的数组中,以便在视图文件中以$issueDataProvider变量的形式提供给它。
修改项目视图文件
正如我们刚才提到的,我们将使用一个名为CListView的框架组件在项目详细信息页面上显示我们的问题列表。打开/protected/views/project/view.php并将以下内容添加到文件的底部:
<br />
<h1>Project Issues</h1>
<?php $this->widget('zii.widgets.CListView', array(
'dataProvider'=>$issueDataProvider,
'itemView'=>'/issue/_view',
)); ?>
在这里,我们将CListView的dataProvider属性设置为我们上面创建的问题数据提供程序。然后我们配置它使用protected/views/issue/_view.php文件作为渲染数据提供程序中每个项目的模板。当我们为问题生成 CRUD 时,Gii 工具已经为我们创建了这个文件。我们在这里使用它来显示项目详细信息页面上的问题。
注意
您可能还记得在第一章认识 Yii中,Zii是 Yii 框架附带的官方扩展库。这些扩展是由核心 Yii 框架团队开发和维护的。您可以在这里阅读更多关于 Zii 的信息:www.yiiframework.com/doc/guide/1.1/en/extension.use#zii-extensions
我们还需要对我们指定为每个问题的布局模板的/protected/views/issue/_view.php文件进行一些更改。将该文件的整个内容修改为以下内容:
<div class="view">
<b><?php echo CHtml::encode($data->getAttributeLabel('name')); ?>:</b>
<?php echo CHtml::link(CHtml::encode($data->name), array('issue/view', 'id'=>$data->id)); ?>
<br />
<b><?php echo CHtml::encode($data->getAttributeLabel('description')); ?>:</b>
<?php echo CHtml::encode($data->description); ?>
<br />
<b><?php echo CHtml::encode($data->getAttributeLabel('type_id')); ?>:</b>
<?php echo CHtml::encode($data->type_id); ?>
<br />
<b><?php echo CHtml::encode($data->getAttributeLabel('status_id')); ?>:</b>
<?php echo CHtml::encode($data->status_id); ?>
</div>
现在,如果我们保存并查看我们的结果,查看项目编号 1 的项目详细信息页面(http://localhost/trackstar/index.php?r=project/view&id=1),并假设您已经在该项目下创建了至少一个示例问题(如果没有,请在此页面使用创建问题链接创建一个),我们应该看到以下截图中显示的内容:
由于我们将数据提供程序的分页属性设置得非常低(记住我们只设置为 1),我们可以添加一个问题来演示内置的分页功能。添加一个问题会改变问题的显示,使我们能够在项目问题列表中从一页到另一页,如下截图所示:
最后的微调
现在我们有了与项目相关联的问题列表,并在项目详细信息页面上显示它们。我们还可以查看问题的详细信息(即阅读它们),以及更新和删除问题的链接。因此,我们的基本 CRUD 操作已经就位。
然而,在完成应用程序的这一部分之前,还有一些问题需要解决。我们会注意到问题显示列表显示了类型,状态,所有者和请求者字段的数字 ID 号。我们应该更改这样,以便显示这些字段的文本值。此外,由于问题已经属于特定项目,将项目 ID 显示为问题列表数据的一部分有点多余。因此,我们可以删除它。最后,我们需要解决一些导航链接,这些链接显示在各种其他与问题相关的表单上,以确保我们始终返回到此项目详细信息页面,作为我们所有问题管理的起始位置。
我们将逐一解决这些问题。
获取状态和类型文本以显示
以前,我们在Issue AR 类中添加了公共方法,以检索状态和类型选项,以填充问题创建表单上的下拉菜单。我们需要在这个 AR 类上添加类似的方法,以返回特定状态或类型 ID 的文本。
在Issue模型类(/protected/models/Issue.php)中添加以下两个新的公共方法,以检索当前问题的状态和类型文本:
/**
* @return string the status text display for the current issue
*/
public function getStatusText()
{
$statusOptions=$this->statusOptions;
return isset($statusOptions[$this->status_id]) ? $statusOptions[$this->status_id] : "unknown status ({$this->status_id})";
}
/**
* @return string the type text display for the current issue
*/
public function getTypeText()
{
$typeOptions=$this->typeOptions;
return isset($typeOptions[$this->type_id]) ? $typeOptions[$this->type_id] : "unknown type ({$this->type_id})";
}
这些方法返回状态文本值("尚未开始","已开始"或"已完成")和类型文本值("错误","功能"或"任务")的Issue实例。
向表单添加文本显示
现在我们有了两个新的公共方法,它们将返回我们列表显示的有效状态和类型文本,我们需要利用它们。更改/protected/views/issue/_view.php中的以下代码行。
将这个<?php echo CHtml::encode($data->type_id); ?>更改为这个:
<?php echo CHtml::encode($data->getTypeText()); ?>
将这个<?php echo CHtml::encode($data->status_id); ?>更改为这个:
<?php echo CHtml::encode($data->getStatusText()); ?>
经过这些更改,我们项目#1的问题列表页面,http://localhost/trackstar/index.php?r=issue&pid=1,不再显示我们问题类型和状态字段的整数值。现在看起来像以下截图中显示的样子:
由于我们使用相同的视图文件在项目详细信息页面上显示我们的问题列表,这些更改也会反映在那里。
更改问题详细视图
我们还需要对问题的详细视图进行一些其他更改。当前,如果查看问题详细信息,它显示如下截图所示:
这是使用我们尚未更改的视图文件。它仍然显示项目 ID,我们不需要显示,以及类型和状态作为整数值,而不是它们关联的文本值。打开用于呈现此显示的视图文件/protected/views/issue/view.php,我们注意到它使用了我们以前没有见过的 Zii 扩展小部件CDetailView。这类似于用于显示列表的CListView小部件,但用于显示单个数据模型实例的详细信息,而不是用于显示许多列表视图。以下是显示此小部件使用的相关代码:
<?php $this->widget('zii.widgets.CDetailView', array(
'data'=>$model,
'attributes'=>array(
'id',
'name',
'description',
'project_id',
'type_id',
'status_id',
'owner_id',
'requester_id',
'create_time',
'create_user_id',
'update_time',
'update_user_id',
),
)); ?>
在这里,我们将CDetailView小部件的数据模型设置为Issue模型类的实例(即我们要显示详细信息的特定实例),然后设置要在渲染的详细视图中显示的模型实例的属性列表。属性可以被指定为Name:Type:Label格式的字符串,其中Type和Label都是可选的,或者作为数组本身。在这种情况下,只指定属性的名称。
如果我们将属性指定为数组,我们可以通过声明值元素来进一步自定义显示。我们将采取这种方法,以指定模型类方法Issue::getTypeText()和Issue::getStatusText()用于获取类型和状态字段的文本值。
让我们将CDetailView的使用更改为以下配置:
<?php $this->widget('zii.widgets.CDetailView', array(
'data'=>$model,
'attributes'=>array(
'id',
'name',
'description',
array(
'name'=>'type_id',
'value'=>CHtml::encode($model->getTypeText())
),
array(
'name'=>'status_id',
'value'=>CHtml::encode($model->getStatusText())
),
'owner_id',
'requester_id',
),
)); ?>
在这里,我们已经删除了一些属性的显示,即project_id,create_time,update_time,create_user_id和update_user_id属性。我们稍后会处理一些这些属性的填充和显示,但现在我们可以将它们从详细显示中删除。
我们还改变了type_id和status_id属性的声明,以使用数组规范,以便我们可以使用值元素。我们已经指定了相应的Issue::getTypeText()和Issue::getStatusText()方法用于获取这些属性的值。有了这些改变,查看问题详细页面显示如下:
好的,我们离我们想要的更近了,但还有一些改变我们需要做。
显示所有者和请求者的名称
事情看起来更好了,但我们仍然看到整数标识符被显示为所有者和请求者,而不是实际的用户名。我们将采取类似的方法来处理类型和状态文本显示。我们将在Issue模型类上添加两个新的公共方法,以返回这两个属性的名称。
使用关联 AR
由于我们的问题和用户分别表示为单独的数据库表,并通过外键关系相关联,我们可以直接从视图文件中的$model中访问owner和requester用户名。利用 Yii 的关联 AR 模型功能,显示相关User模型类实例的用户名属性非常简单。
正如我们所提到的,模型类Issue::relations()方法是定义关系的地方。如果我们来看一下这个方法,我们会看到以下内容:
/**
* @return array relational rules.
*/
public function relations()
{
// NOTE: you may need to adjust the relation name and
//the related class name for the relations automatically generated
//below.
return array(
**'owner' => array(self::BELONGS_TO, 'User', 'owner_id'),**
'project' => array(self::BELONGS_TO, 'Project', 'project_id'),
**'requester' => array(self::BELONGS_TO, 'User', 'requester_id'),**
);
}
突出显示的代码是我们需求最相关的。owner和requester属性都被定义为与User模型类的关系。这些定义指定这些属性的值是User模型类的实例。owner_id和requester_id参数指定了它们各自User类实例的唯一主键。因此,我们可以像访问Issue模型类的其他属性一样访问这些属性。
为了显示所有者和请求者User类实例的用户名,我们再次将CDetailView配置更改为以下内容:
<?php $this->widget('zii.widgets.CDetailView', array(
'data'=>$model,
'attributes'=>array(
'id',
'name',
'description',
array(
'name'=>'type_id',
'value'=>CHtml::encode($model->getTypeText())
),
array(
'name'=>'status_id',
'value'=>CHtml::encode($model->getStatusText())
),
**array(**
**'name'=>'owner_id',**
**'value'=>isset($model->owner)?CHtml::encode($model->owner->username):"unknown"**
**),**
**array(**
**'name'=>'requester_id',**
**'value'=>isset($model->requester)?CHtml::encode($model->requester->username):"unknown" ),**
),
)); ?>
做出这些改变后,我们的问题详细列表开始看起来相当不错。以下截图显示了我们迄今为止取得的进展:
做一些最终的导航调整
我们非常接近完成本章中设定要实现的功能。唯一剩下的事情就是稍微清理一下我们的导航。您可能已经注意到,仍然有一些选项可供用户在项目上下文之外导航到整个问题列表,或者创建一个新问题。对于我们的 TrackStar 应用程序,我们对问题的所有操作都应该在特定项目的上下文中进行。我们之前已经强制要求在创建新问题时使用项目上下文,这是一个很好的开始,但我们仍然需要做一些改变。
我们会注意到的一件事是,应用程序仍然允许用户导航到跨所有项目的所有问题列表。例如,在问题详情页面,如http://localhost/trackstar/index.php?r=issue/view&id=1,我们看到右侧菜单导航中有问题列表和管理问题的链接,分别对应http://localhost/trackstar/index.php?r=issue/index和http://localhost/trackstar/index.php?r=issue/admin(请记住,要访问管理页面,您必须以admin/admin身份登录)。这些链接仍然显示所有项目的所有问题。因此,我们需要将此列表限制为特定项目。
由于这些链接源自问题详情页面,并且特定问题有关联的项目,我们可以首先修改链接以传递特定项目 ID,然后将该项目 ID 作为限制问题查询的条件,分别在IssueController::actionIndex()和IssueController::actionAdmin()方法中使用。
首先让我们修改链接。打开/protected/views/issue/view.php文件,找到文件顶部的菜单项数组。将菜单配置更改为:
$this->menu=array(
**array('label'=>'List Issues', 'url'=>array('index', 'pid'=>$model->project->id)),**
**array('label'=>'Create Issue', 'url'=>array('create', 'pid'=>$model->project->id)),**
array('label'=>'Update Issue', 'url'=>array('update', 'id'=>$model->id)),
array('label'=>'Delete Issue', 'url'=>'#', 'linkOptions'=>array('submit'=>array('delete','id'=>$model->id),'confirm'=>'Are you sure you want to delete this item?')),
**array('label'=>'Manage Issues', 'url'=>array('admin', 'pid'=>$model->project->id)),**
);
所做的更改已经突出显示。我们已经在创建问题链接以及问题列表页面和问题管理列表页面中添加了一个新的查询字符串参数。我们已经知道我们必须对创建链接进行此更改,因为我们先前实施了一个过滤器,以强制在创建新问题之前提供有效的项目上下文。相对于此链接,我们不需要进行进一步的更改。但是对于索引和管理链接,我们需要修改它们对应的操作方法以使用这个新的查询字符串变量。
由于我们已经配置了一个过滤器来使用查询字符串变量加载关联的项目,让我们利用这一点。我们将添加到过滤器配置中,以便在执行IssueController::actionIndex()和IssueController::actionAdmin()方法之前调用我们的过滤器方法。将IssueController::filters()方法更改为:
public function filters()
{
return array(
'accessControl', // perform access control for CRUD operations
**'projectContext + create index admin', //perform a check to ensure valid project context**
);
}
有了这个设置,关联的项目将被加载并可供使用。让我们在IssueController::actionIndex()方法中使用它。修改该方法为:
public function actionIndex()
{
**$dataProvider=new CActiveDataProvider('Issue', array(**
**'criteria'=>array(**
**'condition'=>'project_id=:projectId',**
**'params'=>array(':projectId'=>$this->_project->id),**
**),**
**));**
$this->render('index',array(
'dataProvider'=>$dataProvider,
));
}
在这里,与以前一样,我们只是在创建模型数据提供程序的条件中添加了一个条件,以仅检索与项目相关的问题。这将限制问题列表仅显示项目下的问题。
我们需要对管理列表页面进行相同的更改。但是,这个视图文件/protected/views/issue/admin.php正在使用模型类Issue::search()方法的结果来提供问题列表。因此,我们实际上需要对这个列表强制执行项目上下文进行两次更改。
首先,我们需要修改IssueController::actionAdmin()方法,以在将模型实例发送到视图时设置正确的project_id属性。以下突出显示的代码显示了这个必要的更改:
public function actionAdmin()
{
$model=new Issue('search');
if(isset($_GET['Issue']))
$model->attributes=$_GET['Issue'];
**$model->project_id = $this->_project->id;**
$this->render('admin',array(
'model'=>$model,
));
}
然后我们需要在Issue::search()模型类方法中添加到我们的条件。以下突出显示的代码标识了我们需要对这个方法进行的更改:
public function search()
{
// Warning: Please modify the following code to remove attributes that
// should not be searched.
$criteria=new CDbCriteria;
$criteria->compare('id',$this->id);
$criteria->compare('name',$this->name,true);
$criteria->compare('description',$this->description,true);
$criteria->compare('type_id',$this->type_id);
$criteria->compare('status_id',$this->status_id);
$criteria->compare('owner_id',$this->owner_id);
$criteria->compare('requester_id',$this->requester_id);
$criteria->compare('create_time',$this->create_time,true);
$criteria->compare('create_user_id',$this->create_user_id);
$criteria->compare('update_time',$this->update_time,true);
$criteria->compare('update_user_id',$this->update_user_id);
**$criteria->condition='project_id=:projectID';
$criteria->params=array(':projectID'=>$this->project_id);**
return new CActiveDataProvider(get_class($this), array(
'criteria'=>$criteria,
));
}
在这里,我们使用$criteria->condition()直接移除了$criteria->compare()调用,该调用使用project_id的值必须完全等于我们的项目上下文。有了这些变化,管理页面上列出的问题现在被限制为仅与特定项目相关联的问题。
注意
在/protected/views/issues/下的视图文件中有几个地方包含需要添加pid查询字符串才能正常工作的链接。我们将其留给读者根据这些示例提供的相同方法进行适当的更改。随着我们应用程序的开发,我们将假设所有创建新问题或显示问题列表的链接都已正确格式化,以包含适当的pid查询字符串参数。
总结
在本章中,我们涵盖了许多不同的主题。根据我们应用程序中问题、项目和用户之间的关系,我们在本章中实现问题管理功能的复杂性明显比我们在上一章中处理的项目实体管理要复杂得多。幸运的是,Yii 能够多次帮助我们减轻编写所有需要解决这种复杂性的代码的痛苦。
我们依靠我们的好朋友 Gii 来创建 Active Record 模型,以及对问题实体进行所有基本 CRUD 操作的初始实现。我们再次使用 Yii 迁移来帮助实现我们需要的数据库架构更改,以支持我们的问题功能。我们使用了 Yii 中的关联 Active Record,并看到使用这一特性轻松检索相关的数据库信息。我们引入了控制器过滤器作为一种手段,以在控制器动作方法之前和/或之后实现业务逻辑并进入请求生命周期。我们演示了如何在 Yii 表单中使用下拉菜单。
到目前为止,我们在基本应用程序上取得了很大进展,而且在不必编写大量代码的情况下完成了这一切。Yii 框架本身已经完成了大部分繁重的工作。我们现在有一个可以管理项目并管理项目中问题的工作应用程序。这是我们的应用程序试图实现的核心。到目前为止,我们应该为取得的成就感到自豪。
然而,在这个应用程序真正准备投入生产使用之前,我们还有很长的路要走。一个主要缺失的部分是围绕用户管理的所有必需功能。在接下来的两章中,我们将深入研究用户认证和授权。我们将首先展示 Yii 用户认证的工作原理,并开始对我们的用户进行认证,以验证他们的用户名和密码是否存储在数据库中。