图源:
匿名类
匿名类(anonymouse classes)可以用于创建一次性对象,这在大量使用设计模式的框架代码中很常见,比如Java的图形框架。这里用一个精简的图形框架代码进行说明:
<?php
/**
* 鼠标监听事件接口
*/
interface OnclickListener
{
/**
* 鼠标点击事件
* @param $mouse 鼠标
* @param $view 图形UI控件
*/
public function onclick(mixed $mouse, mixed $view): void;
}
/**
* 基础的图形控件
*/
class View
{
protected ?OnclickListener $listener = null;
public function set_onclick_listener(OnclickListener $listener)
{
$this->listener = $listener;
}
public function click(mixed $mouse)
{
$this->listener->onclick($mouse, $this);
}
}
/**
* 按钮
*/
class Button extends View
{
}
如果要使用这些UI组件,传统的方式可能是这样:
<?php
require_once './ui.php';
$btn = new Button;
class MyOnclickListener implements OnclickListener
{
public function onclick(mixed $mouse, mixed $view): void
{
echo "MyOnclickListener::onclick is called." . PHP_EOL;
}
}
$btn->set_onclick_listener(new MyOnclickListener());
$btn->click(null);
// MyOnclickListener::onclick is called.
我们先要编写一个实现了OnclickListener
的类,并实现onclick
方法,再新建实例并作为参数传给按钮的set_onclick_listener
方法。这里的问题在于这里创建的MyOnclickListener
类往往只会在这里使用一次,因为每个按钮的点击行为往往是不一样的,是无需复用的。这不仅仅是花费了大量代码在构建一个只会用一次的类的问题,还意味着这个一次性类名污染了当前命名空间。
这些都可以通过使用匿名类来避免:
<?php
require_once './ui.php';
$btn = new Button;
$btn->set_onclick_listener(new class implements OnclickListener{
public function onclick(mixed $mouse, mixed $view): void
{
echo "nonymouse classes's onclick method is called.".PHP_EOL;
}
});
$btn->click(null);
// nonymouse classes's onclick method is called.
除了这种常见的使用方式外,可能也会在类中使用,此时往往需要访问所属类的属性或方法:
<?php
require_once './ui.php';
class MyButton extends Button
{
protected string $btnType = 'MyButton';
public function __construct(private string $btnName)
{
}
public function click(mixed $mouse)
{
if (is_null($this->listener)) {
$this->set_onclick_listener(new class implements OnclickListener
{
public function onclick(mixed $mouse, mixed $view): void
{
echo "nonymouse classes's onlick method is called." . PHP_EOL;
}
});
}
parent::click($mouse);
}
}
$btn = new Mybutton("my button");
$btn->click(null);
这里通过自定义的MyButton
类扩展了Button
类,并在click
方法中检查是否设置了listener
,如果没设置,就自定义一个listener
。
假设我们需要在这个自定义listener
中访问外部类实例的私有属性$btnName
,可以通过参数传递的方式实现:
<?php
require_once './ui.php';
class MyButton extends Button
{
...
public function click(mixed $mouse)
{
if (is_null($this->listener)) {
$this->set_onclick_listener(new class($this->btnName) implements OnclickListener
{
public function __construct(private string $btnName)
{
}
public function onclick(mixed $mouse, mixed $view): void
{
echo "nonymouse classes's onlick method is called." . PHP_EOL;
echo "the button name is {$this->btnName}." . PHP_EOL;
}
});
}
parent::click($mouse);
}
}
$btn = new Mybutton("my button");
$btn->click(null);
// nonymouse classes's onlick method is called.
// the button name is my button.
new class($this->btnName)
就相当于new clsName($this->btnName)
,也就是说通过匿名类的构造函数传递$this->btnName
。
如果是访问外部类的public
或protected
属性和方法,可以让匿名类继承外部类,相应的属性和方法可以继承后使用:
<?php
require_once './ui.php';
class MyButton extends Button
{
...
public function click(mixed $mouse)
{
if (is_null($this->listener)) {
$this->set_onclick_listener(new class($this->btnName) extends MyButton implements OnclickListener
{
public function __construct(private string $btnName)
{
}
public function onclick(mixed $mouse, mixed $view): void
{
echo "nonymouse classes's onlick method is called." . PHP_EOL;
echo "the button name is {$this->btnName}." . PHP_EOL;
echo "the button type is {$this->btnType}." . PHP_EOL;
}
});
}
parent::click($mouse);
}
}
$btn = new Mybutton("my button");
$btn->click(null);
// nonymouse classes's onlick method is called.
// the button name is my button.
// the button type is MyButton.
php的这种匿名类访问外部类的方式相比Java就麻烦很多,事实上在Java中,内部类可以简单地通过
OutClsName.this.attrName
的方式访问。
最后要说明的是,匿名类的类名是Zend引擎临时生成的:
<?php
echo get_class(new class{}).PHP_EOL;
// class@anonymousD:\workspace\http\php-notes\ch12\anonymouse.php:2$0
所以不能将匿名类的类名直接用于代码逻辑,但可以用于比对两个匿名类实例是否属于同一个匿名类:
function create_anonymouse_class_obj(){
return new class{};
}
$ac1 = create_anonymouse_class_obj();
$ac2 = create_anonymouse_class_obj();
if (get_class($ac1) === get_class($ac2)) {
echo "ac1 and ac2 is from same anonymouse class." . PHP_EOL;
}
// ac1 and ac2 is from same anonymouse class.
但需要注意的是,“同一个匿名类”和“结构相同的匿名类”是完全不同的概念:
$ac1 = new class
{
};
$ac2 = new class
{
};
if (get_class($ac1) === get_class($ac2)) {
echo "ac1 and ac2 is from same anonymouse class." . PHP_EOL;
}
echo get_class($ac1).PHP_EOL;
echo get_class($ac2).PHP_EOL;
// class@anonymousD:\workspace\http\php-notes\ch12\anonymouse.php:6$1
// class@anonymousD:\workspace\http\php-notes\ch12\anonymouse.php:9$2
重载
php的重载并非传统意义的方法重载或者函数重载,而是提供一种“动态”创建和访问实例属性和方法的途径。
对重载是通过四个魔术方法实现的:
-
__set(string $name,mixed $value):void
-
__get(string $name):mixed
-
__isset(string $name):bool
-
__unset(string $name):void
一个使用重载的用途是实现缓存:
<?php
function fibnaci($n)
{
if ($n <= 2) {
return 1;
}
return fibnaci($n - 1) + fibnaci($n - 2);
}
class Fibnaci
{
public function __get($name)
{
if (strpos($name, 'index_') !== 0) {
throw new Exception("invalidate attrubte name of Fibanaci class.", E_ERROR);
}
$n = substr($name, strlen("index_"));
$n = intval($n);
$result = fibnaci($n);
$this->$name = $result;
return $result;
}
}
这里为请求斐波那契数列设计了一个支持缓存的类Fibnaci
,如果访问它的实例$fibnaci->index_5
,就会返回一个n=5
的斐波那契数列值。__get
魔术方法的作用为,如果访问的是不存在的属性,就会通过__get
方法调用fibnaci
函数来创建该属性并返回。也就是说只要请求过一次某个属性,第二次请求就不会再通过__get
方法,而是直接从对应的属性返回。
下面测试:
function microtime_float()
{
list($usec, $sec) = explode(" ", microtime());
return ((float)$usec + (float)$sec);
}
$fibnaci = new Fibnaci();
$start = microtime_float();
foreach (range(1, 30) as $val) {
$name = "index_{$val}";
echo $fibnaci->$name . " ";
}
echo PHP_EOL;
$end = microtime_float();
echo "use " . sprintf("%.2f", $end - $start) . "s" . PHP_EOL;
$start = microtime_float();
foreach (range(1, 30) as $val) {
$name = "index_{$val}";
echo $fibnaci->$name . " ";
}
echo PHP_EOL;
$end = microtime_float();
echo "use " . sprintf("%.2f", $end - $start) . "s" . PHP_EOL;
// 1 1 2 3 5 8 13 21 34 55 89 144 ...
// use 0.24s
// 1 1 2 3 5 8 13 21 34 55 89 144 ...
// use 0.00s
同样是生成2次30个斐波那契数列,第一次耗时0.24s,第二次接近0s,这就是缓存的威力。
__call
和__callStatic
魔术方法可以在调用不存在的方法或不存在的静态方法时触发:
<?php
class MyClass
{
public function __call($name, $arguments)
{
echo "the method MyClass::{$name}() is called." . PHP_EOL;
}
public static function __callStatic($name, $arguments)
{
echo "the static method MyClass::{$name}() is called." . PHP_EOL;
}
}
MyClass::no_defined_method(1, 2, 3);
// the static method MyClass::no_defined_method() is called.
$mc = new MyClass;
$mc->no_defined_method(1, 2, 3);
// the method MyClass::no_defined_method() is called.
遍历对象
在php中,可以使用foreach
遍历Iterable
伪类型,除此以外,还可以遍历对象:
<?php
class MyClass
{
public string $attr1 = 'val1';
public string $attr2 = 'val2';
public string $attr3 = 'val3';
protected string $attr4 = 'val4';
private string $attr5 = 'val5';
public function access_this()
{
foreach ($this as $attr_name => $attr_val) {
echo "\$this->$attr_name=$attr_val" . PHP_EOL;
}
}
}
$mc = new MyClass;
$mc->access_this();
// $this->attr1=val1
// $this->attr2=val2
// $this->attr3=val3
// $this->attr4=val4
// $this->attr5=val5
echo PHP_EOL;
foreach ($mc as $attr_name => $attr_val) {
echo "\$this->$attr_name=$attr_val" . PHP_EOL;
}
// $this->attr1=val1
// $this->attr2=val2
// $this->attr3=val3
就像示例中的那样,foreach
可以遍历当前可访问到的对象的属性。
魔术方法
魔术方法可以看作是内置的接口,实现某些魔术方法可以实现php文档中规定的相应功能。
php魔术方法的用途和效果和Python的协议相似。
可以为魔术方法添加类型声明,不过必须与php官方文档中定义的类型声明一致,否则会产生错误。
__sleep
和__wakeup
__sleep()
会在serilize()
调用时被调用。该函数的用途是在其所属的对象序列化之前,关闭其关联的资源,或者执行一些必要的清理工作,然后返回一个包含需要序列化的属性名称的数组。
__wakeup()
会在unserilize()
调用时被调用,其用途是将序列化时关闭的资源重新连接,将清理的环境恢复。
官方手册提供了一个数据库类的示例:
<?php
/**
* modified from https://www.php.net/manual/zh/language.oop5.magic.php
*/
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()
{
$con = mysqli_connect($this->server, $this->username, $this->password, $this->db);
if (!$con) {
throw new Exception("db connect error.", E_ERROR);
}
$this->link = $con;
}
public function __sleep()
{
return array('server', 'username', 'password', 'db');
}
public function __wakeup()
{
$this->connect();
}
public function qurey($sql)
{
$result = mysqli_query($this->link, $sql);
$lines = mysqli_fetch_all($result, MYSQLI_ASSOC);
mysqli_free_result($result);
return $lines;
}
public function __destruct()
{
mysqli_close($this->link);
}
}
我修改了官方示例,修改为了sqli
驱动的版本。
测试:
<?php
require_once './connection.cls.php';
require_once '../util/array.php';
function test_conn(Connection $conn)
{
$users = $conn->qurey("SELECT * FROM user");
foreach ($users as $user) {
print_arr($user);
}
}
$conn = new Connection('localhost', 'root', '', 'test');
test_conn($conn);
// [id:1, name:Jack Chen, age:20]
// [id:2, name:Brus Lee, age:15]
因为我们通过__wakeup
实现了反序列化时的数据库重连逻辑,所以经过序列化和反序列化的Connect
类依然可以正常使用:
...
$conn = new Connection('localhost', 'root', '', 'test');
$s_conn = serialize($conn);
echo "{$s_conn}".PHP_EOL;
$un_conn = unserialize($s_conn);
test_conn($un_conn);
// O:10:"Connection":4:{s:18:"Connectionserver";s:9:"localhost";s:20:"Connectionusername";s:4:"root";s:20:"Connectionpassword";s:0:"";s:14:"Connectiondb";s:4:"test";}
// [id:1, name:Jack Chen, age:20]
// [id:2, name:Brus Lee, age:15]
如果你的MySQL不支持
mysqli
驱动,可以通过修改数据库配置文件加载,具体方式可以参考。
__serialize
和__unserialize
__serialize
和__unserialize
方法的用途与__sleep
和__wakeup
类似,也会在序列化和反序列化时调用。区别是参数和返回值略有不同。下面是使用__serialize
和__unserialize
版本的Connect
类:
<?php
/**
* modified from https://www.php.net/manual/zh/language.oop5.magic.php
*/
class Connection
{
...
public function __serialize(): array
{
return array(
'server' => $this->server,
'name' => $this->username,
'pass' => $this->password,
'db' => $this->db,
);
}
public function __unserialize(array $data): void
{
$this->server = $data['server'];
$this->username = $data['name'];
$this->password = $data['pass'];
$this->db = $data['db'];
$this->connect();
}
...
}
完整代码和测试代码请前往代码仓库。
需要注意的是,如果类中同时定义了__serialize
和__sleep
,则序列化时后者会被忽略。类似的,如果同时定义了__unserialize
和__wakeup
,反序列化时后者同样会被忽略:
<?php
class MyClass
{
public function __serialize(): array
{
echo "MyClass::__serialize() is called." . PHP_EOL;
return [];
}
public function __unserialize(array $data): void
{
echo "MyClass::__unserialize() is called." . PHP_EOL;
}
public function __sleep()
{
echo "MyClass::__sleep() is called." . PHP_EOL;
}
public function __wakeup()
{
echo "MyClass::__wakeup() is called." . PHP_EOL;
}
}
$mc = new MyClass;
$mc = serialize($mc);
$mc = unserialize($mc);
// MyClass::__serialize() is called.
// MyClass::__unserialize() is called.
如果类同时实现了Serializable
接口和__serialize
、__unserialize
方法,则Serializable
接口的serialize()
和unserialize()
方法不会被调用:
<?php
class MyClass implements Serializable
{
public $attr = 'attr_val';
public function __serialize(): array
{
echo "MyClass::__serialize() is called." . PHP_EOL;
return [];
}
public function __unserialize(array $data): void
{
echo "MyClass::__unserialize() is called." . PHP_EOL;
}
public function serialize(): string
{
echo "MyClass::serialize() is called." . PHP_EOL;
return json_encode((array)$this);
}
public function unserialize(string $data)
{
echo "MyClass:unserialize() is called." . PHP_EOL;
$arr = json_decode($data);
$this->attr = $arr['attr'];
}
}
$mc = new MyClass;
$mc = serialize($mc);
$mc = unserialize($mc);
// MyClass::__serialize() is called.
// MyClass::__unserialize() is called.
__toString
这是一个很常用的魔术方法,通过它可以让对象“合理地”字符串化,这有助于方便地输出有用的信息:
<?php
class Pointer implements Stringable
{
public function __construct(private int $x, private int $y)
{
}
public function __toString(): string
{
return "({$this->x},{$this->y})";
}
}
$p1 = new Pointer(1, 5);
$p2 = new Pointer(2, 10);
echo "Pointer1:{$p1}" . PHP_EOL;
// Pointer1:(1,5)
从php 8.0.0开始,只要实现了__toString
魔术方法,将视为类隐式实现了Stringable
接口,对应的实例可以通过instanceof
检查。所以最好像示例中那样显式地实现Stringable
接口。
__invoke
之前在中提到过Callable
伪类型,其中包含了实现了__invoke
的类实例。也就是说我们可以像调用函数那样调用实现了__invoke
的类实例:
<?php
class MyClass{
public function __invoke()
{
echo "MyClass::__invoke is called.".PHP_EOL;
}
}
$mc = new MyClass;
$mc();
// MyClass::__invoke is called.
__set_state
debug代码时我们会经常使用var_export
函数,事实上var_export
会打印出创建对应变量的php代码:
<?php
$a = '123';
var_export($a);
// '123'
echo PHP_EOL;
$num = 12.5;
var_export($num);
// 12.5
echo PHP_EOL;
$arr = [1, 2, 3];
var_export($arr);
// array (
// 0 => 1,
// 1 => 2,
// 2 => 3,
// )
echo PHP_EOL;
对于基础数据和数组,上边打印出的结果并没有错误,我们可以直接使用$num = xxx
的方式创建对应的变量。
但如果是一个自定义类的实例:
class MyClass
{
public $a = 'a';
private $b = 'b';
}
$obj = new MyClass();
var_export($obj);
// MyClass::__set_state(array(
// 'a' => 'a',
// 'b' => 'b',
// ))
echo PHP_EOL;
这样就显得有点古怪了,打印的内容是该类的一个静态方法__set_state
,并且传入一个包含实例属性数据的数组。如果此时我们使用$obj = MyClass::__set_state(array(...));
这样的语句去尝试创建对象是不会成功的,因为对应的类根本就没有实现__set_state
方法。但换句话说就是,只要对应的类实现了这个方法,我们就可以根据var_export
打印出的信息去创建类实例:
<?php
class MyClass
{
public $a = 'a';
private $b = 'b';
public static function __set_state($properties): object
{
$mc = new MyClass;
$mc->a = $properties['a'];
$mc->b = $properties['b'];
return $mc;
}
}
$obj = MyClass::__set_state(array('a' => 'a', 'b' => 'b'));
__debugInfo
事实上在debug的时候var_dump
比var_export
更有用,前者可以直接打印数据结构:
<?php
var_dump(12.5);
// float(12.5)
var_dump([1, 2, 3]);
// array(3) {
// [0]=>
// int(1)
// [1]=>
// int(2)
// [2]=>
// int(3)
// }
class MyClass
{
private $a = 'a';
public $b = 'b';
}
var_dump(new MyClass());
// object(MyClass)#1 (2) {
// ["a":"MyClass":private]=>
// string(1) "a"
// ["b"]=>
// string(1) "b"
// }
对于对象,var_dump
可以打印所有的属性,包括私有属性。
一般来说我们并不需要修改var_dump
的默认打印信息,但如果需要,可以使用__debugInfo
魔术方法:
<?php
class MyClass
{
private $a = 'a';
public $b = 'b';
public function __debugInfo()
{
return ["b" => "string(1) 'b'"];
}
}
var_dump(new MyClass());
// object(MyClass)#1 (1) {
// ["b"]=>
// string(13) "string(1) 'b'"
// }
这里我们通过添加__debugInfo
魔术方法,使用var_dump
打印对象时只输出公有属性b
,不会输出其它属性。
就先到这里了,关于类和对象的内容可能还需要1~2篇笔记。
文章评论