8

类与对象

基本概念

new:如果在 new 之后跟着的是一个包含有类名的字符串,则该类的一个实例被创建。如果该类属于一个名字空间,则必须使用其完整名称

Example #3 创建一个实例

<?php
$instance = new stdClass();

// 也可以这样做:
$className = 'stdClass';
$instance = new $className(); // Foo()
?>
在类定义内部,可以用 new selfnew parent 创建新对象。

PHP 5.3.0 引进了两个新方法来创建一个对象的实例:

<?php
class Test
{
    static public function getNew()//单例模式可以使用此方法
    {
        return new static;
    }
}    
class Child extends Test {}

$obj1 = new Test();
$obj2 = new $obj1;
var_dump($obj1 !== $obj2);//bool(true)

$obj3 = Test::getNew();
var_dump($obj3 instanceof Test);//bool(true)

$obj4 = Child::getNew();
var_dump($obj4 instanceof Child);//bool(true)
?>

自 PHP 5.5 起,关键词 class 也可用于类名的解析:ClassName::class

当把一个对象已经创建的实例赋给一个新变量时,新变量会访问同一个实例,就和用该对象赋值一样。此行为和给函数传递入实例时一样。可以用克隆给一个已创建的对象建立一个新实例。

<?php 
Class Object{
   public $foo="bar";
};

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


$objectVar->foo = "qux";
var_dump( $objectVar );
var_dump( $reference );
var_dump( $assignment );
var_dump( $cloneObj );

echo '--------------------', PHP_EOL;

$objectVar = null;
var_dump($objectVar);
var_dump($reference);
var_dump($assignment);
var_dump($cloneObj);

/*
Result:

object(Object)#1 (1) {
  ["foo"]=>
  string(3) "qux"
}
object(Object)#1 (1) {
  ["foo"]=>
  string(3) "qux"
}
object(Object)#1 (1) {
  ["foo"]=>
  string(3) "qux"
}
object(Object)#2 (1) {
  ["foo"]=>
  string(3) "bar"
}
--------------------
NULL
NULL
object(Object)#1 (1) {
  ["foo"]=>
  string(3) "qux"
}
object(Object)#2 (1) {
  ["foo"]=>
  string(3) "bar"
}
*/

类的自动加载

spl_autoload_register() 函数可以注册任意数量的自动加载器,当使用尚未被定义的类(class)和接口(interface)时自动去加载。通过注册自动加载器,脚本引擎在 PHP 出错失败前有了最后一个机会加载所需的类。

尽管 __autoload() 函数也能自动加载类和接口,但更建议使用 spl_autoload_register() 函数。 spl_autoload_register() 提供了一种更加灵活的方式来实现类的自动加载(同一个应用中,可以支持任意数量的加载器,比如第三方库中的)。因此,不再建议使用 __autoload() 函数,在以后的版本中它可能被弃用。

Example #1 自动加载示例

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

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

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

构造函数和析构函数

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

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

析构函数即使在使用 exit() 终止脚本运行时也会被调用。在析构函数中调用 exit() 将会中止其余关闭操作的运行。

访问控制(可见性)

类属性必须定义为公有受保护私有之一。如果用 var 定义,则被视为公有。

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

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

Example #3 访问同一个对象类型的私有成员

<?php
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'));

//string(5) "hello"
//Accessed the private method.
?>

继承和访问控制:

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

Output will be "base". 

If you want the inherited methods to use overridden functionality in extended classes but public sounds too loose, use protected. That's what it is for:) 

A sample that works as intended: 

<?php 
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(); 
?> 
Output will be "child".

范围解析操作符 (::)

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

访问静态变量,静态方法,常量:

<?php    
class A {    
    public static $B = '1'; # Static class variable.    
    const B = '2'; # Class constant.        
    public static function B() { # Static class function.
        return '3';
    }
    
}

echo A::$B . A::B . A::B(); # Outputs: 123

Example #3 调用父类的方法

<?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(静态)关键字

用 static 关键字来定义静态方法属性。static 也可用于定义静态变量以及后期静态绑定

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

抽象类

