PHP 类与对象

PHP 具有完整的对象模型,特性包括: 访问控制、抽象类和 final 类与方法、魔术方法、接口、对象复制。

PHP 对待对象的方式与引用相同,即每个变量都持有对象的引用,而不是整个对象的拷贝。


基本概念

每个类的定义都以关键字 class 开头,后面跟着类名,再跟着一对花括号,里面包含有类的属性与方法的定义。

一个合法类名以字母下划线开头,后面跟着若干字母、数字或下划线。

<?php
class SimpleClass
{
    // 声明属性
    public $var = 'a default value';

    // 声明方法
    public function displayVar() {
        echo $this->var;
    }
}

当一个方法在类定义内部被调用时,有一个可用的伪变量 $this,$this 指向当前对象。

创建实例

要创建一个类的实例,必须使用 new 关键字。

如果该类属于一个命名空间,则必须使用其完整名称。

<?php
$instance = new SimpleClass();

属性和方法

同一个类的属性和方法可以使用同样的名字,在类中访问属性和调用方法使用操作符 -> 

<?php
class Foo
{
    public $bar = 'property';

    public function bar() {
        return 'method';
    }
}

$obj = new Foo();
echo $obj->bar, PHP_EOL, $obj->bar(), PHP_EOL;

继承

一个类可以用 extends 关键字继承另一个类的方法和属性。

PHP 不支持多重继承,一个类只能继承一个基类。

被继承的方法和属性可以通过用同样的名字重新声明被覆盖,但是如果父类定义方法时使用了 final,则该方法不可被覆盖,可以通过 parent:: 来访问被覆盖的方法或属性。

<?php
class ExtendClass extends SimpleClass
{
    // 同样名称的方法,将会覆盖父类的方法
    function displayVar()
    {
        echo "Extending class\n";
        parent::displayVar();
    }
}

$extended = new ExtendClass();
$extended->displayVar();

::class

关键词 class 也可用于类名的解析,使用 ClassName::class 可以获取一个字符串,包含了类 ClassName 的完整名称。

<?php
namespace NS {
    class ClassName {
    }

    echo ClassName::class;    // 输出 NS\ClassName
}

Nullsafe 方法和属性

自 PHP 8.0.0 起,类属性和方法新增加了一个 "nullsafe" 操作符: ?->。在每次访问前使用 is_null() 函数判断方法和属性是否存在,但更加简洁。

<?php

// 自 PHP 8.0.0 起可用
$result = $repository?->getUser(5)?->name;

// 上边那行代码等价于以下代码
if (is_null($repository)) {
    $result = null;
} else {
    $user = $repository->getUser(5);
    if (is_null($user)) {
        $result = null;
    } else {
        $result = $user->name;
    }
}

属性

属性声明是由关键字 public,protected 或者 private 开头,然后跟一个普通的变量声明来组成。

在类的成员方法里面,可以用 ->(对象运算符):$this->property(其中 property 是该属性名)这种方式来访问非静态属性。

静态属性则是用 ::(双冒号):self::$property 来访问。


类常量

可以把在类中始终保持不变的值定义为常量,在定义和使用常量的时候不需要使用 $ 符号。

常量的值必须是一个定值,不能是变量、类属性、表达式或函数调用。

<?php
class MyClass
{
    const constant = 'constant value';
}

echo MyClass::constant;    // 输出 constant value

类的自动加载

从 PHP5 起,spl_autoload_register() 函数可以注册任意数量的自动加载函数,当使用尚未被定义的类(class)和接口(interface)时自动去加载。

本例尝试分别从 MyClass1.php 和 MyClass2.php 文件中加载 MyClass1 和 MyClass2 类。

<?php
spl_autoload_register(function ($class_name) {
    require_once $class_name . '.php';
});

$obj  = new MyClass1();
$obj2 = new MyClass2();

构造函数和析构函数

构造函数

__construct ( mixed ...$values = "" ) : void

具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。

<?php
class Point {
    protected int $x;
    protected int $y;

