php的魔术方法

  1. __construct():类被实例化时调用
  2. __destruct():类被销毁时被调用
  3. __call():对象获取一个不存在或者不可访问的普通方法时触发
  4. __callStatic():类获取一个不存在或者不可访问的静态方法时触发
  5. __get():对象获取一个不存在或者不可访问的普通变量时触发
  6. __set():对象赋值一个不存在或者不可访问的普通变量时触发
  7. __invoke():一个对象被当作方法使用时触发
  8. __toString():一个对象被当作字符变量时触发
  9. __clone():克隆一个对象时触发

__construct()

成为构造器或者构造方法。在类被实例化时会调用该魔术方法,该方法内常做一些初始化操作,这个方法肯定经常用,就不做过多介绍了。

依赖注入通常也是在这个方法中注入的。父类的构造器可以被子类覆盖或者重写。

需要注意几点

  • 该魔术方法只有类被实例化后才会被调用。静态变量和静态方法是在类加载后就被加载,所以静态方法中不能使用__construct()方法中的变量。举个例子
<?php
namespace App\learn;

class User
{
    public $people;
    public function __construct()
    {
       $this->people=new People(); 
    }
    
    static public function numbers(){
       return $this->people->numbers();
    }
}

User类的people变量是__construct()内定义的,也就是在实例化后才会被赋值。而静态方法numbers中却使用对象属性,显然时获取不到的,当如下调用时会报错。

User::number()
  • trait中不能使用该魔术方法
    首先trait不是个class,以至于不会被实例化,因此该魔术方法不会被调用
<?php
namespace App\learn;

trait User
{
    public function __construct()
    {
    }
}

__destruct()

称为析构方法。在类的对象被销毁时调用,这个魔术方法对传统的php-fpm是没什么意义的,为什么这么说呢?

因为传统php-fpm(不包括swoole)是解释型语言,不是编译型语言。通俗点理解就是,每次请求php都会重新加载类,php环境,以及各种变量等等资源,并存放到内存,请求结束后就会立即释放资源,且清除所有内存,包括类,变量,php环境等等。

每次请求都会重复销毁,以至于php不能实现高并发,这也是php被诟病的原因。

由于传统的php的这种模式,使得通常不会有内存泄漏的问题,但是也有内存泄漏的情况,我遇到的有俩种,在一次请求中

  • 有循环依赖,以至于内存泄漏,超出php-fpm的最大内存限制。
  • 查询mysql数据集,一次查询10万条数据。由于变量等都是存放在内存,以致于内存泄漏,超出php-fpm的最大内存限制。

也正是由于php这种模式,在请求结束后会全部销毁所有资源,以致于__destruction()没有太大的实际意义。

在类似java,以及现在的swoole都是常驻内存的。在swoole中php执行环境,类,以及全局变量都会常驻内存的,在请求结束后是不会释放的,在这种模式下该魔术方法可能有点用。

上面提到的全局变量包括3种

  • 类中的静态变量
  • global变量
  • 超全局变量,也就是_SERVER,_SESSION,$_COOKIE等。

__call()

访问对象(类实例化后)不存在的方法或者不可访问的方法(方法修饰符为private或者是protectd)时触发该魔术方法。

该方法有俩参数,name为调用的方法名称,arguments为调用方法时传递的参数,该参数时一个array类型。

正常的代码如果调用对象的一个不存在的方法,php会报致命错误,而该魔术方法可以很好的给用户提示,用户体验性会更好。

为了避免当调用的方法不存在时产生错误,而意外的导致程序中止,可以使用 __call() 方法来避免。

该方法在调用的方法不存在时会自动调用,程序仍会继续执行下去。

class User
{
    protected function sex()
    {
        return 3;
    }

    public function __call($name, $arguments)
    {
        return "您调用的方法不能存在或者不可访问,方法名为" . $name."\n";
    }
}

$user = new User();
echo $user->sex();
echo $user->book(['小说','文学']);

结果为

您调用的方法不能存在或者不可访问,方法名为sex
您调用的方法不能存在或者不可访问,方法名为book

