10. 分层应用程序
10.1. 引言

接下来,我们将探讨如何将 PHP 应用程序按层进行结构化:

在此图中,左侧层调用右侧层。各层的作用如下:
- [1]:名为 [DAO](数据访问对象)的层负责处理与外部数据存储 [4](文件、数据库、Web 服务等)的交互。 该层有时也被称为 [DAL](数据访问层),这一术语更能准确描述该层的作用。该层可以读取数据 [3] 或写入数据 [2]。它由 [业务] 层 [6] 调用,并向其返回结果 [7];
- [5]:名为[业务]的层,包含“业务”流程,这些流程会获得所需的所有数据。这通常是项目中最稳定的层,因为它不依赖于数据的获取方式。数据来自两个来源:
- [9]:由 PHP 脚本提供的数据;
- [6,7]:从[DAO]层请求的数据。
- [8]:主脚本充当协调者。在控制台应用程序中,它将:
- 创建 [business] 和 [DAO] 层;
- 执行应用程序的算法。该算法如同协调器:不包含任何“业务”逻辑或数据访问代码。主脚本仅调用[业务]层的程序[9]。它完全忽略[DAO]层和外部数据。它可以向[业务]层提供数据[9]。 在控制台应用程序中,这些数据可能来自配置文件或脚本用户。它从 [业务] 层接收结果 [10]。它可能需要存储某些结果:为此,它再次使用 [业务] 层的过程,而该层将调用 [DAO] 层来执行任务;
- 在接收的结果中,主脚本可能会收到异常。其职责是处理从所有层级传播上来的异常;
这种分层架构旨在促进应用程序的演进。为此,每一层都实现了相应的接口。假设:
- [DAO] 层由一个实现 [IDao] 接口的 [DAO] 类实现;
- [业务]层由一个实现[IBusiness]接口的[Business]类实现;
因此,[业务]层中的过程将使用[IDao]接口,而非[Dao]类。这使得[Dao]层可以进行更新,而不会影响[业务]层。假设在版本1中,[Dao1]层使用来自数据库的数据。 经过更新,在版本 2 中,这些数据现在由 Web 服务 [Dao2] 提供。我们将确保 [Dao1] 和 [Dao2] 类都实现相同的 [IDao] 接口,并抛出相同的异常。如果验证通过,与 [IDao] 接口交互的 [Business] 层将保持不变;
同样的逻辑也适用于 [Business] 层。
让我们来看一个使用类和接口的实现:

该应用程序将实现以下分层结构:

10.2. 层之间交换的对象
通常,图层之间会交换各种对象。在此,它们将交换以下类 [Person.php]:
<?php
// a person
class Personne {
// identifier
private $id;
// manufacturer
public function __construct(int $id) {
$this->id = $id;
}
// toString
public function __toString(): string {
return "[Personne($this->id)]";
}
}
评论
- 第 6 行:Person 类仅有一个属性,即其标识符 [$id]。我们将假设该标识符在数据仓库中唯一标识某个人;
- 第 9–11 行:构造函数,允许我们使用 ID 创建 [Person] 对象;
- 第 14–16 行:[__toString] 方法,用于显示该人员的标识符;
10.3. [DAO] 层
由 [dao, 1] 层实现的 [IDao] 接口如下 [IDao.php]:
<?php
// layer [dao] ------------------------
interface IDao {
// recovery of a person from an external warehouse
// pass the person's login
public function get(int $id): Personne;
// saving a person in an external warehouse
public function save(Personne $p): void;
}
该接口由以下 [Dao1] 类 [Dao1.php] 实现:
<?php
class Dao1 implements IDao {
// saving a person in an external warehouse
public function save(Personne $p): void {
print "[Dao1] : Sauvegarde de la personne $p en base de données [locale]\n";
}
// recovery of a person from an external warehouse
public function get(int $id): Personne {
print "[Dao1] : Récupération de la personne d'identité ($id) en base de données [locale]\n";
return new Personne($id);
}
}
评论
- 该类不与数据仓库交互。我们仅显示消息以跟踪代码的执行(第 7 行和第 12 行);
- 第13行:我们返回一个ID与方法参数(第11行)相匹配的人员;
我们还通过以下 [Dao2] 类 [Dao2.php] 实现了 [IDao] 接口:
<?php
class Dao2 implements IDao {
// saving a person in an external warehouse
public function save(Personne $p): void {
print "[Dao2] : Sauvegarde de la personne $p en base de données [distante]\n";
}
// recovery of a person from an external warehouse
public function get(int $id): Personne {
print "[Dao2] : Récupération de la personne d'identité ($id) en base de données [distante]\n";
return new Personne($id);
}
}
[Dao2] 类与 [Dao1] 类类似,只是我们修改了显示的消息。
10.4. [业务] 层
[business, 2] 层提供了以下 [IMetier] 接口 [IMetier.php]:
<?php
// business] layer ------------------------
interface IMétier extends IDao {
// we use a person identified by his id
public function doSomething(Personne $p): void;
}
注释
- [IMétier] 接口继承自 [IDao] 接口。这完全不是强制要求的。我们在这里这样做是因为示例很简单;
- 第 7 行:[doSomething] 方法是 [Business] 层特有的;
[IMetier] 接口将由以下 [Métier] 类 [Métier.php] 实现:
<?php
class Métier implements IMétier {
// layer [dao]
private $dao;
// getter / setter
public function setDao(IDao $dao): void {
$this->dao = $dao;
}
public function getDao(): IDao {
return $this->dao;
}
// a person is exploited
public function doSomething(Personne $p): void {
// treatment
print "Métier : Traitement métier de la personne $p\n";
}
// safeguarding the individual
public function save(Personne $p): void {
print "[Métier] : Sauvegarde de la personne $p\n";
// the [dao] layer is asked to make the backup
$this->dao->save($p);
}
public function get(int $id): Personne {
print "[Métier] : récupération de la personne d'identifiant $id\n";
// we ask for the person in the diaper [dao]
$personne = $this->dao->get($id);
return $personne;
}
}
评论
- 第 5 行:[业务] 层必须持有对 [DAO] 层的引用,才能使用其方法;
- 第 8–10 行:[setDao] 方法允许向 [业务] 层提供对 [DAO] 层的引用。 请注意,参数类型是 [IDao]。这意味着 [业务] 层将能够与任何实现 [IDao] 接口的类进行交互。如果我们将 [Dao1] 层替换为 [Dao2] 层,且两者都实现了 [IDao] 接口,则无需重写 [业务] 层;
- 第 17–20 行:[IMetier::doSomething] 的实现;
- 第 23–27 行:[IMetier::save] 的实现。该方法必须将人员数据保存至数据仓库。[业务] 层无法直接执行此操作,因此会调用 [DAO] 层来完成保存;
- 第 29–34 行:实现 [IMetier::get]。该方法必须从数据仓库中检索 ID 作为参数传递给它的用户。[业务] 层不知道如何执行此操作。它调用 [DAO] 层来完成这项工作(第 32 行);
结论
每当[业务]层需要访问存储在数据仓库中的数据时,都必须通过[DAO]层,该层正是为访问这些数据而创建的。
10.5. 主脚本
我们将编写两个脚本,作为该应用程序的协调器。第一个 [main1.php] 将使用 [Dao1] 层,而第二个 [main2.php] 将使用 [Dao2] 层。我们希望以此证明,这不会对 [业务] 层的代码产生任何影响。
[main1.php]脚本如下:
<?php
// strict adherence to function parameters
declare (strict_types=1);
// inclusion classes and interfaces
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao1.php";
require_once __DIR__."/Métier.php";
// test ----------------
// layer creation
$dao1 = new Dao1();
$métier = new Métier();
$métier->setDao($dao1);
// using the [business] layer
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);
注释
- 让我们回顾几个要点:[main1.php] 脚本充当应用程序的协调者。它创建应用程序的分层结构(第 15–17 行),然后开始与 [business] 层进行通信(第 19–21 行)。分层结构如下:

根据此图,[main1.php]脚本应仅与[business]层交互。即使理论上可行,它也不应与[DAO]层交互。
执行结果如下:
[Métier] : récupération de la personne d'identifiant 4
[Dao1] : Récupération de la personne d'identité (4) en base de données [locale]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao1] : Sauvegarde de la personne [Personne(4)] en base de données [locale]
注释
- 代码第 10 行导致结果中的第 1 行和第 2 行被写入;
- 代码第20行导致结果中的第3行被写入;
- 代码的第 21 行导致结果的第 4 行和第 5 行被写入;
脚本 [main2.php] 如下:
<?php
// strict adherence to function parameters
declare (strict_types=1);
// inclusion classes and interfaces
require_once __DIR__."/Personne.php";
require_once __DIR__."/IDao.php";
require_once __DIR__."/IMetier.php";
require_once __DIR__."/Dao2.php";
require_once __DIR__."/Métier.php";
// test ----------------
// layer creation
$dao2 = new Dao2();
$métier = new Métier();
$métier->setDao($dao2);
// using the [business] layer
$personne = $métier->get(4);
$métier->doSomething($personne);
$métier->save($personne);
注释
- 第 36–38 行:分层架构现在使用 [Dao2] 层;
执行结果如下:
[Métier] : récupération de la personne d'identifiant 4
[Dao2] : Récupération de la personne d'identité (4) en base de données [distante]
[Métier] : Traitement métier de la personne [Personne(4)]
[Métier] : Sauvegarde de la personne [Personne(4)]
[Dao2] : Sauvegarde de la personne [Personne(4)] en base de données [distante]
[Dao1] 层模拟对本地数据库的访问,而 [Dao2] 层模拟对远程数据库的访问。只要这两个层都遵循 [IDao] 接口,我们可以看到 [Business] 层的代码无需修改。
我们将把刚刚学到的知识应用到作为本教程主线的税费计算练习中。