PHP-OPaP CH.4:高级特性

静态方法和属性

声明类属性或方法为静态的,就可以不用实例化类而直接访问,使用 static 关键字进行声明。

静态属性不能通过一个类已经实例化的对象来访问,静态方法可以。

1
2
3
4
5
6
7
8
9
// 例子
class StaticExample {
//静态属性
static public $aNum = 0;
//静态方法
static public function sayHello() {
print 'Hello';
}
}

静态方法是以静态类作为作用域的函数。它只能访问静态属性,因为普通属性属于对象的。

外部通过类访问静态元素的方法: 类名::属性[方法],通过 :: (两个冒号)来连接类名和属性(方法)。

1
2
3
// 示例
print StaticExample::$aNum;
StaticExample::sayHello();

当前类中访问静态方法或属性, 需要使用 self 关键字。self 指向当前类。 语法: self::属性名(方法名)

1
2
3
4
5
6
7
class StaticExample {
static public $aNum = 0;
static public function sayHello() {
self::$aNum++;
print "hello (" . self::$aNum . ") \n";
}
}

注解:

  • 只有在使用 parent 关键字调用方法时,才能对一个非静态方法进行静态形式的调用
  • 在文档中会经常看到使用 static 语法来引用方法或属性,这只是表名它属于特定的类(例如: ShopProductWriter::write() 表示为 ShopProductWriter 类的方法 write() ),这是文档中的惯例写法,在代码中不能这样写,否则会出错

常量属性

可以在类中定义常量属性, 类常量一旦设置后将不能改变。使用 const 关键字来声明。

按照惯例,只能用大写字母来命名常量

1
2
3
4
5
// 示例
class ShopProduct {
const AVAILABLE = 0;
const OUT_OF_STOCK = 1;
}

常量属性只能抱憾基本数据类型的值。
和静态属性一样,只能通过类访问常量属性;引用常量时不需要用 $ 作为前导符

print ShopProduct::AVAILABLE;

给已经声明过的常量赋值会引起解析错误

抽象类(abstract class)

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

abstract 关键字定义一个抽象类或抽象方法

1
2
3
4
5
6
7
8
9
10
//声明抽象类
abstract class ShopProductWriter {
protected $product = array();
public function addProduct(ShopProduct $shopProduct) {
$this->product[] = $shopProduct;
}
// 抽象方法
abstract public function write();
}

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

  • 抽象类的每个子类都必须实现抽象类中的所有抽象方法
  • 必须重新声明方法
  • 新的实现方法的访问控制不能比抽象方法的访问控制更严格
  • 新的实现方法的参数个数和抽象方法的参数个数必须一致
1
2
3
4
5
6
7
8
9
10
11
class xml extends ShopProductWriter {
public function write() {
print '<?xml><name>xml</name>';
}
}
class text extends ShopProductWriter {
public function write() {
print 'name: text';
}
}

接口(interface)

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

interface 关键字声明,接口可以包含属性和方法声明,但是方法体为空

1
2
3
4
// 声明一个'Chargeable' 接口
interface Chargeable {
public function getPrice();
}

任何现实接口的类必须实现接口中所定义的所有方法;类可以实现多个接口,用逗号分隔多个接口的名称

一个类可以在声明中使用 implements 关键字来实现某个接口

1
2
3
4
5
class ShopProduct implements Chargeable {
public function getPrice() {
return 'ddd';
}
}

延迟静态绑定: static 关键字

self 指的是当前类的静态引用,取决于定义当前方法所在的类(解析上下文)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {
public static function who() {
echo __CLASS__; //输出类名
}
public static function test() {
self::who();
}
}
class B extends A {
public static function who() {
echo __CLASS__;
}
}
B::test(); //结果为A,因为self指向定义的类(包含的类),而非当前类

static 指向的是被调用的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

static 还可以作为静态方法调用的标志符, 在非静态环境被调用(在非静态环境下,所调用的类即为该对象实力所属的类)

异常(exception)

异常是 PHP5 内置的 Exception 类(或其子类) 实例化得到的特殊对象. 用于存放和报告错误信息.

Exception 类的构造方法介绍两个可选参数: 消息字符串 和 错误代码

Exception 类提供了一些方法来分析错误条件