    public function __construct(int $x, int $y) {
        $this->x = $x;
        $this->y = $y;
    }
}

$p1 = new Point(4, 5);

析构函数

__destruct ( ) : void

析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。

<?php

class MyDestructableClass 
{
    public function __construct() {
        print "In constructor\n";
    }

    public function __destruct() {
        print "Destroying " . __CLASS__ . "\n";
    }
}

$obj = new MyDestructableClass();

访问控制(可见性)

通过在类属性或者类方法前面添加关键字 public(公有)、protected(受保护)或 private(私有)来实现的。

public 公有的类成员可以在任何地方被访问。

protected 受保护的类成员则可以被其自身以及其子类和父类访问。

private 私有的类成员则只能被其定义所在的类访问。

<?php
class MyClass
{
    public $public = 'Public';
    protected $protected = 'Protected';
    private $private = 'Private';

    public function printHello()
    {
        echo $this->public;
        echo $this->protected;
        echo $this->private;
    }
}

$obj = new MyClass();
echo $obj->public; // 这行能被正常执行
echo $obj->protected; // 这行会产生一个致命错误
echo $obj->private; // 这行也会产生一个致命错误
$obj->printHello(); // 输出 Public、Protected 和 Private

对象继承

当用 extends 继承一个类,子类就会继承父类所有公有的和受保护的方法。

<?php
class foo
{
    public function printItem($string) 
    {
        echo 'Foo: ' . $string . PHP_EOL;
    }
    
    public function printPHP()
    {
        echo 'PHP is great.' . PHP_EOL;
    }
}

class bar extends foo
{
    public function printItem($string)
    {
        echo 'Bar: ' . $string . PHP_EOL;
    }
}

$foo = new foo();
$bar = new bar();
$foo->printItem('baz'); // 输出: 'Foo: baz'
$foo->printPHP();       // 输出: 'PHP is great' 
$bar->printItem('baz'); // 输出: 'Bar: baz'
$bar->printPHP();       // 输出: 'PHP is great'

范围解析操作符 (::)

可以用于访问静态成员,类常量,还可以用于覆盖类中的属性和方法。

在类的外部使用 :: 操作符

<?php
class MyClass {
    const CONST_VALUE = 'A constant value';
}

echo MyClass::CONST_VALUE;

在类定义内部使用 :: 操作符

<?php
class OtherClass extends MyClass
{
    public static $my_static = 'static var';

    public static function doubleColon() {
        echo parent::CONST_VALUE . "\n";
        echo self::$my_static . "\n";
    }
}

OtherClass::doubleColon();

调用父类的方法

<?php
class MyClass
{
    protected function myFunc() {
        echo "MyClass::myFunc()\n";
    }
}

class OtherClass extends MyClass
{
    // 覆盖了父类的定义
    public function myFunc()
    {
        // 但还是可以调用父类中被覆盖的方法
        parent::myFunc();
        echo "OtherClass::myFunc()\n";
    }
}

$class = new OtherClass();
$class->myFunc();

静态(static)关键字

声明类属性或方法为静态,就可以不实例化类而直接访问。静态属性不能通过一个类已实例化的对象来访问,但静态方法可以。

静态方法

由于静态方法不需要通过对象即可调用,所以伪变量 $this 在静态方法中不可用。

<?php
class Foo {
    public static function aStaticMethod() {
        // ...
    }
}

Foo::aStaticMethod();

静态属性

静态属性不可以由对象通过 -> 操作符来访问。

<?php
class Foo
{
    public static $my_static = 'foo';

    public function staticValue() {
        return self::$my_static;
    }
}

print Foo::$my_static . "\n";

抽象类

定义为抽象的类不能被实例化,任何一个类,如果它里面至少有一个方法是被声明为抽象的,那么这个类就必须被声明为抽象的。

被定义为抽象的方法只是声明了其调用方式(参数),不能定义其具体的功能实现。

<?php
abstract class AbstractClass
{
 // 强制要求子类定义这些方法
    abstract protected function getValue();
    abstract protected function prefixValue($prefix);

