5. 类
词汇:类(Class)是一种 PHP 类型。该类型的变量称为对象。对象是类的实例(instance)。
5.1. 脚本层次结构

5.2. 任何变量都可以成为具有属性的对象
脚本 [classes-01.php] 如下所示:
<?php
// a generic object
// $obj1=new stdClass();
// any variable can have attributes by construction
$obj1->attr1 = "un";
$obj1->attr2 = 100;
// displays the
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// modify object
$obj1->attr2 += 100;
// displays the
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// copies the value of object1 (address of the object pointed to) to object2
// the two variables are then different but point to the same object
$obj2 = $obj1;
// modify obj2
$obj2->attr2 = 0;
// displays both objects
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
// changes the object pointed to by obj1
$obj1 = new stdClass();
print "obj1 :\n";
print_r($obj1);
print "obj2 :\n";
print_r($obj2);
// assigns the reference (address) from object2 to object3
// $obj2 and $obj3 are then one and the same variable
$obj3 = &$obj2;
print "obj2 :\n";
print_r($obj2);
print "obj3 :\n";
print_r($obj3);
// modify obj3
$obj3->attr2 = 10;
// displays both objects
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
print "objet3=[$obj3->attr1,$obj3->attr2]\n";
// changes the object pointed to by obj2
$obj2 = new stdClass();
$obj2->attr3 = "deux";
$obj2->attr4 = 20;
// displays the two objects $obj2 and $obj3
print "obj2 :\n";
print_r($obj2);
print "obj3 :\n";
print_r($obj3);
// is an object a dictionary?
print count($obj3) . "\n";
while (list($attribut, $valeur) = each($obj3)) {
print "obj3[$attribut]=$valeur\n";
}
// end
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 点]。可以说这两个变量是同一内存位置的别名。它们都指向一个对象,即下文中的对象 A [2];
- 操作 $obj2=new stdClass() 创建了一个新对象 Object B [下文第 3 处],并将该新对象的地址赋值给变量 $obj2。由于 $obj2 和 $obj3 是同一内存位置的两个别名,因此 $obj3 也指向该新对象 Object B。这在结果的第 16–27 行和第 30–41 行中有所体现;

