给力星

Web Developer

重温PHP手册 – 类与对象

美团点评 2018 届校招内推开始啦!
参与内推 = 简历免筛选 + 多一次笔试 + 提前面试/提前拿 Offer
多一次机会,多一份把握,千万不要错过~
内推申请地址:https://wenjuan.meituan.com/survey/70556

基本概念

$this

$this 是一个到主叫对象的引用(如果是从第二个对象静态调用时也可能是另一个对象)。

$this 和 self 的区别:

  • $this 指向当前的 object, self 指向当前的 class。
  • 访问类中声明的静态(static)变量和静态函数,需使用 self(使用 $this 合法,但输出 null)

class A
{
    static $str = 'A';
    function foo()
    {
        if (isset($this)) {
            echo self::$str;    // 静态变量需用 self:: 获取
            echo get_class($this);
        }
    }
}

class B
{
    static $str = 'B';
    function bar()
    {
        A::foo();
    }
}

$a = new A();
$a->foo();      // 输出 AA

$b = new B();
$b->bar();      // 输出 AB

extern

一个类可以在声明中用 extends 关键字继承另一个类的方法和属性。PHP不支持多重继承,一个类只能继承一个基类。

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

当覆盖方法时,参数必须保持一致否则 PHP 将发出 E_STRICT 级别的错误信息。但构造函数例外,构造函数可在被覆盖时使用不同的参数。

class ExtendClass extends SimpleClass
{
    // Redefine the parent method
    function displayVar()
    {
        echo "Extending class\n";
        parent::displayVar();   // 访问父类的displayVar()
    }
}

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

::class

自 PHP 5.5 起,关键词 class 也可用于类名的解析。使用 ClassName::class 你可以获取一个字符串,包含了类 ClassName 的完全限定名称。这对使用了 命名空间 的类尤其有用。

namespace NS {
    class ClassName {
    }

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

Note

对象(Object)和类:

stdClass 是默认的PHP对象,stdClass 没有属性、方法、父类,也不支持魔术方法、接口。将 scalar 或 array 转为对象,就得到了stdClass的一个实例。

stdClass 并不是一个基类,定义一个继承于 stdClass 的类也没有任何意义。

//创建 stdClass 实例
$x = new stdClass;
$y = (object) null;        // same as above
$z = (object) 'a';         // creates property 'scalar' = 'a'
$a = (object) array('property1' => 1, 'property2' => 'b');

关于对象赋值:PHP 中的变量是指向一个数据片段,但对象并不是直接指向数据片段。对象赋值后,是对象的 handle 指向对象所在的数据片段,但有别于引用。

Class Object{
   public $foo="bar";
};

$objectVar = new Object();
$reference =& $objectVar;
$assignment = $objectVar;

// $assignment复制了一份$objectVar,但$assignment 的 handle还是指向同一对象,所以下面更改 $objectVar 的值,$assignment表现得与引用相像。

$objectVar->foo = "qux";
print_r( $objectVar );  // qux
print_r( $reference );  // qux
print_r( $assignment ); // qux

// 如果把$objectVar赋值为NULL,只是将$objectVar的 handle 的数据片段置为NULL,并不影响$assignment。
$objectVar = NULL;
print_r( $objectVar );  // NULL
print_r( $reference );  // NULL
print_r( $assignment ); // qux

属性

属性声明是由关键字 public,protected 或者 private 开头,然后跟一个普通的变量声明来组成。属性中的变量可以初始化,但是初始化的值必须是常数,即 PHP 脚本在编译阶段时就可以得到其值。

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

类常量

在类中保持不变的定义,用 const 定义。自 PHP 5.3.0 开始,可以使用 ClassName::constant 来访问。

const 定义是 public 的,不能设定为 private 或 protected。

在子类中覆盖父类的 const ,可通过静态方法和 get_called_class() 来正确的访问其值。或者通过 static::constant 也可以达到同样的效果。

class dbObject
{    
    const TABLE_NAME='undefined';

    public static function GetAll()
    {
        $c = get_called_class();
        return "SELECT * FROM `".$c::TABLE_NAME."`";
        // return "SELECT * FROM `".static::TABLE_NAME."`"; /* 与上面等价 */
    }    
}

class dbPerson extends dbObject
{
    const TABLE_NAME='persons';
}

echo dbObject::GetAll();    // SELECT * FROM `undefined`
echo dbPerson::GetAll();    // SELECT * FROM `persons`

自动加载类

很多开发者写面向对象的应用程序时对每个类的定义建立一个 PHP 源文件。一个很大的烦恼是不得不在每个脚本开头写一个长长的包含文件列表(每个类一个文件)。

在 PHP5 中,可以定义一个 __autoload() 函数,它会在试图使用尚未被定义的类时自动调用。通过调用此函数,脚本引擎在 PHP 出错失败前有了最后一个机会加载所需的类。

下面的例子尝试分别从 MyClass1.php 和 MyClass2.php 文件中加载 MyClass1 和 MyClass2 类

function __autoload($class_name) {
    require_once $class_name . '.php';
}

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

Tips: 在autoloader 中不必使用 require_one(),因为肯定是未加载过的,直接使用 require()就行(性能也更好)。

sql_autoload_register(func)

spl_autoload_register() 提供了一种更加灵活的方式来实现类的自动加载。因此,不再建议使用 __autoload() 函数。

spl_autoload_register() 将函数注册到SPL __autoload函数栈中。如果该栈中的函数尚未激活,则激活它们。

如果没有提供任何参数,spl_autoload_register() 将自动注册autoload的默认实现函数spl_autoload()。

void sql_autoload($class_name [, file_extensions]): 默认会将类名转化为小写,再加上.php/.inc,在所有的包含路径中检查是否存在该文件。

所以都无需定义 autoloader,可以直接这样:

    spl_autoload_extensions(".php"); // comma-separated list
    spl_autoload_register();

构造函数和析构函数

注意构造函数和析构函数前面是两个下划线

构造函数

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

如果子类中定义了构造函数则不会隐式调用其父类的构造函数。要执行父类的构造函数,需要在子类的构造函数中调用 parent::__construct()。如果子类没有定义构造函数则会如同一个普通的类方法一样从父类继承(假如没有被定义为 private 的话)。

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

class SubClass extends BaseClass {
   function __construct() {
       parent::__construct();
       print "In SubClass constructor\n";
   }
}

class OtherSubClass extends BaseClass {
    // inherits BaseClass's constructor
}

// In BaseClass constructor
$obj = new BaseClass();

// In BaseClass constructor
// In SubClass constructor
$obj = new SubClass();

// In BaseClass constructor
$obj = new OtherSubClass();

析构函数

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

要执行父类的析构函数,必须在子类的析构函数体中显式调用 parent::__destruct()。此外也和构造函数一样,子类如果自己没有定义析构函数则会继承父类的。

析构函数在脚本关闭时调用,此时所有的 HTTP 头信息已经发出。析构函数即使在使用 exit() 终止脚本运行时也会被调用。在析构函数中调用 exit() 将会中止其余关闭操作的运行。

class MyDestructableClass {
    function __construct() {
        // ...
    }

    function __destruct() {
        print "Destroying " . $this->name . "\n";
    }
}

Note

多参数构造器的实现:

class A 
{ 
    function __construct() 
    { 
        $a = func_get_args(); 
        $i = func_num_args(); 
        if (method_exists($this,$f='__construct'.$i)) { 
            call_user_func_array(array($this,$f),$a); 
        } 
    } 

    function __construct1($a1) 
    { 
        echo('__construct with 1 param called: '.$a1.PHP_EOL); 
    } 

    function __construct2($a1,$a2) 
    { 
        echo('__construct with 2 params called: '.$a1.','.$a2.PHP_EOL); 
    } 

} 
$o = new A('sheep');        //__construct with 1 param called: sheep
$o = new A('sheep','cat');  // __construct with 2 params called: sheep,cat 

单例模式设计:

class Foo {

  private static $instance;

  private __construct() {
    // Do stuff
  }

  public static getInstance() {

    if (!isset(self::$instance)) {
      $c = __CLASS__;
      $instance = new $c;
    }

    return self::$instance;
  }

  public function sayHello() {
    echo "Hello World!!";
  }

}

$bar = Foo::getInstance();

// Prints 'Hello World' on the screen.
$bar -> sayHello();

访问控制

  • public(公有): 可以在任何地方被访问。
  • protected(受保护): 可以被其自身以及其子类和父类访问。
  • private(私有): 只能被其定义所在的类访问。

方法的访问控制

类中的方法可以被定义为公有,私有或受保护。如果没有设置这些关键字,则该方法默认为公有。

其它对象的访问控制

同一个类的对象即使不是同一个实例也可以互相访问对方的私有与受保护成员。这是由于在这些对象的内部具体实现的细节都是已知的。

访问同一个对象类型的私有成员,即将对象传入类的方法中,可以访问该对象的私有成员和方法:

class Test
{
    private $foo;

    public function __construct($foo)
    {
        $this->foo = $foo;
    }

    private function bar()
    {
        echo 'Accessed the private method.';
    }

    public function baz(Test $other)
    {
        // We can change the private property:
        $other->foo = 'hello';
        var_dump($other->foo);

        // We can also call the private method:
        $other->bar();
    }
}

$test = new Test('test');
$test->baz(new Test('other'));

Note

类的定义和使用应有一定的规范:

class Item
{
  /**
   * Here's the new INSIDE CODE and the Rules to follow:
   *
   * 1. STOP ACCESS to properties via $item->label and $item->price,
   *    by using the protected keyword.
   * 2. FORCE the use of public functions.
   * 3. ONLY strings are allowed IN & OUT of this class for $label
   *    via the getLabel and setLabel functions.
   * 4. ONLY floats are allowed IN & OUT of this class for $price
   *    via the getPrice and setPrice functions.
   */

  protected $label = 'Unknown Item'; // Rule 1 - protected.
  protected $price = 0.0;            // Rule 1 - protected.

  public function getLabel() {       // Rule 2 - public function.
    return $this->label;             // Rule 3 - string OUT for $label.
  }