继承一个抽象类的时候:
1.子类必须定义父类中的所有抽象方法
2.这些方法的访问控制必须和父类中一样(或者更为宽松)。例如某个抽象方法被声明为受保护的,那么子类中实现的方法就应该声明为受保护的或者公有的,而不能定义为私有的;
3.方法的调用方式必须匹配,即类型和所需参数数量必须一致。例如,子类定义了一个可选参数,而父类抽象方法的声明里没有,则两者的声明并无冲突。 这也适用于 PHP 5.4 起的构造函数。在 PHP 5.4 之前的构造函数声明可以不一样的;

对象接口

接口中定义的所有方法都必须是公有,这是接口的特性。
实现类必须实现接口中定义的所有方法
可以实现多个接口,用逗号来分隔多个接口的名称。
实现多个接口时,接口中的方法不能有重名
类要实现接口,必须使用和接口中所定义的方法完全一致的方式
接口中也可以定义常量。接口常量和类常量的使用完全相同,但是不能被子类或子接口所覆盖

Trait

自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait

Trait 是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 method。Trait 和 Class 组合的语义定义了一种减少复杂性的方式,避免传统多继承和 Mixin 类相关典型问题。

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

优先级:从基类继承的成员会被 trait 插入的成员所覆盖。优先顺序是来自当前类的成员覆盖了 trait 的方法,而 trait 则覆盖了被继承的方法。

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

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

class MyHelloWorld extends Base {
    use SayWorld;
}

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

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

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

<?php
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;
        A::bigTalk insteadof B;
    }
}

class Aliased_Talker {
    use A, B {
        B::smallTalk insteadof A;
        A::bigTalk insteadof B;
        B::bigTalk as talk;
    }
}
?>

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

<?php
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 中的部分或全部成员。

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

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

trait HelloWorld {
    use Hello, World;
}

class MyHelloWorld {
    use HelloWorld;
}

$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
?>

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

<?php
trait Hello {
    public function sayHelloWorld() {
        echo 'Hello'.$this->getWorld();
    }
    abstract public function getWorld();
}

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

Trait 的静态成员Traits 可以被静态成员静态方法定义。

<?php
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
?>

Example using trait:
<?php
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
?>

属性Trait 同样可以定义属性。
Trait 定义了一个属性后,类就不能定义同样名称的属性,否则会产生 fatal error。 有种情况例外:属性是兼容的(同样的访问可见度、初始默认值)。 在 PHP 7.0 之前,属性是兼容的,则会有 E_STRICT 的提醒。

Example #12 解决冲突

<?php
trait PropertiesTrait {
    public $same = true;
    public $different = false;
}

class PropertiesExample {
    use PropertiesTrait;
    public $same = true; // PHP 7.0.0 后没问题,之前版本是 E_STRICT 提醒
    public $different = true; // 致命错误
}
?>

重载(术语滥用)

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

当调用当前环境下未定义或不可见的类属性或方法时,重载方法会被调用。本节后面将使用"不可访问属性(inaccessible properties)"和"不可访问方法(inaccessible methods)"来称呼这些未定义或不可见的类属性或方法。

This is a misuse of the term overloading. This article should call this technique "interpreter hooks".

参加魔术方法php超全局变量,魔术常量,魔术方法

Note:
因为 PHP 处理赋值运算的方式,__set() 的返回值将被忽略。类似的, 在下面这样的链式赋值中,__get() 不会被调用: $a = $obj->b = 8;

Note:
在除 isset() 外的其它语言结构中无法使用重载的属性,这意味着当对一个重载的属性使用 empty() 时,重载魔术方法将不会被调用。
为避开此限制,必须将重载属性赋值到本地变量再使用 empty()

遍历对象

1.用 foreach 语句。默认情况下,所有可见属性都将被用于遍历。

<?php
foreach(new class(10) {
    public $public = [];
    protected $protected = [3, 4, 5];
    private $private = [6, 7, 8];
    function __construct($value = 1)
    {
        for ($i=0; $i < $value; $i++) { 
            $this->public[] = $i;
        }
    }
    function __destruct()
    {
        foreach ($this as $key => list($a, $b, $c)) {
            print "$key => [$a, $b, $c]\n";
        }
    }
} as $key => list($a, $b, $c)) {
    print "$key => [$a, $b, $c]\n";
}
echo "\n";

//Result:
/*
public => [0, 1, 2]
public => [0, 1, 2]
protected => [3, 4, 5]
private => [6, 7, 8]
 */

2.实现 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";
}
?>