方法 描述
getMessage() 获得传递给构造方法的消息字符串
getCode() 获得传递给构造方法的错误代码
getFile() 获得产生异常的文件
getLine() 获得生成异常的行号
getPrevious() 获得一个嵌套的异常对象
getTrace() 获得一个多维数组(该数组追踪导致异常的方法调用, 包含方法、类、文件和参数数据)
getTraceAsString() 获得 getTrace() 返回数据的字符串版本
__toString() 返回一个描述异常细节的字符串(在字符串中使用 Exception d对象时自动调用)

抛出异常

联合使用 throw 关键字和 Exception 对象来抛出异常. 这会停止执行当前方法, 并负责将错误返回给调用代码.

1
2
3
4
5
6
7
8
// 示例
function __construct($file) {
$this->file = $file;
if (!file_exists($file)) {
throw new Exception("file '$file' does not exist");
}
$this->xml = simplexml_load_file($file);
}

如果调用可能会抛出异常的方法, 可以将调用语句放在 try 子句中(try 子句try 关键字及其后的大括号组成), try 子句后面必须跟着至少一个的 catch 子句 才能处理错误.

1
2
3
4
5
6
7
8
// 示例
try {
$conf = new Conf(dirname(__FILE__)."/conf01.xml");
$conf->set("post", "newpass");
$conf->write();
} catch (Exception $e) {
die($e->__toString());
}

异常捕获时会停止执行类方法, 控制权从 try 子句 移交给 catch 子句.

异常的子类化

用户可以创建自定义的异常类(从 Exception 类继承)

  • 扩展异常类的功能
  • 子类定义了新的异常类型, 可以进行自己特有的错误处理

定义多个 catch 子句时, 只需一个 try语句. 调用哪一个 catch 子句 取决于抛出异常的类型和参数中类的类型提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//示例
// 定义 Exception 子类
class xmlException extends Exception {
private $error;
// SimpleXml 扫描到一个损坏的 XML 文件时, 会生成 LibXmlError 对象
function __construct(LibXmlError $error) {
$shortfile = basename($error->file);
$msg = "[{$shortfile}, line {$error->line}, col {$error->column}] {$error->message}";
$this->error = $error;
parent::__construct($msg, $error->code);
}
function getLibXmlError() {
return $this->error;
}
}
class FileException extends Exception {}
// 抛出异常
function __construct($file) {
$this->file = $file;
if(!file_exists($file)) {
throw new FileException("not exist");
}
$this->xml = simplexml_load_file($file, null, LIBXML_NOERROR);
if(!is_object($this->xml)) {
throw new xmlException(libxml_get_last_error());
}
}
// 捕捉错误
class Runner {
static function init() {
try {
.....
} catch (FileException $e) {
// 文件权限问题或者文件不存在
} catch (xmlException $e) {
// xml 文件损坏
} catch (Exception $e) {
// 后备捕捉器
}
}
}

后备 (backstop) 子句, 以防开发时在代码中要增加的新的异常

当方法检测到错误却没有足够的信息来智能地处理异常时, 应该抛出异常.(让类知道过多内容, 将会失去重点并且变得难以重用)

Final 类和方法

如果希望类或者方法完成确定不变的功能, 担心覆写它会破坏这个功能, 可以使用 final 关键字.

final 关键字可以终止类的继承(final 类不能有子类, final 方法不能被覆写)

1
2
3
4
5
6
7
8
9
final class Checkout {
// 继承 Checkout 类, 会引起致命错误
}
class Checkout {
final function totalize() {
// 可以继承 Checkout 类, 但覆写这个方法会引起致命错误
}
}

final 关键字应该放在其他修饰词之前

使用拦截器 (interceptor)

拦截发送到未定义方法和属性的消息, 也被称为重载 (overloading)

重载通过魔术方法 (magic methods) 来实现

魔术方法 描述
__get($property) 访问未定义的属性时被调用
__set($property, $value) 给未定义的属性赋值时被调用
__isset($property) 对未定义的属性调用 isset() 时被调用
__unset($property) 对未定义的属性调用 unset() 时被调用
__call($method, $arg_array) 调用未定义的方法时被调用

拦截器非常有用, 但在使用时要慎重考虑, 最好附上文档, 清楚地说明代码的细节

析构方法

在实例化对象时, __construct() 方法会被自动调用.

在对象被垃圾收集器收集前(即对象从内存中删除之前), __destruct() 方法会被自动调用

利用这个方法可以进行最后必要的清理工作

使用 __clone() 复制对象

1
2
3
4
// 复制示例
class CopyMe {}
$first = new CopyMe();
$second = clone $first;