__callStatic()

访问不存在或者不可访问(方法修饰符为private或者是protectd)的静态方法时触发该魔术方法。

该魔术方法也必须是静态方法

该魔术方法和__call()类似,当用户访问不存在的静态方法时可以更友好的提示。

在laravel中的门面Facades就是该魔术方法来实现的。laravel中的门面就是依赖不需要手动注入__construct()了,直接在类里面通过静态方法调用即可
facades 为应用的 服务容器 提供了一个「静态」 接口。具体实现实现请参考Facades工作原理一节

<?php

namespace App\learn;

class Home
{

    public static function __callStatic($name, $arguments)
    {
        return "该静态方法不存在或者不可访问,静态方法为" . $name . "\n";
    }

    public static function people()
    {
        return "方法明为people\n";
    }

    private static function daughter()
    {
        return "方法明为daughter\n";
    }
}

echo Home::people();
echo Home::son();
echo Home::daughter();

结果为

方法明为people
该静态方法不存在或者不可访问,静态方法为son
该静态方法不存在或者不可访问,静态方法为daughter

__get()

访问对象不存在或者不可访问(方法修饰符为private或者是protectd)的成员变量时触发该魔术方法。

这里的成员变量是指普通变量,不包括静态变量,当访问一个静态变量时,该变量不存在或者不可访问时不会触发__get()魔术方法,会直接报php致命错误

<?php

namespace App\learn;

class Home
{

    public $num = 3;

    private $old = 14;

    private static $height=170;

    public function __get($name)
    {
        return "你访问的成员变量不存在或者不可访问,该变量为" . $name . "\n";
    }

}

$home = new Home();
echo $home->num . "\n";
echo $home->old . "\n";
echo $home->sex;
echo Home::$height;

运行结果为

3
你访问的成员变量不存在或者不可访问,该变量为old

你访问的成员变量不存在或者不可访问,该变量为sex
PHP Fatal error:  Uncaught Error: Access to undeclared static property: App\learn\Home::$height1 in /Users/xiaoyu/mywork/laravel/app/learn/Home.php:26
Stack trace:
#0 {main}
  thrown in /Users/xiaoyu/mywork/laravel/app/learn/Home.php on line 26

__set()

该魔术方法和__get()类似,赋值对象不存在或者不可访问(方法修饰符为private或者是protectd)的成员变量时触发该魔术方法。

这里的成员变量是指普通变量,不包括静态变量,当赋值类中一个静态变量时,该变量不存在或者不可访问时不会触发__set()魔术方法,会直接报php致命错误。

__get()和__set()可以用作一个简易容器的实现。以下为3个类的依赖关系

class Bim
    {
        public function doSomething()
        {
            echo __METHOD__, '|';
        }
    }
    
    class Bar
    {
        private $bim;
    
        public function __construct(Bim $bim)
        {
            $this->bim = $bim;
        }
    
        public function doSomething()
        {
            $this->bim->doSomething();
            echo __METHOD__, '|';
        }
    }
    
    class Foo
    {
        private $bar;
    
        public function __construct(Bar $bar)
        {
            $this->bar = $bar;
        }
    
        public function doSomething()
        {
            $this->bar->doSomething();
            echo __METHOD__;
        }
    }

我们普通调用Foo类的doSomething方法如下

$foo= new Foo(new Bar(new Bim));
$foo-> doSomething();

而我们使用依赖注入的方式实现上面3个类的依赖注入容器。首先定义一个Container类。

class Container
{
    private $containers;

    public function __set($name, $value)
    {
        $this->containers[$name]=$value;
    }

    public function __get($name)
    {
       return $this->containers[$name];
    }
}

然后将依赖注入到容器中。

$containers= new Container();
$containers->bim=function (){
    return new Bim();
};
$containers->bar=function () use ($containers){
    new Bar($containers->bim);
};
$containers->foo=function () use ($containers){
    new Foo($containers->bar);
};

调用Foo类中的doSomething方法,我们只需要从容器中取出foo依赖即可。

$foo=$containers->foo;
$foo->doSomething();

__invoke()

