面向对象系统

类可以有常量、属性、方法、静态属性、静态方法。使用关键字class定义一个类。

<?php
class Myclass {
    public $name = 'a default name'; // 定义一个属性
    public function method1() {} //定义一个方法。没有分号
    private function method2() {}
    public static $my_static = '42'; // 定义一个静态属性
    public static function aStaticMethod() {}
    const constant = "constant value"; // 定义一个类常量
}

无论定义在何处,类名都将具有全局作用域;一个类中的一个属性可以和一个方法同名。

从PHP 5.3起,就像“动态地使用函数”和“动态地使用变量”一样,能够在运行时动态地调用某个类(而不是在代码中硬编码好),例如:

<?php
$classname = "MyClass"; //该变量的值不能为"self","parent" 或 "static"
echo $classname::constant; // 等价于 MyClass::constant

属性与静态属性

  • 属性声明是由关键字 public,protected 或者 private 开头,随后跟一个可选的关键字static以决定是否是静态属性,然后跟一个普通的变量声明(需要$)来组成。属性中的变量可以初始化,但是初始化的值必须是字面量、常量或类常量,因为初始化过程发生在编译期
  • 可以随时为一个实例添加属性,但无法随时为类添加静态属性(但可以修改)
  • 属性需要通过实例访问:实例->属性名进行(不需要$);静态属性挂载在类上,无法通过实例访问,而使用类名::静态属性名进行(需要$
  • 无法声明amda函数作为属性值,但可以随时(包括在构造函数内)为一个属性赋予lamda函数。
  • 注意,当执行形如实例->标识符()的表达式时,解释器不会将标识符解释成属性(哪怕该属性是个lamda函数),在PHP7中可以以(实例->标识符)()的形式调用作为属性的lamda函数

类常量

  • 定义和使用类常量的时候不需要使用 $ 符号。通过类名::类常量名来使用
  • 必须由字面量初始化。从PHP5.6起,可以使用简单的表达式(包括数组字面量)进行初始化,要求表达式中的操作数的值均在编译期就能确定(如字面量、其它常量等),见此文档的example 4
  • PHP7.1起可以为类常量设置访问控制,不设置的话默认为public
  • 自 PHP 5.5 起,每个类可视为拥有(在编译期确定的)常量class。使用 类名::class 可以获取一个字符串,包含了类的完全限定名称(即命名空间前缀 + 类名)

方法与静态方法

  • 方法声明由关键字 public,protected 或者 private 开头(若省略以上关键字,等同于指定为公有),随后跟一个可选的关键字static以决定是否是静态方法,然后跟一个普通的函数声明来组成
  • 在方法中可以使用变量$this表示调用此方法的实例(和JS中的this关键字一样是运行时确定的)。静态方法并不通过实例调用,所以伪变量$this在静态方法中不可用(触发致命错误)
  • 在方法和静态方法中可以使用关键字selfparentstatic来指称类,可用于读取静态属性、静态方法、类常量,以及操作继承链上的同名方法(见下文“继承”)
  • 方法需要通过实例使用:实例->方法名();静态方法使用类名::静态方法名()或者实例::静态方法名()。PHP5中若以非静态形式调用一个静态方法(即实例->静态方法名()),会触发 E_STRICT级别的错误
  • 类的方法可以正常使用《PHP中的变量与函数详解》记载的全部与函数有关的语言功能

$this详解

无论方法是否来自继承链,当其被调用时,$this总是指向当前实例。但欲使用$this上的属性/方法(以及静态方法——静态方法在写法上也可以通过实例调用)时,解释器会优先将$this->标识符解释成“在该$this所属的方法被定义的那个类中的、使用此标识符的私有(private)属性/方法/静态方法”。

$this出现在一个lamda函数(Closure类的实例)内时的情况,可见《PHP中的变量与函数详解》一文中的“Closure类的方法”一节。

parentselfstatic详解

方法中关键字self/parent的所指是在编译期确定的,这意味着当使用了这些关键字的方法被继承,并在子类的实例上被调用时,这些关键字的所指并非子类/子类的父类,而是父类/父类的父类。

为了将所指推迟到运行时确定,从PHP 5.3起,可以使用关键字static代替self,也就是说,可以将关键字static看作关键字self的动态版,即在运行时确定,总是指代实例所属的类。

魔术方法

魔术方法是由双下划线打头的方法/静态方法(且通常是 public 的),是由程序员根据约定定义、由解释器负责根据约定调用的方法。通过定义各魔术方法来定义一个类的实例的各种行为。程序员命名自己的方法时,不应当使用双下划线开头。

构造函数

构造函数的名字是__construct,具有构造函数的类会在每次创建新对象时自动地先调用此方法,所以非常适合在使用对象之前做一些初始化工作。 如果子类中定义了构造函数则不会隐式调用其父类的构造函数。要执行父类的构造函数,需要在子类的构造函数中调用parent::__construct()。如果子类没有定义构造函数则会如同一个普通的类方法一样从父类继承(假如没有被定义为 private 的话)。 与其它方法不同,当 __construct() 被与父类 _construct() 具有不同参数的方法覆盖时,PHP 不会产生一个 ESTRICT 错误信息。

为了实现向后兼容性,如果 PHP 5 在类中找不到 __construct() 函数并且也没有从父类继承一个的话,它就会尝试寻找旧式的构造函数,也就是和类同名的函数。 自 PHP 5.3.3 起,在命名空间中,与类名同名的方法不再作为构造函数。这一改变不影响不在命名空间中的类。

在PHP5中,如果内部类的构造器出错,会返回 NULL 或者一个不可用的对象。 从 PHP 7 开始,如果内部类构造器发生错误, 那么会抛出异常。

可以通过把构造器函数设置成 private 来实现单例模式。

析构函数

析构函数的名字是__destruct。析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时自动地执行。 和构造函数一样,父类的析构函数不会被引擎暗中调用。要执行父类的析构函数,必须在子类的析构函数体中显式调用parent::__destruct()。此外也和构造函数一样,子类如果自己没有定义析构函数则会继承父类的。

析构函数即使在使用 exit() 终止脚本运行时也会被调用。在析构函数中调用 exit() 将会中止其余关闭操作的运行。 试图在析构函数(在脚本终止时被调用)中抛出一个异常会导致致命错误。

重载

PHP文档中使用术语“重载”来指代类似Ruby中的method_missing功能。通过实现类的set、get、isset、unset、call、callStatic这些魔术方法,可以控制当“无法访问(未定义或有访问控制不允许)的属性或方法被访问到”时实例的行为。

属性重载

  • __set(属性名, 属性值):当某个无法访问的属性被要求写入时被自动调用。无须返回值
  • __get(属性名):当某个无法访问的属性被要求读取时被自动调用,其返回值作为读取的结果
  • __isset(属性名):当某个无法访问的属性被传入语言结构isset()或函数empty()时被自动调用,其返回值(布尔类型)作为读取的结果
  • __unset(属性名):当某个无法访问的属性被传入语言结构unset时被自动调用。无须返回值

注意: + 以静态属性形式进行尝试是不会触发以上重载的 + 以上方法均不能使用“作为别名的参数”

方法重载

  • __call(方法名, 保存着实参的数组):当某个无法访问的方法被调用时被自动触发,其返回值作为调用的结果
  • __callStatic(静态方法名, 保存着实参的数组):当某个无法访问的静态方法被调用时自动触发,其返回值作为调用的结果。该方法应当作为类的静态方法

对象克隆

使用关键字clone来对一个实例进行克隆(对一个实例的所有属性,包括private属性进行浅复制)。当克隆过程完成后,克隆出的实例的__clone方法将被调用。一般在该方法中对克隆的来的对象的属性进行调整。

序列化与反序列化

通过函数serialize可以将包括object类型在内的任意值序列化为字符串。PHP中的序列化功能的特殊之处在于,序列化后的字符串中保存着类信息,以便之后可以反序列化(通过函数unserialize),重新得到一个相应的类的实例。对一个由类A的实例序列化得到的字符串进行反序列化时,要求类A必须已经定义过,否则会把没有方法的类_PHPIncompleteClassName作为该实例的类。

对实例进行序列化后,默认得到的字符串中包括了实例的类信息以及实例的全部属性(包括private属性)。通过设定类的以下魔术方法,可以改变其实例被序列化和反序列时的行为:

  • __sleep():该方法在被序列化发生之前时调用。通过返回一个包含了“应当被序列化的属性的名字”的数组来指示序列化时应当保存的信息
  • __wakeup():该方法在被反序列化发生之前被调用,用于预先准备对象需要的资源,例如重新建立数据库连接,或执行其它初始化操作

使实例可转换为字符串:__toString

通过实现返回字符串的__toString方法,指示一个实例在发生到字符串的类型转换时的行为以及转换结果。任何能够被转换为字符串的类的实例都实现了该方法。注意:

  • 该方法必须返回字符串,否则触发致命错误
  • 不得在该方法内抛出未经捕获的异常,否则触发致命错误

使实例可调用:__invoke

PHP5.3引入。当一个实例被像函数一样调用时,会触发该魔术方法,向该方法传入实参。

控制var_export的输出:_setstate

http://php.net/manual/en/language.oop5.magic.php#object.set-state

控制var_dump的输出:__debugInfo

从PHP5.6起,当一个实例被传入函数vardump时,该方法会被调用。它返回一个包含了“应当被显示的属性的名字”的数组来指示vardump时应当输出的信息。如果实例的类并未定义该方法,则将一切属性(public、protected、private)输出。

访问控制

对属性、静态属性、方法、静态方法(从7.1起包括类常量)的访问控制,是通过在前面添加关键字public(公有),protected(受保护)或 private(私有)来实现的。

  • 公有:类成员可以在任何地方被访问
  • 受保护:类成员可以被其自身以及其子类和父类访问。否则将产生致命错误
  • 私有:类成员只能被其定义所在的类访问,其子类或父类以及外部均不能访问。否则将产生致命错误

注意:同一个类的对象即使不是同一个实例也可以互相访问对方的私有与受保护成员。

在定义方法时,若省略以上关键字,等同于指定为公有;定义属性时则不能省略,但允许用关键字var代替`public(兼容PHP4),在 PHP 5.1.3 之前的版本,该语法会产生一个 E_STRICT 警告。

实例化

要创建一个类的实例,必须使用new关键字。当创建新对象时该对象总是被赋值,除非该对象定义了构造函数并且在出错时抛出了一个异常。类应在被实例化之前定义。

创建实例的方式有以下两种:

  • new 类名()。若不打算往构造函数里传参,小括号可以省略
  • new 类的实例()。看上去就像“调用一个实例”。若不打算往构造函数里传参,小括号可以省略(PHP 5.3起)

使用运算符instanceof可以检测一个实例和某个类的关系。见《语法、表达式、语句与操作符》一文。

继承

使用形如class 父类名 extends 子类名的形式进行继承,不支持多重继承。

  • 如果一个类继承了另一个,则父类必须在子类之前被声明。此规则适用于类继承其它类与接口
  • 子类继承父类时,会继承父类所有公有的和受保护的方法/静态方法和属性/静态属性。继承而来的方法/静态方法、属性/静态属性均不是自己的,因此应当注意继承而来的方法/静态方法如果试图(通过$thisstatic关键字)调用子类的实例上的私有成员,是不符合访问控制规则的
  • 定义子类时可以覆盖父类的同名方法和属性。若父类定义方法时使用了关键字final,则该方法不可被覆盖。final关键字还能直接出现在关键字class前,等同于将整个类的所有方法使用final进行定义。不支持用final定义属性
  • 当覆盖方法时,参数必须保持一致,否则 PHP 将发出 E_STRICT 级别的错误信息。但构造函数例外,构造函数可在被覆盖时使用不同的参数
  • 在子类的方法中可以使用关键字selfparent用来分别指代方法所属的类、方法所属类的父类
  • 若子类覆盖了父类的同名方法,但又需要调用父类的同名方法,则使用操作符::,例如:parent::someMethods()

预定义类

这个页面列出了PHP中预定义的类。其中有些是抽象类。

  • Director:由函数dir创建
  • stdClass:当发生到object类型的类型转换时创建
  • _PHPIncomplete_Class:当发生反序列化时可能被创建
  • Exception/ErrorException/PHP7中引入的异常类型
  • Closure:见《PHP中的变量与函数》
  • Genarator:见《命名空间、生成器、错误&异常》
  • 完整列表

抽象类

在方法声明前使用abstract关键字将一个方法定义为抽象方法。被定义为抽象的方法只是声明了其名字和形参(以分号结尾),不能定义其具体的功能实现。任何一个类,如果它里面至少有一个方法是被声明为抽象的,那么这个类就必须被声明为抽象的:在class关键字前使用abstract关键字来定义抽象类。抽象类不能被实例化。

继承一个抽象类的时候,子类必须定义父类中的所有抽象方法;另外,这些方法的访问控制必须和父类中一样(或者更为宽松)。此外方法的调用方式必须匹配,即类型和所需参数数量必须一致。例如,子类定义了一个可选参数,而父类抽象方法的声明里没有,则两者的声明并无冲突。 这也适用于 PHP 5.4 起的构造函数。在 PHP 5.4 之前的构造函数声明可以不一样的。

接口

使用关键字interface代替class以定义一个接口。接口定义中只能定义方法(不给出实现)和类常量:

<?php
interface iTemplate {
    public function setVariable($name, $var);
    public function getHtml($template);
    const a = 42; // 可以在实现该接口的类中覆盖
}

注意:

  • 所有方法必须是public
  • 可以定义构造器

定义类时,可以令类实现一个或多个接口,使用impletents关键字,用逗号分隔各接口名。

  • 实现接口时,类的方法必须使用和接口一样的函数签名,否则会触发致命错误
  • PHP5.3.9之前,一个类不允许实现两个有同名方法的接口;5.3.9之后,只要两个同名方法有相同的函数签名则可以
  • 一个接口可以被另一个接口继承(同样使用extends关键字)

预定义接口

创建类时显式地实现(implements)预定义接口有利于强调代码意图。实现其中的某些接口还能为实例获得语法层面上的功能支持。

  • Traversable接口:一个供内部使用的接口。无法实现它,但可以用它来检测一个实例能否使用foreach进行遍历
  • Iterator接口:实现了该接口的类的实例可以用foreach遍历。见“迭代一个对象”一节
  • IteratorAggregate接口:实现了该接口的类的实例可以用foreach遍历。见“迭代一个对象”一节
  • Throwable接口:PHP7中引入。可通过Throw语句抛出的对象的类都应当实现该接口
  • ArrayAccess接口:实现了该接口的类的实例可以被像数组一样访问
  • Serializable接口:实现此接口的类将不再支持 __sleep() 和 __wakeup(),当被序列化和反序列化时,取而代之的是作为该接口的实现的方法被调用
  • SPL中提供的一些接口

Trait

PHP5.4起添加了语言特性Trait。该特性类似于Ruby中的mixin。使用关键字trait定义一组方法(要给出实现)、属性、静态方法、:

<?php
trait ezcReflectionReturnInfo {
    function getReturnType() { /*1*/ }
    function getReturnDescription() { /*2*/ }
}

定义类时,使用use关键字将某trait中的一组方法混入该类:

class Myclass {
    use ezcReflectionReturnInfo;
    use ...; // 可以混入多个trait
    /* 其它定义,略 */
}

注意:

  • trait中可以定义方法、属性、静态方法,以及它们的访问控制
  • trait中可以定义抽象方法
  • 混入的方法被类视为自己的方法,因此可以随意访问private成员
  • 从trait中混入的方法会覆盖继承而来的同名方法,而子类也可以覆盖由trait混入的方法
  • 如果要在类中定义和混入的trait中同名的属性,则必须指定相同的访问控制和初始值(在PHP7.0之前该行为引发一个E_STRICT级别的错误),否则触发一个致命错误。换句话说,无法覆盖由trait混入的属性
  • 定义trait时,也可以混入其它trait

混入多个trait时,如果这些trait中存在同名方法,则必须使用insteadof关键字显式地说明混入哪一个方法,否则将触发致命错;也可以使用as关键字将某个trait中的重名方法进行重命名:

trait A {
    public function smallTalk() {
        echo 'a';
    }
    public function bigTalk() {
        echo 'A';
    }
}

trait B {
    public function smallTalk() {
        echo 'b';
    }
    public function bigTalk() {
        echo 'B';
    }
}
class Talker {
    use A, B {
        B::smallTalk insteadof A; // 混入B中的smallTalk方法,而不是A中的
        A::bigTalk insteadof B; // 混入A中的bigTalk方法,而不是B中的
        B::bigTalk as talk; // 混入B中的bigTalk方法,但改名为talk
    }
}

当使用as关键字时,可以在新标识符前添加访问控制关键字,以调整混入的方法的访问控制特性。

匿名类

匿名类是一次性的类,在PHP7中引入,见http://php.net/manual/en/language.oop5.anonymous.php

其它

迭代一个对象

在类的内外使用foreach语句时,可迭代到的属性/属性名值对是遵循控制访问规则的。

任何类,通过实现预置的Iterator接口或者IteratorAggregate(仅需实现一个能够返回可迭代类型的实例的方法即可)接口即可迭代。

标准库SPL库中有大量实现了iterator接口的类可供使用。

类的自动加载与PSR-4标准加载器

PHP5起提供了类的自动加载机制,这一机制允许程序员通过使用 splautoloadregister函数注册一系列回调函数(称它们为“自动加载器”,autoloader),令控制流在遇见未知的类名或接口名时,能按照既定方式引入其它文件的代码(回调函数将被逐个调用,被传入类名字符串作为参数)。

如果类的命名与代码文件的组织形式符合PSR-4规范(见《命名空间、生成器、错误&异常》一文),则可以直接注册由composer(根据配置文件)生成的PSR-4自动加载器,从而方便代码在社区中的传播和使用。

与OOP系统有关的函数/反射/操作符

这份列表中列出了大量能够在运行时检测OOP系统信息的函数,SPL中也有一些相关函数。比较常用的有:

  • 操作符instanceof:见《语法、表达式、语句与操作符》一文
  • 函数is_a:操作符instanceof的函数版
  • 函数class_exists:检测一个类是否存在
  • 函数getdeclaredclasses:获得本次程序执行过程中到调用该函数为止所有已定义的类
  • 函数get_class:得到一个实例所属的类的名字
  • 函数getclassmethods:根据类名或一个类的实例,获得该类的所有方法
  • 函数method_exists:根据类名或一个类的实例,检测是否存在某个方法
  • 函数getclassvars:根据类名或一个类的实例,获得该类的所有属性
  • 函数getparentclass:寻找一个类的父类
  • 函数issubclassof:检测继承关系是否成立
  • 函数class_implements:SPL中的函数。用于得到一个类实现的所有接口名
  • 使用函数处理页面中记载的calluserfunc/calluserfunc_array函数可以动态调用类的方法

更多更强大的功能定义在反射模块中。该模块中的常用的类包括:

  • Reflect类:该类上的静态方法export可以导出一个实现了Reflector接口的类的实例内包含的所有信息
  • ReflectionClass类:该类实现了Reflector接口。该类的一个实例代表了某个类的所有信息,具有各种检测类的信息的方法(甚至包括相关源代码在代码文件中的位置等信息)。检测的结果可能以对象的形式返回,例如getMethods方法一个返回ReflectionMethod类的实例,其中有关于某个类的方法的信息
  • ReflectionUtil类:只有一个静态方法getClassSource,接受一个ReflectionClass类的实例,得到一个类的源代码

使用反射功能意味着完全破坏类的封装性。该功能一般用于调试以及框架开发中。