WADWYAP--读书笔记 [ 2.0 版本 ]
本篇是Web Application Development with Yii2 and PHP(简写WADWYAP)的读书笔记,会有多处删减,不过不会影响主体。英文能力不错的推荐去看原版:链接
项目简介
实现对customer的增加和根据电话号码查询。
为了方便,规定一个customer只有name,birth_date,notes,phone number四个字段,而phone number可以有多个。所以有两张表:
- customer: id,name,birth_date,notes
- phone:id,customer_id,number
环境准备
- 安装Composer,并将Composer加入系统path环境变量。任何目录下打开命令行,执行
composer -v
会输入版本信息。 - 安装PHP,并将PHP加入系统path环境变量。任何目录下打开命令行,执行
php -v
会输入版本信息。由于是案列开发展示,为了方便,使用PHP内置的服务器,不需要安装Apache等WEB服务器。 - 安装Mysql。并用账户密码登陆mysql查看是否安装成功。
必须安装完已上三个程序再进行下一步。不熟悉的自行百度谷歌解决
测试实践
安装测试环境
测试先行(test first)开发方法已经耳熟能详,因此本书进行尝试,全部采用基于acceptance的测试进行开发。对于测试开发的详细概念并不做深究,推荐小伙伴自行拓展。Yii2 内置了Codeception测试框架,并提供了一系列的助手类方便和Yii2结合进行测试。但是本书中并不才用他,而是只接使用Codeception官方类库。
首先建立项目根文件夹,然后在项目根文件夹下打开命令行执行composer require "codeception/codeception:2.3.8"
,该命令会自动建立vendor文件夹并下载相应文件。(请确保下载相同版本,以免运行出现错误。)接着打开vendor\bin
文件夹,会看到有codecept等一系列可执行文件,然后把该目录加入系统path环境变量,以使在系统任何目录下不必输入全路径直接使用codecept就可以执行该命令。
codeception是一个复杂的系统。因此在项目根文件夹下执行codecept bootstrap
,该命令讲建立一个test
目录并配置默认的codeception。
建立第一个测试
项目跟文件夹下执行codecept g:cept acceptance SmokeTest
,该命令在tests\acceptance
文件夹下创建SmokeTest.php
文件。打开并修改其中内容如下
$I = new AcceptanceTester($scenario);
//提示我们想要执行什么操作
$I->wantTo('查看主页');
//访问链接,相对路径,默认会和下面的设置组成 http://localhost:8080/index.php
$I->amOnPage('/');
//查看打开的页面是否有改内容
$I->see('Our CRM');
AcceptanceTester是拥有所有测试方法的类,我们可以使用该类去模拟一个浏览器上的真实用户,以便测试我们的应用。
现在我们运行codecept run
,提示错误。因为我们并没有开启web服务器及配置codeception。
打开tests/acceptance.suite.yml
修改如下
actor: AcceptanceTester
modules:
enabled:
- PhpBrowser:
url: http://localhost:8080/index.php
- \Helper\Acceptance
配置使用PhpBrowser模拟用户行为,url是默认访问地址。codeception有两种模拟用户的行为,一种是PhpBrowser软件模拟,二是WebDriver,打开一个真是浏览器访问。详细对比看链接。
接着在项目根文件夹打开命令行,执行php -S localhost:8080
建立一个web服务器。然后在新建一个index.php
文件。内容为Our CRM
.
命令行执行codecept run
。显示执行成功。
建立测试
改案例执行下列操作:
- 打开增加顾客页面
- 增加顾客customer1到数据库,顾客列表应该显示一个顾客
- 增加顾客customer2到数据库,顾客列表应该显示两个顾客
- 打开通过手机号查询顾客页面
- 输入customer1的手机号,查询结果页面应该显示customer1的信息而没有customer2的信息
把操作转换为测试操作
执行 codecept g:cept acceptance QueryCustomerByPhoneNumber
,生成文件修改内容如下
<?php
$I = new \Step\Acceptance\CRMOperatorSteps($scenario);
$I->wantTo('add two different customers to database');
//增加第一个用户到数据库
$I->amInaddCustomerUI();
$first_customer = $I->imagineCustomer();
$I->fillCustomerDataForm($first_customer);
$I->submitCustomerDataForm();
$I->seeIAmInListCustomersUI();
//增加第二个用户到数据库
$I->amInaddCustomerUI();
$second_customer = $I->imagineCustomer();
$I->fillCustomerDataForm($second_customer);
$I->submitCustomerDataForm();
$I->seeIAmInListCustomersUI();
//查询用户
$I = new \Step\Acceptance\CRMUserSteps($scenario);
$I->wantTo('query the customers info using his phone number');
$I->amInQueryCustomerUI();
$I->fillInPhoneFieldWithDataFrom($first_customer);
$I->clickSearchButton();
$I->seeIAmInListCustomersUI();
$I->seeCustomerInList($first_customer);
$I->dontSeeCustomerInList($second_customer);
该测试中使用了StepObject,StepObject是AcceptanceTester的子类,继承了其所有方法,我们可以在其中添加方法,达到复用的目的。详细教程
执行 codecept g:stepobject acceptance CRMOperatorSteps
一路回车,不用输入。然后根据提示找到该文件增加方法如下。
public function amInAddCustomerUI()
{
$I = $this;
$I->amOnPage('/customers/add');
# code...
}
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
];
}
public function fillCustomerDataForm($fieldsData)
{
$I = $this;
foreach ($fieldsData as $key => $value) {
$I->fillField($key,$value);
}
}
public function submitCustomerDataForm()
{
$I = $this;
$I->click('Submit');
}
public function seeIAmInListCustomersUi()
{
$I = $this;
$I->seeCurrentUrlMatches('/customers/');
}
public function amInListCustomersUi()
{
$I = $this;
$I->amOnPage('/customers');
}
其中我们使用了Faker库生成模拟数据。执行composer require "fzaninotto/faker:1.7.1"
导入该库。
执行 codecept g:stepobject acceptance CRMUserSteps
一路回车,不用输入。然后根据提示找到该文件增加方法如下。
public function seeLargeBodyOfText()
{
$I =$this;
$text = $I->grabTextFrom('p');
$I->seeContentIsLong($text);
}
public function amInQueryCustomerUI()
{
$I = $this;
$I->amOnPage('/customers/query');
}
public function fillInPhoneFieldWithDataFrom($customer_data)
{
$I = $this;
$I->fillField(
'phone_number',
$customer_data['PhoneRecord[number]']
);
}
public function clickSearchButton()
{
$I =$this;
$I->click('Search');
}
public function seeIAmInListCustomersUi()
{
$I = $this;
$I->seeCurrentUrlMatches('/customers/');
}
public function seeCustomerInList($customer_data)
{
$I = $this;
$I->see($customer_data['CustomerRecord[name]'],
'#search_results');
}
public function dontSeeCustomerInList($customer_data)
{
$I = $this;
$I->dontSee(
$customer_data['CustomerRecord[name]'],
'#search_results');
}
仔细看看这三个类,方法名都说明了功能。然后我们需要做的就是让测试通过。
导入Yii2
本书是将Yii2作为一个包引入,并不是直接使用Yii提供的模板。下面事详细步骤。
安装Yii2
执行composer require "yiisoft/yii2:2.0.14"
。注意的是安装过程中需要下载大量文件,可能要求输入github token,这时用自己的github账号生成一个粘贴过来就行了。具体步骤谷歌。运行完后可以看到有个vendor/yiisoft/yii2
。说明Yii2已经导入了。
配制Yii2
Yii2是一个单一入口的MVC的框架(有疑问自行谷歌)。因此一个请求的流程如下:
- WEB服务器收到请求发送给入口文件 index.php
- 一个Yii Application对象被实例化。它决定处理该请求的Controller。
- 该Controller实例化,然后决定处理该请求的action,并运行该action
- 该action运行,开发者可以在该action返回一个view,或者是xml,json,甚至不返回任何东西。
- 特殊的组件可以在数据返回给用户前格式化该数据。
知道这些步骤来建立我们的目录结构:
+-- config
| +-- web.php
+-- controllers
+-- view
+-- models
+-- tests
+-- vender
+-- web
| +-- index.php
修改index.php如下
//设置开发模式,会提示错误
define('YII_DEBUG', 'true');
//导入composer自动加载,可以加载composer require进来的包
require __DIR__.'\\..\\vendor\\autoload.php';
//导入Yii框架自身
require(__DIR__.'\\..\\vendor\\yiisoft\\yii2\\Yii.php');
//载入配置文件
$config = require(__DIR__.'\\..\\config\\web.php');
//实例化一个应用并运行
(new yii\web\Application($config))->run();
config/web.php
配置文件如下。
<?php
return [
//id是必须设置的项目,是该应用的唯一标识
'id' => 'app',
//必须设置的属性。告诉Yii自身存在系统中的位置。
'basePath' => dirname(__DIR__),
'components'=>[
'request'=>[
//这是安全设置,防止应用被恶意攻击。自由填写。可以通过设置enableCookieValidation 为 false 关闭。
'cookieValidationKey'=>'qweqweqw21',
],
],
];
测试Yii控制器
理解Yii如何寻找控制类是重要的,Yii2自动载入兼容PSR-4标准。Yii定义了我们的项目根目录为\app
命名空间,因此默认的控制器命名空间是\app\controllers
。
建立controller/SiteController.php
,内容如下
<?php
namespace app\controllers;
use \yii\web\Controller;
class SiteController extends Controller
{
public function actionIndex()
{
return 'Our CRM';
}
}
然后进入web文件夹,执行php -S localhost:8080
建立web服务器。
运行codecept run tests\acceptance\SmokeTestCept
,查看是否成功,成功则说明Yii已经正常运行了。
建立控制器
由前面测试可以。我们首先需要提供两个路由 /customers/add
和/customers
建立 controllers\CustomersController.php
,内容如下
<?php
namespace app\controllers;
use yii\web\Controller;
class CustomersController extends Controller
{
public function actionIndex()
{
$records = $this->getRecordsAccordingToQuery();
$this->render('index', compact('records'));
}
public function actionAdd()
{
$this->render('add');
}
}
为了获得数据,我们需要定义Model。在数据层创立一个customer模型(采用了领域模型设计模式,能从ORM中实现解耦,不过究竟如何实现解耦的,我依旧没研究透,希望有大神分析一下。)
建立 models\customer\Customer.php
,内容如下:
<?php
namespace app\models\customer;
/**
*
*/
class Customer
{
/** @var string [string] */
public $name;
/** @var \DateTime [\DateTime] */
public $birth_date;
/** @var string [description] */
public $notes;
/** @var array [description] */
public $phones= [];
public function __construct($name, $birth_date)
{
$this->name = $name;
$this->birth_date = $birth_date;
}
}
再建立 models\customer\Phone.php
,内容如下:
<?php
namespace app\models\customer;
/**
*
*/
class Phone
{
/** @var string [string] */
public $number;
public function __construct($number)
{
$this->number = $number;
}
}
创键数据表
传统的我们手动使用sql语句去建立数据库,但是Yii提供了自动化创建数据库的方法。并能将sql纳入版本管理。
在项目根目录下建立yii文件,无后缀名。内容如下
#!/usr/bin/env php
<?php
define('YII_DEBUG', true);
require(__DIR__ . '/vendor/autoload.php');
require(__DIR__ . '/vendor/yiisoft/yii2/Yii.php');
$config = require(__DIR__ . '/config/console.php');
//yii提供了两种环境下的运行方式,一种是web,一种是命令行
$application = new yii\console\Application($config);
$exitCode = $application->run();
exit($exitCode);
linux下,直接./yii
就能运行,windows下执行php yii
运行。注意linux还必须赋予执行权限。大家可以运行下看看有多少操作命令及功能。
由上建立config/console.php
配置文件。
<?php
return [
'id' => 'crmapp-console',
'basePath' => dirname(__DIR__),
'components' => [
'db' => require(__DIR__ . '/db.php'),
],
];
在建立config/db.php
配置文件。
<?php
return [
'class' => '\yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=crmapp',
'username' => 'root',
'password' => 'cheesy/hamburger'
];
然后执行./yii migrate/create init_customer_table
,windows下执行php yii migrate/create init_customer_table
,后不赘述。该命令讲自动生成migrations文件夹,并生成名字如m140204_190825_init_
customer_table
d的文件。修改内容如下。
<?php
use yii\db\Migration;
/**
* Class m180222_125258_init_customer_table
*/
class m180222_125258_init_customer_table extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->createTable(
'customers',[
'id'=>'pk',
'name'=>'string',
'birth_date'=>'date',
'notes'=>'text',
],
'ENGINE=InnoDB'
);
echo "ok";
return true;
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
$this->dropTable('customers');
echo "m180222_125258_init_customer_table cannot be reverted.\n";
return false;
}
/*
// Use up()/down() to run migration code without a transaction.
public function up()
{
}
public function down()
{
echo "m180222_125258_init_customer_table cannot be reverted.\n";
return false;
}
*/
}
safeUp()
和up()
的区别是有无事务,没用事务的话可能执行一半被打断会产生错误。
再执行./yii migrate/create init_Phone_table
。修改内容:
<?php
use yii\db\Migration;
/**
* Class m180222_130137_init_phone_table
*/
class m180222_130137_init_phone_table extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->createTable(
'phone',
[
'id' => 'pk',
'customer_id' => 'int unique',
'number' => 'string',
],
'ENGINE=InnoDB'
);
$this->addForeignKey('customer_phone_numbers', 'phone',
'customer_id', 'customers', 'id');
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
$this->dropForeignKey('customer_phone_numbers', 'phone');
$this->dropTable('phone');
echo "m180222_130137_init_phone_table cannot be reverted.\n";
return false;
}
/*
// Use up()/down() to run migration code without a transaction.
public function up()
{
}
public function down()
{
echo "m180222_130137_init_phone_table cannot be reverted.\n";
return false;
}
*/
}
执行 ./yii migrate
在数据库建立这两个表。想要删除时。执行./yii migrate/down
建立AR类
建立models\customer\CustomerRecord.php
:
<?php
namespace app\models\customer;
use yii\db\ActiveRecord;
class CustomerRecord extends ActiveRecord
{
public static function tableName()
{
return 'customer';
}
//增加字段限制规则,具体可以查看手册。
public function rules()
{
return [
['id', 'number'],
['name', 'required'],
['name', 'string', 'max' => 256],
['birth_date', 'date', 'format' => 'Y-m-d'],
['notes', 'safe']
];
}
}
同时建立models\customer\PhoneRecord.php
<?php
namespace app\models\customer;
use yii\db\ActiveRecord;
class PhoneRecord extends ActiveRecord
{
public static function tableName()
{
return 'phone';
}
public function rules()
{
return [
['customer_id', 'number'],
['number', 'string'],
[['customer_id', 'number'], 'required'],
];
}
}
从ORM(即Yii中的AR)中去耦
为了从Yii框架中解耦,我们建立了Customer和Phone两个微小的领域模型。因此我们需要一个把Yii2中的ORM和领域模型进行转换的转换层。然而她需要大量的篇幅去说明,因此本书采取一个这种的办法,仅仅在customersController
中添加两个方法。但是在大型应用中你必须建立适当的转换层。原因如下:
- ORM使用是方便的,但是他产生了太多对数据库的请求,这是应该避免的。因此,你可以使用更低的一层比如DAO,或者绕过Yii使用原生PDO。如果没有这个领域模型,你将非常麻烦。
- 如果未来你打算使用其他的ORM而不使用Yii的,没有领域模型,你需要做大量重写。
- 未来Yii可能从2升级到3,大量的API改变,如果没有与ORM解耦,那么就不能升级。
(此处不是很明白,望大佬指点)。
customersController
增加方法
private function store(Customer $customer)
{
$customer_record = new CustomerRecord();
$customer_record->name = $customer->name;
$customer_record->birth_date = $customer->birth_date->format('Y-m-d');
$customer_record->notes = $customer->notes;
$customer_record->save();
foreach ($customer->phones as $phone)
{
$phone_record = new PhoneRecord();
$phone_record->number = $phone->number;
$phone_record->customer_id = $customer_record->id;
if (!$phone_record->save()){
var_dump($phone_record->errors);
exit();
}
}
}
private function makeCustomer(CustomerRecord $customer_record,PhoneRecord $phone_record)
{
$name = $customer_record->name;
$birth_date = new \DateTime($customer_record->birth_date);
$customer = new Customer($name,$birth_date);
$customer->notes = $customer_record->notes;
$customer->phones[] = new Phone($phone_record->number);
return $customer;
}
建立视图
前面已经建立了/customer/add
对应的方法。现在建立对应渲染的视图。
建立views/customer/add.php
<?php
/**
* Created by PhpStorm.
* User: xiyaozhe
* Date: 2018/2/23
* Time: 17:59
*/
use app\models\customer\CustomerRecord;
use app\models\customer\PhoneRecord;
use yii\web\View;
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/**
* Add customers UI.
*
* @var View $this
* @var CustomerRecord $customer
* @var PhoneRecord $phone
*/
$form = ActiveForm::begin([
'id' => 'add-customers-form',
]);
echo $form->errorSummary([$customer, $phone]);
echo $form->field($customer, 'name');
echo $form->field($customer, 'birth_date');
echo $form->field($customer, 'notes');
echo $form->field($phone, 'number');
echo Html::submitButton('Submit', ['class' => 'btn btn-primary']);
ActiveForm::end();
接着修改customersController
中actionAdd()方法
public function actionAdd()
{
$customer = new CustomerRecord();
$phone = new PhoneRecord();
if ($this->load($customer,$phone,$_POST))
{
$this->store($this->makeCustomer($customer,$phone));
return $this->redirect('/customers');
}
return $this->render('add',compact('customer','phone'));
}
//验证两个输入都符合验证规则才能提交
private function load(CustomerRecord $customer, PhoneRecord
$phone, array $post)
{
return $customer->load($post)
and $phone->load($post)
and $customer->validate()
and $phone->validate(['number']);
}
现在访问是通过http://yourdomain/index.php?r=controller/action
访问,这样并不美观。
我们可以在配置文件中的components
中加入
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false
]
现在可以通过http://yourdomain/index.php/customers/add
访问。当然你可以进一步隐藏隐藏index.php,不过这里就不做详细介绍了。
布局文件
当使用render()
时,他会假定有一个布局文件。默认为views/layouts/main.php
,建立该文件。内容为下
<!DOCTYPE html>
<html>
<head>
<title>CRM</title>
</head>
<body>
<?= $content; ?>
</body>
</html>
<?= $content; ?>
就是add.php
等渲染出来放置的位置。现在访问该页面,应该已经能看到前台界面了
显示数据
接着在customersController
中添加
//actionIndex()调用
private function findRecordsByQuery()
{
$number = Yii::$app->request->get('phone_number');
$records = $this->getRecordsByPhoneNumber($number);
$dataProvider = $this->wrapIntoDataProvider($records);
return $dataProvider;
}
private function warpIntoDataProvider($data)
{
return new ArrayDataProvider(
['allModels' => $data,
'pagination'=>false]
);
}
private function getRecordsByPhoneNumber($number)
{
$phone_record = PhoneRecord::findOne(['number'=>$number]);
if (!$phone_record)
return [];
$customer_record = CustomerRecord::findOne($phone_record->customer_id);
if (!$customer_record)
return [];
return [$this->makeCustomer($customer_record, $phone_record)];
}
建立views/customers/index.php
echo \yii\widgets\ListView::widget(
[
'options' => [
'class' => 'list-view',
'id' => 'search_results'
],
'itemView' => '_customer',
'dataProvider' => $records
]
);
建立views/customers/_customer.php
文件。
echo \yii\widgets\DetailView::widget(
[
'model' => $model,
'attributes' => [
['attribute' => 'name'],
['attribute' => 'birth_date', 'value' => $model->birth_
date->format('Y-m-d')],
'notes:text',
['label' => 'Phone Number', 'attribute' =>
'phones.0.number']
]
]);
关于widgets的详细使用,大家就查查官方文档吧,这里做过多解释。
建立查询界面
在customersController
中添加
public function actionQuery()
{
return $this->render('query');
}
建立views/customers/query.php
文件。
<?php
use yii\helpers\Html;
echo Html::beginForm(['/customers'], 'get');
echo Html::label('Phone number to search:', 'phone_number');
echo Html::textInput('phone_number');
echo Html::submitButton('Search');
echo Html::endForm();
运行测试
执行codecept run acceptance
,此时显示全部通过测试则大功告成。
最后
文章写作匆忙,难免有所纰漏,大家见谅。最近要准备考试,下一篇不知什么时候才能发布,大家就期待一下吧。
石头杨
最后登录:2021-01-23
在线时长:13小时6分
- 粉丝39
- 金钱325
- 威望120
- 积分1655
共 1 条评论
有没有测试通过的源码?