当对象被复制后,PHP 5 会对对象的所有属性执行一个浅复制(shallow copy)。所有的引用属性 仍然会是一个指向原来的变量的引用
(假设被复制的对象 (A)与数据库中的某一条记录对应的,那么复制的对象(B)也将与该记录对应,这样修改对象 B 的记录将会影响到对象 A)

__clone() 方法,可用于修改属性的值。在一个对象上调用 clone 关键字时,该方法会被自动调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
private $name;
private $age;
private $id;
public $account; //引用另外一个对象
function __construct($name, $age) {
$this->name = $name;
$this->age = $age;
}
function setId($id) {
$this->id = $id;
}
function __clone() {
$this->id = 0;
$this->account = clone $this->account //强制复制, 否则仍然指定同一个对象
}
}

定义对象的字符串值

php 5.2 起,直接打印对象将产生致命错误。

__toString() 方法,当对象传递给 printecho 时,会自动调用这个方法,并用方法的返回值控制输出字符串的格式(应当返回一个字符串值)来替代默认的输出内容。

1
2
3
4
5
6
7
8
9
10
11
class Person {
function getName() {return 'Bob';}
function getAge() {return 44;}
function __toString() {
$desc = this->getName . ' (age ' . $this->getAge(). ')';
return $desc;
}
}
$person = new Person();
print $person //Bob (age 44)

对于日志和错误报告, __toString() 方法非常有用。
也可用于设计专门用来传递信息的类,比如将关于异常数据的总结信息写到该方法中

回调、匿名函数和闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ProcessSale {
private $callbacks;
function registerCallback($callback) {
if(!is_callable($callback)) {
throw new Exception('callback not callback');
}
$this->callbacks[] = $callback;
}
function sale($product) {
print "{$product->name}: processing";
foreach ($this->callbacks as $callbacks) {
call_user_func($callback, $product);
}
}
}

该代码用于运行各种回调。

  • registerCallback() 方法用于测试标量并将其添加到回调数组中。is_callable() 函数确保传递进来的值能被call_user_func() 等函数调用
  • sale 方法接受一个 product 对象,输出与该对象有关的一条信息,然后遍历 $callbacks 数组属性. 将对象传递给 $callback 函数
  • call_user_func() 把第一个参数作为回调函数调用,其余参数时回调函数的参数

利用回调,可以在运行时将与组件的核心人物没有直接关系的功能插入到组件中(有了组件回调,可以赋予其他人在不知道上下文中扩展代码的权利)

1
2
3
4
// 5.3 之后匿名函数的实现
$logger = function($product) {
print "logging ({$product->name})";
};
  • 匿名函数可以存储在一个变量中
  • 以内联方式使用 function 关键字并且没有函数名.
  • 因为是内联语句,需要在代码块的末尾使用分号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 匿名回调 示例
class Product {
public $name;
public $price;
function __construct($name, $price) {
$this->name = $name;
$this->price = $price;
}
}
$processor = new ProcessSale();
$processor->registerCallback($logger);
$processor->sale(new Product('shoes', 6));
$processor->sale(new Product('coffee', 6));
/* 结果
shoes: processing
logging (shoes)
coffee: processing
logging (coffee)
*/

回调并不是一定是匿名的,还可以使用函数名、对象引用和方法

使用对象和方法的回调,需要使用数组形式(对象作为第一个元素, 方法名作为第二个元素)

1
2
3
4
5
6
7
8
class Mailer {
function doMail($product) {
print 'mailing ({$product->name})';
}
}
$processor = new ProcessSale();
$processor->registerCallback(array(new Mailer(), 'doMail'));

还可以让方法返回匿名函数,利用闭包(新风格的匿名函数)可以引用在其父作用域中声明的变量。

利用 use 子句 让匿名函数追踪来自其父作用域的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Totalizer {
static function warnAmount($amt) {
$count = 0;
return function($product) use ($amt, &$count) {
$count += $product->price;
print "count: $count";
if ($count > $amt) {
print "high price reached: {$count}";
}
};
}
}
$processor = new ProcessSale();
$processor->registerCallback(Totalizer::warnAmount(8));
$processor->sale(new Product('shoes', 6));
$processor->sale(new Product('coffee', 6);
/* 结果
shoes: processing
count: 6
coffee: processing
count: 12
high price reached: 12
*/
  • &$count 表示通过引用传递给函数,还不是值传递