  public function getPrice() {       // Rule 2 - public function.    
    return $this->price;             // Rule 4 - float OUT for $price.
  }

  public function setLabel($label)   // Rule 2 - public function.
  {
    /**
     * Make sure $label is a PHP string that can be used in a SORTING
     * alogorithm, NOT a boolean, number, array, or object that can't
     * properly sort -- AND to make sure that the getLabel() function
     * ALWAYS returns a genuine PHP string.
     *
     * Using a RegExp would improve this function, however, the main
     * point is the one made above.
     */

    if(is_string($label))
    {
      $this->label = (string)$label; // Rule 3 - string IN for $label.
    }
  }

  public function setPrice($price)   // Rule 2 - public function.
  {
    /**
     * Make sure $price is a PHP float so that it can be used in a
     * NUMERICAL CALCULATION. Do not accept boolean, string, array or
     * some other object that can't be included in a simple calculation.
     * This will ensure that the getPrice() function ALWAYS returns an
     * authentic, genuine, full-flavored PHP number and nothing but.
     *
     * Checking for positive values may improve this function,
     * however, the main point is the one made above.
     */

    if(is_numeric($price))
    {
      $this->price = (float)$price; // Rule 4 - float IN for $price.
    }
  }
}

定义为 private 的方法只有定义该方法的类能够访问,因此无法在子类中进行重载。

abstract class base { 
    public function inherited() { 
        $this->overridden(); 
    } 
    private function overridden() { 
        echo 'base'; 
    } 
} 

class child extends base { 
    private function overridden() { 
        echo 'child'; 
    } 
} 

$test = new child(); 
$test->inherited();     // 输出 base

这种情况下应该定义为 protected :

abstract class base { 
    public function inherited() { 
        $this->overridden(); 
    } 
    protected function overridden() { 
        echo 'base'; 
    } 
} 

class child extends base { 
    protected function overridden() { 
        echo 'child'; 
    } 
} 

$test = new child(); 
$test->inherited();     // 输出 child

对象继承

除非使用了自动加载,否则一个类必须在使用之前被定义。如果一个类扩展了另一个,则父类必须在子类之前被声明。此规则适用于类继承其它类与接口。

Note

PHP可以多层继承,如 class A, B extend A, C extend B,但不能多重继承。

可以通过 parent::method() 来访问父类,父类若还有再上一层,则应通过 class::method() 显式访问。

class foo
{
    public function something()
    {
        echo __CLASS__; // foo
    }
}

class foo_bar extends foo
{
    public function something()
    {
        echo __CLASS__; // foo_bar
    }
}

class foo_bar_baz extends foo_bar
{
    public function something()
    {
        echo __CLASS__; // foo_bar_baz
    }

    public function call()
    {
        echo self::something();     // self
        echo parent::something();   // parent
        echo foo::something();      // grandparent,显式调用
    }
}

使用 abstract 关键词可以让一个类只能作为可继承的类。若想要建立一个 abstract 类的实例,就会产生一个致命错误。

abstract class Cheese
{
    //can ONLY be inherited by another class
}

class Cheddar extends Cheese
{
}

$dinner = new Cheese; //fatal error
$lunch = new Cheddar; //works!

范围解析操作符(::)

范围解析操作符(也可称作 Paamayim Nekudotayim)或者更简单地说是一对冒号,可以用于访问静态成员,类常量,还可以用于覆盖类中的属性和方法。

在类的外部使用:

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

echo MyClass::CONST_VALUE;

$classname = 'MyClass';
echo $classname::CONST_VALUE; // 自 PHP 5.3.0 起

在类的内部使用:

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

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

在类的方法中可以使用 static ,区别于 self :

class A { 
    const C = 'constA'; 
    public function m() { 
        echo $this::C;  // 输出 constB
        echo static::C; // 与上面等价
        echo self::C;   // 输出 constA
    } 
} 

class B extends A { 
    const C = 'constB'; 
} 

$b = new B(); 
$b->m(); 

可以使用 call_user_func(array('Class', 'method') [, $params] ); 来调用类的静态方法。

Static 关键字

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