可以用 IteratorAggregate 接口以替代实现所有的 Iterator 方法。IteratorAggregate 只需要实现一个方法 IteratorAggregate::getIterator()其应返回一个实现了 Iterator 的类的实例

Example #3 通过实现 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";
}
?>
PHP 5.5 及以后版本的用户也可参考生成器,其提供了另一方法来定义 Iterators。

魔术方法

参见php超全局变量,魔术常量,魔术方法

final

如果父类中的方法被声明为 final,则子类无法覆盖该方法。如果一个类被声明为 final,则不能被继承

属性不能被定义为 final,只有类和方法才能被定义为 final。可以使用 const 定义为常量来代替。

对象复制(clone)

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

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

对象比较

==:如果两个对象的属性和属性值都相等,而且两个对象是同一个类的实例,那么这两个对象变量相等。
===:这两个对象变量一定要指向某个类的同一个实例(即同一个对象,但不需要引用赋值)。

<?php
function compareObjects(&$o1, &$o2)
{
    echo 'o1 == o2 : ' , var_export($o1 == $o2) . "\r\n";
    echo 'o1 === o2 : ' , var_export($o1 === $o2) . "\r\n";
}

class Flag
{
    public $flag;
    function __construct($flag = true) { $this->flag = $flag; }
}

class OtherFlag
{
    public $flag;
    function __construct($flag = true) { $this->flag = $flag; }
}

$o = new Flag;
$p = new Flag;
$q = $o;
$r = new OtherFlag;
$s = new Flag(false);

echo "Two instances of the same class\r\n";
compareObjects($o, $p);

echo "\r\nTwo references to the same instance\r\n";
compareObjects($o, $q);

echo "\r\nInstances of two different classes\r\n";
compareObjects($o, $r);

echo "Two instances of the same class\r\n";
compareObjects($o, $s);

//Result:
/*
Two instances of the same class
o1 == o2 : true
o1 === o2 : false

Two references to the same instance
o1 == o2 : true
o1 === o2 : true

Instances of two different classes
o1 == o2 : false
o1 === o2 : false

Two instances of the same class
o1 == o2 : false
o1 === o2 : false
 */
?>

后期静态绑定

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

Example #2 static:: 简单用法

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

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

B::test();
//AB
?>

Finally we can implement some ActiveRecord methods:

<?php 

class Model 
{ 
    public static function find() 
    { 
        echo static::$name; 
    } 
} 

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

Product::find(); 

?> 

Output: 'Product'

对象和引用

Notes on reference:
A reference is not a pointer. However, an object handle IS a pointer. Example:
<?php
class Foo {
  private static $used;
  private $id;
  public function __construct() {
    $id = $used++;
  }
  public function __clone() {
    $id = $used++;
  }
}

$a = new Foo; // $a is a pointer pointing to Foo object 0
$b = $a; // $b is a pointer pointing to Foo object 0, however, $b is a copy of $a
$c = &$a; // $c and $a are now references of a pointer pointing to Foo object 0
$a = new Foo; // $a and $c are now references of a pointer pointing to Foo object 1, $b is still a pointer pointing to Foo object 0
unset($a); // A reference with reference count 1 is automatically converted back to a value. Now $c is a pointer to Foo object 1
$a = &$b; // $a and $b are now references of a pointer pointing to Foo object 0
$a = NULL; // $a and $b now become a reference to NULL. Foo object 0 can be garbage collected now
unset($b); // $b no longer exists and $a is now NULL
$a = clone $c; // $a is now a pointer to Foo object 2, $c remains a pointer to Foo object 1
unset($c); // Foo object 1 can be garbage collected now.
$c = $a; // $c and $a are pointers pointing to Foo object 2
unset($a); // Foo object 2 is still pointed by $c
$a = &$c; // Foo object 2 has 1 pointers pointing to it only, that pointer has 2 references: $a and $c;
const ABC = TRUE;
if(ABC) {
  $a = NULL; // Foo object 2 can be garbage collected now because $a and $c are now a reference to the same NULL value
} else {
  unset($a); // Foo object 2 is still pointed to $c
}

命名空间

定义命名空间

虽然任意合法的PHP代码都可以包含在命名空间中,但只有以下类型的代码受命名空间的影响,它们是:(包括抽象类traits)、接口函数常量

