PHP学习笔记12:类和对象IV

PHP学习笔记12:类和对象IV

image-20211129162010327

图源:php.net

匿名类

匿名类(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

如果是访问外部类的publicprotected属性和方法,可以让匿名类继承外部类,相应的属性和方法可以继承后使用:

<?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驱动,可以通过修改数据库配置文件加载,具体方式可以参考PHP开启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

之前在PHP学习笔记3:其它类型和类型声明中提到过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_dumpvar_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篇笔记。

谢谢阅读。

往期内容

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值