5. 类
术语:类(class)是一种类型。该类型的变量称为对象。对象是类的实例(副本)。
5.1. 脚本树结构

5.2. 任何变量都可以成为具有属性的对象
脚本 [classes-01.php] 如下:
<?php
// 一个通用对象
// $obj1=new stdClass();
// 任何变量在构造时都可能具有属性
$obj1->attr1 = "un";
$obj1->attr2 = 100;
// 显示该对象
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// 修改对象
$obj1->attr2 += 100;
// 显示对象
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// 将对象1的值(所指对象的地址)复制到对象2
// 此时两个变量虽然不同,但指向同一个对象
$obj2 = $obj1;
// 修改对象2
$obj2->attr2 = 0;
// 显示这两个对象
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
// 更改 obj1 所指向的对象
$obj1 = new stdClass();
print "obj1 :\n";
print_r($obj1);
print "obj2 :\n";
print_r($obj2);
// 将对象2的引用(地址)赋值给对象3
// 此时 $obj2 和 $obj3 成为同一个变量
$obj3 = &$obj2;
print "obj2 :\n";
print_r($obj2);
print "obj3 :\n";
print_r($obj3);
// 修改 obj3
$obj3->attr2 = 10;
// 显示这两个对象
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
print "objet3=[$obj3->attr1,$obj3->attr2]\n";
// 更改 obj2 所指向的对象
$obj2 = new stdClass();
$obj2->attr3 = "deux";
$obj2->attr4 = 20;
// 显示两个对象 $obj2 和 $obj3
print "obj2 :\n";
print_r($obj2);
print "obj3 :\n";
print_r($obj3);
// 某个对象是否为字典?
print count($obj3) . "\n";
while (list($attribut, $valeur) = each($obj3)) {
print "obj3[$attribut]=$valeur\n";
}
// 结束
exit;
结果:
Warning: Creating default object from empty value in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_14.php on line 6
objet1=[un,100]
objet1=[un,200]
objet1=[un,0]
objet2=[un,0]
obj1 :
stdClass Object
(
)
obj2 :
stdClass Object
(
[attr1] => un
[attr2] => 0
)
obj2 :
stdClass Object
(
[attr1] => un
[attr2] => 0
)
obj3 :
stdClass Object
(
[attr1] => un
[attr2] => 0
)
objet2=[un,10]
objet3=[un,10]
obj2 :
stdClass Object
(
[attr3] => deux
[attr4] => 20
)
obj3 :
stdClass Object
(
[attr3] => deux
[attr4] => 20
)
Warning: count(): Parameter must be an array or an object that implements Countable in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_14.php on line 50
1
Deprecated: The each() function is deprecated. This message will be suppressed on further calls in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_14.php on line 51
obj3[attr3]=deux
obj3[attr4]=20
注释
- 第 6 行:$obj->attr 表示变量 $obj 的属性 attr。 如果该属性不存在,则会创建它,从而将变量 $obj 转换为一个具有属性的对象。我们已经看到,PHP 默认会创建一个类型为 stdClass 的对象;
- 第16行:当$obj1是一个对象时,表达式$obj2=$obj1属于按引用复制对象: $obj2 和 $obj1 是指向同一个对象的引用(地址)。该对象本身可以通过任一引用进行修改;
- 第 23-27 行:旨在说明 $obj1 和 $obj2 是两个不同的变量:它们位于不同的内存地址:
- $obj2=$obj1 将 $obj1 的值复制到了变量 $obj2 中(即上文中的操作 1)。 $obj1 的值是一个对象的地址。因此,$obj1 和 $obj2 指向同一个对象。 当操作变量 $obj 且该变量指向某个对象时,PHP 操作的是由变量 $obj 所指向的对象。 根据下图所示,我们可以看到既可以通过 $obj1 也可以通过 $obj2 修改被引用的对象。结果中的第 4 行和第 5 行正是说明了这一点;

- 第30行:表达式 $obj3=&$obj2 使得 $obj2 和 $obj3 指向同一地址 [1 ci-dessous]。 可以说这两个变量是同一内存位置的别名。它们都指向一个对象,即下文中的对象 A [2];
- 操作 $obj2=new stdClass() 会创建一个新对象(下文称为对象 B,即 [3 ci-dessous]),并将该新对象的地址赋值给变量 $obj2。 由于 $obj2 和 $obj3 是同一内存位置的两个别名,因此 $obj3 也指向新对象 B。结果中的第 16-27 行和第 30-41 行显示了这一点;