命名空间通过关键字 namespace 来声明。如果一个文件中包含命名空间,它必须在其它所有代码之前声明命名空间,除了一个以外:declare关键字

define() will define constants exactly as specified:

Regarding constants defined with define() inside namespaces...

define() will define constants exactly as specified.  So, if you want to define a constant in a namespace, you will need to specify the namespace in your call to define(), even if you're calling define() from within a namespace.  The following examples will make it clear.

The following code will define the constant "MESSAGE" in the global namespace (i.e. "\MESSAGE").

<?php
namespace test;
define('MESSAGE', 'Hello world!');
?>

The following code will define two constants in the "test" namespace.

<?php
namespace test;
define('test\HELLO', 'Hello world!');
define(__NAMESPACE__ . '\GOODBYE', 'Goodbye cruel world!');

echo \test\HELLO, PHP_EOL;//Hello world!
echo namespace\HELLO, PHP_EOL;//Hello world!
echo constant('\\'. __NAMESPACE__ . '\HELLO') , PHP_EOL;//Hello world!
echo constant(__NAMESPACE__ . '\HELLO') , PHP_EOL;//Hello world!
echo HELLO, PHP_EOL;//Hello world!
echo __NAMESPACE__, PHP_EOL;//test
?>

在同一个文件中定义多个命名空间(不建议)

也可以在同一个文件中定义多个命名空间。在同一个文件中定义多个命名空间有两种语法形式:简单组合语法大括号语法

Example #4 定义多个命名空间和不包含在命名空间中的代码

<?php
declare(encoding='UTF-8');
namespace MyProject {

const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */  }
}

namespace { // 全局代码
session_start();
$a = MyProject\connect();
echo MyProject\Connection::start();
}
?>

使用命名空间:基础

(PHP 5 >= 5.3.0, PHP 7)
在讨论如何使用命名空间之前,必须了解 PHP 是如何知道要使用哪一个命名空间中的元素的。可以将 PHP 命名空间与文件系统作一个简单的类比。在文件系统中访问一个文件有三种方式:

  1. 相对文件名形式foo.txt。它会被解析为 currentdirectory/foo.txt,其中 currentdirectory 表示当前目录。因此如果当前目录是 /home/foo,则该文件名被解析为/home/foo/foo.txt
  2. 相对路径名形式subdirectory/foo.txt。它会被解析为 currentdirectory/subdirectory/foo.txt
  3. 绝对路径名形式/main/foo.txt。它会被解析为/main/foo.txt

PHP 命名空间中的元素使用同样的原理。例如,类名可以通过三种方式引用:

  1. 非限定名称,或不包含前缀的类名称,例如 $a=new foo(); 或 foo::staticmethod();。如果当前命名空间是 currentnamespacefoo 将被解析为 currentnamespace\foo。如果使用 foo 的代码是全局的,不包含在任何命名空间中的代码,则 foo 会被解析为foo。 警告:如果命名空间中的函数或常量未定义,则该非限定的函数名称或常量名称会被解析为全局函数名称或常量名称。详情参见 使用命名空间:后备全局函数名称/常量名称
  2. 限定名称,或包含前缀的名称,例如 $a = new subnamespace\foo(); 或 subnamespace\foo::staticmethod();。如果当前的命名空间是 currentnamespace,则 foo 会被解析为 currentnamespace\subnamespace\foo。如果使用 foo 的代码是全局的,不包含在任何命名空间中的代码,foo 会被解析为subnamespace\foo
  3. 完全限定名称,或包含了全局前缀操作符的名称,例如, $a = new \currentnamespace\foo(); 或 \currentnamespace\foo::staticmethod();。在这种情况下,foo 总是被解析为代码中的文字名(literal name)currentnamespace\foo

下面是一个使用这三种方式的实例:

file1.php

<?php
namespace Foo\Bar\subnamespace;

const FOO = 1;
function foo() {}
class foo
{
    static function staticmethod() {}
}
?>

file2.php

<?php
namespace Foo\Bar;
include 'file1.php';

const FOO = 2;
function foo() {}
class foo
{
    static function staticmethod() {}
}

/* 非限定名称 */
foo(); // 解析为 Foo\Bar\foo resolves to function Foo\Bar\foo
foo::staticmethod(); // 解析为类 Foo\Bar\foo的静态方法staticmethod。resolves to class Foo\Bar\foo, method staticmethod
echo FOO; // resolves to constant Foo\Bar\FOO

