Skip to content

5.

词汇:类(Class)是一种 PHP 类型。该类型的变量称为对象。对象是类的实例(instance)。

5.1. 脚本层次结构

Image

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 行所展示的内容;

Image

  • 第 30 行:表达式 $obj3=&$obj2 导致 $obj2 和 $obj3 具有相同的地址 [下文第 1 点]。可以说这两个变量是同一内存位置的别名。它们都指向一个对象,即下文中的对象 A [2]
    • 操作 $obj2=new stdClass() 创建了一个新对象 Object B [下文第 3 处],并将该新对象的地址赋值给变量 $obj2。由于 $obj2 $obj3 是同一内存位置的两个别名,因此 $obj3 也指向该新对象 Object B。这在结果的第 16–27 行和第 30–41 行中有所体现;

Image

  • 第 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;

结果

personne=[Paul,Langevin,48]

注释

  • 第 3–13 行:定义 Person。类是一种用于创建对象的模板。它包含属性以及称为方法的函数。属性无需声明;
  • 第 8–11 行:identity 方法显示了三个未在类中声明的属性的值。关键字 $this 指代该方法所作用的对象;
  • 第 17 行:我们创建了一个类型为 Person 的对象 $p。关键字 new 用于创建新对象。该操作返回对所创建对象的引用(即地址)。可以使用多种写法:new Person()new Personnew 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;

结果

personne=[Paul,Langevin,48]

注释

  • 第 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;

结果

personne=[Paul,Langevin,48]
personne=[Paul,Langevin,14]

注释

  • 第 6–50 行:Person
  • 第 7–9 行:类的私有属性。这些属性仅在类内部可见。其他可用的关键字包括:
  • public:使属性成为公共属性,可在类外部访问;
  • protected:将属性设为受保护属性,可在类内部及其派生类中访问;
  • 由于这些属性是私有的,因此无法从类外部访问。因此,我们无法编写以下代码:
$p=new Personne() ;
$p->nom="Landru" ;

这里,我们处于 Person 类之外。由于 name 属性是私有的,第 2 行是错误的。要初始化 $p 对象的私有字段,有两种方法:

  • 使用第 12–34 行中的公共 set get 方法(这些方法的名称可以是任意名称)。然后我们可以这样写:
$p=new Personne() ;
$p->setNom("Landru") ;
  • 使用第37至42行的构造函数。那么我们就会写成:
$p=new Personne("Michel","Landru",44) ;

上述代码会自动调用 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);

结果

personne=[Paul,Langevin,48]
personne=[Laure,Adeline,67]

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;

结果

groupe[0]=[Paul,Langevin,48]
groupe[1]=[Sylvie,Lefur,70]

注释

  • 第 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 类的方法。这种情况总是如此:当在对象上调用方法时,系统会按以下顺序进行搜索:首先在对象本身中查找,如果该对象有父类则在其父类中查找,然后在父类的父类中查找,以此类推……一旦找到该方法,搜索即刻停止。

结果

groupe[0]=[[Paul,Langevin,48],anglais]
groupe[1]=[Sylvie,Lefur,70]

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();

结果

1
2
3
4
5
test1---------
constructeur de la classe Classe2
test2---------
constructeur de la classe Classe1
constructeur de la classe 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 方法;

结果

1
2
3
11
2
1

评论

  • 代码第 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]

结果

13

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 方法;

结果

1
2
3
4
1
11
2
21

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] 类型的对象时才会被报告;

结果

Fatal error: Class Exemples\Classe2 may not inherit from final class (Exemples\Classe1) in C:\Data\st-2019\dev\php7\php5-exemples\exemples\classes\classes-11.php on line 14

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] 类;

结果

Fatal error: Cannot override final method Exemples\Classe1::f() in C:\Data\st-2019\dev\php7\php5-exemples\exemples\classes\classes-12.php on line 26

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]

结果

hello

现在请看以下代码 [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] 可见性。但子类的属性呢?它在父类中可见吗?

以下是该首次测试的结果:

---------------------------test1
parent :
object(SomeParent)#1 (1) {
  ["attributeOfParent":"SomeParent":private]=>
  int(4)
}
parent : attributeOfParent=4

Notice: Undefined property: SomeParent::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php on line 14
parent : attributeOfChild=
---------------------------test2

Notice: Undefined property: SomeChild::$attributeOfParent in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php on line 26
child : attributeOfParent=
child : attributeOfChild=14
parent :
object(SomeChild)#1 (2) {
  ["attributeOfChild":"SomeChild":private]=>
  int(14)
  ["attributeOfParent":"SomeParent":private]=>
  int(4)
}
parent : attributeOfParent=4

Fatal error: Uncaught Error: Cannot access private property SomeChild::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php:14
Stack trace:
#0 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php(30): SomeParent->doTest()
#1 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php(39): SomeChild->doTest()
#2 {main}
  thrown in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php on line 14

注释

  • 第 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();

结果

---------------------------test1
parent :
object(SomeParent)#1 (1) {
  ["attributeOfParent":protected]=>
  int(4)
}
parent : attributeOfParent=4

Notice: Undefined property: SomeParent::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php on line 14
parent : attributeOfChild=
---------------------------test2
child : attributeOfParent=4
child : attributeOfChild=14
parent :
object(SomeChild)#1 (2) {
  ["attributeOfChild":"SomeChild":private]=>
  int(14)
  ["attributeOfParent":protected]=>
  int(4)
}
parent : attributeOfParent=4

Fatal error: Uncaught Error: Cannot access private property SomeChild::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php:14
Stack trace:
#0 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php(30): SomeParent->doTest()
#1 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php(39): SomeChild->doTest()
#2 {main}
  thrown in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php on line 14

注释

  • 第 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();

执行结果如下:

---------------------------test1
parent :
object(SomeParent)#1 (1) {
  ["attributeOfParent":protected]=>
  int(4)
}
parent : attributeOfParent=4

Notice: Undefined property: SomeParent::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-21.php on line 14
parent : attributeOfChild=
---------------------------test2
child : attributeOfParent=4
child : attributeOfChild=14
parent :
object(SomeChild)#1 (2) {
  ["attributeOfChild":protected]=>
  int(14)
  ["attributeOfParent":protected]=>
  int(4)
}
parent : attributeOfParent=4
parent : attributeOfChild=14
  • 第 22 行:这次,在父类内部,类型为 [SomeChild] [$this](第 15–20 行)看到了其自身类 [SomeChild] 的受保护属性 [$attributeOfChild]

我们能从这些测试中得到什么启示?

  • 父类方法中使用的父类 [$this] 实例可以看到:
    • 父类的属性和方法,无论它们的可见性如何;
    • 无法访问其子类的任何属性和方法;

这是预期的行为。

  • 子类方法中使用的 [$this] 实例可以看到:
    • 父类的属性和方法(只要它们的可见性至少为 [protected])。可见性为 [private] 的则不可见;
    • 子类的属性和方法,无论其可见性如何;

这是预期的行为。

  • 在父类的某个方法中,子类的 [$this] 实例可以看到:
    • 父类的属性和方法,无论其可见性如何;
    • 仅能访问自身类的属性与方法,且这些属性与方法的可见性必须至少为 [protected]。可见性为 [private] 的属性与方法不可见;

这是意料之外的行为。

5.19. 类的 JSON 编码

在类中,通常会存在 [__toString] 方法:它应返回一个字符串,该字符串代表调用该方法的对象。人们可能会希望这个字符串是一个 JSON 字符串。我们将现在探讨这一路径。

Image

我们将使用以下 [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]

执行结果如下:

------------------------père
père={"nom":"Bertholomé","prénom":"Dieudonné","âge":58,"enfants":[{"nom":"Bertholomé","prénom":"Sylvain","âge":17,"enfants":null},{"nom":"Bertholomé","prénom":"Géraldine","âge":12,"enfants":null}]}

这次,我们成功地获取了父亲及其子女的信息。

之前的解决方案并不理想,因为这些子女可能又有自己的子女。这样一来,我们又遇到了之前的问题。

[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
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}]}

如果对该结果进行格式化,我们将得到以下内容:


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 字符串。