- 第 52-54 行:表明对象可以像字典一样遍历。字典的键是属性的名称,字典的值则是这些属性的值;
- 第51行:函数count可应用于对象(会触发警告),但并未如预期返回属性数量。因此,对象虽与字典有相似之处,但并非字典;
5.3. 未声明属性的 Personne 类
脚本 [classes-02.php] 如下:
<?php
class Personne {
// 类的属性
// 未声明的属性——可以动态创建
// 方法
function identite() {
// 先验地,使用了不存在的属性
return "[$this->prenom,$this->nom,$this->age]";
}
}
// 测试
// 属性为公共属性,可动态创建
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// 调用方法
print "personne=" . $p->identite() . "\n";
// 结束
exit;
结果:
注释
- 第 3-13 行:定义了一个名为 Personne 的类。类是用于创建对象的模板,它包含属性以及称为方法的函数。属性无需强制声明;
- 第8-11行:方法identité显示了类中三个未声明属性的值。关键字$this指代应用该方法的对象;
- 第17行:创建了一个类型为Personne的$p对象。 关键字 new 用于创建新对象。该操作返回对所创建对象的引用(即一个地址)。有多种写法:new Personne()、new Personne、new personne。类名不区分大小写;
- 第 18-20 行:在 $p 对象中创建了 identité 方法所需的三个属性;
- 第 22 行:对 $p 对象应用了 Personne 类的 identité 方法。 在方法 identité 的代码(第 8-11 行)中,$this 引用的对象与 $p 相同;
5.4. 具有声明属性的 Personne 类
脚本 [classes-03.php] 如下:
<?php
class Personne {
// 类属性
var $prenom;
var $nom;
var $age;
// 方法
function identite() {
return "[$this->prenom,$this->nom,$this->age]";
}
}
// 测试
// 属性是公有的
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// 调用方法
print "personne=" . $p->identite() . "\n";
// 结束
exit;
结果:
注释
- 第6-8行:类属性使用关键字var显式声明;
5.5. 带有构造函数的 Personne 类
前面的示例展示了类似 Personne 这种在 PHP 4 中常见的特殊类。不建议效仿这些示例。 现在我们介绍一个符合 PHP 7 版最佳实践的 Personne [classes-04.php] 类:
<?php
// 严格遵守函数参数的声明类型
declare (strict_types=1);
class Personne {
// 类属性
private $prenom;
private $nom;
private $age;
// 获取器和设置器
public function getPrenom(): string {
return $this->prenom;
}
public function getNom(): string {
return $this->nom;
}
public function getAge(): int {
return $this->age;
}
public function setPrenom(string $prenom): void {
$this->prenom = $prenom;
}
public function setNom(string $nom): void {
$this->nom = $nom;
}
public function setAge(int $age): void {
$this->age = $age;
}
// 构造函数
public function __construct(string $prenom, string $nom, int $age) {
// 通过 set 方法
$this->setPrenom($prenom);
$this->setNom($nom);
$this->setAge($age);
}
// 方法toString
public function __toString(): string {
return "[$this->prenom,$this->nom,$this->age]";
}
}
// 测试
// 创建 Personne 对象
$p = new Personne("Paul", "Langevin", 48);
// 此人的身份
print "personne=$p\n";
// 更改年龄
$p->setAge(14);
// 该人的身份
print "personne=$p\n";
// 结束
exit;
结果:
注释
- 第 6-50 行:Personne 类;
- 第 7-9 行:类的私有(private)属性。这些属性仅在类内部可见。其他可用的关键字包括:
- public:使属性成为可从类外部访问的属性;
- protected:使属性在类内部及其派生类中可见;
- 由于属性是私有的,因此无法从类外部访问它们。因此无法编写以下代码:
此处位于 Personne 类外部。由于属性 nom 为私有,第 2 行代码是错误的。要初始化 $p 对象的私有字段,有两种方法:
- 使用第12至34行中的公共方法set和get(这些方法的名称可以任意设定)。此时可以这样写:
- 使用第 37-42 行中的构造函数。此时应写为:
上述写法会自动调用 Personne 类中名为 __construct 的方法;
- 第59行:该行将对象$p以字符串形式输出。为此,使用了Personne类中名为__toString的方法(第45-47行);
- 该类的所有方法(函数)均以关键字 public 作为前缀,这表示该函数在类外部可见。其他可用的关键字与属性相同,且含义一致,包括 private 和 protected。若未显式指定可见性属性,则函数的默认可见性为 public;
5.6. 带构造函数有效性检查的 Personne 类
类的构造函数是验证对象初始化值是否正确的理想位置。但对象也可以通过其方法(如 set 或等效方法)进行初始化。为避免在两个不同位置重复相同的验证,可以将这些验证逻辑放入方法中(如 set)。 如果对象的初始化值被发现不正确,将抛出异常。以下是一个示例。
首先,我们将类 Personne 的定义移至其独立文件 [Personne.php] 中:
<?php
// 严格遵守函数参数的声明类型
declare (strict_types=1);
// 命名空间;
namespace Exemples;
// Personne类
class Personne {
// 类的属性
private $prenom;
private $nom;
private $age;
// 获取器和设置器
public function getPrenom(): string {
return $this->prenom;
}
public function getNom(): string {
return $this->nom;
}
public function getAge(): int {
return $this->age;
}
public function setPrenom(string $prénom): void {
// 名字字段不能为空
$prénom = trim($prénom);
if ($prénom === "") {
throw new \Exception("Le prénom doit être non vide");
} else {
$this->prenom = $prénom;
}
}
public function setNom(string $nom): void {
// 姓氏不能为空
$nom = trim($nom);
if ($nom === "") {
throw new \Exception("Le nom doit être non vide");
} else {
$this->nom = $nom;
}
}
public function setAge(int $âge): void {
// 年龄必须有效
if ($âge < 0) {
throw new \Exception("L'âge doit être un entier positif ou nul");
} else {
$this->age = $âge;
}
}
// 构造函数
public function __construct(string $prenom, string $nom, int $age) {
// 通过 set 方法进行设置
$this->setPrenom($prenom);
$this->setNom($nom);
$this->setAge($age);
}
// 方法
public function initWithPersonne(Personne $p): void {
// 用一个人初始化当前对象 $p
$this->__construct($p->prenom, $p->nom, $p->age);
}
// 方法toString
function __toString(): string {
return "[$this->prenom,$this->nom,$this->age]";
}
}
注释
- 第 4 行:要求在声明函数参数时必须遵守其类型;
- 第7行:定义了一个命名空间(namespace)。因此,类Personne的完整名称(即qualifié)为\Exemples\Personne。 请注意限定名开头的 \ 字符:此时限定名为 absolu。如果缺少该字符,则限定名为 relatif(相对于当前命名空间)。 因此,如果两个类 A 和 B 属于同一个命名空间 E,在类 A 的代码中,可以通过 relative B 这种写法访问类 B。 如果类 A 属于命名空间 E1,类 B 属于命名空间 E2,那么在类 A 的代码中,可以通过 absolue \E2\ 这种写法访问类 B。B. 在命名空间内定义类并非强制要求,但若不这样做,NetBeans 会发出警告。因此我们还是会这样做。此外,命名空间应与文件树结构相对应。 因此,位于命名空间 E1 中的类 A 应位于文件 E1/A.php 中。虽然这不是强制要求的,但如果未按此操作,NetBeans 仍会发出警告。 以类 [\Exemples\Personne] 为例,NetBeans 会发出警告,因为文件 [Personne.php] 的目录结构是 [exemples/classes/Personne.php],因此与命名空间不匹配。 请勿将文件路径与命名空间混淆。类的完全限定名使用的是命名空间,与类文件 PHP 的路径结构毫无关系。文件路径与命名空间之间的关联是可选的,可以不遵循,正如我们在此处所做的那样;
- 第 12-14 行:类的三个私有属性;
- 第 29-37 行:初始化属性 prenom 并验证初始化值;
- 第 31 行:函数 trim($chaine) 用于去除 $chaine 字符串首尾的空格。因此,trim("abcd ") 返回字符串 "abcd",而 trim(" ") 返回空字符串;
- 第 32 行:如果名字为空,则抛出异常(第 33 行);否则将名字存储起来(第 35 行)。此处使用预定义类 [Exception] 来抛出异常。 此处必须使用其绝对名称 [\Exception]。 若使用其相对名称 [Exception],则该类将在当前命名空间中被查找,即类 Personne 的命名空间 [Exemples] 中。 因此,解释器 PHP 将查找一个名为 [\Exemples\Exception] 的绝对类,但该类并不存在;
类 [Personne] 被以下脚本 [classes-05.php] 使用:
<?php
// 严格遵守函数参数类型
declare(strict_types=1);
// 包含 Personne 类的定义
require_once __DIR__."/Personne.php";
// Personne类的限定名
use \Exemples\Personne;
// 测试
// 创建 Personne 对象
$p = new Personne("Paul", "Langevin", 48);
// 此人的身份
print "Exemple1, personne=$p\n";
// 创建错误的 Personne 对象
try {
$p = new Personne("xx", "yy", "zz");
} catch (\Exception $e1) {
print "Exemple2, erreur : " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
print "Exemple2, erreur : " . $e2->getMessage() . "\n";
}
// 创建错误的Personne对象
try {
$p = new Personne("", "yy", 10);
} catch (\Exception $e1) {
print "Exemple3, erreur : " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
print "Exemple3, erreur : " . $e2->getMessage() . "\n";
}
// 结束
exit;
注释
- 第 7 行:脚本将使用类 [Personne]。因此,需要告知解释器 PHP 该类定义的位置。 这就是指令 [include fichier] 和 [require fichier] 的作用。此处使用了指令 [include]。 这两条指令的区别在于:如果 [include fichier] 在加载 [fichier] 时遇到错误,则会触发 [E_WARNING] 级别的错误,但执行仍会继续;而在相同情况下,[require] 则会引发致命错误并终止脚本执行。这两条指令各自都有 [include_once] 和 [require_once] 这两个变体。 这两个变体用于处理同一文件被多次包含的情况。可以设想一个由多个 PHP 脚本组成的项目,其中多个脚本引用了类 [Personne]。 此时,执行这些脚本将导致多次包含文件 [Personne.php],并引发错误,因为一个类不能被定义两次。解决方案是使用 [_once] 变体,它确保该文件仅在项目的全局脚本中被包含一次;
- 第 7 行:常量 [__DIR__] 是一个 PHP 常量,它指代包含常量 [__DIR__] 的脚本所在文件夹的完整路径。因此,第 17 行的表达式:
require_once __DIR__."/Personne.php";
将等同于类似以下内容:
require_once ‘C:\Data\st-2019\dev\php7\php5-exemples\exemples\classes/Personnes.php’
在文件路径中,/ 和 \ 可以互换使用;
- 第 14 行:使用我们刚刚定义的类 [Personne]。脚本 [classes-05.php] 没有命名空间。第 14 行使用的是没有命名空间的类 [Personne] 的相对名称。 由于类 [Personne] 没有命名空间,系统会在脚本 [classes-05.php] 本身中搜索该类,因此无法找到。此问题有两种解决方案:
- 使用类 [\Exemples\Personne] 的完整名称;
- 使用第 10 行的 use 语句。该语句表明后续代码使用的是类 [\Exemples\Personne];
- 第 10 行:use 语句使解释器知道,第 14 行引用的类 [Personne] 实际上是类 [\Exemples\Personne]。话虽如此,解释器将从何处找到该类的代码? 答案就在第 7 行。该行指出,要执行当前脚本,还需加载脚本 [Personne.php]。此处使用了文件的相对路径,因此系统将在包含脚本 [classes-05.php] 的文件夹中进行查找。 因此,脚本 [Personne.php] 和 [classes-05.php] 必须位于同一文件夹中。本例中正是如此,它们都位于 [exemples/classes] 文件夹内。第 10 行的指令等同于:
use \Exemples\Personne as Personne;
上文中的 [use] 语句表示别名 [Personne] 指向类 [\Exemples\Personne];
- 第14行:创建了一个[Personne]对象。这里将隐式执行[Personne]类的[__construct]方法;
- 第 16 行:显示 Person $p。 为了显示,变量 $p 的值必须转换为字符串。此时隐式执行的是方法 [Personne.__toString]。因此该方法必须返回一个字符串;
- 我们已看到,类 [Personne] 的构造函数可能会抛出类型为 [\Exception] 的异常。 因此必须对此进行处理。此外,第 14 行的代码是不完整的。必须使用第 18-24 行的代码来正确处理可能发生的异常。在此,我们通过传入一个非整数的年龄值来故意触发一个异常。 在此特殊情况下,触发的异常是由解释器 PHP 抛出的,而非由类 [Personne] 的代码抛出。事实上,方法 [Personne.__construct] 的签名如下:
function __construct(string $prenom, string $nom, int $age)
因此,传递给构造函数的参数 [age] 必须是整数类型。否则,解释器 PHP 将抛出类型为 [TypeError] 的错误。 此外,[Personne] 类的 [set] 方法会抛出类型为 [\Exception] 的异常。 由于调用它们的构造函数没有 try/catch 结构,异常会向上传递一层,到达调用构造函数的代码,即脚本 [classes-05.php] 的代码。最终,脚本 [classes-05.php] 可能会收到两种类型的异常: \Exception 或 \TypeError。需要注意的是,当开发人员确信某些异常不会发生时,通常不会使用相应的 catch 语句。此处仅出于演示目的而系统性地使用了这些 catch 语句。但实际上,对于任何可能发生的异常(即使概率极低),都应使用 catch 语句;
因此,第 18-24 行中的 try 结构包含两个 catch 语句,以分别处理这两种异常;
- 第20行:可以写成[Exception],也可以写成[\Exception]:
- 第一种写法使用了相对于脚本命名空间的类名。该脚本没有命名空间,因此其命名空间即为命名空间的根节点:\。因此,在此处写 [Exception] 相当于写 [\Exception]。 然而,类 [Exception] 确实位于命名空间 [\] 中;
在本身不带命名空间的脚本中,最好使用 PHP 预定义异常的绝对名称。 因此,如果决定为该脚本指定命名空间,类名的绝对写法仍然有效;而在另一种情况下,命名空间的变更会导致类名的相对写法出现错误;
- 第 21 行:当发生异常时,[Exception→getMessage] 方法可用于获取异常的错误消息。对于 [TypeError] 类型的错误也是如此。在 [Personne.setPrenom] 方法中,我们写道:
public function setPrenom(string $prénom) {
// 名字字段不能为空
$prénom = trim($prénom);
if ($prénom === "") {
throw new \Exception("Le prénom doit être non vide");
} else {
$this->prenom = $prénom;
}
}
第 5 行,抛出一个带有错误消息 [Le prénom doit être non vide] 的异常。脚本 [classes-05.php] 第 29 行中的方法 [Exception→getMessage] 将捕获该异常。
结果:
Exemple1, personne=[Paul,Langevin,48]
Exemple2, erreur : Argument 3 passed to Exemples\Personne::__construct() must be of the type integer, string given, called in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_18.php on line 19
Exemple3, erreur : Le prénom doit être non vide
5.7. 添加一个充当第二个构造函数的方法
在 PHP 7 中,无法拥有多个具有不同参数的构造函数,从而无法以多种方式构建对象。因此,可以使用充当构造函数的方法:
// 方法
public function initWithPersonne(Personne $p) {
// 使用 $p 对象初始化当前对象
$this->__construct($p->prenom, $p->nom, $p->age);
}
注释
- 第2-5行:initWithPersonne方法允许将另一个对象Personne的属性值赋给当前对象。 此处它调用了构造函数 __construct,但这并非强制要求。它也可以自行初始化 [Personne] 类的属性;
新类 [Personne] 被以下脚本 [classes-06.php] 所使用:
<?php
// 包含 Personne 类的定义
require_once __DIR__."/Personne.php";
// 声明 Personne 类的限定名
use \Exemples\Personne;
// 测试
// 创建 Personne 对象
try {
$p = new Personne("Paul", "Langevin", 48);
} catch (\Exception $e) {
print "erreur : " . $e->getMessage();
exit;
}
// 此人的身份
print "personne=$p\n";
// 创建第二个Personne对象
try {
$p2 = new Personne("Laure", "Adeline", 67);
} catch (\Exception $e) {
print "erreur : " . $e->getMessage();
exit;
}
// 使用第二个人的值初始化第一个
try {
$p->initWithPersonne($p2);
} catch (\Exception $e) {
print "erreur : " . $e->getMessage();
exit;
}
// 验证
print "personne=$p\n";
// 结束
exit;
- 第 14、23、30 行:在发生异常后,如果遇到的错误无法恢复,通常需要停止控制台脚本的执行。 但在 Web 脚本中情况不同:不会停止脚本的执行,而是显示一个错误页面。如果处于函数中,则不使用 exit 语句,而是使用 return:在设置错误后,不停止脚本的执行(exit),而是退出函数(return);
结果:
5.8. 一个对象数组 [Personne]
下面的示例 [classes-07.php] 表明可以拥有对象数组。
<?php
require_once __DIR__."/Personne.php";
use \Exemples\Personne;
// 测试
// 创建一个 Personne 对象数组
// 为便于理解代码,不处理可能出现的异常
$groupe = [new Personne("Paul", "Langevin", 48), new Personne("Sylvie", "Lefur", 70)];
// 这些人员的身份
for ($i = 0; $i < count($groupe); $i++) {
print "groupe[$i]=$groupe[$i]\n";
}
// 结束
exit;
结果:
注释
- 第 9 行:创建一个包含 2 个人的数组;
- 第12行:遍历数组;
- 第13行:$groupe[$i]是Personne类型的对象。使用方法[Personne.__toString]来显示它;
5.9. 创建从 Personne 类派生的子类
在文件 [Enseignant.php] 中创建以下 [Enseignant] 类:
<?php
// 严格遵守函数参数的声明类型
declare (strict_types=1);
// 命名空间
namespace Exemples;
// 从 Person 派生的类
class Enseignant extends Personne {
// 属性
private $discipline; // 所授学科
// getter 和 setter
public function getDiscipline(): string {
return $this->discipline;
}
public function setDiscipline(string $discipline): void {
$this->discipline = $discipline;
}
// 构造函数
public function __construct(string $prénom, string $nom, int $âge, string $discipline) {
// 父类属性
parent::__construct($prénom, $nom, $âge);
// 其他属性
$this->setDiscipline($discipline);
}
// 重载父类的 __toString 函数
public function __toString(): string {
return "[" . parent::__toString() . ",$this->discipline]";
}
}
注释
- 第 7 行:类 [Enseignant] 也属于命名空间 [Exemples];
- 第 10 行:类 Enseignant 继承自类 Personne.。派生类 Enseignant 继承了其父类的属性和方法;
- 第 12 行:类 Enseignant 添加了一个属于它自己的新属性 discipline;
- 第 25 行:类 Enseignant 的构造函数接收 4 个参数:
- 3 个用于初始化其父类(名字、姓氏、年龄),第 27 行;
- 1 个用于自身初始化(学科),第 29 行;
- 第 27 行:派生类可通过关键字 parent:: 访问其父类的方法和构造函数。此处将参数(名字、姓氏、年龄)传递给父类的构造函数;
- 第33-35行:派生类的__toString方法调用了父类的__toString方法;
类 [Enseignant] 被以下脚本 [classes-08.php] 调用:
<?php
// 包含两个类的定义
require_once __DIR__."/Personne.php";
require_once __DIR__."/Enseignant.php";
// 声明所使用的两个类
use \Exemples\Personne;
use \Exemples\Enseignant;
// 测试
// 创建 Personne 对象及其派生类的数组
// 为简化示例,不处理异常
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70));
// 这些人员的身份
for ($i = 0; $i < count($groupe); $i++) {
print "groupe[$i]=$groupe[$i]\n";
}
// 结束
exit;
注释
- 第 4-5 行:我们需要告诉 PHP 解释器两个类 [Enseignant, Personne] 的位置;
- 第 7-8 行:声明这两个类的完整名称。这样我们就可以在代码中仅通过其名称(不带命名空间后缀)来引用它们;
- 第 13 行:创建一个包含 [Personne] 类型和 [Enseignant] 类型的数组;
- 第16-18行:显示数组中的元素;
- 第 17 行:将调用每个元素 $groupe[$i] 的 __toString 方法。 类 Personne 有一个方法 __toString。 类 Enseignant 有两个方法:其父类的和它自己的。人们可能会问哪个会被调用。运行结果显示,被调用的其实是类 Enseignant 的方法。 情况总是如此:当在对象上调用方法时,系统会按以下顺序进行查找:首先在对象本身中查找,如果存在父类则在其父类中查找,然后在父类的父类中查找,以此类推……一旦找到该方法,搜索即告结束。
结果:
5.10. 创建一个从 Personne 类派生的第二个类
以下示例在文件 [Etudiant.php] 中创建了一个从类 Personne 派生的类 Etudiant:
<?php
// 严格遵守函数参数的声明类型
declare (strict_types=1);
// 命名空间
namespace Exemples;
class Etudiant extends Personne {
// 属性
private $formation; // 已参加的培训
// getter 和 setter
public function getFormation(): string {
return $this->formation;
}
public function setFormation(string $formation): void {
$this->formation = $formation;
}
// 构造函数
public function __construct(string $prénom, string $nom, int $âge, string $formation) {
// 父类属性
parent::__construct($prénom, $nom, $âge);
// 其他属性
$this->setFormation($formation);
}
// 重载父类的 __toString 函数
public function __toString(): string {
return "[" . parent::__toString() . ",$this->formation]]";
}
}
该类由以下脚本 [classes-09.php] 使用:
<?php
// 包含并定义脚本使用的类
require_once __DIR__."/Personne.php";
use \Exemples\Personne;
require_once __DIR__."/Enseignant.php";
use \Exemples\Enseignant;
require_once __DIR__."/Etudiant.php";
use \Exemples\Etudiant;
// 测试
// 创建一个包含 Person 对象及其派生类的数组
// 为便于理解示例,此处不处理异常
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70), new Etudiant("Steve", "Boer", 23, "iup2 qualité"));
// 这些人员的身份
for ($i = 0; $i < count($groupe); $i++) {
print "groupe[$i]=$groupe[$i]\n";
}
// 结束
exit;
结果:
groupe[0]=[[Paul,Langevin,48],anglais]
groupe[1]=[Sylvie,Lefur,70]
groupe[2]=[[Steve,Boer,23],iup2 qualité]
5.11. 派生类构造函数与父类构造函数的关系
在某些面向对象的语言中,派生类的构造函数会自动调用其父类的构造函数。以下代码 [classes-16.php] 表明,在 PHP 7 中并非如此:
<?php
class Classe1 {
// 构造函数
public function __construct() {
print "constructeur de la classe Classe1\n";
}
}
class Classe2 extends Classe1 {
// 构造函数
public function __construct() {
// 父类的构造函数不会被隐式调用
print "constructeur de la classe Classe2\n";
}
}
class Classe3 extends Classe1 {
// 构造函数
public function __construct() {
// 显式调用父类的构造函数
parent::__construct();
// Classe3 专属代码
print "constructeur de la classe Classe3\n";
}
}
// 测试
print "test1---------\n";
new Classe2();
print "test2---------\n";
new Classe3();
结果
5.12. 重写父类的方法
我们已经看到,父类的某个方法可以在子类中被重写。 例如,类 [Personne](参见链接)中的方法 [__toString] 已在子类 [Enseignant](参见链接)和 [Etudiant](参见链接)中被重写。 脚本 [classes-13.php] 再次说明了这一概念:
<?php
// 严格遵守函数参数类型
declare(strict_types=1);
// 主类
class Classe1 {
public function f(): int {
return 1;
}
function g(): int {
return 2;
}
}
// 派生类
class Classe2 extends Classe1 {
// 重定义父类的函数 f
public function f(): int {
return parent::f() + 10;
}
}
// 代码
$c2 = new Classe2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c1=new Classe1();
print $c1->f()."\n";
注释
- 第 7-17 行:类 [Classe1] 定义了两个方法 f 和 g;
- 第 20-27 行:类 [Classe2] 继承自类 [Classe1],并重写了该类的 f 方法;
结果
注释
- 代码第 30 行创建了一个类型为 [Classe2] 的 $c2 对象;
- 代码第 31 行调用了对象 $c2 的 f 方法。由于该方法存在,因此被执行;
- 代码第32行调用了对象$c2的方法g。由于该方法不存在,系统会在父类中进行查找,并找到后执行;
- 代码第33行创建了一个类型为[Classe1]的$c1对象;
- 代码第 34 行调用了对象 $c1 的 f 方法。由于该方法存在,因此被执行;
5.13. 将对象作为函数参数传递
考虑以下脚本 [classes-14.php]:
<?php
// 严格遵守函数参数类型
declare(strict_types=1);
// 主类
class Classe1 {
public function f(): int {
return 1;
}
function g(): int {
return 2;
}
}
// 派生类
class Classe2 extends Classe1 {
// 重定义父类的函数 f
public function f(): int {
return parent::f() + 10;
}
}
// 函数参数的类型为 Class1 或其派生类
function doSomething(Classe1 $c1): void {
print $c1->f() + $c1->g() . "\n";
}
// 代码
// 创建一个从 Classe1 派生的 Classe2 类型的对象
$c2 = new Classe2();
// 调用 doSomething,参数为
doSomething($c2);
注释
- 第 7-17 行:类 [Classe1];
- 第 20-27 行:一个从 [Classe1] 派生的类 [Classe2];
- 第 30 行:一个期望参数类型为 [Classe1] 的函数。当期望类型为类时,实际参数可以是该类或其派生类的对象;
- 第35-38行:调用函数[doSomething]时,传入的参数类型为[Classe2],而预期类型应为[Classe1];
结果
5.14. 抽象类
抽象类是一种不完整的类,无法被实例化。必须通过继承才能使用。
抽象类有何用途?有时某些类共享一个或多个方法,但在其他方法或属性上存在差异。此时,将所有共同点归纳到父类中是可取的做法。 目前我们尚不需要抽象类。但假设子类仅在方法 M 上存在差异:该方法的签名在所有子类中相同,但实现方式不同。为了强制子类实现方法 M:
- 我们将在父类中声明方法 M 的签名。由于父类本身不知道如何实现该方法,因此我们在方法前加上 abstract 关键字:这意味着方法 M 的实现将交由子类负责;
- 由于父类尚未完全实现,因此也使用相同的 abstract 关键字将其声明为抽象类。这导致该类无法被实例化。必须创建一个子类来定义方法 M 的实现,这样父类的类体才能被使用;
以下是一个示例 [classes-15.php]:
<?php
// 严格遵守函数参数类型
declare(strict_types=1);
// 抽象主类
abstract Class Classe1 {
// 所有派生类都已知的方法
public function f(): int {
return 1;
}
// 抽象方法 g - 将由派生类定义
abstract function g(): int;
}
// 派生类
Class Classe2 extends Classe1 {
// 必须定义父类的 g 方法
public function g(): int {
return parent::f() + 10;
}
}
// 派生类
Class Classe3 extends Classe1 {
// 父类的 g 方法必须已定义
public function g(): int {
return parent::f() + 20;
}
// 可以重写父类的 f 方法
public function f(): int {
return 2;
}
}
// 代码
$c2 = new Classe2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c3 = new Classe3();
print $c3->f() . "\n";
print $c3->g() . "\n";
注释
- 第 7-16 行:类 [Classe1] 是抽象类(第 7 行),因为它无法实现第 15 行中的 g 方法。因此,必须对其进行派生才能使用;
- 第19-26行:类[Classe2]继承自类[Classe1],并重写了父类的g方法(第22-24行);
- 第 29-41 行:类 [Classe3] 继承自类 [Classe1],并重定义了父类的 g 方法(第 32-34 行);
- 第 37-39 行:类 [Classe3] 重定义了其父类的 f 方法;
- 第 44-49 行:创建两个类型分别为 [Classe2] 和 [Classe3] 的对象,并调用它们的 f 和 g 方法;
结果
5.15. 最终类
最终类是指无法被派生的类。请看以下脚本 [classes-11.php]:
<?php
// 命名空间
namespace Exemples;
// 不可派生的类
final Class Classe1 {
}
// 派生类
Class Classe2 extends Classe1 {
}
// 代码 - 必须引发错误
new Classe2();
注释
- 第 7-9 行:关键字 final 使类 [Classe1] 成为一个无法派生的最终类;
- 第 12-14 行:类 [Classe2] 继承了最终类 [Classe1],这属于错误;
- 第 17 行:该错误仅会在脚本运行时尝试操作 [Classe2] 类型的对象时被报告;
结果
5.16. final 方法
最终方法是指无法通过派生重定义的方法。以下是一个示例 [classes-12.php]:
<?php
// 严格遵守函数参数类型
declare(strict_types=1);
// 命名空间
namespace Exemples;
// 主类
Class Classe1 {
// 该方法不能在派生类中重写
public final function f(): int {
return 1;
}
}
// 派生类
Class Classe2 extends Classe1 {
public function f(): int {
return 2;
}
}
// 代码 - 应引发错误
new Classe2();
注释
- 第 13 行:类 [Classe1] 中的方法 f 通过关键字 final 被声明为 final 方法;
- 第 20 行:类 [Classe2] 继承自类 [Classe1];
- 第 22-23 行:重新定义了父类 [Classe1] 的 f 函数。这将导致错误;
- 第29行:创建一个类型为[Classe2]的对象,以强制解释器PHP检查类[Classe2];
结果
5.17. 静态方法和属性
静态方法是与定义它的类相关联的方法,而非与该类的实例对象相关联。因此,如果类 C 声明了一个静态方法 M,要使用该方法,应写为:
- C::M 若位于类外部;
- self::M 若位于类内部;
以下是一个示例 [classes-17.php]:
<?php
class Classe1 {
// 静态方法
static function say(string $message): void {
print "$message\n";
}
}
// 测试 -------------------
Classe1::say("hello");
注释
- 第 6 行:使用 static 关键字将方法 [say] 声明为静态方法;
- 第 13 行:使用以下语法调用静态方法 [say]:Classe1::say;
结果
现在考虑以下代码 [classes-18.php]:
<?php
class Classe1 {
// 静态属性
private static $nbObjects = 0;
public function __construct() {
print "constructeur Classe1\n";
self::$nbObjects++;
}
// 静态方法
static function say(): void {
print self::$nbObjects ." objets de type [Classe1] ont été construits\n";
}
}
// 测试 -------------------
new Classe1();
new Classe1();
Classe1::say();
注释
- 第 5 行:声明了一个静态属性,用于统计已创建的 [Classe1] 类实例的数量。 这并非属于类实例的属性。事实上,如果创建了两个对象 O1 和 O2,它们彼此之间并无关联。 在实例中设置计数器毫无意义:当创建新对象时,该在哪个实例中递增计数器?这将迫使我们仅递增特定对象的计数器,而忽略其他实例的计数器。静态属性是类属性,而非类实例的属性;
- 第7-10行:应在构造函数中统计创建的对象数量,因为每个新对象的创建都会触发构造函数的执行;
- 第14行:请注意self::$nbObjects这种写法,它表示引用了包含该执行代码的类中的静态属性;
- 第13-15行:静态方法 [say] 的作用是显示已创建的对象数量;
- 第 20-22 行:创建两个对象并显示对象计数器;
结果
constructeur Classe1
constructeur Classe1
2 objets de type [Classe1] ont été construits
5.18. 父类与子类之间的可见性
让我们来分析以下脚本 [classes-19.php]:
<?php
class SomeParent {
// 属性
private $attributeOfParent = 4;
// 方法
public function doTest(): void {
// 谁在调用?
print "parent :\n";
var_dump($this);
// 父级显示
print "parent : attributeOfParent={$this->attributeOfParent}\n";
print "parent : attributeOfChild={$this->attributeOfChild}\n";
}
}
class SomeChild extends SomeParent {
// 属性
private $attributeOfChild = 14;
// 方法
public function doTest(): void {
// 子视图
print "child : attributeOfParent={$this->attributeOfParent}\n";
print "child : attributeOfChild={$this->attributeOfChild}\n";
// 父级
parent::doTest();
}
}
// 主脚本
print "---test1\n";
(new SomeParent())->doTest();
print "---test2\n";
(new SomeChild())->doTest();
注释
- 第 3-17 行:类 [SomeParent];
- 第 19-32 行:子类 [SomeChild]。可以看到它继承了类 [SomeParent](第 19 行);
- 第 5 行:类 [SomeParent] 仅有一个属性;
- 第8-17行:方法[SomeParent::doTest]旨在显示两个属性:
- [$attributeOfParent],该方法属于类 [SomeParent];
- [$attributeOfChild] 属于类 [SomeChild](第 21 行);
- 第 10-11 行:显示调用者的身份:实际上,我们将通过两种不同的方式调用该方法:
- 从父类 [SomeParent] 调用;
- 从子类 [SomeChild] 调用;
- 第13-14行:显示两个属性;
- 第 19-32 行:子类 [SomeChild] 继承自类 [SomeParent](第 19 行);
- 第 21 行:类 [SomeChild] 仅有一个属性;
- 第24行:方法[SomeChild::doTest]旨在显示两个属性:
- [$attributeOfParent],该类属于 [SomeParent] 类;
- [$attributeOfChild] 属于类 [SomeChild];
- 第 26-27 行:显示这两个属性;
- 第30行:调用父类的[doTest]方法,该方法将依次显示这两个属性;
- 第 36 行:调用方法 [SomeParent::doTest];
- 第 38 行:调用方法 [SomeChild::doTest];
在第一个测试中,这两个属性的可见性为 [private]。 因此,我们可以预期子类无法看到其父类的属性。该属性至少应具有 [protected] 的可见性。但子类的属性又如何呢?它在父类中是否可见?
以下是第一次测试的结果:
注释
- 第1-10行:首次测试结果,其中调用了方法 [SomeParent::doTest];
- 第3-6行:可见调用该方法的对象类型为[SomeParent];
- 第7行:显示属性[$attributeOfParent];
- 第9-10行:可见[SomeParent::$attributeOfChild]属性不存在,因此未显示;
- 第11-30行:第二次测试结果,其中调用了方法[SomeChild::doTest];
- 第13-14行:可见属性[SomeChild::$attributeOfParent]不存在。 因此未显示。这是正常的:属性 [SomeParent::$attributeOfParent] 实际为 [private],因此在子类中未被识别;
- 第 15 行:显示属性 [$attributeOfChild];
- 第 16-30 行:当前位于由子类调用的 [SomeParent::doTest] 方法中;
- 第 17-22 行:可以看到 [$this] 的类型为 [SomeChild],并具有两个私有属性;
- 第23行:非常令人惊讶的是,类型为[SomeChild]的[$this]在此处可见父类[$attributeOfParent]的属性;
- 第 25-30 行:同样令人惊讶的是,类型为 [SomeChild] 的 [$this] 无法看到其父节点 [$attributeOfChild] 的属性;
这一结果非常令人惊讶:尽管第17-21行显示[$this]属于[SomeChild]类型, 但位于 [SomeParent::doTest] 方法内部的 [$this] 却表现得仿佛是 [SomeParent] 类的实例,而非 [SomeChild] 类的实例。
让我们使用脚本 [classes-20.php] 进行一次新的测试。属性 [$attributeOfParent] 现在具有 [protected] 的可见性(第 5 行):
<?php
class SomeParent {
// 属性
protected $attributeOfParent = 4;
// 方法
public function doTest(): void {
// 谁调用的?
print "parent :\n";
var_dump($this);
// 父级视图
print "parent : attributeOfParent={$this->attributeOfParent}\n";
print "parent : attributeOfChild={$this->attributeOfChild}\n";
}
}
class SomeChild extends SomeParent {
// 属性
private $attributeOfChild = 14;
// 方法
public function doTest(): void {
// 子视图
print "child : attributeOfParent={$this->attributeOfParent}\n";
print "child : attributeOfChild={$this->attributeOfChild}\n";
// 父级
parent::doTest();
}
}
// 主脚本
print "---------------------------test1\n";
(new SomeParent())->doTest();
print "---------------------------test2\n";
(new SomeChild())->doTest();
结果
注释
- 第 12 行:类 [SomeChild] 现在可以看到其父类 [$attributeOfParent] 的属性。这是正常的,因为该父类的作用域现在为 [protected];
- 在方法 [someParent::doTest] 中,对象 [$this] 的类型为 [SomeChild](第 15-20 行)。 它能看到其父对象 [$attributeOfparent] 的属性(第 21 行),但仍无法访问其自身的属性 [$attributeOfChild](第 23-28 行);
在第三次尝试中,[$attributeOfChild] 属性同样具有 [protected] 的作用域:
<?php
class SomeParent {
// 属性
protected $attributeOfParent = 4;
// 方法
public function doTest(): void {
// 谁调用的?
print "parent :\n";
var_dump($this);
// 父级视图
print "parent : attributeOfParent={$this->attributeOfParent}\n";
print "parent : attributeOfChild={$this->attributeOfChild}\n";
}
}
class SomeChild extends SomeParent {
// 属性
protected $attributeOfChild = 14;
// 方法
public function doTest(): void {
// 子视图
print "child : attributeOfParent={$this->attributeOfParent}\n";
print "child : attributeOfChild={$this->attributeOfChild}\n";
// 父级
parent::doTest();
}
}
// 主脚本
print "---------------------------test1\n";
(new SomeParent())->doTest();
print "---------------------------test2\n";
(new SomeChild())->doTest();
执行结果如下:
- 第 22 行:这次,在父元素内部, 类型为 [SomeChild](第 15-20 行)的 [$this] 能够访问其自身类 [SomeChild] 的受保护属性 [$attributeOfChild]。
从这些测试中能得出什么结论?
- 当 [$this] 作为父类的实例,并在父类的某个方法中被调用时,它能够访问:
- 父类的属性和方法,无论其可见性如何;
- 无法访问其子类的任何属性和方法;
这是预期的行为。
- 子类的实例 [$this],当在子类的某个方法中使用时,可以看到:
- 父类的属性和方法(只要它们的可见性至少为 [protected])。可见性为 [private] 的则不可见;
- 无论可见性如何,都能看到子类的属性和方法;
这是预期的行为。
- 当子类的实例([$this])在父类的某个方法中被调用时,它将看到:
- 父类的属性和方法,无论其可见性如何;
- 仅能看到自身类的属性和方法,且这些属性和方法的可见性至少为 [protected]。可见性为 [private] 的属性和方法则不可见;
这是意料之外的行为。
5.19. 类的 jSON 编码
在类中通常存在 [__toString] 方法:该方法旨在返回一个字符串,该字符串代表调用它的对象。人们可能会倾向于让该字符串成为 jSON 字符串。我们现在来探讨这一方案。

