【翻譯】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章,整體行爲,會給你一個詳細的解釋。

相關文章
相關標籤/搜索