现在,我们已经制定了一个基本的应用程序并配置连接了数据库,现在我们的工作是开发一些实用的功能。 我们知道项目(project)是这个应用程序最根本的组成部份之一。 用户首先会在TrackStar应用程序创建或选择一个已经存在的项目(project)并在其中添加任务和问题。 出于这个原因,我们想在第二个迭代中集中精力在项目(project)这个模块上。
这个迭代相当明了,在这个迭代结束时,我们的应用程序将允许用户创建新的项目(project),在项目(project)列表中选择现有的项目(project),更新/编辑现有项目,并删除现有的项目。
为了实现这个目标,我们需要确定具体更细小的任务,下面列出了这次迭代需要完成的所有任务:
这些已经足够了,让我们开始吧。我们将很快就把这些任务放到TrackStar中并管理。现在,我想我们只能先将它们记在记事本上
在我们进入正式开发之前,我们应该选执行现有的测试套件并确保所有测试全部通过。现在我们只有一个测试,这个测试是在第4章中添加的用来验证数据库连接是否有效。所以,不会花太多的时间运行我们的测试套件。打开你的命令提示符,进入/protected/tests目录,并运行以下单元测试:
%phpunit unit/ PHPUnit 3.3.17 by Sebastian Bergmann. Time: ::0 seconds OK (1 test, 1 assertion)
随着所有的测试都通过,我们更有信心了。现在我们可以开始进行修改。
早在第3章,我们谈论关于一个项目(project)的基本数据,并在第4章,我们决定使用MySQL关系数据库构建这个应用程序的持久层。现在我们将这个项目(project)的内容变成一个真正的数据库表。
我们知道,项目(project)需要有一个名字和描述。我们也将继续保持一些基本信息,跟踪每个记录的创建时间,更新时间以及谁创建的,谁更新的。这些已经足够了,让我们开始达到这个目标。
基于这些所需的属性,如何创建项目(project)表,如下所示:
CREATE TABLE tbl_project ( id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(128), description TEXT, create_time DATETIME, create_user_id INTEGER, update_time DATETIME, update_user_id INTEGER );
如何使用第三方数据库管理工具,已经超出了本书范围。我们也希望让你跟着使用其他可能使用到的一些其他软件。基于这些原因,我们将简单地提供低级别的数据定义语言(DLL)。创建数据库结构。所以,启动Yii支持的数据库服务器,并在trackstar_dev数据库中,打开你的数据库编辑器,执行上面的 DLL语句创建表。
根据你选择使用的数据库,有许有可用的工具帮助你管理和维护数据库结构,我们建议你使用这些工具,这将使事情变的更加容易。我们实际上是使用 MySQLWorkbench (http://dev.mysql.com/downloads/workbench/5.1.html) 进行设计,文档和管理我们的数据库结构。我们也使用 phpMyAdmin (http://www.phpmyadmin.net/home_page/downloads.php) 帮助管理。还有许多类似的工具。花较少时间来熟悉使用这些工具将为你节省很多的时间。
你可能已经注意到,我们定义的数据库表名,以及所有的列名都是小写。在我们的开发中,我们将所有表名和列名都使用小写字母。这主要是因为不同的DBMS是区分大小写的。举个例子,PostgreSQL的列名在默认情况下是区分大小写的,但我们在一个查询条件中必须引用一列,如果该列包含大小写字母。使用小写字母将有助于消除这个问题。
你可能还注意到,我们为项目(project)表名使用了一个tbl_前缀。从1.1.0版本起,Yii提供了使用表前缀的支持。表前缀是一个字符串,它是预先决定表的名称。它通常用在共享主机的环境下,多个应用程序共享同一个数据库的情况,使用不同的表前缀加以区分。例如,一个应用程序使用前缀tbl_ 而另一个可以使用yii_。另外,一些数据库管理员把它当成一个命令规则,用来前缀标识数据库对象是什么类型,或使用前缀来进行分组。
在Yii中为了采用表前缀支持,必须设置CDbConnection::tablePrefix属性为期望的表前缀。然后在整个应用程序的SQL语句中,可以使用{{TableName}}做为参考表名,其中TableName就是表的名称,但不用前缀。例如,如果我们需要修改这个配置,我们仍然可以使用如下代码查询所有项目(project):
$sql='SELECT * FROM {{project}}'; $projects=Yii::app()->db->createCommand($sql)->queryAll();
但这个问题有点超前了。现在让我们离开我们的配置。重新回到正题。稍后再进入数据库查询。
现在,我们已经创建了tbl_project,我们需要创建Yii模型类来管理该表中的数据。早在第1章,我们介绍了Yii的对象关系映射(ORM)层和Active Record(AR)。现在我们根据应用程序的上下文来看一个具体的例子。
以前,我们使用yiic shell命令来帮助我们自动生成一些代码。如在第2章,我们正是使用shell命令来创建我们的第一个控制器,还有许多其他的shell命令可以执行,以帮助自动创建应用程序代码。然而,从1.1.2版本起,Yii有一个新的和更复杂的界面工具Gii。Gii是一个高度可定制和可扩展的基础于Web的代码生成平台,把yiic shell命令提升到了新的高度。我们将使用这个新平台,创建我们的新模型类。
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( 'gii'=>array( 'class'=>'system.gii.GiiModule', 'password'=>'[add_your_password_here]', ), ),
这为应用程序配置了Gii模块。我们本书后面章节详细讲解Yii模块。这里的重点是要确保这些代码添加到了配置文件,并提供你的密码。现在,访问此工具地址:http://localhost/trackstar/index.php?r=gii。
下面的屏幕快照显示了验证表单:
首先输入您在配置文件中提供的密码,成功后将带你进入Gii的主菜单页面:
你可能还记得,这些菜单的选项都类似于第2章使用yiic shell命令行工具的帮助信息。因为我们想要创建tbl_project表的模型,这个模型生成器选项似乎是一个正确的选择。点击链接将带我们进入以下页面:
在创建过程中,表前缀字段域主要用于帮助Gii确定如何命名AR类。如果您使用一个前缀,你可以在此添加。这样,不会使用该前缀来命名新的类。就我们而言,我们使用的是tbl_前缀,这也恰好是表单的默认值。因此,指定此值将意味着我们新生成的AR类将被命名为Project,而不是 tbl_project。
接下来的两个字段域是要求输入表名和我们希望生成的类名。我们表的名称是tbl_project,查看模型类的名称会自动填空。它使用表名,但没有前缀,并以大写字母开头来覆盖模型类的名称。因此,Project为我们模型类的名称,但你也可以自己定义此名称。
接下来的几个字段域是用来进一步进行定制的。Base Class字段用来指定继承哪个模型类。这个类是CActiveRecord或它的子类。Model Path字段类允许你指定这个模型类文件在应用程序中的位置。默认值是protected/models/(即application.models)。最后一个字段指定使用的生成器的模板。我们可以自定义默认模板以应对其它可能出现的情况,比如所有的模型需要有共同的需求。现在,这些字段的默认 值已经可以满足我们的需求了。
点击Preview(预览) 按钮继续,将在页面底部显示如下表格:
当你点击models/Project.php这个链接,可以预览将要生成的代码。下面的截图显示这个预览的样子:
它提供了一个可以滚动的弹出窗口,使我们可以预览到整个文件的代码。
好了,关闭这个弹出窗口,点击生成按钮。如果一切顺利,你应该在屏幕底部看到如下画面:
确保 /protected/models(或你在Model Path字段域填写的路径)是有Web方式写入的权限。否则,你将收到一个权限错误信息。
Gii已经为我们创建了一个新的AR模型类。它的名字是Project.php,默认情况下,放在 protected/models/目录。这个类封装了是我们的tbl_project表。表中的所有字段访问是通过Project AR类的属性。
让我们熟悉的方式为新创建的AR类写一些测试。
一个好的方式是为新代码或功能编写测试。用单元测试的方式来了解AR类是如何在Yii中工作的。由于本次迭代的重点是项目(projects)的创建,读取,更新和删除(CRUD)。我们将针对Project AR类来编写有关这些操作的测试。在我们新建的Project.php这个AR类中,已经包含了这个CRUD功能的公共方法。所以我们不需要编写这些代码。我们可以只专心来写测试。
首先,需要创建一个新的单元测试文件,让我们创建这个文件在:protected/tests/unit/ProjectTest.php,并输入如下代码:
这个类继承自CDbTestCase,这是Yii框架中单测元类的基类,最门用来测试与数据库相关的功能。这个特定的基类提供了一些管理操作。下面我们将介绍更多的细节。
我们将使用testCRUD()方法对Project AR类进行所有的CRUD操作测试。我们将开始测试创建一个新项目(project)。
我们并没有真正从事TDD,其原因是我们进行测试的并不是我们所写的代码。我们正在使用Yii中的AR类,帮助你熟悉这种测试方法和编写基本的测试。由于不是真正的TDD,我们将不会完全遵循在第3章所说的TDD的步骤。例如,由于我们测试的是Yii的核心代码,所以我们没有做到先写一个失败的测试。
为testCRUD()方法添加代码,创建一个Project AR类,设置它的属性,然后保存它:
public function testCRUD() { //Create a new project $newProject=new Project; $newProjectName = 'Test Project 1'; $newProject->setAttributes( array( 'name' => $newProjectName, 'description' => 'Test project number one', 'create_time' => '2010-01-01 00:00:00', 'create_user_id' => 1, 'update_time' => '2010-01-01 00:00:00', 'update_user_id' => 1, ) ); $this->assertTrue($newProject->save(false)); }
此代码首先创建了一个Project AR类实例,然后,我们使用AR类的setAttributes()方法以批量的方式传入数组来设置AR类的属性。我们看到,类的属性是这个数组的键,具体的数据是这个数组对应键的值。
设置好属性后,我们调用Project类的save()方法保存。我们传递给save()方法一个参数false,用来告诉它绕过属性的数据验证(我们将在数据模型验证部份介绍它,添加自己的验证字段)。然后我们测试返回值,保存成功将返回true。
现在我们切换到命令行下执行这个新的测试,以确保成功;
% cd Webroot/protected/tests % phpunit unit/ProjectTest.php PHPUnit 3.3.17 by Sebastian Bergmann. . Time: ::0 seconds OK (1 test, 1 assertion)
不错,它通过了。因此,我们已经成功的添加了一个新的项目(project)。你可以直接查询数据库来验证。使用你的数据库维护工具,查询项目 (project)表返回的内容,你会看到一条新记录的详细属性与Project AR类的属性匹配。在下面的例子中,我们使用MySQL命令行查看:
mysql> select * from tbl_project\G *************************** 1. row *************************** id: 1 name: Test Project 1 description: Test project number one create_time: 2010-01-01 00:00:00 create_user_id: 1 update_time: 2010-01-01 00:00:00 update_user_id: 1 1 row in set (0.00 sec)
你可能已经注意到,当设置Project AR类属性时,我们没有指定id字段。这是因为该列被定义为一个自增长主键。当插入新行时数据库会自动分配个值。一旦插入成功,AR类会自已设置此属性。你可以很容易地获得id属性,方法如下:
$newProject->id
我们将继续下个测试。
现在,我们已经验证创建测试Project AR类的save()方法,让我们继续,添加下面高亮部份的代码,保存在testCRUD()方法中现有代码的最后。
public function testCRUD() { //Create a new project //READ back the newly created project $retrievedProject=Project::model()->findByPk($newProject->id); $this->assertTrue($retrievedProject instanceof Project); $this->assertEquals($newProjectName,$retrievedProject->name); }
在这里,我们使用静态方法model(),它已经定义在了每一个AR类中。这个方法返回了Project AR类的实例,从而进行部访问该类的方法。我们调用findByPk()方法来查询Project特定实例。
这种方法(正如你期望的)通过传递主键的值,返回一个相匹配的行。我们传递的是之前新创建的Project类的实例的自增长属性id。通过这种方式,我们试图当我们保存$newProject后,读取插入的这行。我们有两个断言,我们首先确认返回的是否为Project AR类的实例。然后我们确认项目(project)的名称是否与读取的项目(project)名称一致。
让我们再次在命令行下运行如下测试:
% phpunit unit/ProjectTest.php ... OK (1 test, 3 assertions))) )
非常好!我们已经验证了CURD中的"R"是按着我们预期的完成了工作。
接下来让我们快速完成Update(更新)和Delete(删除)的测试。
现在,同样在testCRUD()方法的底部,添加如下代码:
//Create a new project ... //READ back the newly created project ... //UPDATE the newly created project $updatedProjectName = 'Updated Test Project 1'; $newProject->name = $updatedProjectName; $this->assertTrue($newProject->save(false)); //read back the record again to ensure the update worked $updatedProject=Project::model()->findByPk($newProject->id); $this->assertTrue($updatedProject instanceof Project); $this->assertEquals($updatedProjectName,$updatedProject->name); //DELETE the project $newProjectId = $newProject->id; $this->assertTrue($newProject->delete()); $deletedProject=Project::model()->findByPk($newProjectId); $this->assertEquals(NULL,$deletedProject);
在这里,我们增加了用来测试项目(project)的更新和删除的代码。首先,我们给$newProject实例更新name属性,然后保存这个项目 (project)。由于这里的AR实例已经存在,我们知道这个AR类是更新,而不是插入新记录,当我们调用->save()后,我们再读回这行,来确定是否更新了name。
为了测试删除,我们保存$newProject实例的id属性值到一个局部变量$newProjectId。然后,我们调用$newProject实例的 ->delete()方法,你可能猜到了从数据库中删除记录,将销毁AR实例。然后我们尝试通过这个局部变量作为主键,查询Project返回这行。由于这条记录已经被删除,我们将得到的结果为NULL。测试断言将使用NULL比较。
让我们再测运行测试,以确保成功:
% phpunit unit/ProjectTest.php ... OK (1 test, 8 assertions)
因此,我们已经证实了,我们的Project AR类的CRUD操作是可以达到预期的工作。
当谈到TDD的软件开发方法时,人们不停地面对测试和时间哪个更重新来作出决定,哪些部份不需要测试。
这些问题必须由你自己来回答。提供足够的测试可使代码质量提高,但很显然测试应用程序中的每一行代码可能有些夸张。
一般的经验规则是,不要为外部库编写测试代码(除非你有特别的理由不信任它)。
我们已经对Project AR类编写了CRUD操作的测试,它们背后的是Yii框架的代码,而不是我们写的代码。我们没有写这些测试,因为我们信任框架的代码,只是想让你了解怎么使用一个Active Record。这项工作只是我们产品中测试套件的一部份,然则,没有必要再去测试我们创建的每一个AR类,因为,我们不会再对创建的AR类做这件事情了。
前面提到的测试向我们介绍了如何使用AR类。它展示了如何创建新记录,查询现有的记录,更新现有的记录,并删除现有记录。我们花了很多时间测试 Project表的AR类的一些低级别的操作。但我们的TrackStar应用还没有公开这些功能给用户(user)。我们真正是需要让用户(user) 来创建,读取,更新和删除项目(project)。现在,我们知道了一些围绕AR操作的方法,我们可以在控制器中开始编写一些功能代码。幸运的是,我们不用做。
让我们再次进入Gii代码生成工具帮我们我生成一个繁琐,耗时又很常见的代码。Yii为我们提供这个明确的方式,在应用程序中开发者使用共同的CRUD进行数据库操作。如果你已经熟悉了其他框架,你可能知道脚手架这个词。让我们来看看在Yii中利用它的优势。
访问http://localhost/trackstar/index.php?r=gii,并选择Crud Generator链接。你将看到如下画面:
在这里,我们看到两个表单字段域。第一个是要求我们指定模型类,我们希望生成所有的CRUD操作。在我们的例子中,使用Project.php这个AR 类。所以在这个字段域中填写Project。当我们输入后,我们注意到,Controller ID字段域自动填充成了project,这是基于Yii的约定。我们现在保持默认即可。
这两个字段域填写后,点击预览按钮,将会在页面底部看到如下表格:
我们可以看到将要生成很多的文件,其实包括一个ProjectContrller.php控制器类文件(它的包括了CRUD的所有操作方法)和许多的视图文件,每一个单独的视图文件对应每一个操作同时提供了可以搜索项目(project)记录。你当然也可以通过去掉复选框的选择来不生成一些相应文件。然后,对我们而言,我们很喜欢Gii为我们创建的这些文件。
继续并单击Generate(生成)按钮。我们应该在页面底部看到如下画面:
你可能需要确保Web服务器可以对/protected/controllers和/protected/views两个目录有可写权限,否则,你将收到权限错误消息,则不是成功的结果。
现在,我们可以点击try it now链接到这个页面去测试新功能了。
首先会带你进入项目(project)的列表页面。这个页面显示了当前系统中所有的项目(project)。你可能现在还看不到任务项目 (project),因为我们还没有创建新的项目(project)。然而,我们的项目(project)列表页面显示了几个项目(project),如下图所示:(访问地址:http://localhost/trackstar/index.php?r=project)
但是,这些项目是从何而来呢?你的项目(project)列表与截图相比可能或多于或少于3列,这取决于你在之前执行单元测试时的次数。我们在单元测试中为Project AR类编写的CRUD操作,实际上每次运行会在数据库中创建新记录。这些记录被创建出来时,我们还没有完成我们的测试代码,最终的测试代码是删除被创建的记录。在这个种特殊情况下,这已经有一些项目(project)了,这样我们就可以看清它们如何被显示出来的。然而,一般情况下,运行单元测试或功能测试使用开发数据库这种方式很不好。所以在后面,我们将讨论如何改变这些测试,让它们运行在一个单独的测试数据库。现在让我们继续试用新生成的代码。
你可能发现在项目(project)列表页(前面的那张截图)的右侧有一个小导航栏。点击Create Project链接。你会发现会把你们带到登录页面,而不是一个创建新项目(project)的表单。其原因是在Gii中定义了生成代码的规则,只有验证用户(即登录用户)可以创建新的项目(project)。任何匿名用户尝试访问创建新项目(project)页面都会被重定向到登录页面。好的,让我们使用demo作为帐号与密码进行登陆。
登陆成功后,你应该重定向到下面的网址:http://localhost/trackstar/index.php?r=project/create
此页面显示了一个用来添加新项目(project)的表单,如下图所示:
让我们快速填写此表单来建立一个新项目(project)。虽然这个表单中没有必填字段,让我们在Name字段填写Test Project,在Descrition字段填写Test project description。点击Create按钮将数据发送给服务器。如果出现错误,会显示错误信息,并且出错字段域会高亮显示。成功保存会重定向到新创建的项目(project)的具体列表。我们的操作成功了,页面被重定向到http://localhost/trackstar/index.php?r=project/view&id=4,如下图所示:
正如前面提到的,有一件事是关于我们新创建的项目(project)表单中所有字段域没有标记为必填,我们可以不输入任何数据就能提交成功。但是,我们知道,每个表单至少需要一个必填字段。让我们设置一个必填字段。
在Yii中当表单与AR模型类交互时,设置一个验证规则来限制字段域的范围。这需要在Project AR模型类的rules()方法中,添加一个数组,数组中包含特定的值。
打开 /protected/models/Project.php类,已经看到了公共的rules方法被定义了,并且在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('create_user_id, update_user_id', 'numerical', 'integerOnly'=>true), array('name', 'length', 'max'=>128), 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(验证器)指的是使用什么样的规则执行验证。on这个参数指定了一个scenario(情景)列表来使用这条验证规则。
scenario(情景)允许你限制验证规则应用在特定的上下文中。一种典型的例子是insert(插入)或update(更新)。例如:如果被指定为 'on'=>'insert',这将表明验证规则只适用于模型的插入情景。这同样适用于'update'或其它的任何你希望定义的情景。你可以设置一个模型的scenario(情景)属性或通过构造函数传给一个模型的实例。
如果这里没有设置,该规则将适用于调用save()方法的所有情景。最后,additional options(附加选项)是name/value(键值对)出现的用来初始化validator(验证器)的属性。
validator(验证器)可以是模型类中的一个方法或一个单独的验证器类。如果定义为模型类中的方法,它的格式必须是如下的形式:
/** * @param string the name of the attribute to be validated * @param array options specified in the validation rule */ public function ValidatorName($attribute,$params) { ... }
如果我们使用一个validator(验证器)类,则这个类必须继承CValidator。其实有三种方法可以指定validator(验证器),包括前面提到的一种格式:
Yii为你提供了很多预定义的验证器类,同时也指定了别名,用在定义规则时。Yii1.1版本,预定义的验证器别名的完整列表如下:
因为我们想使项目(project)的name属性字段必填,这个看着好像应该使用required别名可以满足我们的需要。让我们添加一验证规则指定name属性的验证器为required别名。我们将追加到现有的规则中:
public function rules() { // NOTE: you should only define rules for those attributes that will receive user inputs. return array( array('create_user_id, update_user_id', 'numerical','integerOnly'=>true), array('name', 'length', 'max'=>128), 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'), array('name', 'required'), ); }
保存项目(project)模型文件,并再次访问:http://localhost/trackstar/index.php?r=project /create, 我们看到在字段名称旁边有一个红色星号。这表明现在这个字段域是必需填写的。尝试不填写任何信息提交表单,你应该看到一个错误消息。指出这个字段域不能为空,如下图所示:
虽然我们已经做了一些改变,让我们继续为Description字段添加required验证。我们只需将Description字段添加到之前指定的那条规则中,如下所示:
array('name, description', 'required'),
这样,我们看到,我们可以在Arrtibute List(属性列表处)指定多个字段域,用逗号分隔。这样,我们声明的形式表明,无论是name(名称)或description(描述)属性都是必需有值的。尝试不填写任何信息提交表单,你应该看到一个错误消息。
如果我们在创建表时,规定name和description列为NOT NULL,则当我们使用Gii代码生成器创建模型类时,这个规则将会帮我们自动生成。它会在表中列的定义的基础上自动添加规则。例如:列包含了NOT NULL约束将会添加required验证器,另外,列有长度限制,比如name列定义为varchar(128),将会自动添加字符限制的规则。我们注意到,在Project AR类的rules()方法中,Gii自己动创建了一条规则
array('name', 'length', 'max'=>128)
查看我们新项目(project)详细信息: http://localhost/trackstar/index.php?r=project/view&id=4,那么,这基本上就是 CRUD中的"R",但是,要查看整个列表,我们可以点击右侧的List Project链接。这让我们回到了项目(project)列表页面,其实包含了我们新创建的项目(project)。所以,我们可以读取所有的项目 (project),以及每个项目的细节。
访问一项目的详细信息页面可以在项目的列表中点击每个项目的ID链接。让我们在项目列表中选择这个新创建的项目ID:4。点击这个链接将把会转到此项目的详细页面。本页面的几个操作(Operations)动作列表在页面的右侧,如下图所示:
我们看到了更新项目(Update Project)和删除项目(Delete Project)的链接分别指的是CRUD操作中的"U"和"D"。我们将会离开这个部份,你可以去验证这些链接所做的工作。
最后,我们讲一下前面没有提到的项目操作——管理项目(Manage Project)。点击这个连接,它可能看到一个授权错误,如下图所示:
此错误的原因是当我们登陆应用程序创建项目时,我们使用的是demo/demo的用户名/密码。由于这些代码是由Gii生成的,它限制了管理项目权由管理员可以访问。
进入管理员模式,仅仅需要使用admin/admin的用户名/密码组合。来吧,点击页面顶部导航上的Logout(demo)(注销demo帐号)。然后重新登陆,但这次使用管理员帐号。如果使用admin登陆成功(你可以通过页面顶部导航上的Logout(admin)验证是否成功)。访问一个特定的项目页,列如:http://localhost/trackstar/index.php?r=project/view&id=4,并尝试再次点击Manage Project链接。我们现在应该看到类似下图所示的页面:
我们现在所看到的是一个交互度更高的项目列表页面。它在一个可交互的数据表格中显示了所有项目。每一行都内置了查看、更新和删除这个项目的超链接。点击一列的表头上的链接会将按这一列值进行排序。第二行的一些输入框允许你按关键词搜索这个项目中这个列的值。Advanced Search(高级搜索)链接展示了一个搜索表单,并提供了多个搜索条件。下图展示了高级搜索表单:
在本次迭代中我们基本实现了目标提到的所有功能。但并没有写太多的代码。实际上,是在Gii的帮助下我们实现目标中没有期望到的基于项目(project)的搜索功能。虽然基本,但我用很少的代码完成了项目任务踪跟系统中的一个特有的功能。
但是现在尚不是搁置的时候。脚手架的代码并没有真正完全替代应用程序开发。相反,它会帮助我们建立真正的应用程序。当我们通过了解项目的功能如何工作的所有细节和差别,我们可以依靠自动生成的代码保持项目前进。我们将一样可以根据项目的求要,继续前进,但这些由脚手架自动生成的代码没有完成所有的功能,我们需要一个应用程序中管理项目(project)的完整的解决方案。
在我们继续为TrackStar应用程序添加新功能前,我们需要再次配置我们的测试。正如我们前面所讨论的,我们的单元测试实际上实现了在开发环境中为应用程序添加新项目(project),此外,我们完成了创建项目后并删除的测试,数据库将重复使用与插入相同的ID标识符。所以当我们继续执行我们的测试,我们将发现我们的项目ID越来越大(这可能与正常的开发产生混淆)。
这个问题是由于创建新项目时单元测试与web表单形式针对的是同一个数据库。因此,存在一些潜在的问题。我们需要做的是为测试环境配置一个独立的镜相数据库。它只专注于测试。我们也需要一种方式可以确保我们的测试对针相同的数据总是以相同方式运行。前者是通过配置文件很容易的就可以改变。后者是通过一个使用一个夹具(fixtures)完成。
一个测试夹具是指在测试运行中的一个系统状态或上下文。我们想多次运行我们的测试,每次运行我们希望能够让他们可以返回重复的结果。夹具的目的是提供一个众所周知在固定的环境中运行验证。通常情况下,一个夹具的主要工作是确保所有参与测试的对象在测试运行中初始化一个指定的状态。一个典型的夹具例子是使用固定和已知的数据加载一个数据库的表。
在Yii的PHP文件中夹具返回一个数组指定初始数据配置。它们通常的命名与它们所代表的数据库中表的名称相同,并保存在 protected/tests/fixtures/目录下。因此,要指定项目的夹具数据,我们将要在此目录下创建一个名叫tbl_project.php文件。此文件包含固定和已知的用来初始化在/tests/unit/ProjectTest.php文件中进行测试的数据库中项目表的数据。这个夹具文件是指定在ProjectTest.php测试文件顶部:
class ProjectTest extends CDbTestCase { public $fixtures=array ( 'projects'=>'Project', ); }
测试过程中,建立这些类型的数据库夹具可能是一个非常耗时的部份。Yii来了,它提供了CdbFixtureManager类再次把我们从这个乏味的程过拯救出来。当前我们配置一个应用组件时,它将提供以下功能:
要使用夹具管理器,我们需要配置应用程序中的配置文件。这实际上当我们创建初始应用程序时已经创建了。如果你打开了应用程序的指定的测试配置文件protected/config/test.php。你将看到如下的应用组件定义:
fixture'=>array( 'class'=>'system.test.CDbFixtureManager', ),
因此应用程序已经配置了并使用了夹具管理器。现在我们需要创建一个新夹具。
在Yii中一个夹具是在一个PHP文件中返回一个数组用来初始化特定表的数据行。文件名同表名相同。默认情况下,这些夹具文件将放置在 protected/tests/fixtures目录下。如果需要可以在应用程序配置中你可以使用 CDbFixtureManager::basePath属性改变这个位置。让我们先看一个为tbl_project表创建一个夹具的例子。创建一个新文件protected/tests/fixtures/tbl_project.php,并保存如下内容:
array( 'name' => 'Test Project 1', 'description' => 'This is test project 1', 'create_time' => '', 'create_user_id' => '', 'update_time' => '', 'update_user_id' => '', ), 'project2'=>array( 'name' => 'Test Project 2', 'description' => 'This is test project 2', 'create_time' => '', 'create_user_id' => '', 'update_time' => '', 'update_user_id' => '', ), 'project3'=>array( 'name' => 'Test Project 3', 'description' => 'This is test project 3', 'create_time' => '', 'create_user_id' => '', 'update_time' => '', 'update_user_id' => '', ), );
我们可以看到,我们的夹具数组的键象征我们的数据表。这些键的值是一个key=>value对的数组代表数据表中的列。我们添加了三行,但如果你喜欢可以添加更多行。为了简单起见,我们仅仅为值不能是NULL的字段预设了值,这是name和description字段。这些数据将足够夹具使用了。
你可能还注意到夹具数据没有指定id列。因为这列被定义为自增长类型,当我们插入新行时这列的值将会由数据库本身处理。
我们仍然需要告诉我们的单元测试实际使用这个刚刚创建的夹具.我们把它放在单元测试文件中.在这种情况下,我们需要在测试文件protected/tests/unit/ProjectTest.php的顶部添加我们的夹具声明:
'Project', ); }
因此,我们所做的是指定成员变量$fixtures,它的值是一个数组,指定用来测试使用的夹具。该数据表示一个夹具的名称与将要用于测试的模型类名称或夹具所指的名称的关系映射(例如,夹具的名称projects对应模型类Project)。当在使用模型类名称的情况下,则模型类的基础表将被视为夹具对应的表。正如我们前面所述,它是夹具管理器在每次执行测试方法时用来管理这个基本的表并重置数据。
如果你使用的夹具是针对一个表而不是一个AR类,你需要在表名前加上前缀冒号(例如,:tbl_project)来与模型类区分。
夹具的名字允许我们方便在测试方法里访问夹具数据。例如,我们已经在ProjectTest类定义了,我们可以通过以下的方式访问我们的夹具数据:
// return all rows in the 'Project' fixture table $projects = $this->projects; // return the row whose alias is 'project1' in the `Project` fixture table $projectOne = $this->projects['project1']; // If our fixture is associated with an active record, return the AR instance representing // the 'project1' fixture data row $project = $this->projects('project1');
我们将提供更具体的例子,当我们改变我们这些用于实际测试的夹具数据,首先,我们需要改变我们的测试环境。
正如我们之前提到的,我们需要区分开发数据库与测试数据库,使我们的测试不干涉开发。
Yii已经为我们指定了一个用于测试的配置文件。我们需要创建另一个数据库,它的名字叫trackstar_test。我们还需要复制当前的 trackstar_dev数据库的结构。这很容易,我们目前只有一个tbl_project表。请根据之前介绍创建tbl_project表。一旦创建后,我们可以在指定的配置文件protected/config/test.php中添加数据库连接信息作为应用程序的组件。你可以从main.php配置文件中复制db组件到test.php文件中。对于MySQL的用户保持一致即可。我们将要添加以下的高亮显示的代码到测试配置文件中:
return CMap::mergeArray( require(dirname(__FILE__).'/main.php'), array( 'components'=>array( 'fixture'=>array( 'class'=>'system.test.CDbFixtureManager', ), 'db'=>array( 'connectionString' => 'mysql:host=localhost;dbname=trackstar_test', 'emulatePrepare' => true, 'username' => '[your db username]', 'password' => '[your db password]', 'charset' => 'utf8', ), ), ) );
当我们运行测试会自动加载测试配置文件,而不是主配置文件。实际上它会合并主配置文件到测试配置文件中。如果定义了相同的组件或配置的值,则会优先考虑在测试配置文件定义的。现在,当我们运行单元测试时,我们将操作测试数据库而不是开发数据库,运行我们的测试套件不会对开发进度有负面影响。
现在,我们已经调整了我们的测试环境使用单独的数据库。我们应该采用夹具的优势。当我们最初在单元测试中为Project AR类编写CRUD操作时,我们把所有的创建,读取,更新,删除测试都放到了一个测试方法testCRUD()中。所有这些离散的测试放到一个大的测试中存在很多的缺点。如果首先创建失败,则整个测试方法将停止执行,读取,更新和删除的测试永远不会运行。理想的情况下,我们应该分离这些测试使彼此不产生依赖。这样做的最主要的原因是我们可以避免需要顺序运行测试方法。如果我们分开了创建测试,和读取测试,有一个读取方法在创建方法之前执行,这将导致测试失败,因为没有读取到创建返回的的这行数据。但是,如果我们使用夹具数据,我们就能够避免这个问题。
现在,我们新的测试环境已配置了专用数据库并定义了夹具数据,我们能分离CRUD单元测试。通过这一些具体的例子来了解如何使用夹具数据。
让我们开始读取测试。打开ProjectTest.php单元测试文件并添加如下测试方法:
public function testRead() { $retrievedProject = $this->projects('project1'); $this->assertTrue($retrievedProject instanceof Project); $this->assertEquals('Test Project 1',$retrievedProject->name); }
我们知道,在此测试运行之前,夹具管理器将使用已经定义的夹具数据重置trackstar_test数据库中的tbl_project表。在这里,我们只是简单的读取第一行数据,引用该行的别名project1,它将返回一个Project AR实例基于定义在protected/tests/fixture/tbl_project.php中的夹具数据的第一行数据。然后,我们测试刚刚返回的变量是否为一个Project类的实例,并测试它的名称(name)是否与夹具数据中的一致。
同样,我们可以添加单独的testCreate(), testUpdate()和testDelete()方法。整个测试文件修改后如下所示:
'Project', ); public function testCreate() { //CREATE a new Project $newProject=new Project; $newProjectName = 'Test Project Creation'; $newProject->setAttributes(array( 'name' => $newProjectName, 'description' => 'This is a test for new project creation', 'createTime' => '2009-09-09 00:00:00', 'createUser' => '1', 'updateTime' => '2009-09-09 00:00:00', 'updateUser' => '1', )); $this->assertTrue($newProject->save(false)); //READ back the newly created Project to ensure the creation worked $retrievedProject=Project::model()->findByPk($newProject->id); $this->assertTrue($retrievedProject instanceof Project); $this->assertEquals($newProjectName,$retrievedProject->name); } public function testRead() { $retrievedProject = $this->projects('project1'); $this->assertTrue($retrievedProject instanceof Project); $this->assertEquals('Test Project 1',$retrievedProject->name); } public function testUpdate() { $project = $this->projects('project2'); $updatedProjectName = 'Updated Test Project 2'; $project->name = $updatedProjectName; $this->assertTrue($project->save(false)); //read back the record again to ensure the update worked $updatedProject=Project::model()->findByPk($project->id); $this->assertTrue($updatedProject instanceof Project); $this->assertEquals($updatedProjectName,$updatedProject->name); } public function testDelete() { $project = $this->projects('project2'); $savedProjectId = $project->id; $this->assertTrue($project->delete()); $deletedProject=Project::model()->findByPk($savedProjectId); $this->assertEquals(NULL,$deletedProject); } }
现在,如果其中任何一个测试运行失败,其余的不同操作仍然可以提供更细的反馈。
虽然我们没有在这章进行实际编码,但我们也完成了很多事情。我们创建一个新的数据库表,并看到了Yii中AR的操作。我们使用Gii代码生成器首先创建了一个封装了tbl_project表的AR类。然后我们编写了测试,去尝试了解和使用AR类。
然后我们演示了怎么使用Gii代码生成工具生成了Web应用程序的CRUD功能。有了这个非常棒的工具,我们实现了本次迭代计划的提供的大部分功能。我们修改了提交表单中项目的名称(name)和描述(description)的验证功能。
最后,我们介绍了Yii中的测试夹具,并利用夹具的优势对测试环境作出了一些调整。
在接下来的迭代中,我们将通过我们在本章中学习到的知识,继续建立相关的数据模型。