一个实例对象被当作方法使用时会触发该魔术方法。

如果类中没有__invoke()方法的话,该类的实例化对象是不能当作方法被调用的,否则会报致命性错误。

class Home
{

    public $name;

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

    public function getName()
    {
        return $this->name;
    }

    public function __invoke()
    {
        // TODO: Implement __invoke() method.
        return "name是" . $this->name . "\n";
    }
}

$home = new Home('小雨');
echo $home();

结果为

name是小雨

可以看出该魔术方法内是和其他方法一样的,都可以获取整个对象的属性。

__toString()

和__invoke()类似,一个实例对象被当作字符类型变量使用时会触发该魔术方法。就是使用echo或者print等输出时调用。

__toString()魔术方法return的必须是一个字符串,如果return返回数组,对象或者布尔等类型时都会报致命错误。

class Home
{

    public $name;

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

    public function __toString()
    {
        // TODO: Implement __toString() method.
        return "name为".$this->name;
    }
}

$home = new Home('小雨');
echo $home;

返回结果

name为小雨

__clone()

clone意思为克隆,该魔术方法是在对象被克隆完成时调用

可能会有一个误区:只有定义了__clone魔术方法的类才可以被克隆。很明显理解错了,__clone()魔术方法只是在对象被克隆时会触发的方法。任何对象都是可以使用关键字clone来克隆的。

当使用关键字clone时,就会复制出一个和当前实例化对象完全一样的新对象。也就是全局有俩个某类的实例化对象

<?php

namespace App\learn;

class Home
{

    public $name = '小雨';

    public function __construct()
    {
        echo "我被实例化了\n";
    }

    public function __clone()
    {
        echo "我被克隆了\n";
    }
}

$home = new Home();
$home->name="大医院";

$cloneHome = clone $home;
var_dump($home);
echo "\n";
var_dump($cloneHome);

运行结果

我被实例化了
我被克隆了
object(App\learn\Home)#1 (1) {
  ["name"]=>
  string(9) "大医院"
}

object(App\learn\Home)#2 (1) {
  ["name"]=>
  string(9) "大医院"
}

可以看出__construct()只触发了一次,在clone时并没有触发。所以clone只是将某个对象的所有成员属性复制到了另一个对象而已,并没有重新实例化该类

怎么禁止对象被克隆?

有的对象是不想被克隆的,例如单例模式。这时我们只需要在该类中写一个空的__clone方法,并将该魔术方法设置为private或者protected即可。外部在使用clone关键字克隆对象时将会报错。

<?php

namespace App\learn;

class Home
{

    public $name = '小雨';

    public function __construct()
    {
        echo "我被实例化了\n";
    }

    protected function __clone()
    {
    }
}

$home = new Home();

$cloneHome = clone $home;

运行

我被实例化了
PHP Fatal error:  Uncaught Error: Call to protected App\learn\Home::__clone() from context '' in /Users/xiaoyu/mywork/laravel/app/learn/Home.php:23
Stack trace:
#0 {main}
  thrown in /Users/xiaoyu/mywork/laravel/app/learn/Home.php on line 23

为什么在单例模式中要禁止克隆?

单例模式必须保证系统中一个类仅有一个对象实例,如果可以被克隆则会有多个对象实例。

在单例模式中通常使用一个private修饰的空函数的__clone()魔术方法来禁止克隆。

class LogFile{
    //创建静态私有的变量保存该类对象
    static private $instance;
    //参数
    private $config;
    //防止直接创建对象
    private function __construct($config){
        $this -> config = $config;
        echo "我被实例化了";
    }
    //防止克隆对象
    private function __clone(){

    }
    static public function getInstance($config){
        //判断$instance是否是Uni的对象
        //没有则创建
        if (!self::$instance instanceof self) {
            self::$instance = new self($config);
        }
        return self::$instance;

    }
    public function getName(){
        echo $this -> config;
    }
}


$db1 = LogFile::getInstance(1);
$db1 -> getName();

$db2 = LogFile::getInstance(4);
$db2 -> getName();

运行结果

我被实例化了1
1
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容