文档章节

【翻译】Yii2 第2章 用Yii2创建自定义应用(第2部分)

zcgly
 zcgly
发布于 2015/09/29 12:53
字数 4511
阅读 454
收藏 3

将Yii框架引入我们的应用

现在,我们拥有了可以工作的全套基础设施,让我们回到在设计阶段时定义的第一个特性,让我们先为它写一个验收测试。

第一个端到端测试

端到端验收测试的要点就是,我们必须只通过UI来处理我们的应用。我们不能用任何方式去直接访问数据库,更糟糕的是,直接访问应用的文件系统。所以,测试一个数据库查询,数据应首先插入数据库。然后依靠UI来完成测试。

这里是结果测试的步骤:

  1. 打开添加客户数据到数据库的界面

  2. 添加第一个客户到数据库。你应该看到客户列表,只有一条记录

  3. 添加第二个客户到数据库。你应该看到客户列表里有两条数据了

  4. 打开通过手机号码查询客户的界面

  5. 用客户1的手机号码进行查询,你应该看到界面查询结果中有客户1,并且没有客户2

因此,测试强制我们提供三个页面:新建客户,客户列表,以及查询页面。这部分就是为什么我们要称之为”端到端测试“。

翻译成Codeception的测试代码,刚刚描述的过程就像这样:

$I = new \AcceptanceTester\CRMOperatorSteps($scenario);
$I->wantTo('add two different customers to database');

$I->amInAddCustomerUi();
$first_customer = $I->imageCustomer();
$I->fillCustomerDataForm($first_customer);
$I->submitCustomerDataForm();

$I->seeIAmInListCustomersUi();

$I->amInAddCustomerUi();
$second_customer = $I->imagineCustomer();
$I->fillCustomerDataForm($second_customer);
$I->submitCustomerDataForm();

$I->seeIAmInListCustomersUi();

$I = new \AcceptanceTester\CRMUserSteps($scenario);
$I->wantTo('query the customer info using his phone number');

$I->amInQueryCustomerUi();
$I->fillInPhoneFieldWithDataForm($first_customer);
$I->clickSearchButton();

$I->seeIAmInListCustomersUi();
$I->seeCustomerInList($first_customer);
$I->dontSeeCustomerInList($second_customer);

让我们把这段代码放到 tests/acceptance/QueryCustomerByPhoneNumberCept.php 文件中。这就是本章我们要完成的目标。

让我们重新浏览这些不那么显而易见的测试脚本。

首先,我们将整个场景拆分成两个逻辑部分,使用了两个Acceptance的子类来强调它们的不同之处。Codeception有一个非常好的辅助生成不同Guy类子类的方法,使用它,我们可以用下面的命令来创建 \AcceptanceTester\CRMOperatorSteps 类:

cept generate:stepobject acceptance CRMOperatorSteps

在对象被生成前,Codeception(译注:作者此处笔误为Composer)会提示你输入方法名。直接回车,就是告诉codeception你打算重新开始。

