14. 应用练习 – 第6版

我们刚刚实现了以下分层结构:

示例中使用的 SGBD 实为 MySQL。 在“链接”一节中,我们注意到,在实现 [dao] 层的类中,没有任何内容表明我们使用了特定的 SGBD。 现在我们将通过使用另一个 SGBD(即 SGBD PostgreSQL)来验证这一点。分层架构如下:

14.1. 安装 SGBD PostgreSQL
SGBD 和 PostgreSQL 的发行版可在 URL 和 [https://www.postgresql.org/download/](2019年5月)中获取。以下展示 64 位 Windows 版本的安装过程:


- 在 [1-4] 中,下载 SGBD 的安装程序;
运行下载的安装程序:

- 在 [6] 中,指定安装目录;

- 在 [8] 步骤中,选项 [Stack Builder] 对于此处的操作没有用;
- 在 [10] 中,保留系统提示的默认值;

- 在 [12-13] 中,此处已设置密码 [root]。 这将是名为 [postgres] 的 SGBD 管理员的密码。PostgreSQL 也称其为超级用户;
- 在 [15] 处,保留默认值:这是 SGBD 的监听端口;

- 在 [17] 中,保留默认值;
- 在 [19] 中,显示安装配置摘要;


在 Windows 系统中,SGBD 和 PostgreSQL 被安装为自动启动的 Windows 服务。大多数情况下,这并非理想状态。 我们将修改此配置。在 Windows 搜索栏中输入 [services] [24-26]:

- 为 [29],可以看到 SGBD 和 PostgreSQL 服务处于自动模式。通过访问 [30] 服务的属性来更改此设置:

- 在 [31-32] 中,将启动方式设为手动;
- 对于 [33],停止该服务;
当您需要手动启动 SGBD 时,请返回 [services] 应用程序,右键单击 [postgresql] 服务 (34) 并启动它 (35)。
14.2. 启用 PDO 扩展(隶属于 SGBD 和 PostgreSQL)
我们将修改配置 PHP 的文件 [php.ini](参见链接段落):

- 将其修改为 [2],并确认 PostgreSQL 的扩展 PDO 已启用。完成上述操作后,保存修改并重新启动 Laragon,以确保修改生效。 随后请直接在 Laragon 中检查 PHP 的配置。
14.3. 使用工具 [pgAdmin] 管理 PostgreSQL
启动 SGBD 的 Windows 服务 PostgreSQL(参见链接段落)。然后,与启动 [services] 工具的方式相同, 启动 [pgadmin] 工具,该工具用于管理 SGBD、PostgreSQL 和 [1-3]:

在某些情况下,系统可能会要求您输入超级用户的密码。该密码名为 [postgres]。 您在安装 SGBD 时已设置了该密码。在本文档中,我们在安装过程中为超级用户设置的密码是 [root]。
- 在 [4] 中,[pgAdmin] 是一个 Web 应用程序;
- 在 [5] 中,[pgAdmin] 检测到的 PostgreSQL 服务器列表,此处为 1;
- 在 [6] 中,是我们启动的 PostgreSQL 服务器;
- 在 [7] 中,SGBD 的数据库,此处为 1;
- 在 [8] 中,[postgresql] 数据库由超级用户 [postgres] 管理;
首先创建用户 [admimpots],密码为 [mdpimpots]:


- 在 [17] 中,我们设置了 [mdpimpots];

- 在 [21] 中,工具 [pgAdmin] 将向 SGBD PostgreSQL 输出代码 SQL。 这是学习 PostgreSQL 专有语言 SQL 的一种方式;
- 在[22]中,经过[Save]向导的验证后,已创建用户[admimpots];
现在我们创建数据库 [dbimpots-2019]:

右键单击 [23],然后单击 [24-25] 以创建新的数据库。 在 [26] 选项卡中,将数据库名称定义为 [27],所有者定义为 [admimpots] [28]。

- 在 [30] 中,数据库创建代码 SQL;
- 在 [31] 中,经过向导 [Save] 的验证后,创建了数据库 [dbimpots-2019];
现在,我们将创建包含列 [id, limites, coeffr, coeffn] 的表 [tbtranches]。 PostgreSQL的一个特点是列名区分大小写(大写/小写),而其他SGBD通常不区分大小写。 因此,对于 MySQL,即使表 [tbtranches] 的实际列为 [LIMITES, COEFFR, COEFFN],命令 [select limites, coeffR, coeffN from tbtranches] 仍可正常运行。 对于 PostgreSQL,命令 SQL 将无法运行。 此时虽然可以写成 [select LIMITES, COEFFR, COEFFN from tbtranches],但仍然无法运行,因为 PostgreSQL 会执行 [select limites, coeffr, coeffn from tbtranches] 命令:它默认将列名转换为小写。 要避免这种情况,需写成:[select "LIMITES", "COEFFR", "COEFFN" from tbtranches],即用引号将列名进行保护。基于这些原因,我们将为列命名使用小写字母。 数据库对象的名称可能导致 SGBD 之间出现不兼容,因为某些名称在某些 SGBD 中是保留字,而在其他 SGBD 中则不是。
我们创建表 [tbtranches]:

- 使用 [40] 按钮创建列;


- 通过 [Save] 完成创建向导后,将生成表 [tbtranches] 和 [52-53];
我们需要告知 SGBD,在向表中插入一行时,它应自行生成主键 [id]:

- 在 [56] 中,访问主键 [id] 的属性;
- 在 [59] 中,需指定该列的类型为 [Identity]。这将导致 SGBD 生成主键的值;

- 在 [62] 中,为该操作生成的代码为 SQL;
[tbtranches]表现已准备就绪。
我们重复相同的操作来创建表 [tbconstantes]。我们给出要获得的结果:



数据库 [dbimpots-2019] 现已准备就绪。我们将向其中填充数据。
如同之前对 MySQL 所做的那样,可以将数据库 [dbimpots-2019] 导出为文件 SQL。 如果数据库丢失或损坏,可以导入该SQL文件来重建数据库。在此我们仅导出数据库结构,而不导出其数据:


生成的文件如下:
--
-- PostgreSQL 数据库转储
--
-- 从数据库版本 11.2 导出
-- 由 pg_dump 版本 11.2 导出
-- 开始于 2019-07-04 08:20:31
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_with_oids = false;
--
-- TOC 条目 198(类 1259 OID 16408)
-- 名称:tbconstantes;类型:TABLE;模式:public;所有者:postgres
--
CREATE TABLE public.tbconstantes (
plafond_qf_demi_part double precision NOT NULL,
id integer NOT NULL,
plafond_revenus_celibataire_pour_reduction double precision NOT NULL,
plafond_revenus_couple_pour_reduction double precision NOT NULL,
valeur_reduc_demi_part double precision NOT NULL,
plafond_decote_celibataire double precision NOT NULL,
plafond_decote_couple double precision NOT NULL,
plafond_impot_celibataire_pour_decote double precision NOT NULL,
plafond_impot_couple_pour_decote double precision NOT NULL,
abattement_dix_pourcent_max double precision NOT NULL,
abattement_dix_pourcent_min double precision NOT NULL
);
ALTER TABLE public.tbconstantes OWNER TO postgres;
--
-- TOC 条目 199(类 1259 OID 16411)
-- 名称:tbconstantes_id_seq;类型:SEQUENCE;模式:public;所有者:postgres
--
ALTER TABLE public.tbconstantes ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
SEQUENCE NAME public.tbconstantes_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
);
--
-- TOC 条目 196(类 1259 OID 16399)
-- 名称:tbtranches;类型:TABLE;模式:public;所有者:admimpots
--
CREATE TABLE public.tbtranches (
limites double precision NOT NULL,
id integer NOT NULL,
coeffr double precision NOT NULL,
coeffn double precision NOT NULL
);
ALTER TABLE public.tbtranches OWNER TO admimpots;
--
-- TOC 条目 197(类 1259 OID 16404)
-- 名称:tbimpots_id_seq;类型:SEQUENCE;架构:public;所有者:admimpots
--
ALTER TABLE public.tbtranches ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
SEQUENCE NAME public.tbimpots_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1
);
--
-- TOC 条目 2694(类 2606 OID 16429)
-- 名称:tbconstantes tbconstantes_pkey; 类型:CONSTRAINT; 模式:public; 所有者:postgres
--
ALTER TABLE ONLY public.tbconstantes
ADD CONSTRAINT tbconstantes_pkey PRIMARY KEY (id);
--
-- TOC 条目 2692(类 2606 OID 16403)
-- 名称:tbtranches tbimpots_pkey; 类型:CONSTRAINT; 模式:public; 所有者:admimpots
--
ALTER TABLE ONLY public.tbtranches
ADD CONSTRAINT tbimpots_pkey PRIMARY KEY (id);
--
-- TOC 条目 2821(类 0 OID 0)
-- 依赖项:198
-- 名称:TABLE tbconstantes; 类型:ACL; 模式:public; 所有者:postgres
--
GRANT ALL ON TABLE public.tbconstantes TO admimpots;
-- 完成于 2019-07-04 08:20:32
--
-- PostgreSQL 数据库转储完成
--
14.4. 填充表格 [tbtranches]
我们在“链接”段落中已经完成了 SGBD 和 MySQL 的相关工作。现在只需修改描述数据库的 [database.json] 文件即可:

[database.json]文件修改后如下:
{
"dsn": "pgsql:host=localhost;dbname=dbimpots-2019",
"id": "admimpots",
"pwd": "mdpimpots",
"tableTranches": "public.tbtranches",
"colLimites": "limites",
"colCoeffR": "coeffr",
"colCoeffN": "coeffn",
"tableConstantes": "public.tbconstantes",
"colPlafondQfDemiPart": "plafond_qf_demi_part",
"colPlafondRevenusCelibatairePourReduction": "plafond_revenus_celibataire_pour_reduction",
"colPlafondRevenusCouplePourReduction": "plafond_revenus_couple_pour_reduction",
"colValeurReducDemiPart": "valeur_reduc_demi_part",
"colPlafondDecoteCelibataire": "plafond_decote_celibataire",
"colPlafondDecoteCouple": "plafond_decote_couple",
"colPlafondImpotCelibatairePourDecote": "plafond_impot_celibataire_pour_decote",
"colPlafondImpotCouplePourDecote": "plafond_impot_couple_pour_decote",
"colAbattementDixPourcentMax": "abattement_dix_pourcent_max",
"colAbattementDixPourcentMin": "abattement_dix_pourcent_min"
}
- 第2行:DSN已更改,[pgsql]表示当前处理的是Postgres的SGBD;
- 第 5 行和第 9 行:在表名前添加了其所属模式的名称 [public]。这并非绝对必要,因为当表名中未指定模式时,默认使用的模式是 [public];
- 第 6-8 行、第 10-19 行:列名已更改;
用于填充数据库 [dbimpots-2019] 的脚本 [MainTransferAdminDataFromJsonFile2PostgresDatabase.php] 如下:
<?php
// 严格遵守函数参数的声明类型
declare (strict_types=1);
// 命名空间
namespace Application;
// PHP 错误处理
// ini_set("display_errors", "0");
// 接口和类的引入
require_once __DIR__ . "/../../version-05/Entities/BaseEntity.php";
require_once __DIR__ . "/../../version-05/Entities/TaxAdminData.php";
require_once __DIR__ . "/../../version-05/Entities/TaxPayerData.php";
require_once __DIR__ . "/../../version-05/Entities/Database.php";
require_once __DIR__ . "/../../version-05/Entities/ExceptionImpots.php";
require_once __DIR__ . "/../../version-05/Utilities/Utilitaires.php";
require_once __DIR__ . "/../../version-05/Dao/InterfaceDao.php";
require_once __DIR__ . "/../../version-05/Dao/TraitDao.php";
require_once __DIR__ . "/../../version-05/Dao/InterfaceDao4TransferAdminData2Database.php";
require_once __DIR__ . "/../../version-05/Dao/DaoTransferAdminDataFromJsonFile2Database.php";
//
// 常量定义
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
//
try {
// 创建 [dao] 层
$dao = new DaoTransferAdminDataFromJsonFile2Database(DATABASE_CONFIG_FILENAME, TAXADMINDATA_FILENAME);
// 将数据传输到数据库
$dao->transferAdminData2Database();
} catch (ExceptionImpots $ex) {
// 显示错误
print "L'erreur suivante s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// 结束
print "Terminé\n";
exit;
注释
仅第12-21行发生了变化,这些行负责加载应用程序运行所需的文件。之所以发生变化,是因为值[__DIR__]发生了变化:它现在指向文件夹[version-07/Main]。
执行此脚本后,在表 [tbtranches] 中将得到以下结果:

- 右键单击 [1],然后单击 [2-3];
- 在 [4] 中,确实包含了税率区间的数据;
对常量表 [tbconstantes] 重复上述操作:



需要注意的是,在执行脚本时,Laragon 应用程序无需处于活动状态:既不需要 Apache 服务器,也不需要 SGBD 和 MySQL。 只需启动了 Windows 服务的 SGBD PostgreSQL 即可。
14.5. 税款计算

[dao] (3) 和 [métier] (2) 层已编写完成。我们在“链接”段落中已编写了 SGBD MySQL 的主脚本。 我们只需复用 [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase.php] 脚本,并将其适配至 SGBD 和 PostgreSQL。该脚本现更名为 [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase.php]:

[MainCalculateImpotsWithTaxAdminDataInPostgresDatabase.php]脚本如下:
<?php
// 严格遵守函数参数的声明类型
declare (strict_types=1);
// 命名空间
namespace Application;
// 由 PHP 处理错误
//ini_set("display_errors", "0");
// 接口和类的引入
require_once __DIR__ . "/../../version-05/Entities/BaseEntity.php";
require_once __DIR__ . "/../../version-05/Entities/TaxAdminData.php";
require_once __DIR__ . "/../../version-05/Entities/TaxPayerData.php";
require_once __DIR__ . "/../../version-05/Entities/Database.php";
require_once __DIR__ . "/../../version-05/Entities/ExceptionImpots.php";
require_once __DIR__ . "/../../version-05/Utilities/Utilitaires.php";
require_once __DIR__ . "/../../version-05/Dao/InterfaceDao.php";
require_once __DIR__ . "/../../version-05/Dao/TraitDao.php";
require_once __DIR__ . "/../../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once __DIR__ . "/../../version-05/Métier/InterfaceMetier.php";
require_once __DIR__ . "/../../version-05/Métier/Metier.php";
//
// 常量定义
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
const RESULTS_FILENAME = "../Data/resultats.json";
const ERRORS_FILENAME = "../Data/errors.json";
const TAXPAYERSDATA_FILENAME = "../Data/taxpayersdata.json";
try {
// 创建 [dao] 层
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// 创建 [métier] 层
$métier = new Metier($dao);
// 批处理模式下的税款计算
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// 显示错误
print "Une erreur s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// 结束
print "Terminé\n";
exit;
注释
仅第12至22行发生了变化,这些行负责加载应用程序运行所需的文件。之所以发生变化,是因为[__DIR__]的值发生了变化:它现在指向文件夹[version-07/Main]。
运行结果
与之前版本中获得的结果相同。
14.6. [Codeception] 测试
与之前版本一样,我们通过 [Codeception] 测试来验证此版本:

14.6.1. [dao]层测试
[DaoTest.php]的测试内容如下:
<?php
// 严格遵守函数参数的声明类型
declare (strict_types=1);
// 命名空间
namespace Application;
// 根目录
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-06");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// 接口和类的包含
require_once ROOT . "/../version-05/Entities/BaseEntity.php";
require_once ROOT . "/../version-05/Entities/TaxAdminData.php";
require_once ROOT . "/../version-05/Entities/TaxPayerData.php";
require_once ROOT . "/../version-05/Entities/Database.php";
require_once ROOT . "/../version-05/Entities/ExceptionImpots.php";
require_once ROOT . "/../version-05/Utilities/Utilitaires.php";
require_once ROOT . "/../version-05/Dao/InterfaceDao.php";
require_once ROOT . "/../version-05/Dao/TraitDao.php";
require_once ROOT . "/../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
// 第三方库
require_once VENDOR . "/autoload.php";
// 常量定义
const DATABASE_CONFIG_FILENAME = ROOT ."../Data/database.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// 创建 [dao] 层
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// 测试
public function testTaxAdminData() {
…
}
}
注释
- 第 9-28 行:定义测试环境。我们使用与链接段落中描述的主脚本 [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase] 相同的环境,但不包含 [métier] 层;
- 第 34-39 行:构建 [dao] 层;
- 第38行:[$this→taxAdminData]属性包含待测试的数据;
- 第42-44行:方法[testTaxAdminData]即链接段落中所述的方法;
测试结果如下:

14.6.2. [métier] 层测试
[MetierTest.php] 的测试如下:
<?php
// 严格遵守函数参数的声明类型
declare (strict_types=1);
// 命名空间
namespace Application;
// 根目录
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-06");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// 接口和类的包含
require_once ROOT . "/../version-05/Entities/BaseEntity.php";
require_once ROOT . "/../version-05/Entities/TaxAdminData.php";
require_once ROOT . "/../version-05/Entities/TaxPayerData.php";
require_once ROOT . "/../version-05/Entities/Database.php";
require_once ROOT . "/../version-05/Entities/ExceptionImpots.php";
require_once ROOT . "/../version-05/Utilities/Utilitaires.php";
require_once ROOT . "/../version-05/Dao/InterfaceDao.php";
require_once ROOT . "/../version-05/Dao/TraitDao.php";
require_once ROOT . "/../version-05/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once ROOT . "/../version-05/Métier/InterfaceMetier.php";
require_once ROOT . "/../version-05/Métier/Metier.php";
// 第三方库
require_once VENDOR . "/autoload.php";
// 常量定义
const DATABASE_CONFIG_FILENAME = ROOT . "../Data/database.json";
class MetierTest extends \Codeception\Test\Unit {
// 业务层
private $métier;
public function __construct() {
parent::__construct();
// 创建该层[dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// 创建图层 [métier]
$this->métier = new Metier($dao);
}
// 测试
public function test1() {
…
}
--------------------------------------------------------------------
public function test11() {
…
}
}
注释
- 第 9-28 行:定义测试环境。我们使用与链接段落中描述的主脚本 [MainCalculateImpotsWithTaxAdminDataInPostgresDatabase] 相同的环境;
- 第34-40行:构建[dao]和[métier]层;
- 第 39 行:[$this→métier] 属性引用 [métier] 图层
- 第43-49行:[test1, test2…, test11]方法即链接段落中所述的方法;
测试结果如下:
