php的魔术方法

2020-01-08  本文已影响0人  PENG先森_晓宇
  1. __construct():类被实例化时调用
  2. __destruct():类被销毁时被调用
  3. __call():对象获取一个不存在或者不可访问的普通方法时触发
  4. __callStatic():类获取一个不存在或者不可访问的静态方法时触发
  5. __get():对象获取一个不存在或者不可访问的普通变量时触发
  6. __set():对象赋值一个不存在或者不可访问的普通变量时触发
  7. __invoke():一个对象被当作方法使用时触发
  8. __toString():一个对象被当作字符变量时触发
  9. __clone():克隆一个对象时触发

__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()
<?php
namespace App\learn;

trait User
{
    public function __construct()
    {
    }
}

__destruct()

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

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

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

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

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

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

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

__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
上一篇下一篇

猜你喜欢

热点阅读