我们将使用以下 [Personne] 类:
<?php
class Personne {
// 属性
private $nom;
private $prénom;
private $âge;
private $enfants;
// 全局设置器
public function setFromArray(array $arrayOfAttributes): Personne {
// 初始化类的某些属性
foreach ($arrayOfAttributes as $attribute => $value) {
$this->$attribute = $value;
}
// 返回对象
return $this;
}
// 获取器
public function getNom() {
return $this->nom;
}
public function getPrénom() {
return $this->prénom;
}
public function getÂge() {
return $this->âge;
}
public function getEnfants() {
return $this->enfants;
}
// __toString
public function __toString(): string {
// 识别对象
var_dump($this);
// 获取其属性
$attributes = \get_object_vars($this);
var_dump($attributes);
// 返回属性的字符串 jSON
return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
}
}
注释
- 第 5-8 行:类的四个属性;
- 第20-35行:用于获取这些属性值的getter方法;
- 第 11-18 行:一个全局设置器,用于通过关联数组 [$arrayOfAttributes] 初始化属性,该数组的键与类属性名称相同;
- 第38-46行:该类的[__toString]方法;
- 第 42 行:函数 PHP [get_object_vars] 可获取类属性的值,并以关联数组 [‘姓’=>’姓1’, ‘名’=>’名1’, ‘年龄’=>’年龄1’, ‘子女’=>[]] 的形式返回;
- 第 45 行:返回该属性数组的字符串 jSON;
让我们来分析一下利用类 [Personne] 的脚本 [json-01.php]:
<?php
// Person类
require "Personne.php";
// 父类的实例化
$père = new Personne();
// 父类的初始化
$père->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Dieudonné",
"âge" => 58
]);
// 子对象1的实例化和初始化
$enfant1 = (new Personne())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Sylvain",
"âge" => 17
]);
// 子类2的实例化和初始化
$enfant2 = (new Personne())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Géraldine",
"âge" => 12
]);
// 父对象子对象的初始化
$père->setFromArray([
"enfants" => [$enfant1, $enfant2]
]);
// 显示父对象的元素
$enfant1=($père->getEnfants())[0];
$enfant2=($père->getEnfants())[1];
print "------------------------enfant1\n";
print "enfant1=$enfant1\n";
print "------------------------enfant2\n";
print "enfant2=$enfant2\n";
print "------------------------père\n";
print "père=$père\n";
注释
- 第 6-13 行:使用 [Personne::setFromArray] 方法初始化 [Personne] [$père] 对象,该方法允许使用一个数组初始化 [Personne] 对象,该数组的键与 [Personne] 类的属性相同;
- 第14-19行:以相同方式初始化一个[Personne] [$enfant1]对象;
- 第 21-25 行:初始化 [Personne] 和 [$enfant2] 对象;
- 第 27-29 行:使用包含两个子节点的数组初始化 [$père→enfants] 属性;
- 第32-33行:将这两个子节点分配给父节点;
- 第35行:操作[print]将尝试将对象[$enfant1]转换为字符串。为此,它使用了该对象的[__toString]方法。 因此,我们期望看到该对象的字符串 jSON;
- 第 38-39 行:对父对象也进行同样的操作;
结果如下:
------------------------enfant1
object(Personne)#2 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(7) "Sylvain"
["âge":"Personne":private]=>
int(17)
["enfants":"Personne":private]=>
NULL
}
array(4) {
["nom"]=>
string(11) "Bertholomé"
["prénom"]=>
string(7) "Sylvain"
["âge"]=>
int(17)
["enfants"]=>
NULL
}
enfant1={"nom":"Bertholomé","prénom":"Sylvain","âge":17,"enfants":null}
------------------------enfant2
object(Personne)#3 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(10) "Géraldine"
["âge":"Personne":private]=>
int(12)
["enfants":"Personne":private]=>
NULL
}
array(4) {
["nom"]=>
string(11) "Bertholomé"
["prénom"]=>
string(10) "Géraldine"
["âge"]=>
int(12)
["enfants"]=>
NULL
}
enfant2={"nom":"Bertholomé","prénom":"Géraldine","âge":12,"enfants":null}
------------------------père
object(Personne)#1 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(10) "Dieudonné"
["âge":"Personne":private]=>
int(58)
["enfants":"Personne":private]=>
array(2) {
[0]=>
object(Personne)#2 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(7) "Sylvain"
["âge":"Personne":private]=>
int(17)
["enfants":"Personne":private]=>
NULL
}
[1]=>
object(Personne)#3 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(10) "Géraldine"
["âge":"Personne":private]=>
int(12)
["enfants":"Personne":private]=>
NULL
}
}
}
array(4) {
["nom"]=>
string(11) "Bertholomé"
["prénom"]=>
string(10) "Dieudonné"
["âge"]=>
int(58)
["enfants"]=>
array(2) {
[0]=>
object(Personne)#2 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(7) "Sylvain"
["âge":"Personne":private]=>
int(17)
["enfants":"Personne":private]=>
NULL
}
[1]=>
object(Personne)#3 (4) {
["nom":"Personne":private]=>
string(11) "Bertholomé"
["prénom":"Personne":private]=>
string(10) "Géraldine"
["âge":"Personne":private]=>
int(12)
["enfants":"Personne":private]=>
NULL
}
}
}
père={"nom":"Bertholomé","prénom":"Dieudonné","âge":58,"enfants":[{},{}]}
注释
- 第2-11行:对象[$enfant1];
- 第12-21行:对象[$enfant1]的属性表。所有属性均已获取;
- 第22行:确实包含对象[$enfant1]的字符串jSON;
- 第23-44行:对象[$enfant2]的情况相同;
- 第45-112行:对于父节点则略有不同,因为其属性[enfants]并非像子节点那样是NULL;
- 第112行:可以看到,在父亲的字符串jSON中,缺少了子女;
- 第79-111行:可以看到,在父亲的属性表中,孩子1(第89-98行)仍是一个对象,孩子2(第99-110行)也是如此。 简而言之,表达式 [\get_object_vars($this)](其中 [$this] 代表父对象)并非递归的: 如果类 [Personne] 的某个属性本身是一个对象,则表达式 [\get_object_vars($this)] 不会尝试获取其属性数组;
我们可以改进这一点。我们将类 [Personne] 修改为以下类 [Personne2]:
<?php
class Personne2 {
// 属性
private $nom;
private $prénom;
private $âge;
private $enfants;
// 全局设置器
public function setFromArray(array $arrayOfAttributes): Personne2 {
…
// 返回对象
return $this;
}
// 获取器
public function getNom() {
return $this->nom;
}
…
// __toString
public function __toString(): string {
// 获取对象的属性
$attributes = $this->getAttributes($this);
$enfants = $attributes["enfants"];
if ($enfants != NULL) {
$attributes["enfants"] = [$enfants[0]->getAttributes(), $enfants[1]->getAttributes()];
}
// 生成属性字符串 JSON
return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
}
public function getAttributes(): array {
return \get_object_vars($this);
}
}
注释
- 第 36-38 行:函数 [getAttributes] 返回调用它的对象的属性数组;
- 第 25-34 行:函数 [__toString];
- 第27行:将类[Personne]的属性存入数组[$attributes]中;
- 第 28 行:根据前面的示例,我们知道 [$attributes["enfants"]] 是一个包含两个 [Personne] 类型对象的数组;
- 第 29-31 行:这两个对象被它们的属性数组所替换;
- 第33行:最后只需将构建好的属性数组编码为jSON;
脚本 [json-02.php] 通过以下方式调用类 [Personne2]:
<?php
// Personne2 类
require "Personne2.php";
// 父类的实例化
$père = new Personne2();
// 初始化
$père->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Dieudonné",
"âge" => 58
]);
// 子对象1的实例化和初始化
$enfant1 = (new Personne2())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Sylvain",
"âge" => 17
]);
// 子类2的实例化和初始化
$enfant2 = (new Personne2())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Géraldine",
"âge" => 12
]);
// 父类子类的初始化
$père->setFromArray([
"enfants" => [$enfant1, $enfant2]
]);
// 显示父对象
print "------------------------père\n";
print "père=$père\n";
脚本 [json-02.php] 与脚本 [json-01.php] 完全相同,唯一区别在于类 [Personne2] 取代了类 [Personne]。
执行结果如下:
这次,我们确实得到了包含父亲的子节点。
之前的解决方案并不理想,因为子节点本身可能也有子节点。这样又回到了之前的问题。
类 [Personne3] 通过以下方式解决了该问题:
<?php
class Personne3 {
// 属性
private $nom;
private $prénom;
private $âge;
private $enfants;
// 全局设置器
public function setFromArray(array $arrayOfAttributes): Personne3 {
…
}
// 获取器
…
// __toString
public function __toString(): string {
// 将属性字符串 JSON
$attributes = [];
$this->getRecursiveAttributes($attributes, $this, []);
// 属性字符串 JSON
return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
}
public function getAttributes(): array {
return \get_object_vars($this);
}
private function getRecursiveAttributes(array &$attributes, $value, $keys): void {
// 对值 [$value] 进行分析
// $keys 是一个数组 [key1, key2, .., keyn]
// $value=$attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX….[keyn]
// 如果 [$value] 是一个对象,则使用其方法 [getAttributes]
if (\is_object($value)) {
// 对象 [$value] 的属性
$objectAttributes = $value->getAttributes();
// 如何处理结果?
if ($keys) {
// 在 [$attributes] 中,我们将用其属性的数组替换 $value
// 需要构建元素 $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[keyn]
// 其中 $keys 是数组 [key1, key2, .., keyn]
// 获取数组 [$attributes] 的引用
$attribute = &$attributes;
// 扫描键表
foreach ($keys as $key) {
// 获取属性的引用
$attribute = &$attribute[$key];
}
// 此处 $attribut 与 $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[key(n)] 完全相同
// 它们共享相同的内存位置
// 对象 [$value] 被其属性数组替换;
// 需要写入 $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[keyn]=$objectAttributes
// 这等同于 $attribute = $objectAttributes
$attribute = $objectAttributes;
} else {
// 无键值——我们正处于对象探索的初期
// $objectAttributes 代表该类的顶级属性
$attributes += $objectAttributes;
}
// 也许在 [$objectAttributes] 中还有其他对象
// 正在探索 [$objectAttributes] 的属性
$this->getRecursiveAttributes($attributes, $objectAttributes, $keys);
} else {
if (\is_array($value)) {
// 我们有一个数组——分析其中的每个元素
foreach ($value as $key => $élément) {
// 将当前键添加到数组中 $keys
\array_push($keys, $key);
// 解析 $élément
$this->getRecursiveAttributes($attributes, $élément, $keys);
// 从数组 $keys 中移除刚刚解析的键
\array_pop($keys);
}
}
}
}
注释
- 第21-22行:这次,方法 [__toString] 获取其类的属性,并要求递归处理:如果某个属性是一个对象或对象数组,则每个对象都应被替换为该对象的属性数组,以构建类最终的属性数组;
- 第31-78行:函数[getRecursiveAttributes]负责执行此操作。 我们已对该代码进行了注释。编写递归函数通常较为复杂,此处亦是如此。读者即使无法理解也不必担心。已有现成的库可完成此项工作。递归调用发生在第64行和第72行;
- 该代码的优势在于,它并非仅针对 [Personne3] 类而编写。只要主类所使用的子类也像它一样拥有第 27-29 行中的 [getAttributes] 方法,且这些子类的属性值属于不同类型的对象,该代码对所有此类类均适用
脚本 [json-03.php] 以如下方式使用类 [Personne3]:
<?php
// 类 Personne3
require "Personne3.php";
// 实例化父类
$père = new Personne3();
// 初始化
$père->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Dieudonné",
"âge" => 58
]);
// 实例化并初始化子对象1
$enfant1 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Sylvain",
"âge" => 27
]);
// 子对象2的实例化和初始化
$enfant2 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Géraldine",
"âge" => 12
]);
// 父类子类的初始化
$père->setFromArray([
"enfants" => [$enfant1, $enfant2]
]);
// 子对象11的实例化和初始化
$enfant11 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Gaëtan",
"âge" => 2
]);
// 子节点12的实例化和初始化
$enfant12 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Mathilde",
"âge" => 1
]);
// 子节点1的子节点的初始化
$enfant1->setFromArray([
"enfants" => [$enfant11, $enfant12]
]);
// 显示父对象
print "------------------------père\n";
print "père=$père\n";
- 第30-45行:为[$enfant1]分配两个子节点;
执行结果如下:
如果对该结果进行格式化,将得到以下内容:
père={
"nom": "Bertholomé",
"prénom": "Dieudonné",
"âge": 58,
"enfants": [
{
"nom": "Bertholomé",
"prénom": "Sylvain",
"âge": 27,
"enfants": [
{
"nom": "Bertholomé",
"prénom": "Gaëtan",
"âge": 2,
"enfants": null
},
{
"nom": "Bertholomé",
"prénom": "Mathilde",
"âge": 1,
"enfants": null
}
]
},
{
"nom": "Bertholomé",
"prénom": "Géraldine",
"âge": 12,
"enfants": null
}
]
}
我们确实成功提取了构成父节点的所有 [Personne] 对象中的字符串 jSON。