    // 普通方法(非抽象方法)
    public function printOut() {
        print $this->getValue() . "\n";
    }
}

class ConcreteClass1 extends AbstractClass
{
    protected function getValue() {
        return "ConcreteClass1";
    }

    public function prefixValue($prefix) {
        return "{$prefix}ConcreteClass1";
    }
}

class ConcreteClass2 extends AbstractClass
{
    public function getValue() {
        return "ConcreteClass2";
    }

    public function prefixValue($prefix) {
        return "{$prefix}ConcreteClass2";
    }
}

$class1 = new ConcreteClass1;
$class1->printOut();
echo $class1->prefixValue('FOO_') ."\n";

$class2 = new ConcreteClass2;
$class2->printOut();
echo $class2->prefixValue('FOO_') ."\n";

接口

使用接口(interface),可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容。

接口中定义的所有方法都必须是公有,这是接口的特性。

实现

要实现一个接口,使用 implements 操作符,类中必须实现接口中定义的所有方法。

类可以实现多个接口,用逗号来分隔多个接口的名称。

常量

接口中也可以定义常量。接口常量和类常量的使用完全相同,但是不能被子类或子接口所覆盖。

<?php

// 声明一个'iTemplate'接口
interface iTemplate
{
    public function setVariable($name, $var);
    public function getHtml($template);
}


// 实现接口
// 下面的写法是正确的
class Template implements iTemplate
{
    private $vars = array();
  
    public function setVariable($name, $var)
    {
        $this->vars[$name] = $var;
    }
  
    public function getHtml($template)
    {
        foreach($this->vars as $name => $value) {
            $template = str_replace('{' . $name . '}', $value, $template);
        }
 
        return $template;
    }
}

// 下面的写法是错误的,会报错,因为没有实现 getHtml():
class BadTemplate implements iTemplate
{
    private $vars = array();
  
    public function setVariable($name, $var)
    {
        $this->vars[$name] = $var;
    }
}

Trait

自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait,使开发人员能够自由地在不同层次结构内独立的类中复用 method。

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

class ezcReflectionMethod extends ReflectionMethod {
    use ezcReflectionReturnInfo;
    /* ... */
}

class ezcReflectionFunction extends ReflectionFunction {
    use ezcReflectionReturnInfo;
    /* ... */
}

匿名类

PHP 7 开始支持匿名类。 匿名类很有用,可以创建一次性的简单对象。

<?php

// PHP 7 之前的代码
class Logger
{
    public function log($msg)
    {
        echo $msg;
    }
}

$util->setLogger(new Logger());

// 使用了 PHP 7+ 后的代码
$util->setLogger(new class {
    public function log($msg)
    {
        echo $msg;
    }
});

重载

PHP所提供的重载(overloading)是指动态地创建类属性和方法。我们是通过魔术方法(magic methods)来实现的。

当调用当前环境下未定义或不可见的类属性或方法时,重载方法会被调用。

所有的重载方法都必须被声明为 public。

属性重载

public __set ( string $name , mixed $value ) : void
public __get ( string $name ) : mixed
public __isset ( string $name ) : bool
public __unset ( string $name ) : void

在给不可访问属性赋值时,__set() 会被调用。

读取不可访问属性的值时,__get() 会被调用。

当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。

当对不可访问属性调用 unset() 时,__unset() 会被调用。

参数 $name 是指要操作的变量名称。__set() 方法的 $value 参数指定了 $name 变量的值。

<?php
class PropertyTest {
     /**  被重载的数据保存在此  */
    private $data = array();
 
     /**  重载不能被用在已经定义的属性  */
    public $declared = 1;

     /**  只有从类外部访问这个属性时,重载才会发生 */
    private $hidden = 2;

    public function __set($name, $value) 
    {
        echo "Setting '$name' to '$value'\n";
        $this->data[$name] = $value;
    }

    public function __get($name) 
    {
        echo "Getting '$name'\n";
        if (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        }

        $trace = debug_backtrace();
        trigger_error(
            'Undefined property via __get(): ' . $name .
            ' in ' . $trace[0]['file'] .
            ' on line ' . $trace[0]['line'],
            E_USER_NOTICE);
        return null;
    }

