php的魔术方法
- __construct():类被实例化时调用
- __destruct():类被销毁时被调用
- __call():对象获取一个不存在或者不可访问的普通方法时触发
- __callStatic():类获取一个不存在或者不可访问的静态方法时触发
- __get():对象获取一个不存在或者不可访问的普通变量时触发
- __set():对象赋值一个不存在或者不可访问的普通变量时触发
- __invoke():一个对象被当作方法使用时触发
- __toString():一个对象被当作字符变量时触发
- __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变量
- 超全局变量,也就是_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