/* 限定名称 */
subnamespace\foo(); // 解析为函数 Foo\Bar\subnamespace\foo
subnamespace\foo::staticmethod(); // 解析为类 Foo\Bar\subnamespace\foo,
                                  // 以及类的方法 staticmethod
echo subnamespace\FOO; // 解析为常量 Foo\Bar\subnamespace\FOO
                                  
/* 完全限定名称 */
\Foo\Bar\foo(); // 解析为函数 Foo\Bar\foo
\Foo\Bar\foo::staticmethod(); // 解析为类 Foo\Bar\foo, 以及类的方法 staticmethod
echo \Foo\Bar\FOO; // 解析为常量 Foo\Bar\FOO
?>

注意访问任意全局类、函数或常量,都可以使用完全限定名称,例如 \strlen()\Exception\INI_ALL

Example #1 在命名空间内部访问全局类、函数和常量

<?php
namespace Foo;

function strlen() {}
const INI_ALL = 3;
class Exception {}

$a = \strlen('hi'); // 调用全局函数strlen
$b = \INI_ALL; // 访问全局常量 INI_ALL
$c = new \Exception('error'); // 实例化全局类 Exception
?>

命名空间和动态语言特征

php.php

<?php
namespace testt;

class cls {}
function func($value='') {}
const VAL = __NAMESPACE__;
 ?>

test.php

<?php
namespace test {
    include 'php.php';
    //new testt\cls;    //Fatal error: Uncaught Error: Class 'test\testt\cls' not found in D:\php\test\test.php:4
    //new cls;  //Fatal error: Uncaught Error: Class 'test\cls' not found in D:\php\test\test.php:5
    new \testt\cls;
    echo \testt\VAL, PHP_EOL;
    \testt\func();
}

namespace tests {
    use testt\cls;
    use testt\VAL;//未报错,也没效果
    use testt\func;//未报错,也没效果
    //new testt\cls;    //Fatal error: Uncaught Error: Class 'test\testt\cls' not found in D:\php\test\test.php:4
    new cls;
    new \testt\cls;
    echo \testt\VAL, PHP_EOL;
    //echo VAL, PHP_EOL;    //Notice:  Use of undefined constant VAL - assumed 'VAL' in D:\php\test\test.php on line 19
    \testt\func();
    //func();   //Fatal error:  Uncaught Error: Call to undefined function test\func() in D:\php\test\test.php:21
}

namespace {    
    //new cls;  //Fatal error: Uncaught Error: Class 'cls' not found in D:\php\test\test.php:12
    new \testt\cls;
    new testt\cls;
    echo testt\VAL, PHP_EOL;
    testt\func();
}

namespace {
    use testt\cls;
    use testt\VAL;//未报错,也没效果
    use testt\func;//未报错,也没效果
    new cls;
    new \testt\cls;
    new testt\cls;
    echo testt\VAL, PHP_EOL;
    testt\func();
    //func();   //Fatal error:  Uncaught Error: Call to undefined function func() in D:\php\test\test.php:41
}

//Result
/*
testt
testt
testt
testt
 */

namespace关键字和__NAMESPACE__常量

常量__NAMESPACE__的值是包含当前命名空间名称的字符串。在全局的,不包括在任何命名空间中的代码,它包含一个空的字符串。
关键字 namespace 可用来显式访问当前命名空间或子命名空间中的元素。它等价于类中的 self 操作符。

<?php
namespace test;

class cls {}
function func($value='') {}
const VAL = __NAMESPACE__;

namespace\func();
echo namespace\VAL;//test
echo constant(__NAMESPACE__ . '\VAL');//test
new namespace\cls;

使用命名空间:别名/导入

别名是通过操作符 use 来实现的:所有支持命名空间的PHP版本支持三种别名或导入方式:为类名称使用别名为接口使用别名为命名空间名称使用别名

PHP 5.6开始允许导入函数常量或者为它们设置别名。

Example #1 使用use操作符导入/使用别名

<?php
namespace foo;
use My\Full\Classname as Another;

//为命名空间设置别名
// 下面的例子与 use My\Full\NSname as NSname 相同
use My\Full\NSname;

// 导入一个全局类
use ArrayObject;