    /**  PHP 5.1.0之后版本 */
    public function __isset($name) 
    {
        echo "Is '$name' set?\n";
        return isset($this->data[$name]);
    }

    /**  PHP 5.1.0之后版本 */
    public function __unset($name) 
    {
        echo "Unsetting '$name'\n";
        unset($this->data[$name]);
    }

    /**  非魔术方法  */
    public function getHidden() 
    {
        return $this->hidden;
    }
}

$obj = new PropertyTest;

$obj->a = 1;
echo $obj->a . "\n\n";

var_dump(isset($obj->a));
unset($obj->a);
var_dump(isset($obj->a));
echo "\n";

echo $obj->declared . "\n\n";

echo "Let's experiment with the private property named 'hidden':\n";
echo "Privates are visible inside the class, so __get() not used...\n";
echo $obj->getHidden() . "\n";
echo "Privates not visible outside of class, so __get() is used...\n";
echo $obj->hidden . "\n";

方法重载

public __call ( string $name , array $arguments ) : mixed
public static __callStatic ( string $name , array $arguments ) : mixed

在对象中调用一个不可访问方法时,__call() 会被调用。

在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。

$name 参数是要调用的方法名称。$arguments 参数是一个枚举数组,包含着要传递给方法 $name 的参数。

<?php
class MethodTest 
{
    public function __call($name, $arguments) 
    {
        // 注意: $name 的值区分大小写
        echo "Calling object method '$name' "
             . implode(', ', $arguments). "\n";
    }

    /**  PHP 5.3.0之后版本  */
    public static function __callStatic($name, $arguments) 
    {
        // 注意: $name 的值区分大小写
        echo "Calling static method '$name' "
             . implode(', ', $arguments). "\n";
    }
}

$obj = new MethodTest;
$obj->runTest('in object context');

MethodTest::runTest('in static context');  // PHP 5.3.0之后版本

遍历对象

方法一:可以用 foreach 语句遍历所有可见属性。

<?php
class MyClass
{
    public $var1 = 'value 1';
    public $var2 = 'value 2';
    public $var3 = 'value 3';

    protected $protected = 'protected var';
    private   $private   = 'private var';

    function iterateVisible() {
       echo "MyClass::iterateVisible:\n";
       foreach($this as $key => $value) {
           print "$key => $value\n";
       }
    }
}

$class = new MyClass();

foreach($class as $key => $value) {
    print "$key => $value\n";
}
echo "\n";


$class->iterateVisible();

以上例程会输出:

var1 => value 1
var2 => value 2
var3 => value 3

MyClass::iterateVisible:
var1 => value 1
var2 => value 2
var3 => value 3
protected => protected var
private => private var

方法二:可以实现 Iterator 接口的对象遍历。

<?php
class MyIterator implements Iterator
{
    private $var = array();

    public function __construct($array)
    {
        if (is_array($array)) {
            $this->var = $array;
        }
    }

    public function rewind() {
        echo "rewinding\n";
        reset($this->var);
    }

    public function current() {
        $var = current($this->var);
        echo "current: $var\n";
        return $var;
    }

    public function key() {
        $var = key($this->var);
        echo "key: $var\n";
        return $var;
    }

    public function next() {
        $var = next($this->var);
        echo "next: $var\n";
        return $var;
    }

    public function valid() {
        $var = $this->current() !== false;
        echo "valid: {$var}\n";
        return $var;
    }
}

$values = array(1,2,3);
$it = new MyIterator($values);

foreach ($it as $a => $b) {
    print "$a: $b\n";
}

以上例程会输出:

rewinding
current: 1
valid: 1
current: 1
key: 0
0: 1
next: 2
current: 2
valid: 1
current: 2
key: 1
1: 2
next: 3
current: 3
valid: 1
current: 3
key: 2
2: 3
next:
current:
valid:

方法三:实现 IteratorAggregate 来遍历对象。

<?php
class MyCollection implements IteratorAggregate
{
    private $items = array();
    private $count = 0;

    // Required definition of interface IteratorAggregate
    public function getIterator() {
        return new MyIterator($this->items);
    }

    public function add($value) {
        $this->items[$this->count++] = $value;
    }
}

$coll = new MyCollection();
$coll->add('value 1');
$coll->add('value 2');
$coll->add('value 3');

foreach ($coll as $key => $val) {
    echo "key/value: [$key -> $val]\n\n";
}

以上例程会输出:

rewinding
current: value 1
valid: 1
current: value 1
key: 0
key/value: [0 -> value 1]

next: value 2
current: value 2
valid: 1
current: value 2
key: 1
key/value: [1 -> value 2]

next: value 3
current: value 3
valid: 1
current: value 3
key: 2
key/value: [2 -> value 3]

next:
current:
valid:

魔术方法

__construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __serialize(), __unserialize(), __toString(), __invoke(), __set_state(), __clone() 和 __debugInfo() 等方法在 PHP 中被称为魔术方法(Magic methods)。在命名自己的类方法时不能使用这些方法名。

__sleep() 和 __wakeup()

public function __sleep ( ) : array
public function __wakeup ( ) : void

serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。

与之相反,unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup() 方法。

__serialize() 和 __unserialize()

此特性自 PHP 7.4.0 起可用。

public function __serialize ( ) : array
public function __unserialize ( array $data ) : void

serialize() 函数会检查类中是否存在一个魔术方法 __serialize(),如果存在,该方法将在任何序列化之前优先执行,它必须以一个代表对象序列化形式的 键/值 成对的关联数组形式来返回。

相反,unserialize()会检查是否存在一个名字为__unserialize()的函数。如果存在,该方法将在任何反序列化之前优先执行。

如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。

__toString()

public function  __toString ( ) : string

__toString() 方法用于一个类被当成字符串时应怎样响应。

__invoke()

public function __invoke ( $... = ? ) : mixed

当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。

__set_state()

public static __set_state ( array $properties ) : object

自 PHP 5.1.0 起,当调用 var_export() 导出类时,此静态 方法会被调用。

__debugInfo()

public function __debugInfo ( ) : array

自 PHP 5.6.0 起,当把对象当做参数调用 var_dump() 时,此方法被调用。


Final 关键字

PHP 5 新增了一个 final 关键字。如果父类中的方法被声明为 final,则子类无法覆盖该方法。如果一个类被声明为 final,则不能被继承。


对象复制

对象复制可以通过 clone 关键字来完成,如果可能,这将调用对象的 __clone() 方法。

public function __clone ( ) : void

当对象被复制后,PHP 5 会对对象的所有属性执行一个浅复制(shallow copy),所有的引用属性 仍然会是一个指向原来的变量的引用。


对象比较

当使用比较运算符(==)比较两个对象变量时,比较的原则是:如果两个对象的属性和属性值 都相等,而且两个对象是同一个类的实例,那么这两个对象变量相等。

而如果使用全等运算符(===),这两个对象变量一定要指向某个类的同一个实例(即同一个对象)。


后期静态绑定

后期静态绑定,用于在继承范围内引用静态调用的类。

用法:

<?php
class A {
    public static function who() {
        echo __CLASS__;
    }
    public static function test() {
        static::who(); // 后期静态绑定从这里开始
    }
}

class B extends A {
    public static function who() {
        echo __CLASS__;
    }
}

B::test();

以上例程会输出:

B

对象和引用

默认情况下对象是通过引用传递的。


对象序列化

所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。

为了能够unserialize()一个对象,这个对象的类必须已经定义过。


协变与逆变

在 PHP 7.2.0 里,通过对子类方法里参数的类型放宽限制,实现对逆变的部分支持。 自 PHP 7.4.0 起开始支持完整的协变和逆变。

协变使子类比父类方法能返回更具体的类型; 逆变使子类比父类方法参数类型能接受更模糊的类型。