PHP动态实例化对象并向构造函数传递参数

在框架开发,模块化开发等场合,我们可能有一种需求,那就是在PHP运行时动态实例化对象。

什么是动态实例化对象呢?我们先来看一下PHP有一种变量函数(可变函数)的概念,例如如下代码:

function foo() {
    echo 'This is the foo function';
}
$bar = 'foo';
$bar();

运行上述代码将会输出“This is the foo function”。具体请参考PHP手册:可变函数。当然,如果需要动态调用的话,那么就使用call_user_func或call_user_func_array函数。这两个函数的用法不是本文的重点,不懂的同学请查阅其它资料。回到本文的话题上:什么是动态实例化对象?本人认为动态实例化对象,即是:需要实例化的对象是在程序运行时(run-time)动态决定(由变量决定)需要实例化什么样的对象,而不是直接写死在代码里。

通过上述例子我们已经知道了如何在运行时动态调用一个函数了,在现在面向对向如此流行的今天,在一些代码中,我们需要去动态去实例化一个类,该怎么做呢?

情况一:类的构造函数没有参数或参数的个数是确定的

如果类的构造函数没有参数或者我们要实例化的类根本没有构造函数,似乎简单一点,可以照上面的例子改一个嘛,嗯,照葫芦画瓢,谁不会:

代码示例:(构造函数没有参数)

class FOO {
    private $a, $b;
    public function __construct() {
        $this->a = 1;
        $this->b = 2;
    }
    public function test() {
        echo 'This is the method test of class FOO<br />';
        echo '$this->a=', $this->a, ', $this->b=', $this->b;
    }
}
$bar = 'FOO';
$foo = new $bar();
$foo->test();

运行一下,就看到了输出了如下结果:

This is the method test of class FOO
$this->a=1, $this->b=2

嗯,我们要传参的话,那么就这样吧:

class FOO {
    private $a, $b;
    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }
    public function test() {
        echo 'This is the method test of class FOO<br />';
        echo '$this->a=', $this->a, ', $this->b=', $this->b;
    }
}
$bar = 'FOO';
$foo = new $bar('test', 5);
$foo->test();

一样可以得到类似的结果:

This is the method test of class FOO
$this->a=test, $this->b=5

很理想嘛。

情况二:类的构造函数的参数个数不确定

这种情况就要麻烦很多了,但是如果要写得比较通用的话,就不得不考虑这种情况了。例如,我们有如下两个类

class FOO {
    public function test() {
        echo 'This is the method test of class FOO';
    }
}
class BAR {
    private $a, $b;
    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }
    public function test() {
        echo 'This is the method test of class BAR<br />';
        echo '$this->a=', $this->a, ', $this->b=', $this->b;
    }
}

我们想要一个通用的方式来实例化这两个类。我们注意到FOO类是没有写构造函数的,或者是可以认为FOO类的构造函数的参数个数为零;而BAR类的构造函数却有参数。还好,PHP5已经足够强大了,引入了反射的概念,具体请参考PHP手册:反射,虽然手册上也没有什么可参考的:)。还好,命名写得不错,从类名和方法名上面已经可以看出大概的端倪,不需要太多的文字。

那么好吧,就让我们用PHP5的反射来着手这个事情:

(还在用PHP4的同学请不要走开,如果你拥有一个没有反射的PHP版本或者是你为了兼容也好还是不想升级也好,反正不想用反射的,下面有解决方案)

$class = new ReflectionClass('FOO');
$foo = $class->newInstance(); //或者是$foo = $class->newInstanceArgs();
$foo->test();

看到什么了没有?接着来:

$class = new ReflectionClass('BAR');
$bar = $class->newInstanceArgs(array(55, 65));
$bar->test();

OK,似乎可以了,那么就整理一下吧,来个通用函数,我们想这么设计,此函数的第一个函数是要实例化的类名,从第二个参数开始就是要实例化类的构造函数的参数,有几个就写几个上去,没有就不写。要想实现参数个数可变的函数,我们有两种方法:

第一种是类似于:

function foo($arg1, $arg2 = 123, $arg3 = 'test', $arg4 = null, ....... ) {
    //some code;
}

的办法,这种方法有两个缺点,第一个是你如果需要传100个参数难道就写100个参数吗?第二个是你还要在程序里判断哪个参数是不是null,或是其它默认值。(题外话:这种写法的参数默认值必须放在最后,你不能将没有默认值的参数插在有默认值的中间或前面,否则你调用的时候也必须显式地写上有默认值参数的值)