这个辅助器被用来支持StepObject模式(http://codeception.com/docs/07-AdvancedUsage#StepObjects),因此,它会自动添加Steps后缀到 CRMOperatorSteps 类名后。当然,把AcceptanceTester子类分成不同的角色,比只是定义一些抽象的steps容器要更自然。然而,如果我们强制重命名生成的类,删除后缀,我们将失去Codeception提供的自动加载能力,相比之下,我们还是忍受这种命名方式吧。CRMOperatorSteps.php 类会被放在 tests/acceptance/_steps 子目录中。

我们用同样的方法生成 CRMUserSteps 类。

现在,让我们来定义之前提到测试场景的steps。几乎所有的高级steps正好是Codeception内建的低级step的容器。

首先,我们来看看CRMOpeator的steps。

“I am in Add Customer UI”step是一个完成添加客户特性的开放路由,因此,代码差不多像这样:

function amInAddCustomerUi()
{
    $I = $this;
    $I->amOnPage('/customers/add');
}

"Imagine Customer"是进入添加客户界面后,自动随机生成客户数据的辅助方法。占位数据可以用任何方式来生成。我们将使用一个令人吃惊的Faker库(https://github.com/fzaninotto/Faker),来生成看起来真实的数据。稍后,我再来深入分析一下。现在,需要在添加客户的实际界面中录入数据。我们在这里不去追求很炫的界面,只是一个带提交按钮的HTML表单就够了。但是,哪些字段需要填充呢?让我们回到客户模型,来看看哪些部分在测试场景中是必须填充的。

为了简化问题,我们把电子邮件和地址留到以后处理。我们也完全没有考虑联系人集合,同样是出于简化的目的。我们包含了客户所有的唯一部分:姓名、生日、备注。记住,姓名是一个结构,而不只是像Notes一样的文本行。

现在,让我们把注意力集中在添加客户表单的字段上。请注意表单上的姓名字段,这不是任意指定的,而是跟我们的未来数据库结构以及Yii2的模型配置是一致的。让我们来看看这张表:

注意,虽然我们设计是客户可以有多个电话,但我们只有一个也是允许的。我们推荐不直接去实现一个特性,而是应该先为它写一个测试。我们的测试没有去明确检查,允许存在多个电话的能力。

所以,我们现在来定义 CRMOperatorSteps.imagineCustomer 方法。首先,我们将 Faker 库引入项目:

php composer.phar require "fzaninotto/faker:*"

然后,我们用以下代码来配置客户的属性:

public function imagineCustomer()
{
    $faker = \Faker\Factory::create();
    return [
        'CustomerRecord[name]' => $faker->name,
        'CustomerRecord[birth_date]' => $faker->date('Y-m-d'),
        'CustomerRecord[notes]' => $faker->sentence(8),
        'PhoneRecord[number]' => $faker->phoneNumber,
    ];
}

这样,我们创建了一个很容易使用的结构,在 fillCustomerData 方法中,我们可以这样使用:

function fillCustomerDataForm($fieldsData)
{
    $I = $this;
    foreach($fieldsData as $key => $value){
        $I->fillField($key, $value);
    }
}

提交表单的操作就比较直接了当,我们把按钮命名为Submit:

function submitCustomerDataForm()
{
    $I = $this;
    $I->click('Submit');
}

然后,我们需要两个方法,一个是用来检查我们是不是处于客户列表界面,另一个是转到客户列表页面:

public function seeIAmInListCustomersUi()
{
    $I = $this;
    $I->seeCurrentUrlMatches('/customers/');
}

function amInListCustomersUi()
{
    $I = $this;
    $I->amOnPage('/customers');
}

在Codeception的概念中,断言方法应该在方法名中带有see前缀,所以我们遵守了这一条约定。

我们使用方法 CurrentUrlMatches 利用正则表达式来匹配URL,而不是采用更加严格的 CurrentUrlEquals,这是因为我们假定在URL的尾部,还会含有一些查询参数。

写完这些定义在 CRMOperatorSteps 类中的方法,我们首个测试用例就完成一半了(这意味着可运行了)。

让我们从CRM用户视角,来做完整个测试,他们需要使用查询功能。在 CRMUserSteps 类中,我们需要写如下代码。首先,比较显而易见的是:

function amInQueryCustomerUi()
{
    $I = $this;
    $I->amOnPage('/customers/query');
}

让我们用在 添加客户界面 中相同的命名方式,来命名 填充电话号码字段 这个方法。

function fillInPhoneFieldWithDataForm($customer_data)
{
    $I = $this;
    $I->fillField('PhoneRecord[number]', $customer_data['PhoneRecord[number]']);
}

让我们将查询客户数据的按钮命名为Search:

function clickSearchButton()
{
    $I = $this;
    $I->click('Search');
}

复制一下 CRMOperatorSteps.seeIAmInListCustomersUi:

function seeIAmInListCustomersUi()
{
    $I = $this;
    $I->seeCurrentUrlMatches('/customers');
}

这是为了让我们遵守 Refactoring: Improving the Design of Existing Code, Martin Fowler, Kent Beck, John Brant, William Opdyke, and Don Roberts, Addison-Wesley Professional 的第三规则。

最后,我们来添加断言:

function seeCustomerInList($customer_data)
{
    $I = $this;
    $I->see($customer_data['CustomerRecord[name]'], '#search_results');
}

function dontSeeCustomerInList($customer_data)
{
    $I = $this;
    $I->dontSee($customer_data['CustomerRecord[name]'], '#search_results');
}

我们需要注意,这个极其简单的实现,是基于几个在开发阶段有效的假设的:

  • 所有客户都定义了姓名

  • 没有重名客户

  • 搜索结果呈现在id为 search_results 的HTML元素中

让我们保持这个测试简单,但是,当我们有超过一个的搜索结果时,我们需要思考怎样正确检测一条具体结果是否存在(最可能的是,see方法提供的缺省的see语义就不够用了)。

一个很重要的问题是,为什么我们不能在每新增一个客户时,通过客户列表的UI来检测客户数据。在我们用电话号码查询后,毕竟我们会得到相同的客户列表UI。

原因非常简单:我们的目标是我们能通过电话号码查询客户。并且,中途存在的断言会违反“单一断言原则”(Clean Code, Robert Martin, Prentice Hall 里有详细的解释)。然而,因为这是一个端到端的验收测试,这样做也并非坏事。无论如何,没什么会妨碍我们今后扩展这个测试(这只是一个模拟真实用户行为的端对端测试)。但现在,让我们保持简单的场景。

如果你现在运行完整的测试场景,你可能会遇到下面的错误:

1)Failed to add two different customers to database in QueryCustomerByPhoneNumberCept

Sorry, I couldn't fill field "CustomerRecord[first_name]", "Cheyanne": Field by name, label, CSS or XPath 'CustomerRecord[first_name]' was not found on page.

Scenario Steps:

2. I fill field "CustomerRecord[first_name]", "Cheyanne"

1. I am on page "/customers/add"

遇到这些错误的原因是,我们还没有处理 /customers/add 请求。

下面我们到了该安装Yii2的时候了。

安装 Yii2 代码

我们打算完成一个完整的自定义应用,并不想依赖Yii框架的目录结构,只要能方便的使用它提供的类就可以了。

首先,在应用中声明对Yii2的依赖。

手工在composer.json文件中加入require行,与执行下面的命令的作用是一样的:

php composer.phar require "yiisoft/yii2:*"

如果你是手工编辑composer.json文件,记得还要运行安装命令:

php composer.phar install

这样,Composer会把Yii2安装到你的代码中,位于 vendor/yiisoft/yii2 目录。

检查安装环境

Yii2包含了一个重要的特性,提供了一个内建的需求环境检查器。当你安装在第一章中讨论的应用模版时,在代码的根目录会有一个 requirements.php 的脚本。它非常有用,所以拷贝一份,粘贴到web子目录中。你也可以从Yii2代码仓库下载这个文件https://github.com/yiisoft/yii2/blob/master/apps/basic/requirements.php。取得这个文件后,在命令行运行它:

php web/requirements.php

或者,你也可以用浏览器访问 http://<your_domain>/requirements.php 得到一个更加友好的页面,来查看部署环境是否满足框架的需求。

Yii2约定介绍

真正高层的说明如下所述。为了服务发送到应用的请求,Yii实例化一个 \yii\web\Application 对象,它使用 MVC 模式来处理请求,返回结果。如果你忘记或是对MVC模式不熟悉,你可能需要阅读一下Yii的官方文档,为后面更深的内容做准备。

Yii对 MVC 模式的解释是:

  • View 是负责呈现的类,无论发送什么到客户端,都由它来展现。通常,是HTML页面,但也不局限于此

  • Model 是包含商业规则的类

  • Controller 是接受用户请求,决定如何处理,如果必要,调用 model 进行实际处理工作,并使用 view 来呈现结果,将结果返回给用户

这个模式最微妙的部分是 model 的概念。依照解释,model 既可以是 controller 用来获取数据,再推送给 view,也可以直接就是 controller 推送给 view。Yii2没有做强制性规定,但 model 的实现假定它是数据容器,短暂(只在内存中)或持久(通过Active Record模式实现)存在。

因此,一个请求经历了如下步骤:

  1. web服务器接收到请求,传递给 index.php 脚本。

  2. 一个Yii的Application对象被创建。它决定使用哪个Controller来处理请求。

  3. 一个Controller对象被创建。它决定使用哪个Action来执行(可以是Controller的方法,或者另一个分离的类),将详细的请求信息传递给Action来执行

  4. action被执行,通常会通过view来返回结果。这不是框架强制性的要求,你也可以不呈现任何东西。

  5. 在将结果返回给用户之前,一个特殊的应用组件负责格式化数据。

  6. 结果数据,HTML或JSON或XML甚至是一个空的返回,发送给客户

理解以上这些步骤,让我们修改当前的入口脚本,利用Yii框架而非直接输出原始文本,来完成同样的工作。我们将在第12章,路由管理中看到更好更详细的流程图。

构建框架代码

现在,我们的项目结构如下图所示:

我们将从入口点脚本开始介绍Yii2。为了简化处理,index.php 文件看起来应该如下所示:

<?php
// Including the Yii framework itself (1)
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
// Getting the configuration (2)
$config = require(__DIR__ . '/../config/web.php');
// Making and launching the application immediately (3)
(new yii\web\Application($config))->run();

在代码(1)处,我们将Yii框架引入了环境中。

在代码(2)处,我们引入了应用的配置文件。Yii应用的配置是一个巨大的PHP数组,包含应用的初始配置,以及众多组件的配置。

在代码(3)处,我们创建了一个Application子类WebApplication的实例,并立即调用了run方法。

再回到代码(2)处,我们载入了一个并不存在的文件 config/web.php,让我们来实现它:

<?php
return [
    'id' => 'crmapp',
    'basePath' => realpath(__DIR__ . '/../'),
    'components' => [
        'request' => [
            'cookieValidationKey' => 'your secret key here',
        ],
    ],
];

我们必须详细说明一下这三个设置:

  • id:这是应用的强制性标识符。它是必须的,我们用它来跟应用的其它模块区分开。顶层应用,是跟普通模块遵从同样的规则的。

  • basePath:这也是强制性的,因为对Yii来说,这是在文件系统中定位应用的基本方法。在其它地方设置的相对路径,都是基于这里设置的基础路径。

  • components.request.cookieValidationKey:这是用户认证子系统的一个漏洞,我们将在第5章用户认证中进行讨论。该项设置是一个私有key,用于“记住我”这个特性,依赖于cookies。在早期的Yii2的beta版中,这个key是自动生成的。从4e4e76e8提交可以看到(https://github.com/yiisoft/yii2/commit/4e4e76e8838cbe097134d6f9c2ea58f20c1deed6)。除了这个设置项之外,你也可以将components.request.enableCookieValidation设置为false,这样禁用基于cookie的认证。这样,应用也可以正常工作(译注:如果这两个设置项都没有设置,请求将会显示一个错误提示)

接下来,我们将添加一些强制性的目录,因为如果没有这些目录,Yii将抛出一些异常。注意,请不要创建 web/assets 和 runtime 目录。这些目录在应用运行时被框架使用。

添加控制器

每一个控制器都应该具备以下三个特征:

  • 必须属于在 Application 类的 controllerNamesapce 项定义的命名空间。

  • 名称中必须包含 Controller 后缀

  • 必须是 \yii\base\Controller 的扩展类。当前示例是一个web应用,而不是一个控制台应用,因此,我们应从 \yii\web\Controller 继承。

另外,这对理解Yii2实际查找控制器类非常重要。

在通常情况下,Yii2利用一个兼容PSR-4标准的类自动装载器(http://www.php-fig.org/psr/psr-4/)。为了简化处理,自动装载器把命名空间作为文件中的路径,利用一个已经定义的特殊根命名空间,映射到代码根目录。

在我们的案例中,Yii2为我们定义了 \app 这个命名空间,映射到代码根目录。controllerNamespace 设置项的缺省值就是 \app\controllers,映射到代码根目录下的 controllers 目录,因此,所有的控制器都应该放在这里。

采用这种机制,所有的类都可以通过Yii2的自动装载器进行正确的加载。

现在,我们来创建第一个控制器来通过冒烟测试。我们不去改变缺省的控制器命名空间设置,只需要在 controllers/SiteController.php 文件中写入如下代码:

namespace app\controllers;
use \yii\web\Controller;
class SiteController extends Controller
{
    public function actionIndex()
    {
        return 'Our CRM';
    }
}

这段代码依赖了Yii的约定。不用深入研究Yii的路由,我们就知道,不进行特殊的设置时,Yii会调用 SiteController 控制器的 actionIndex 方法来处理“/”请求。

定义控制器action最简单直接的方法,是将它作为控制器的public方法,并且名称带有action前缀。显式请求SiteController.actionIndex方法,应该请求 site/index.php。

冒烟测试通过了,让我们来添加一些调试用的辅助工具吧。

处理可能产生的错误

在开发阶段,你可能会碰到各种奇葩的错误。让我们看看,有没有办法简单快捷的对应用进行设置,收集尽可能详细的出错信息。

首先,当你犯下一个可怕的错误时,比如没有定义id或bathPath配置项,你基本上就会得到一个空白页,这时,你只能去查看web服务器的日志。例如,在Apache中,你可以可以使用指令 ErrorLog 来指定错误报告文件,只要不是浏览器渲染阶段的错误,都可以在这里找到。

与“空白页”斗争,你需要在 index.php 入口点中加入 display_errors 设置,放在 Yii 库的后面,并且必须放在Application对象创建和执行的前面,代码如下:

ini_set('display_errors', true);

同样,你也可以在引入Yii之前,添加一个方便的常量。在引入Yii之前加入,代码放置的位置非常重要,因为如果你没有定义,Yii会用缺省值定义它。代码如下:

define('YII_DEBUG', true);

这将改变应用的调试模式,如果有异常抛出,通常会得到500错误页或空白页面,但同时,详细的错误报告会将最重要的行高亮显示。

最后,你需要将添加自定义的日志添加到应用中,这会将应用中的错误记录到文件中。第8章,总体行为,会给你一个详细的解释。





© 著作权归作者所有

zcgly

zcgly

粉丝 7
博文 6
码字总数 13340
作品 0
成都
技术主管
私信 提问
加载中

评论(1)

y
yihuage
这本教材感觉写的挺好的,博主翻译也挺好,后续的还有就更好了
【翻译】Yii2 第1章 开始

让我们看看,怎样以最小的代价使用Yii2创建一个站点。目的是学习使用Yii2应用模版的安装过程,并开始体验模版里提供的一系列特性。 一个基本应用 开始使用Yii2最基本和直接的方式,是使用Yii...

zcgly
2015/09/15
966
7
Yii 2.0开发一个仿京东商城平台

第1章 课程简介 介绍了课程内容、背景和案例展示。 第2章 项目的准备工作 介绍了如何使用PHP依赖管理工具Composer安装Yii2框架,模拟配置真实企业开发项目运行环境和编辑器。 第3章 项目前台...

15543595340
2018/05/19
0
0
yii2源码分析之执行基本流程

用yii2框架用了将近2年,一直都没有去看过它底层源码, 马上快不用了,最近对其源码研究一番,哈哈 废话少说,上代码, 入口文件是web/index.php

china_lx1
2018/04/22
0
0
选择 Yii 2 框架的 7 个理由

去年,SitePoint网站发布了一篇文章重点介绍了一些顶尖的PHP开发框架。 排名第四的是Yii(发音同Yee)框架。 那时Yii框架最新的版本是1.1.14。最近,Yii 2.0版发布了,你可以在产品中使用2.0...

oschina
2014/10/16
18.4K
66
Yii2 2.0.12 发布,高性能 PHP 框架

Yii2 2.0.12 发布了。Yii 2 完全根据 Yii 1.1 版本重写,后者是最流行的 PHP 框架之一。Yii 2 继承了 Yii 的简洁、快速、和高扩展性。Yii 2 需要 PHP 5.4,并且拥有现代 Web 应用开发中最好的...

达尔文
2017/06/06
1K
19

没有更多内容

加载失败,请刷新页面

加载更多

高速PCB设计软件allegro中与网络有关的约束规则设置

在allegro pcb的设计过程中,设计约束规则包括时序规则、间距规则、信号完整性规则以及物理规则等,本期主要详细讲解与物理、间距与电气约束中的线宽、线间距物理规则的设置。 一、线宽设置 ...

demyar
21分钟前
2
0
Linux 启动停止SpringBoot jar 程序部署Shell 脚本

#!/bin/bash #这里可替换为你自己的执行程序,其他代码无需更改 APP_NAME=algorithm.jar #使用说明,用来提示输入参数 usage() { echo "Usage: sh 执行脚本.sh [start|stop|restart|status]...

草庐过客
23分钟前
3
0
mysql-connector-java驱动升级到8.0后数据库保存时间出现时差

1.问题:在一个新项目中用到了新版的mysql jdbc 驱动后,发现保存到数据库的时间出现了时差 <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId>......

ValSong
24分钟前
3
0
好程序员大数据教程Scala系列之隐式转换和隐式参数

5.1. 概念 隐式转换和隐式参数是Scala中两个非常强大的功能,利用隐式转换和隐式参数,你可以提供优雅的类库,对类库的使用者隐匿掉那些枯燥乏味的细节。 5.2. 作用 隐式的对类的方法进行增强...

好程序员官网
28分钟前
3
0
多线程必备

初次接触线程,可能有很多初学者搞不明白,始终云里雾里,那么本篇文章直接带大家介绍多线程必须知道的几个点 接下来没有多余,直接上干货 1. 进程和线程的区别是什么? 进程是执行着的应用程序,...

理性思考
31分钟前
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部