  • 由于静态方法不需要通过对象即可调用,所以伪变量 $this 在静态方法中不可用。
  • 静态属性不可以由对象通过 -> 操作符来访问。
  • 静态属性只能被初始化为文字或常量,不能使用表达式。
  • 自 PHP 5.3.0 起,可以用一个变量来动态调用类($class= 'MyClass'; $class::staticMethod();'
class Foo
{
    public static $my_static = 'foo';

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

class Bar extends Foo
{
    public function fooStatic() {
        return parent::$my_static;
    }
}

echo Foo::$my_static; // 输出 foo

$foo = new Foo();
echo $foo::$my_static;  // 输出 Foo
echo $foo->my_static;   // 错误

$bar = new Bar();
echo $bar->fooStatic(); // 输出 foo

检查一个函数是否被静态地调用:

function foo () {
    $isStatic = !(isset($this) && get_class($this) == __CLASS__);
}

抽象类

定义为抽象的类不能被实例化。若一个类中有任一方法被声明为抽象的,那么这个类就必须被声明为抽象的,被定义为抽象的方法只是声明了其调用方式(参数),不能定义其具体的功能实现。

  • 继承一个抽象类的时候,子类必须定义父类中的所有抽象方法。
  • 这些抽象方法的访问控制必须和父类中一样(或者更为宽松)。
  • 方法的调用方式必须匹配,即类型和所需参数数量必须一致。
  • 子类可以定义父类中没有的可选参数。
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, $suffix = NULL) {
        return "{$prefix}ConcreteClass1";
    }
}

Note

接口与继承的区别:

接口就像是协议,不指定对象的行为,而是指定你的代码如何告诉对象该怎么做。就像是语言,规定了如何交流。当“I implement interface Y”,即代表I使用与接口Y的对象相同的方法。

抽象类则是像是一个有空格的文档,需要我们去填写。当“I extend abstract class Y”,即I使用的方法是class Y已定义的。

class X implements Y { } // this is saying that "X" agrees to speak language "Y" with your code.

class X extends Y { } // this is saying that "X" is going to complete the partial class "Y".

不能创建抽象类的实例,但依然可以访问抽象类的静态方法。

abstract class Foo
{
    static function bar()
    {
        echo "test\n";
    }
}

Foo::bar();

对象接口

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

  • 接口中定义所有的方法都是空的
  • 接口中定义的所有方法都必须是共有的,这是接口的特性。
  • 接口加上类型约束,提供了一种很好的方式来确保某个对象包含有某些方法。

实现 implements

要实现一个接口,使用 implements 操作符。类中必须实现接口中定义的所有方法,否则会报一个致命错误。

  • 类可以实现多个接口,用逗号来分隔多个接口的名称,但多个接口的方法不能有重名。
  • 接口也可以继承,通过使用 extends 操作符,可以继承多个接口,继承后的所有方法都要实现。
  • 接口中也可以定义常量,和类常量的使用完全相同,但是不能被子类或子接口所覆盖。
  • 实现也可以使用父类中没有的可选参数。

抽象类和接口的区别:

  • 抽象类的继承,访问控制必须和父类一样或更为宽松;接口则都是 public。
  • 抽象类则可以定义普通方法;接口定义的所有方法都要是空的,
  • 不能继承多个抽象类;可以实现多个接口。

接口示例:

// 声明一个'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;
    }
}

扩充接口

interface a
{
    public function foo();
}

interface b extends a
{
    public function baz(Baz $baz);
}

// 接口 b 中包含了 foo() 和 baz() 两个方法
// c 实现接口 b ,则两个方法都要实现。
class c implements b
{
    public function foo()
    {
    }

    public function baz(Baz $baz)
    {
    }
}

使用接口常量

interface a
{
    const b = 'Interface constant';
}

// 输出接口常量
echo a::b;

Note

接口的使用:接口提供了一系列方法的描述,但隐藏了这些方法在类中的具体实现。这允许你改变实现的方法而无需改变用法。

例如,有一个数据库,希望写一个类来访问数据库。定义接口如下:

interface Database {
    function listOrders();
    function addOrder();
    function removeOrder();
}

首先写一个类来访问 MySQL 数据库