另一种实现参数数量可变的方法是在函数里用PHP的内置函数func_get_args(猛击这里看手册)取得传给函数的参数。类似的函数有func_get_num和func_get_arg,算了,我懒,你们自己找手册看吧。

那么,用这个函数似乎要方便很多,我们根据想象的函数参数的排列,代码应该是这个样子的:

function newInstance() {
    $arguments = func_get_args();
    $className = array_shift($arguments);
    $class = new ReflectionClass($className);
    return $class->newInstanceArgs($arguments);
}

OK,让我们来看一下效果:

$foo = newInstance('FOO');
$foo->test();
//输出结果:
//This is the method test of class FOO

$bar = newInstance('BAR', 3, 5);
$bar->test();
//输出结果:
//This is the method test of class BAR
//$this->a=3, $this->b=5

短短四行代码,效果相当完美啊。那么,如果应用到类里面,我们可以利用这种思想,直接写成魔术方法,可以让我们的类更酷哦!

class INSTANCE {
    function __call($className, $arguments) {
        $class = new ReflectionClass($className);
        return $class->newInstanceArgs($arguments);
    }
}
$inst = new INSTANCE();
$foo = $inst->foo();
$foo->test();
//输出结果:
//This is the method test of class FOO

$bar = $inst->bar('arg1', 'arg2');
$bar->test();
//输出结果:
//This is the method test of class BAR
//$this->a=3, $this->b=5

咔咔,爽吧。

接下来讨论一下不使用反射类的情况。例如PHP4中就没有反射,而一些老项目就是运行在PHP4上面的。或者是要保证项目对未知环境的兼容性,Whatever,来关心一下怎么动态传参吧。PHP中动态传参的函数只有一个:call_user_func_array(轻击此处查看手册)。这是一个动态调用函数的函数,作用是可以将函数的参数以数组的形式传递给要调用的函数。好吧,我自己也被自己绕晕了,直接来看实例:

function foo($a, $b) {
    echo '$a=', $a, '<br />';
    echo '$b=', $b;
}
call_user_func_array('foo', array(1, 'string'));
//本例输出结果:
//$a=1
//$b=string

那么,要实现用这种方法来动态实例化对象并传参,呃……,只有曲线救国了,我们得先写一个函数,让这个函数来实例化对象,而这个函数的参数就原原本本地传给要实例化对象的类的构造函数就好了。打住!那这个函数得有几个参数啊?怎么实现传递不同个数的参数呢?嘿嘿,我一声冷笑,你忘了PHP里提供一个创建匿名函数的函数吗?(又开始绕起来了……)create_function(手册在此),照着手册里面的例子直接画就可以了,我也懒得打字了,直接看下面的代码,注释我写清楚点大家都明白了:

function newInst() {
    //取得所有参数
    $arguments = func_get_args();

    //弹出第一个参数,这是类名,剩下的都是要传给实例化类的构造函数的参数了
    $className = array_shift($arguments);

    //给所有的参数键值加个前缀
    $keys = array_keys($arguments);
    array_walk($keys, create_function('&$value, $key, $prefix', '$value = $prefix . $value;'), '$arg_');

    //动态构造实例化类的函数,主要是动态构造参数的个数
    $paramStr = implode(', ',$keys);
    $newClass=create_function($paramStr, "return new {$className}({$paramStr});");

    //实例化对象并返回
    return call_user_func_array($newClass, $arguments);
}

好了,至于效果是什么,就麻烦各位看官自己动动手,运行一下看看,是不是自己期望的结果。

如果改写成类里面的魔术方法,哼哼,威力嘛,你懂的!然后呢,我还是懒了,如果要写成魔术方法的话,相信这么easy的事情你很轻松就办到了。就当一个作业吧。另,本文的代码都是本人运行过的,但是,写文章的时候没有使用复制/粘贴功能,所以,你最好是也不要复制粘贴。如果从本文中的代码copy下来运行出错的话,还烦请各位自己debug一下,编程不就是要自己写么。

本来这个话题到这里就应该可以结束了,但是想到我在想到这个办法之前用的方法(好绕……),本着重在交流的态度,一起放出来。我例两个关键函数吧:extracteval。只是我个人觉得用eval函数比较山寨,能不用最好不用。于是又想出了上面的办法,哈哈,编程最重要的是思想,不是吗?好,又是一道作业题了,大家可以试试用这两个函数(当然也会用到别的函数)来实现带不定数量的参数动态实例化对象的函数。

转自:http://blog.unlink.link/php/php_runtime_instance_class_and_pass_parameters.html

发表评论

电子邮件地址不会被公开。

*