// importing a function (PHP 5.6+)
use function My\Full\functionName;

// aliasing a function (PHP 5.6+)
use function My\Full\functionName as func;

// importing a constant (PHP 5.6+)
use const My\Full\CONSTANT;

$obj = new namespace\Another; // 实例化 foo\Another 对象
$obj = new Another; // 实例化 My\Full\Classname 对象
NSname\subns\func(); // 调用函数 My\Full\NSname\subns\func
$a = new ArrayObject(array(1)); // 实例化 ArrayObject 对象
// 如果不使用 "use \ArrayObject" ,则实例化一个 foo\ArrayObject 对象
func(); // calls function My\Full\functionName
echo CONSTANT; // echoes the value of My\Full\CONSTANT
?>
对命名空间中的名称(包含命名空间分隔符的完全限定名称如 Foo\Bar以及相对的不包含命名空间分隔符的全局名称如 FooBar)来说,前导的反斜杠是不必要的也不推荐的,因为导入的名称必须是完全限定的,不会根据当前的命名空间作相对解析。

导入操作只影响非限定名称和限定名称。完全限定名称由于是确定的,故不受导入的影响。

全局空间

如果没有定义任何命名空间,所有的类与函数的定义都是在全局空间,与 PHP 引入命名空间概念前一样。在名称前加上前缀 \ 表示该名称是全局空间中的名称,即使该名称位于其它的命名空间中时也是如此。

使用命名空间:后备全局函数/常量

在一个命名空间中,当 PHP 遇到一个非限定的类、函数或常量名称时,它使用不同的优先策略来解析该名称。类名称总是解析到当前命名空间中的名称。因此在访问系统内部或不包含在命名空间中的类名称时,必须使用完全限定名称,例如:

<?php
namespace test;

class Exception extends \Exception {
    protected $message = 'My Exception';
}

try {
    throw new Exception;//如果未定义Exception类,则会报致命错误。
} catch (\Exception $e) {
    echo $e->getMessage();
} finally {
    echo PHP_EOL, "finally do";
}

//Result
/*
My Exception
finally do
 */

名称解析规则

在说明名称解析规则之前,我们先看一些重要的定义:

命名空间名称定义

  • 非限定名称Unqualified name

名称中不包含命名空间分隔符的标识符,例如 Foo

  • 限定名称Qualified name

名称中含有命名空间分隔符的标识符,例如 Foo\Bar

  • 完全限定名称Fully qualified name

名称中包含命名空间分隔符,并以命名空间分隔符开始的标识符,例如 \Foo\Barnamespace\Foo 也是一个完全限定名称。

名称解析遵循下列规则:

  1. 对完全限定名称的函数,类和常量的调用在编译时解析。例如 new \A\B 解析为类 A\B
  2. 所有的非限定名称和限定名称(非完全限定名称)根据当前的导入规则在编译时进行转换。例如,如果命名空间 A\B\C 被导入为 C,那么对 C\D\e() 的调用就会被转换为 A\B\C\D\e()
  3. 在命名空间内部,所有的没有根据导入规则转换的限定名称均会在其前面加上当前的命名空间名称。例如,在命名空间 A\B 内部调用 C\D\e(),则 C\D\e() 会被转换为 A\B\C\D\e()
  4. 非限定类名根据当前的导入规则在编译时转换(用全名代替短的导入名称)。例如,如果命名空间 A\B\C 导入为C,则 new C() 被转换为 new A\B\C()
  5. 在命名空间内部(例如A\B),对非限定名称的函数调用是在运行时解析的。例如对函数 foo()
    的调用是这样解析的:

    1. 在当前命名空间中查找名为 A\B\foo() 的函数
    2. 尝试查找并调用 全局(global) 空间中的函数 foo()
  6. 在命名空间(例如A\B)内部对非限定名称或限定名称类(非完全限定名称)的调用是在运行时解析的。下面是调用 new C()new D\E() 的解析过程: new C()的解析:

    1. 在当前命名空间中查找A\B\C类。
    2. 尝试自动装载类A\B\C

new D\E()的解析:

  1. 在类名称前面加上当前命名空间名称变成:A\B\D\E,然后查找该类。
  2. 尝试自动装载类 A\B\D\E

为了引用全局命名空间中的全局类,必须使用完全限定名称 new \C()


Maxiye
648 声望19 粉丝