```php
class MySqlDatabase implements Database {
    function listOrders() {...
}

$database = new MySqlDatabase();
foreach ($database->listOrders() as $order) {...

如果决定迁移至 Oracle 数据库,那我们只要修改一行代码:

// 只需要修改使用的类
$database = new OracleDatabase();
// 其他的代码都无需更改
foreach ($database->listOrders() as $order) {

所以,在这个例子中,接口描述了我们访问数据库的方法,实现类则描述了具体如何去访问。

接口一方面可以让我们无需去改变使用它的代码,另一方面可以实现渐进式发展:有时一些复杂的功能一时无法实现,可以预留接口以后再去实现;或者当前实现的效率,以后再去优化。只要定义好了接口,使用上就无需去变动。

Traits

自 PHP 5.4.0 起,PHP 实现了代码复用的一个方法,称为 traits,是一种为类似 PHP 的单继承语言而准备的代码复用机制。

Trait 和一个类相似,但仅仅旨在用细粒度和一致的方式来组合功能。Trait 不能通过它自身来实例化。它为传统继承增加了水平特性的组合;也就是说,应用类的成员不需要继承。

Trait 示例:

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

class ezcReflectionMethod extends ReflectionMethod {
    // 使用ezcReflectionReturnInfo中定义的方法
    use ezcReflectionReturnInfo;
    /* ... */
}

优先级

优先顺序是当前类中的方法会覆盖 trait 方法,而 trait 方法又覆盖了基类中的方法: 当前类 > trait > 基类。

class Base {
    public function sayHello() {
        echo 'Hello ';
    }
}

trait SayWorld {
    public function sayHello() {
        parent::sayHello();
        echo 'World!';
    }
}

class MyHelloWorld extends Base {
    // 使用了 trait 中的 sayHello()
    use SayWorld;
}

$o = new MyHelloWorld();
$o->sayHello();             // 输出 Hello World!

多个 trait

通过逗号分隔,在 use 声明列出多个 trait,可以都插入到一个类中。

class MyHelloWorld {
    use Hello, World;
    public function sayExclamationMark() {
        echo '!';
    }
}

冲突的解决

为了解决多个 trait 在同一个类中的命名冲突,需要使用 insteadof 操作符来明确指定使用冲突方法中的哪一个。

以上方式仅允许排除掉其它方法,as 操作符可以将其中一个冲突的方法以另一个名称来引入。

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 {
    // 使用 insteadof 解决冲突
    use A, B {
        B::smallTalk insteadof A;
        A::bigTalk insteadof B;
    }
}

class Aliased_Talker {
    // 冲突重命令
    use A, B {
        B::smallTalk insteadof A;
        A::bigTalk insteadof B;
        B::bigTalk as talk;
    }
}

修改方法的访问控制

使用 as 语法还可以用来调整方法的访问控制。

trait HelloWorld {
    public function sayHello() {
        echo 'Hello World!';
    }
}

// 修改 sayHello 的访问控制
class MyClass1 {
    use HelloWorld { sayHello as protected; }
}

// 给方法一个改变了访问控制的别名
// 原版 sayHello 的访问控制则没有发生变化
class MyClass2 {
    use HelloWorld { sayHello as private myPrivateHello; }
}

从 trait 来组成 trait

正如类能够使用 trait 一样,其它 trait 也能够使用 trait。在 trait 定义时通过使用一个或多个 trait,它能够组合其它 trait 中的部分或全部成员。

trait Hello {
    public function sayHello() {
        echo 'Hello ';
    }
}

trait World {
    public function sayWorld() {
        echo 'World!';
    }
}

trait HelloWorld {
    use Hello, World;
}

trait 的抽象成员

为了对使用的类施加强制要求,trait 支持抽象方法的使用

trait Hello {
    public function sayHelloWorld() {
        // 即使没有使用到 getWorld(), MyHelloworld 类中也必须定义 getWorld() 方法
        echo 'Hello'.$this->getWorld();
    }
    abstract public function getWorld();
}

class MyHelloWorld {
    private $world;
    use Hello;
    public function getWorld() {
        return $this->world;
    }
}

属性

Trait 同样可以定义属性。如果 trait 中定义了一个属性,那类将不能定义同样名称的属性。

Note

  • 若类中有静态属性,则每个继承的类共享这个静态属性的值。
  • 若 trait 中有静态属性,则每个使用 trait 的类都拥有一份独立的值。
class TestClass {
    public static $_bar;
}
class Foo1 extends TestClass { }
class Foo2 extends TestClass { }
Foo1::$_bar = 'Hello';
Foo2::$_bar = 'World';
echo Foo1::$_bar . ' ' . Foo2::$_bar; // Prints: World World

trait TestTrait {
    public static $_bar;
}
class Foo1 {
    use TestTrait;
}
class Foo2 {
    use TestTrait;
}
Foo1::$_bar = 'Hello';
Foo2::$_bar = 'World';
echo Foo1::$_bar . ' ' . Foo2::$_bar; // Prints: Hello World

use 操作符在 trait 和 namespace 中的用法不同:

  • namespace 中 use 的参数是绝对的命名空间。
  • trait 中 use 的参数是指向当前的命名空间。
namespace Foo\Bar;
use Foo\Test;  // means \Foo\Test - the initial \ is optional

/* ---------------------------- */

namespace Foo\Bar;
class SomeClass {
    use Foo\Test;   // means \Foo\Bar\Foo\Test
}

魔术常量 __CLASS__ 在 trait 中会返回使用该 trait 的类,是使用(use) trait 的类,而不是调用 trait 的类。__TRAIT__ 则是返回 trait 的名称。

trait TestTrait {
    public function testMethod() {
        echo "Class: " . __CLASS__ . PHP_EOL;
        echo "Trait: " . __TRAIT__ . PHP_EOL;
    }
}

class BaseClass {
    use TestTrait;
}

class TestClass extends BaseClass {

}

$t = new TestClass();
$t->testMethod();

//Class: BaseClass      -> 使用 TestTrait 的是 BaseClass
//Trait: TestTrait

重载

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

  • 当调用当前环境下未定义或不可见的类属性或方法时,重载方法会被调用。
  • 所有的重载方法都必须被声明为 public。
  • 属性重载只能在对象中进行。在静态方法中,这些魔术方法将不会被调用。
  • 更恰当的说法应该是 “interpreter hooks”。

属性重载

  • public void __set ( string $name , mixed $value ): 在给不可访问属性赋值时,__set()会被调用
  • public mixed __get ( string $name ): 读取不可访问属性的值时,__get() 会被调用。
  • public bool __isset ( string $name ): 当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
  • public void __unset ( string $name ): 当对不可访问属性调用 unset() 时,__unset() 会被调用。

__set() 的返回值将被忽略。所以不能用于链式赋值 $a = $obj->b = 8;

属性重载示例,用一个数组来存储重载的属性

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;                // Setting 'a' to '1'
echo $obj->a . "\n\n";      // Getting 'a' 

var_dump(isset($obj->a));   // Is 'a' set? bool(true)
unset($obj->a);             // Unsetting 'a'
var_dump(isset($obj->a));   // Is 'a' set? bool(false)

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

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";  // 2
echo "Privates not visible outside of class, so __get() is used...\n";
echo $obj->hidden . "\n";       // Getting 'hidden'
// 因为 hidden 没有 set,所以会有一个 notice
// Notice:  Undefined property via __get(): hidden

方法重载

  • public mixed __call ( string $name , array $arguments ): 在对象中调用一个不可访问方法时,__call() 会被调用。
  • public static mixed __callStatic ( string $name , array $arguments ): 用静态方式中调用一个不可访问方法时,__callStatic() 会被调用。

方法重载实例:

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'); // Calling object method 'runTest' in object context

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

遍历对象

PHP 5 提供了一种定义对象的方法使其可以通过单元列表来遍历,例如用 foreach 语句。默认情况下,所有可见属性都将被用于遍历。

用 foreach 简单地遍历对象

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";
}
/* 输出如下, foreach $class 能访问所有的可见属性。
var1 => value 1
var2 => value 2
var3 => value 3
*/


$class->iterateVisible();
/* 输出如下
MyClass::iterateVisible:
var1 => value 1
var2 => value 2
var3 => value 3
protected => protected var
private => private var
*/

更进一步,可以实现 Iterator 接口。可以让对象自行决定如何遍历以及每次遍历时那些值可用。

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";     // true -> 1
        return $var;
    }
}

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

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

输出如下:

rewinding
current: 1
valid: 1
current: 1
key: 0
foreach => 0: 1
next: 2
current: 2
valid: 1
current: 2
key: 1
foreach => 1: 2
next:
current:
valid:

Note

Iterator 在 PHP 和 Java 中的区别:

  • PHP 的 valid() 与 hasNext() 基本等价;
  • PHP 的 next() 不同于 Java 的 next(),PHP 中不返回任何东西,而 Java 则返回下一个对象。
/** Java 的 iterator */
interface Iterator<O> {
  boolean hasNext();
  O next();
  void remove();
}

/** PHP 的 iterator */
interface Iterator<O> {
  boolean valid();
  mixed key();
  O current();
  void next();
  void previous();
  void rewind();
}

魔术方法

PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,不要以 __ 为前缀。

__sleep() 和 __wakeup()

用于序列化操作中:

  • public array __sleep ( void ): serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,如用于提交未提交的数据,或者一个大的对象并不需要全部保存。函数应返回一个包含对象中所有应被序列化的变量名称的数组。
  • void __wakeup ( void ): 与之相反,unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源,例如重新建立数据库连接,或执行其它初始化操作。
class Connection 
{
    protected $link;
    private $server, $username, $password, $db;

    public function __construct($server, $username, $password, $db)
    {
        $this->server = $server;
        $this->username = $username;
        $this->password = $password;
        $this->db = $db;
        $this->connect();
    }

    private function connect()
    {
        $this->link = mysql_connect($this->server, $this->username, $this->password);
        mysql_select_db($this->db, $this->link);
    }

    public function __sleep()
    {
        // 返回应被序列化的变量名
        return array('server', 'username', 'password', 'db');
    }

    public function __wakeup()
    {
        // 反序列化时重新执行连接操作。
        $this->connect();
    }
}

__toString()

public string __toString ( void ): 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 时应该显示些什么。此方法必须返回一个字符串。不能在 __toString() 方法中抛出异常。

class TestClass
{
    public $foo;

    public function __construct($foo) 
    {
        $this->foo = $foo;
    }

    public function __toString() {
        return $this->foo;
    }
}

$class = new TestClass('Hello');
echo $class;    // 输出 Hello

PHP 5.2.0 之后,则可以在任何字符串环境生效(例如通过 printf(),使用 %s 修饰符);如果将一个未定义 __toString() 方法的对象转换为字符串,会产生 E_RECOVERABLE_ERROR 级别的错误。

__invoke()

mixed __invoke ([ $... ] ): 当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用(HP 5.3.0 及以上)。函数可以传入任意数量的参数。

class CallableClass 
{
    function __invoke($x) {
        var_dump($x);
    }
}
$obj = new CallableClass;
$obj(5);                        // 输出int(5)
var_dump(is_callable($obj));    // 输出bool(true)

__set_state()

static object __set_state ( array $properties ): 自 PHP 5.1.0 起当调用 var_export() 导出类时,此静态 方法会被调用。本方法的唯一参数是一个数组(不需要自己传入),其中包含按 array(‘property’ => value, …) 格式排列的类属性。

var_export(): 返回关于传递给该函数的变量的结构信息,它和 var_dump() 类似,不同的是其返回的表示是合法的 PHP 代码。可以通过将函数的第二个参数设置为 TRUE,从而返回变量的表示。

class A
{
    public $var1;
    public $var2;

    public static function __set_state($an_array) // As of PHP 5.1.0
    {
        $obj = new A;
        $obj->var1 = $an_array['var1'];
        $obj->var2 = $an_array['var2'];
        return $obj;
    }
}

$a = new A;
$a->var1 = 5;
$a->var2 = 'foo';

eval('$b = ' . var_export($a, true) . ';'); // $b = A::__set_state(array(
                                            //    'var1' => 5,
                                            //    'var2' => 'foo',
                                            // ));
var_dump($b);
/* 输出如下:
object(A)#2 (2) {
  ["var1"]=>
  int(5)
  ["var2"]=>
  string(3) "foo"
}
*/

Final 关键字

Final 可以防止类被继承,或者防止类中的方法被重载、重新定义。

  • 如果父类中的方法被声明为 final,则子类无法覆盖该方法。
  • 如果一个类被声明为 final,则不能被继承。
  • 只要父类声明为 final,父类中的方法是否声明为 final 没有关系,因为父类无法被继承。
  • 属性不能声明为 final。

对象复制

对象复制可以通过 clone 关键字来完成: $copy_of_object = clone $object;

  • 对象中的 __clone() 方法不能被直接调用。
  • 对象被复制后,PHP 5 会对对象的所有属性执行一个浅复制(shallow copy)。所有的引用属性仍然会是一个指向原来的变量的引用
  • 当复制完成时,如果定义了 __clone() 方法 void __clone ( void ) ,则复制生成的对象中的 __clone() 方法会被调用,可用于修改属性的值。

复制一个对象的实例:

class SubObject
{
    static $instances = 0;
    public $instance;

    public function __construct() {
        $this->instance = ++self::$instances;
    }

    public function __clone() {
        $this->instance = ++self::$instances;
    }
}

class MyCloneable
{
    public $object1;
    public $object2;

    function __clone()
    {
        // 强制复制一份this->object, 否则仍然指向同一个对象
        $this->object1 = clone $this->object1;
    }
}

$obj = new MyCloneable();
$obj->object1 = new SubObject();  // 自 PHP5 起,new 自动返回引用
$obj->object2 = new SubObject();

$obj2 = clone $obj;

print("Original Object:\n");
print_r($obj);
/*
MyCloneable Object
(
    [object1] => SubObject Object
        (
            [instance] => 1
        )
    [object2] => SubObject Object
        (
            [instance] => 2
        )
)
*/

print("Cloned Object:\n");
print_r($obj2);
/*
MyCloneable Object
(
    [object1] => SubObject Object
        (
            [instance] => 3
        )
    [object2] => SubObject Object
        (
            [instance] => 2
        )
)
*/

可以看到$obj->object1新复制了一个对象3,而$obj->object2仍然指向原来的对象2。

Note

若要使所有复制的对象中的某些属性都指向同一个值,可以将属性值

class A
{
    public $name ;

    public function __construct()
    {
        $this->name = & $this->name;
    }
}

$a = new A;
$a->name = "George";

$b = clone $a;
$b->name = "Somebody else";

var_dump($a);
var_dump($b);

/* 输出如下
object(A)#1 (1) {
  ["name"]=>
  &string(13) "Somebody else"
}
object(A)#2 (1) {
  ["name"]=>
  &string(13) "Somebody else"
}
*/

可以看到复制后对象的属性指向了同一个值,并且是引用类型,修改原始对象或复制对象的值,其他的也都改变了。

也有另外一种方式:

class A { var $p; }

$a = new A;
$a->p = 'Hello';  // $a->p 是 值 类型
$ref =& $a->p;    // 注意这将 $a->p 转化成了 引用 类型

$b = clone $a;       
$b->p = 'World';  // $b->p 也是 引用 类型,但改变 $b->p 不会改变 $a->p

$c = clone $b;
$c->p = 'new string'; // 此时 $c->p 和 $b->p 指向同一个值。

对象比较

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

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

<> 操作符,返回第一个不相等的值的结果:

$o1 = new stdClass();
$o1->prop1 = 'c';
$o1->prop2 = 24;
$o1->prop3 = 201;

$o2 = new stdClass();
$o2->prop1 = 'c';
$o2->prop2 = 25;
$o2->prop3 = 200;

echo (int)($o1 < $o2); // 1

类型约束

PHP 5 可以使用类型约束。

  • 函数的参数可以指定必须为对象(在函数原型里面指定类的名字),接口,数组(PHP 5.1 起)或者 callable(PHP 5.4 起)。
  • 如果使用 NULL 作为参数的默认值,那么在调用函数的时候依然可以使用 NULL 作为实参。
  • 如果一个类或接口指定了类型约束,则其所有的子类或实现也都如此。
  • 类型约束不能用于标量类型如 int 或 string。
  • 类型约束不只是用在类成员函数中,也能用在普通函数中。
class MyClass
{
    /**
     * 第一个参数必须为 OtherClass 类的一个对象
     */
    public function test(OtherClass $otherclass) {
        echo $otherclass->var;
    }
}

/**
 * 第一个参数必须是 MyClass 类的一个对象
 */
function MyFunction(array $arr) {
    print_r($arr);
}

/**
 * 可以接受 NULL 值
 */
function test(stdClass $obj = NULL) {

}

后期静态绑定

自 PHP 5.3.0 起,后期静态绑定用于在继承范围内引用静态调用的类。有以下几种方式进行的静态调用:self::parent::static:: 以及 forward_static_call()

“后期绑定”的意思是说,static:: 不再被解析为定义当前方法所在的类,而是在实际运行时计算的。也可以称之为“静态绑定”,因为它可以用于(但不限于)静态方法的调用。

static:: 的用法(用于静态方法中):

/** ActiveRecord methods */
class Model 
{
    protected static $name = 'Model'; 
    public static function find() 
    {
        echo static::$name; 
    }
} 

class Product extends Model 
{ 
    protected static $name = 'Product'; 
}

Product::find();      // Product
$b = new Product();   
$b->find();           // 等价于 Product::find();

对象和引用

在php5,一个对象变量已经不再保存整个对象的值。只是保存一个标识符来访问真正的对象内容。 当对象作为参数传递,作为结果返回,或者赋值给另外一个变量,另外一个变量跟原来的不是引用的关系,只是他们都保存着同一个标识符的拷贝,这个标识符指向同一个对象的真正内容。

引用和对象:

class A {
    public $foo = 1;
}  

$a = new A;
$b = $a;     // $a ,$b都是同一个标识符的拷贝
             // ($a) = ($b) = <id>
$b->foo = 2;
echo $a->foo;   // 2
echo $b->foo;   // 2


$c = new A;
$d = &$c;    // $c ,$d是引用
             // ($c,$d) = <id>
$d->foo = 3;
echo $c->foo;   // 2
echo $d->foo;   // 2


$e = new A;

function foo($obj) {
    // ($obj) = ($e) = <id>
    $obj->foo = 4;
}

foo($e);
echo $e->foo;   // 4

指针(pointer)和引用(references)

  • 指针存储访问对象的内存地址。任何一个时候分配对象时,就生成一个指针。
  • 函数传参默认传递的是值,也就是复制了一个副本,但对象传递的是引用
  • & 将另一变量指向同一内容地址。可以使用 unset() 分离。
$a = "Clark Kent"; // a == Clark Kent
$b = &$a;          // The two will now share the same fate.
$b = "Superman";   // $a == "Superman" too.

unset($b);         // $b divorced from $a
$b="Bizarro";      // $a == "Superman" still.

Note

对象和标识符的区别:

  • 传递对象变量时获得了一个对象值的标识符,如果改变对象的值,所有指向实例的对象的值都会改变。
  • 如果将对象值指向新的实例,它会以新的标识符取代旧的。

实例一:

class A {
    public $foo = 1;
}  

$a = new A;
$b = $a;        // 复制了一个标识符,指向同一内容。
$a->foo = 2;
$a = NULL;
echo $b->foo;   // 2

$c = new A;
$d = &$c;
$c->foo = 2;
$c = NULL;    
echo $d->foo;   // Notice:  Trying to get property of non-object...

实例二:

class A {
    public $foo = 1;
}  

class B {
    public function foo(A $bar)
    {
        $bar->foo = 42;
    }

    public function bar(A $bar)
    {
        // 传值也只是拷贝了一份实例A的指示符
        $bar = new A;
    }
}

$a = new A;
$b = new B;
echo $a->foo;   // 1

$b->foo($a);
echo $a->foo;   // 42

$b->bar($a);
echo $a->foo;   // 是42 而不是 1

/* 虽然 B 中 bar() 替换传入的指示符为一新的A指示符,但并不会改变$a,因为$a传入的并不是引用。也就是说传值也只是拷贝了一份指示符而已。*/

对象序列化

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

unserialize() 函数能够重新把字符串变回php原来的值。序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。

为了能够unserialize()一个对象,这个对象的类必须已经定义过。如果序列化类A的一个对象,将会返回一个跟类A相关,而且包含了对象所有变量值的字符串。 如果要想在另外一个文件中解序列化一个对象,这个对象的类必须在解序列化之前定义,可以通过包含一个定义该类的文件或使用函数spl_autoload_register()来实现。

在应用程序中序列化对象以便在之后使用,强烈推荐在整个应用程序都包含对象的类的定义。 不然有可能出现在解序列化对象的时候,没有找到该对象的类的定义。

// classa.inc:

  class A {
      public $one = 1;

      public function show_one() {
          echo $this->one;
      }
  }

// page1.php:

  include("classa.inc");

  $a = new A;
  $s = serialize($a);
  // 把变量$s保存起来以便文件page2.php能够读到
  file_put_contents('store', $s);

// page2.php:

  // 要正确了解序列化,必须包含下面一个文件,即类的定义
  include("classa.inc");

  $s = file_get_contents('store');
  $a = unserialize($s);

  // 现在就可以使用对象$a里面的函数 show_one()
  $a->show_one();

3条评论

  1. "对象复制"小节:
    $a = new A;
    $a->p = ‘Hello’; // $a->p 是 值 类型
    $ref =& $a->p; // 注意这将 $a->p 转化成了 引用 类型

    $b = clone $a;
    $b->p = ‘World’; // $b->p 也是 引用 类型,但改变 $b->p 不会改变 $a->p

    我测试,发现改变$b->p,会同时改变$a->p的。

  2. 最前面的$this介绍里的代码,居然可以这样用~~,不过,在php5里运行正常,在php7里不行了。

发表评论

电子邮件地址不会被公开。