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

将Yii框架引入咱们的应用

如今,咱们拥有了能够工做的全套基础设施,让咱们回到在设计阶段时定义的第一个特性,让咱们先为它写一个验收测试。
php

第一个端到端测试

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

这里是结果测试的步骤:github

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

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

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

  4. 打开经过手机号码查询客户的界面数据库

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

所以,测试强制咱们提供三个页面:新建客户,客户列表,以及查询页面。这部分就是为何咱们要称之为”端到端测试“。数组

翻译成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章,整体行为,会给你一个详细的解释。

相关文章
相关标签/搜索