- 第 52–54 行:表明对象可以像字典一样进行遍历。字典的键是属性的名称,字典的值则是这些属性的值;
- 第 51 行:count 函数可以应用于对象(会触发警告),但并不会如预期那样返回属性的数量。因此,对象与字典有相似之处,但并非字典;
5.3. 一个未声明属性的 Person 类
脚本 [classes-02.php] 如下:
<?php
class Personne {
// class attributes
// undeclared - can be created dynamically
// method
function identite() {
// a priori, uses non-existent attributes
return "[$this->prenom,$this->nom,$this->age]";
}
}
// test
// attributes are public and can be created dynamically
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// method call
print "personne=" . $p->identite() . "\n";
// end
exit;
结果:
注释
- 第 3–13 行:定义 Person 类。类是一种用于创建对象的模板。它包含属性以及称为方法的函数。属性无需声明;
- 第 8–11 行:identity 方法显示了三个未在类中声明的属性的值。关键字 $this 指代该方法所作用的对象;
- 第 17 行:我们创建了一个类型为 Person 的对象 $p。关键字 new 用于创建新对象。该操作返回对所创建对象的引用(即地址)。可以使用多种写法:new Person()、new Person、new person。类名不区分大小写;
- 第 18–20 行:在 $p 对象中创建了 identity 方法所需的三项属性;
- 第 22 行:将 Person 类的 identity 方法应用于 $p 对象。在 identity 方法的代码(第 8–11 行)中,$this 指代与 $p 相同的对象;
5.4. 声明了属性的 Person 类
脚本 [classes-03.php] 如下:
<?php
class Personne {
// class attributes
var $prenom;
var $nom;
var $age;
// method
function identite() {
return "[$this->prenom,$this->nom,$this->age]";
}
}
// test
// attributes are public
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// method call
print "personne=" . $p->identite() . "\n";
// end
exit;
结果:
注释
- 第 6–8 行:类属性使用 var 关键字显式声明;
5.5. 带有构造函数的 Person 类
前面的示例展示了在 PHP 4 中可能出现的非标准 Person 类。不建议照搬这些示例。现在我们提供一个遵循 PHP 7 最佳实践的 Person 类 [classes-04.php]:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
class Personne {
// class attributes
private $prenom;
private $nom;
private $age;
// getters and setters
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;
}
// manufacturer
public function __construct(string $prenom, string $nom, int $age) {
// we go through sets
$this->setPrenom($prenom);
$this->setNom($nom);
$this->setAge($age);
}
// method toString
public function __toString(): string {
return "[$this->prenom,$this->nom,$this->age]";
}
}
// test
// creation of a Personne object
$p = new Personne("Paul", "Langevin", 48);
// identity of this person
print "personne=$p\n";
// we change the age
$p->setAge(14);
// identity of the person
print "personne=$p\n";
// end
exit;
结果:
注释
- 第 6–50 行:Person 类;
- 第 7–9 行:类的私有属性。这些属性仅在类内部可见。其他可用的关键字包括:
- public:使属性成为公共属性,可在类外部访问;
- protected:将属性设为受保护属性,可在类内部及其派生类中访问;
- 由于这些属性是私有的,因此无法从类外部访问。因此,我们无法编写以下代码:
这里,我们处于 Person 类之外。由于 name 属性是私有的,第 2 行是错误的。要初始化 $p 对象的私有字段,有两种方法:
- 使用第 12–34 行中的公共 set 和 get 方法(这些方法的名称可以是任意名称)。然后我们可以这样写:
- 使用第37至42行的构造函数。那么我们就会写成:
上述代码会自动调用 Person 类的 __construct 方法;
- 第 59 行:此行将对象 $p 作为字符串显示。为此,使用了 Person 类的 __toString 方法(第 45–47 行);
- 该类的所有方法(函数)均以关键字 public 开头,这表示该函数在类外部可见。其他可用的关键字包括 private 和 protected,其用法与属性相同,含义也相同。如果未显式指定可见性属性,则函数默认具有 public 可见性;
5.6. 在构造函数中包含有效性检查的 Person 类
类的构造函数是验证对象初始化值是否正确的合适位置。不过,对象也可以通过其 set 方法或等效方法进行初始化。为了避免在两个不同位置重复进行相同的检查,可以将这些检查放在 set 方法中。如果发现对象的初始化值不正确,则会抛出异常。以下是一个示例。
首先,我们将 Person 类的定义移至独立文件 [Person.php] 中:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace;
namespace Exemples;
// person class
class Personne {
// class attributes
private $prenom;
private $nom;
private $age;
// getters and setters
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 {
// first name must be non-empty
$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 {
// name must be non-empty
$nom = trim($nom);
if ($nom === "") {
throw new \Exception("Le nom doit être non vide");
} else {
$this->nom = $nom;
}
}
public function setAge(int $âge): void {
// age must be valid
if ($âge < 0) {
throw new \Exception("L'âge doit être un entier positif ou nul");
} else {
$this->age = $âge;
}
}
// manufacturer
public function __construct(string $prenom, string $nom, int $age) {
// we go through sets
$this->setPrenom($prenom);
$this->setNom($nom);
$this->setAge($age);
}
// method
public function initWithPersonne(Personne $p): void {
// initializes the current object with a person $p
$this->__construct($p->prenom, $p->nom, $p->age);
}
// method toString
function __toString(): string {
return "[$this->prenom,$this->nom,$this->age]";
}
}
评论
- 第4行:规定在声明函数时必须遵守函数参数的类型;
- 第 7 行:定义了一个命名空间。因此,Person 类的完整(或限定)名称为 \Examples\Person。请注意限定名称开头的 \ 字符:这将生成一个绝对限定名称。如果缺少该字符,则名称为相对名称(相对于当前命名空间)。 因此,如果两个类 A 和 B 属于同一个命名空间 E,在类 A 的代码中,可以使用相对表示法 B 来访问类 B。如果类 A 属于命名空间 E1,而类 B 属于命名空间 E2,在类 A 的代码中,则需使用绝对表示法 \E2\B 来访问类 B。 在命名空间内定义类并非强制要求,但若未这样做,NetBeans 会发出警告。因此,我们将遵循此规范。此外,命名空间应与文件目录结构相对应。因此,命名空间 E1 中的类 A 应位于名为 E1/A.php 的文件中。这虽非强制要求,但同样地,若未这样做,NetBeans 会发出警告。 在类 [\Exemples\Personne] 的示例中,NetBeans 会发出警告,因为 [Personne.php] 的文件路径是 [exemples/classes/Personne.php],因此与命名空间不匹配。 请勿将文件路径与命名空间混淆。类的完全限定名使用命名空间,与该类 PHP 文件的文件路径毫无关系。文件路径与命名空间的关系是可选的,可以忽略,正如我们在此处所做的那样;
- 第 12–14 行:类的三个私有属性;
- 第 29–37 行:初始化 first_name 属性并验证初始化值;
- 第 31 行:trim($string) 函数用于去除 $string 字符串首尾的空格。因此,trim("abcd") 返回字符串 "abcd",而 trim(" ") 返回空字符串;
- 第 32 行:如果名字为空,则抛出异常(第 33 行);否则,将名字存储起来(第 35 行)。为了抛出异常,我们在此使用了预定义类 [Exception]。 在此处,我们必须使用其绝对名称 [\Exception]。如果使用其相对名称 [Exception],则会在当前命名空间(即 Person 类的 [Examples] 命名空间)中搜索该类。因此,PHP 解释器将尝试查找绝对名称为 [\Examples\Exception] 的类,而该类并不存在;
以下脚本 [classes-05.php] 使用了 [Person] 类:
<?php
// strict adherence to function parameter types
declare(strict_types=1);
// including definition of the Person class
require_once __DIR__."/Personne.php";
// qualified name of the Person class
use \Exemples\Personne;
// test
// creation of a Personne object
$p = new Personne("Paul", "Langevin", 48);
// identity of this person
print "Exemple1, personne=$p\n";
// creation of an erroneous Person object
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";
}
// creation of an erroneous Person object
try {
$p = new Personne("", "yy", 10);
} catch (\Exception $e1) {
print "Exemple3, erreur : " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
print "Exemple3, erreur : " . $e2->getMessage() . "\n";
}
// end
exit;
评论
- 第 7 行:脚本将使用 [Person] 类。因此,我们必须告诉 PHP 解释器在哪里可以找到该类的定义。这就是 [include file] 和 [require file] 语句的作用。这里我们使用了 [include] 语句。 这两个语句的区别如下:如果 [include file] 语句在加载 [file] 时遇到错误,会触发 [E_WARNING] 警告但执行继续;而在相同情况下,[require] 会引发致命错误并停止脚本执行。这两个语句各有其变体:[include_once] 和 [require_once]。 这两个变体允许您处理同一文件被多次包含的情况。假设有一个由多个 PHP 脚本组成的项目,其中部分脚本引用了 [Person] 类。这些脚本的执行将导致 [Person.php] 文件被多次包含,从而引发错误,因为类不能被定义两次。解决方案是使用 [_once] 变体,它们可确保该文件在项目整体脚本中仅被包含一次;
- 第 7 行:常量 [__DIR__] 是 PHP 常量,它指向包含该脚本的目录的完整路径。因此,第 17 行中的表达式:
require_once __DIR__."/Personne.php";
将等同于如下形式:
require_once ‘C:\Data\st-2019\dev\php7\php5-exemples\exemples\classes/Personnes.php’
在文件路径中,您可以使用 / 或 \;
- 第 14 行:我们使用了刚刚定义的 [Person] 类。[classes-05.php] 脚本中没有命名空间。 第 14 行使用了 [Person] 类的相对名称,且未指定命名空间。由于 [Person] 类没有命名空间,系统会在 [classes-05.php] 脚本内部进行搜索,因此无法找到该类。此问题有两种解决方法:
- 使用完整类名 [\Examples\Person];
- 在第 10 行使用 `use` 语句。这表明后续代码将使用类 `[Examples\Person]`;
- 第 10 行:`use` 语句告知解释器,第 14 行引用的 [Person] 类实际上是 [\Examples\Person] 类。那么,解释器将从何处查找该类的代码?第 7 行给出了答案。该行表明,要执行当前脚本,必须同时加载 [Person.php] 脚本。 此处使用了相对文件名。因此,系统将在包含脚本 [classes-05.php] 的文件夹中进行搜索。因此,脚本 [Person.php] 和 [classes-05.php] 必须位于同一文件夹中。本例中正是如此,它们都位于 [examples/classes] 文件夹中。第 10 行的语句等同于:
use \Exemples\Personne as Personne;
上方的 [use] 语句指定别名 [Person] 指向类 [\Exemples\Personne];
- 第 14 行:创建了一个 [Person] 对象。此处将隐式执行 [Person] 类的 [__construct] 方法;
- 第 16 行:显示 Person $p。为了显示,变量 $p 的值必须转换为字符串。此时会隐式执行 [Person.__toString] 方法。因此,该方法必须返回一个字符串;
- 我们已经看到,[Person] 类的构造函数可能会抛出类型为 [\Exception] 的异常。因此,必须对该异常进行处理。由此可见,第 14 行的代码是不完整的。 我们必须使用第 18–24 行的代码来正确处理可能发生的异常。在此,我们通过传入一个非整数的年龄值来故意触发一个异常。在这种特定情况下,生成的异常————是由 PHP 解释器抛出的,而非由 [Person] 类的代码抛出。事实上,[Person.__construct] 方法的签名如下:
function __construct(string $prenom, string $nom, int $age)
因此,传递给构造函数的 [age] 参数必须是整数类型。如果不符合此要求,PHP 解释器将抛出 [TypeError]。 此外,[Person] 类的 [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] 也是如此。在 [Person.setFirstName] 方法中,我们写道:
public function setPrenom(string $prénom) {
// le prénom doit être non vide
$prénom = trim($prénom);
if ($prénom === "") {
throw new \Exception("Le prénom doit être non vide");
} else {
$this->prenom = $prénom;
}
}
在第 5 行,抛出一个异常,错误信息为 [名字不能为空]。这是 [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 中,无法拥有多个具有不同参数的构造函数,从而无法通过多种方式创建对象。因此,我们可以使用充当构造函数的方法:
// méthode
public function initWithPersonne(Personne $p) {
// initialise l'objet courant avec une personne $p
$this->__construct($p->prenom, $p->nom, $p->age);
}
注释
- 第 2-5 行:initWithPerson 方法允许将另一个 Person 对象的属性值赋给当前对象。此处它调用了 __construct 构造函数,但这并非强制要求。它也可以直接初始化 [Person] 类本身的属性;
新的 [Person] 类被以下 [classes-06.php] 脚本所使用:
<?php
// including definition of the Person class
require_once __DIR__."/Personne.php";
// declaration of the qualified name of the Personne class
use \Exemples\Personne;
// test
// creation of a Personne object
try {
$p = new Personne("Paul", "Langevin", 48);
} catch (\Exception $e) {
print "erreur : " . $e->getMessage();
exit;
}
// identity of this person
print "personne=$p\n";
// creation of a second person
try {
$p2 = new Personne("Laure", "Adeline", 67);
} catch (\Exception $e) {
print "erreur : " . $e->getMessage();
exit;
}
// initialization of the first with the values of the second
try {
$p->initWithPersonne($p2);
} catch (\Exception $e) {
print "erreur : " . $e->getMessage();
exit;
}
// check
print "personne=$p\n";
// end
exit;
- 第 14、23、30 行:在控制台脚本中,如果遇到无法恢复的错误,通常需要停止脚本的执行。但在 Web 脚本中情况不同:我们不会停止脚本的执行,而是显示一个错误页面。 如果我们处于函数内部,则不使用 exit 语句,而是使用 return:我们不停止脚本的执行(exit),而是在设置错误后退出函数(return);
结果:
5.8. [Person] 对象数组
以下示例 [classes-07.php] 展示了您可以创建对象数组。
<?php
require_once __DIR__."/Personne.php";
use \Exemples\Personne;
// test
// create an array of Personne objects
// to make the code easier to understand, we don't handle the possible exception
$groupe = [new Personne("Paul", "Langevin", 48), new Personne("Sylvie", "Lefur", 70)];
// identity of these persons
for ($i = 0; $i < count($groupe); $i++) {
print "groupe[$i]=$groupe[$i]\n";
}
// end
exit;
结果:
注释
- 第 9 行:创建一个包含 2 人的数组;
- 第 12 行:遍历数组;
- 第13行:$group[$i] 是一个 Person 类型的对象。使用 [Person.__toString] 方法来显示它;
5.9. 创建一个继承自 Person 类的子类
我们在名为 [Teacher.php] 的文件中创建以下 [Teacher] 类:
<?php
// strict adherence to declared function parameter types
declare (strict_types=1);
// namespace
namespace Exemples;
// a class derived from personne
class Enseignant extends Personne {
// attributes
private $discipline; // subject taught
// getter and setter
public function getDiscipline(): string {
return $this->discipline;
}
public function setDiscipline(string $discipline): void {
$this->discipline = $discipline;
}
// manufacturer
public function __construct(string $prénom, string $nom, int $âge, string $discipline) {
// parent attributes
parent::__construct($prénom, $nom, $âge);
// other attributes
$this->setDiscipline($discipline);
}
// overloading the __toString function of the parent class
public function __toString(): string {
return "[" . parent::__toString() . ",$this->discipline]";
}
}
评论
- 第 7 行:[Teacher] 类也属于 [Examples] 命名空间;
- 第 10 行:Teacher 类继承自 Person 类。派生类 Teacher 继承了其父类的属性和方法;
- 第 12 行:Teacher 类添加了一个新的属性 subject,该属性是该类特有的;
- 第 25 行:Teacher 类的构造函数接受 4 个参数:
- 其中 3 个用于初始化其父类(first_name、last_name、age),第 27 行;
- 1 个用于自身初始化(subject),第 29 行;
- 第 27 行:派生类可以通过关键字 parent:: 访问其父类的的方法和构造函数。在此,我们将参数(first_name、last_name、age)传递给父类的构造函数;
- 第 33–35 行:派生类的 __toString 方法调用了父类的 __toString 方法;
[Teacher] 类在以下 [classes-08.php] 脚本中被使用:
<?php
// inclusion of the definition of the two classes
require_once __DIR__."/Personne.php";
require_once __DIR__."/Enseignant.php";
// declaration of the two classes used
use \Exemples\Personne;
use \Exemples\Enseignant;
// test
// creation of an array of Personne and derived objects
// for the simplicity of the example, we don't handle exceptions
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70));
// identity of these persons
for ($i = 0; $i < count($groupe); $i++) {
print "groupe[$i]=$groupe[$i]\n";
}
// end
exit;
注释
- 第 4-5 行:我们需要告诉 PHP 解释器这两个类 [Teacher, Person] 的位置;
- 第7-8行:声明这两个类的全名。这样我们就可以在代码中直接通过类名来引用它们,而无需添加命名空间后缀;
- 第13行:我们创建一个包含[Person]类型和[Teacher]类型的数组;
- 第 16-18 行:显示数组的元素;
- 第 17 行:将调用每个元素 $group[$i] 的 __toString 方法。Person 类有一个 __toString 方法。Teacher 类有两个:父类的和它自己的。 有人可能会疑惑究竟会调用哪个方法。执行结果显示,最终调用的是来自 Teacher 类的方法。这种情况总是如此:当在对象上调用方法时,系统会按以下顺序进行搜索:首先在对象本身中查找,如果该对象有父类则在其父类中查找,然后在父类的父类中查找,以此类推……一旦找到该方法,搜索即刻停止。
结果:
5.10. 创建一个从 Person 类派生的第二个类
以下示例在 [Student.php] 文件中创建了一个从 Person 类派生的 Student 类:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Exemples;
class Etudiant extends Personne {
// attributes
private $formation; // training pursued
// getter and setter
public function getFormation(): string {
return $this->formation;
}
public function setFormation(string $formation): void {
$this->formation = $formation;
}
// manufacturer
public function __construct(string $prénom, string $nom, int $âge, string $formation) {
// parent attributes
parent::__construct($prénom, $nom, $âge);
// other attributes
$this->setFormation($formation);
}
// overloading the __toString function of the parent class
public function __toString(): string {
return "[" . parent::__toString() . ",$this->formation]]";
}
}
以下脚本 [classes-09.php] 使用了该类:
<?php
// inclusion and definition of classes used by the script
require_once __DIR__."/Personne.php";
use \Exemples\Personne;
require_once __DIR__."/Enseignant.php";
use \Exemples\Enseignant;
require_once __DIR__."/Etudiant.php";
use \Exemples\Etudiant;
// test
// creation of a table of person objects and derivatives
// to make the example easier to understand, we don't handle exceptions
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70), new Etudiant("Steve", "Boer", 23, "iup2 qualité"));
// identity of these persons
for ($i = 0; $i < count($groupe); $i++) {
print "groupe[$i]=$groupe[$i]\n";
}
// end
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 {
// manufacturer
public function __construct() {
print "constructeur de la classe Classe1\n";
}
}
class Classe2 extends Classe1 {
// manufacturer
public function __construct() {
// the constructor of the parent class is not called implicitly
print "constructeur de la classe Classe2\n";
}
}
class Classe3 extends Classe1 {
// manufacturer
public function __construct() {
// explicit call to parent class constructor
parent::__construct();
// code specific to Classe3
print "constructeur de la classe Classe3\n";
}
}
// tests
print "test1---------\n";
new Classe2();
print "test2---------\n";
new Classe3();
结果
5.12. 重写父类的方法
我们已经看到,子类可以重写父类的某个方法。因此,[Person] 类(参见链接)的 [__toString] 方法已在子类 [Teacher](参见链接)和 [Student](参见链接)中被重写。脚本 [classes-13.php] 再次演示了这一概念:
<?php
// strict adherence to function parameter types
declare(strict_types=1);
// main class
class Classe1 {
public function f(): int {
return 1;
}
function g(): int {
return 2;
}
}
// derived class
class Classe2 extends Classe1 {
// redefine the f function of the parent class
public function f(): int {
return parent::f() + 10;
}
}
// code
$c2 = new Classe2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c1=new Classe1();
print $c1->f()."\n";
注释
- 第 7–17 行:类 [Class1] 定义了两个方法 f 和 g;
- 第 20–27 行:类 [Class2] 继承自类 [Class1] 并重写了其 f 方法;
结果
评论
- 代码第 30 行创建了一个类型为 [Class2] 的对象 $c2;
- 代码第 31 行调用了 $c2 对象的 f 方法。由于该方法存在,因此被执行;
- 代码第 32 行调用了 $c2 对象的 g 方法。由于该方法不存在,因此会在父类中查找,并在找到后执行;
- 代码第 33 行创建了一个类型为 [Class1] 的对象 $c1;
- 代码第 34 行调用了 $c1 对象的 f 方法。由于该方法存在,因此被执行;
5.13. 将对象作为函数参数传递
请看以下脚本 [classes-14.php]:
<?php
// strict adherence to function parameter types
declare(strict_types=1);
// main class
class Classe1 {
public function f(): int {
return 1;
}
function g(): int {
return 2;
}
}
// derived class
class Classe2 extends Classe1 {
// redefine the f function of the parent class
public function f(): int {
return parent::f() + 10;
}
}
// the function parameter is of type Class1 or derived
function doSomething(Classe1 $c1): void {
print $c1->f() + $c1->g() . "\n";
}
// code
// create a Class2 object derived from Class1
$c2 = new Classe2();
// we call doSomething with
doSomething($c2);
注释
- 第 7–17 行:[Class1] 类;
- 第 20–27 行:从 [Class1] 派生的 [Class2] 类;
- 第 30 行:一个期望参数类型为 [Class1] 的函数。当期望类型为类时,实际参数可以是期望类型的对象,也可以是派生类型的对象;
- 第 35–38 行:函数 [doSomething] 被调用时传入了一个类型为 [Class2] 的参数,尽管预期类型是 [Class1];
结果
5.14. 抽象类
抽象类是一种无法实例化的不完整类。必须从其派生才能使用。
抽象类有何用途?有时我们会遇到一些类,它们共享一个或多个方法,但在其他方法或属性上有所不同。此时,将所有共同部分归入一个父类中是可取的。目前,我们并不需要抽象类。但假设子类之间仅在方法 M 上有所不同:该方法在所有子类中的签名相同,但实现不同。若要求子类实现方法 M:
- 我们将在父类中声明方法 M 的签名。由于父类不知道如何实现该方法,因此我们在方法名前添加了 abstract 关键字:这意味着方法 M 的实现将推迟到子类中完成;
- 由于父类尚未完全实现,因此也使用相同的 abstract 关键字将其声明为抽象类。这意味着该类无法再被实例化。必须创建一个子类来定义方法 M 的实现,这样才能使用父类的类体;
以下是一个示例 [classes-15.php]:
<?php
// strict adherence to function parameter types
declare(strict_types=1);
// abstract main class
abstract Class Classe1 {
// method known to all derived classes
public function f(): int {
return 1;
}
// abstract g method - will be defined by derived classes
abstract function g(): int;
}
// derived class
Class Classe2 extends Classe1 {
// the g method of the parent class must be defined
public function g(): int {
return parent::f() + 10;
}
}
// derived class
Class Classe3 extends Classe1 {
// the g method of the parent class must be defined
public function g(): int {
return parent::f() + 20;
}
// we can redefine the f method of the parent class
public function f(): int {
return 2;
}
}
// code
$c2 = new Classe2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c3 = new Classe3();
print $c3->f() . "\n";
print $c3->g() . "\n";
注释
- 第 7–16 行:[Class1] 类是抽象类(第 7 行),因为它不知道如何实现第 15 行中的 g 方法。因此,必须通过继承才能使用它;
- 第 19–26 行:类 [Class2] 继承自类 [Class1],并重定义了父类的 g 方法(第 22–24 行);
- 第 29–41 行:类 [Class3] 继承自类 [Class1],并重写了父类的 g 方法(第 32–34 行);
- 第 37–39 行:类 [Class3] 重定义了其父类的 f 方法;
- 第 44–49 行:创建了两个类型分别为 [Class2] 和 [Class3] 的对象,并调用了它们的 f 和 g 方法;
结果
5.15. 终结类
最终类是指无法被派生的类。请看以下脚本 [classes-11.php]:
<?php
// namespace
namespace Exemples;
// non-derivable class
final Class Classe1 {
}
// derived class
Class Classe2 extends Classe1 {
}
// code - must cause an error
new Classe2();
注释
- 第 7–9 行:final 关键字使 [Class1] 类成为一个 final 类,无法被继承;
- 第 12–14 行:类 [Class2] 继承自 final 类 [Class1],这属于错误;
- 第 17 行:该错误仅在脚本执行时尝试操作 [Class2] 类型的对象时才会被报告;
结果
5.16. final 方法
final 方法是指无法被子类重写的方法。以下是一个示例 [classes-12.php]:
<?php
// strict adherence to function parameter types
declare(strict_types=1);
// namespace
namespace Exemples;
// main class
Class Classe1 {
// this method cannot be redefined in a derived class
public final function f(): int {
return 1;
}
}
// derived class
Class Classe2 extends Classe1 {
public function f(): int {
return 2;
}
}
// code - must cause an error
new Classe2();
注释
- 第 13 行:类 [Class1] 的 f 方法使用 final 关键字声明为 final;
- 第 20 行:类 [Class2] 继承自类 [Class1];
- 第 22-23 行:父类 [Class1] 的 f 函数被重新定义。这应该会引发错误;
- 第 29 行:创建了一个 [Class2] 类型的对象,以迫使 PHP 解释器检查 [Class2] 类;
结果
5.17. 静态方法和属性
静态方法是与定义它的类相关联的方法,而非与类的实例对象相关联。因此,如果类 C 声明了一个静态方法 M,那么在类外部使用它时,我们应写为:
- C::M(若在类外部);
- 若在类内部,则写 self::M;
以下是一个示例 [classes-17.php]:
<?php
class Classe1 {
// static method
static function say(string $message): void {
print "$message\n";
}
}
// test -------------------
Classe1::say("hello");
注释
- 第 6 行:使用 static 关键字将 [say] 方法声明为静态方法;
- 第 13 行:使用语法 Class1::say 调用静态方法 [say];
结果
现在请看以下代码 [classes-18.php]:
<?php
class Classe1 {
// static attribute
private static $nbObjects = 0;
public function __construct() {
print "constructeur Classe1\n";
self::$nbObjects++;
}
// static method
static function say(): void {
print self::$nbObjects ." objets de type [Classe1] ont été construits\n";
}
}
// test -------------------
new Classe1();
new Classe1();
Classe1::say();
注释
- 第 5 行:我们声明了一个静态属性,用于统计已创建的 [Class1] 类实例的数量。这不是一个可以属于该类实例的属性。事实上,如果创建了两个对象 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 {
// attribute
private $attributeOfParent = 4;
// method
public function doTest(): void {
// who's calling?
print "parent :\n";
var_dump($this);
// parent display
print "parent : attributeOfParent={$this->attributeOfParent}\n";
print "parent : attributeOfChild={$this->attributeOfChild}\n";
}
}
class SomeChild extends SomeParent {
// attribute
private $attributeOfChild = 14;
// method
public function doTest(): void {
// children's display
print "child : attributeOfParent={$this->attributeOfParent}\n";
print "child : attributeOfChild={$this->attributeOfChild}\n";
// parent
parent::doTest();
}
}
// main script
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 行:继承自类 [SomeParent] 的子类 [SomeChild](第 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 {
// attribute
protected $attributeOfParent = 4;
// method
public function doTest(): void {
// who's calling?
print "parent :\n";
var_dump($this);
// parent display
print "parent : attributeOfParent={$this->attributeOfParent}\n";
print "parent : attributeOfChild={$this->attributeOfChild}\n";
}
}
class SomeChild extends SomeParent {
// attribute
private $attributeOfChild = 14;
// method
public function doTest(): void {
// children's display
print "child : attributeOfParent={$this->attributeOfParent}\n";
print "child : attributeOfChild={$this->attributeOfChild}\n";
// parent
parent::doTest();
}
}
// main script
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 {
// attribute
protected $attributeOfParent = 4;
// method
public function doTest(): void {
// who's calling?
print "parent :\n";
var_dump($this);
// parent display
print "parent : attributeOfParent={$this->attributeOfParent}\n";
print "parent : attributeOfChild={$this->attributeOfChild}\n";
}
}
class SomeChild extends SomeParent {
// attribute
protected $attributeOfChild = 14;
// method
public function doTest(): void {
// children's display
print "child : attributeOfParent={$this->attributeOfParent}\n";
print "child : attributeOfChild={$this->attributeOfChild}\n";
// parent
parent::doTest();
}
}
// main script
print "---------------------------test1\n";
(new SomeParent())->doTest();
print "---------------------------test2\n";
(new SomeChild())->doTest();
执行结果如下:
- 第 22 行:这次,在父类内部,类型为 [SomeChild] 的 [$this](第 15–20 行)看到了其自身类 [SomeChild] 的受保护属性 [$attributeOfChild]。
我们能从这些测试中得到什么启示?
- 父类方法中使用的父类 [$this] 实例可以看到:
- 父类的属性和方法,无论它们的可见性如何;
- 无法访问其子类的任何属性和方法;
这是预期的行为。
- 子类方法中使用的 [$this] 实例可以看到:
- 父类的属性和方法(只要它们的可见性至少为 [protected])。可见性为 [private] 的则不可见;
- 子类的属性和方法,无论其可见性如何;
这是预期的行为。
- 在父类的某个方法中,子类的 [$this] 实例可以看到:
- 父类的属性和方法,无论其可见性如何;
- 仅能访问自身类的属性与方法,且这些属性与方法的可见性必须至少为 [protected]。可见性为 [private] 的属性与方法不可见;
这是意料之外的行为。
5.19. 类的 JSON 编码
在类中,通常会存在 [__toString] 方法:它应返回一个字符串,该字符串代表调用该方法的对象。人们可能会希望这个字符串是一个 JSON 字符串。我们将现在探讨这一路径。

我们将使用以下 [Person] 类:
<?php
class Personne {
// attributes
private $nom;
private $prénom;
private $âge;
private $enfants;
// setter global
public function setFromArray(array $arrayOfAttributes): Personne {
// initialization of certain class attributes
foreach ($arrayOfAttributes as $attribute => $value) {
$this->$attribute = $value;
}
// object is returned
return $this;
}
// getters
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 {
// we identify the object
var_dump($this);
// retrieve its attributes
$attributes = \get_object_vars($this);
var_dump($attributes);
// render the jSON attribute string
return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
}
}
评论
- 第 5–8 行:类的四个属性;
- 第20–35行:用于获取这些属性值的getter方法;
- 第 11–18 行:一个全局设置器,用于从关联数组 [$arrayOfAttributes] 中初始化属性,该数组的键与类属性匹配;
- 第 38–46 行:类的 [__toString] 方法;
- 第 42 行:PHP 函数 [get_object_vars] 将类属性的值作为关联数组 [‘name’=>’name1’, ‘first_name’=>’first_name1’, ‘age’=>’age1’, ‘children’=>[]] 返回;
- 第 45 行:返回该属性数组的 JSON 字符串;
让我们来查看使用 [Person] 类的脚本 [json-01.php]:
<?php
// person class
require "Personne.php";
// father instantiation
$père = new Personne();
// father initialization
$père->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Dieudonné",
"âge" => 58
]);
// instantiation and initialization child1
$enfant1 = (new Personne())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Sylvain",
"âge" => 17
]);
// instantiation and initialization child2
$enfant2 = (new Personne())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Géraldine",
"âge" => 12
]);
// father's children initialization
$père->setFromArray([
"enfants" => [$enfant1, $enfant2]
]);
// display father elements
$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 行:我们使用 [Person::setFromArray] 方法初始化一个 [Person] 对象 [$father],该方法允许我们使用一个数组来初始化 [Person] 对象,该数组的键与 [Person] 类的属性相匹配;
- 第 14–19 行:我们以相同的方式初始化 [Person] 对象 [$child1];
- 第 21–25 行:我们初始化一个 [Person] 对象 [$child2];
- 第 27–29 行:将 [$father→children] 属性初始化为包含两个孩子的数组;
- 第 32–33 行:我们将两个孩子分配给父亲;
- 第 35 行:[print] 操作尝试将 [$child1] 对象转换为字符串。为此,它使用了该对象的 [__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 行:对象 [$child1];
- 第 12–21 行:对象 [$child1] 的属性数组。我们已经拥有了所有属性;
- 第 22 行:我们获得了对象 [$child1] 的 JSON 字符串;
- 第 23–44 行:对象 [$child2] 也是如此;
- 第 45–112 行:对于父对象,情况略有不同,因为其 [children] 属性并非 NULL,而子对象的该属性是 NULL;
- 第 112 行:我们看到父亲的 JSON 字符串中缺少子节点;
- 第 79–111 行:我们看到在父节点的属性数组中,子节点 1(第 89–98 行)仍是一个对象,子节点 2(第 99–110 行)也是如此。 简而言之,表达式 [\get_object_vars($this)](其中 [$this] 代表父对象)并非递归的:如果 [Person] 类的某个属性本身是一个对象,则表达式 [\get_object_vars($this)] 不会尝试获取该属性的属性数组;
我们可以改进这一点。我们将 [Person] 类修改为以下 [Person2] 类:
<?php
class Personne2 {
// attributes
private $nom;
private $prénom;
private $âge;
private $enfants;
// setter global
public function setFromArray(array $arrayOfAttributes): Personne2 {
…
// object is returned
return $this;
}
// getters
public function getNom() {
return $this->nom;
}
…
// __toString
public function __toString(): string {
// retrieve the object's attributes
$attributes = $this->getAttributes($this);
$enfants = $attributes["enfants"];
if ($enfants != NULL) {
$attributes["enfants"] = [$enfants[0]->getAttributes(), $enfants[1]->getAttributes()];
}
// render the JSON attribute string
return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
}
public function getAttributes(): array {
return \get_object_vars($this);
}
}
注释
- 第 36–38 行:[getAttributes] 函数返回调用它的对象的属性数组;
- 第 25–34 行:[__toString] 函数;
- 第 27 行:我们将 [Person] 类的属性提取到 [$attributes] 数组中;
- 第 28 行:根据前面的示例,我们知道 [$attributes["children"]] 是一个包含两个 [Person] 类型对象的数组;
- 第 29–31 行:将这两个对象替换为它们的属性数组;
- 第 33 行:剩下的就是将构建好的属性数组编码为 JSON;
脚本 [json-02.php] 如下所示使用 [Person2] 类:
<?php
// person2 class
require "Personne2.php";
// father instantiation
$père = new Personne2();
// initialization
$père->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Dieudonné",
"âge" => 58
]);
// instantiation and initialization child1
$enfant1 = (new Personne2())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Sylvain",
"âge" => 17
]);
// instantiation and initialization child2
$enfant2 = (new Personne2())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Géraldine",
"âge" => 12
]);
// father's children initialization
$père->setFromArray([
"enfants" => [$enfant1, $enfant2]
]);
// father display
print "------------------------père\n";
print "père=$père\n";
脚本 [json-02.php] 与脚本 [json-01.php] 完全相同,只是类 [Person2] 取代了类 [Person]。
执行结果如下:
这次,我们成功地获取了父亲及其子女的信息。
之前的解决方案并不理想,因为这些子女可能又有自己的子女。这样一来,我们又遇到了之前的问题。
[Person3] 类通过以下方式解决了这个问题:
<?php
class Personne3 {
// attributes
private $nom;
private $prénom;
private $âge;
private $enfants;
// setter global
public function setFromArray(array $arrayOfAttributes): Personne3 {
…
}
// getters
…
// __toString
public function __toString(): string {
// render the JSON attribute string
$attributes = [];
$this->getRecursiveAttributes($attributes, $this, []);
// string JSON of attributes
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 analysis [$value]
// $keys is an array [key1, key2, .., keyn]
// $value=$attributes[key1][key2]….[keyn]
// if [$value] is an object, we use its method [getAttributes]
if (\is_object($value)) {
// attributs de l'objet [$value]
$objectAttributes = $value->getAttributes();
// what do we do with the result?
if ($keys) {
// in [$attributes], we replace $value with the array of its attributes
// element $attributes[key1][key2]...[keyn] must be constructed
// where $keys is the array [key1, key2, .., keyn]
// we take the table reference [$attributes]
$attribute = &$attributes;
// scan the key table
foreach ($keys as $key) {
// we take the reference of the
$attribute = &$attribute[$key];
}
// here $attribut and $attributes[key1][key2]...[key(n)] are identical
// they share the same memory location
// object [$value] is replaced by its array of attributes;
// write $attributes[key1][key2]...[keyn]=$objectAttributes
// which is equivalent to $attribute = $objectAttributes
$attribute = $objectAttributes;
} else {
// no keys - we're just beginning to explore the object
// $objectAttributes represents the 1st level attributes of the class
$attributes += $objectAttributes;
}
// maybe in [$objectAttributes] there are still objects
// explore [$objectAttributes] attributes
$this->getRecursiveAttributes($attributes, $objectAttributes, $keys);
} else {
if (\is_array($value)) {
// we have a table - we analyze each of its elements
foreach ($value as $key => $élément) {
// add the current key to the $keys array
\array_push($keys, $key);
// we analyze $élément
$this->getRecursiveAttributes($attributes, $élément, $keys);
// remove the key just analyzed from the $keys array
\array_pop($keys);
}
}
}
}
评论
- 第 21–22 行:这次,[__toString] 方法获取其类的属性,并要求以递归方式完成此操作:如果某个属性是一个对象或对象数组,则必须将每个对象替换为该对象在类最终属性数组中的属性数组;
- 第 31–78 行:[getRecursiveAttributes] 函数负责执行此操作。我们已在代码中添加了注释。编写递归函数通常较为复杂,此处亦是如此。读者即使不理解这些内容,也不会遗漏任何关键信息。已有现成的库可处理此类任务。递归调用分别出现在第 64 行和第 72 行;
- 这段代码的优势在于,它并非专为 [Person3] 类编写。只要主类所使用的子类也像它一样在第 27–29 行拥有 [getAttributes] 方法,该代码就适用于任何属性值类型各异的类。
脚本 [json-03.php] 如下所示使用 [Person3] 类:
<?php
// person3 class
require "Personne3.php";
// father instantiation
$père = new Personne3();
// initialization
$père->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Dieudonné",
"âge" => 58
]);
// instantiation and initialization child1
$enfant1 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Sylvain",
"âge" => 27
]);
// instantiation and initialization child2
$enfant2 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Géraldine",
"âge" => 12
]);
// father's children initialization
$père->setFromArray([
"enfants" => [$enfant1, $enfant2]
]);
// instantiation and initialization child11
$enfant11 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Gaëtan",
"âge" => 2
]);
// instantiation and initialization child12
$enfant12 = (new Personne3())->setFromArray([
"nom" => "Bertholomé",
"prénom" => "Mathilde",
"âge" => 1
]);
// initialization children of child1
$enfant1->setFromArray([
"enfants" => [$enfant11, $enfant12]
]);
// father display
print "------------------------père\n";
print "père=$père\n";
- 第30-45行:我们将两个子节点赋值给 [$child1];
执行结果如下:
如果对该结果进行格式化,我们将得到以下内容:
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
}
]
}
我们已成功检索到构成父节点的所有 [Person] 对象的 JSON 字符串。