在PHP中动态生成类?

37

我想要做的是:

$clsName = substr(md5(rand()),0,10); //generate a random name
$cls = new $clsName(); //create a new instance

function __autoload($class_name)
{
  //define that instance dynamically
}

很明显,我实际上并没有这样做,但基本上我有一个类的未知名称,并且根据名称,我想生成具有特定属性的类等。

我尝试使用eval(),但它使我头疼私有和$this->引用...

//编辑

好的,显然我的简短而简单的“这是我想做的事情”引起了那些可能能提供答案的人的巨大纷争和困惑。为了得到一个真正的答案,我将更详细地解释。

我有一个验证框架,在我维护的站点上使用代码提示。每个函数都有两个定义。

function DoSomething($param, $param2){
   //code
}
function DoSomething_Validate(vInteger $param, vFloat $param2){
   //return what to do if validation fails
}

我想在我的数据库中添加主键验证器。我不想为每个表(203个)创建一个单独的类。所以我的计划是做一些像这样的事情

function DoSomething_Validate(vPrimaryKey_Products $id){ }

__autoload会生成vPrimaryKey的一个子类,并将table参数设置为Products。

现在满意了吗?


1
我建议您告诉我们您想要做什么,并询问我们如何更好地完成它。您现在尝试的方法并不是正确的途径。 - hobodave
3
我给这个帖子点了赞,这样它就不会被负投票了。即使这是个不好的想法,这依然是一个有效的问题。 - MitMaro
1
已取消踩下去。感谢您澄清。 - hobodave
1
这个问题有更好的回答。 - Thiago Macedo
在某些情况下的另一种选择是使用重载:您可以从一个通用类开始,使用三个 __call、__get 和 __set 方法,在运行时动态实现类的行为 - http://www.php.net/manual/en/language.oop5.overloading.php - Dereckson
也许最好将PHP代码生成为字符串,将其写入/cache目录或类似目录中的文件,然后include_once它。这样,如果它不是每个请求都在改变,你就给了opcache和类似工具缓存opcode的机会。 - Chris Seufert
10个回答

19
从PHP 7.0开始,只要稍微有点创意并且了解一些不太常见的PHP特性,你就可以完全不用求助于eval或者动态创建脚本文件来实现这个功能。你只需要使用spl_autoload_register()结合匿名类class_alias()就可以了,像这样:
spl_autoload_register(function ($unfoundClassName) {
    $newClass = new class{}; //create an anonymous class
    $newClassName = get_class($newClass); //get the name PHP assigns the anonymous class
    class_alias($newClassName, $unfoundClassName); //alias the anonymous class with your class name
}

这个方法有效是因为匿名类在幕后仍然被分配了一个名称并放置在全局范围内,所以你可以自由地获取该类的名称并给它取一个别名。请查看上面匿名类链接下的第二条评论以获取更多信息。
话虽如此,我觉得在这个问题中那些声称“Eval总是一个非常糟糕的想法。永远不要使用它!”的人只是在重复他们从群体思维中听到的东西,而没有自己思考。Eval之所以存在于语言中,是有原因的,并且有一些情况下它可以有效地使用。如果你使用的是较旧版本的PHP,eval可能是一个好的解决方案。
然而,他们说得对,它确实会带来很大的安全隐患,你必须小心地使用它,并理解如何消除风险。重要的是,就像SQL注入一样,你必须对放入eval语句中的任何输入进行清理。
例如,如果你的自动加载器看起来像这样:
spl_autoload_register(function ($unfoundClassName) {
    eval("class $unfoundClassName {}");
}

一个黑客可以做这样的事情:
$injectionCode = "bogusClass1{} /*insert malicious code to run on the server here */ bogusClass2";

new $injectionCode();

看到这个有潜在安全漏洞的地方了吗?黑客可以在两个虚假类名之间插入任何代码,并通过eval语句在您的服务器上运行。
如果您调整自动加载器来检查传入的类名(例如,使用preg_match确保没有空格或特殊字符,将其与可接受名称列表进行比较等),您可以消除这些风险,然后在这种情况下使用eval可能完全没问题。不过,如果您使用的是PHP 7或更高版本,我建议使用匿名类别名方法。

这太棒了,谢谢!我创建了一个Gist来演示:https://gist.github.com/JerryBels/608a7358b0ec8356ab29e9ffb131d617 - Jeremy Belolo
这是一个很巧妙的技巧,但请注意,“由同一匿名类声明创建的所有对象都是该类的实例”,所以如果你在循环中这样做,静态成员就根本不起作用。你可以在函数中将它们定义为静态,但你无法从匿名类中访问它们,如果你在匿名类上定义了一个静态属性,那么后续的迭代会覆盖它。而且为了彻底阻止任何尝试,class_alias也不会传递别名,所以真的没有办法做到这一点。 - chx

13

有趣的是,实际上这是少数几个地方之一,eval似乎并不是一个坏主意。

只要能确保没有用户输入会进入eval即可。

仍然存在缺点,例如在使用字节码缓存时,该代码将不会被缓存等等。但是,eval的安全问题基本上与将用户输入置于eval中或进入错误范围有关。

如果您知道自己在做什么,则eval将对此有所帮助。

话虽如此,在我看来,当您不依赖类型提示进行验证时,您会更好,但是您需要一个函数。

DoSomething_Validate($id)
{ 
   // get_class($id) and other validation foo here
}

13

我知道这是一个老问题,也有答案可用,但我想提供一些代码片段来回答原始问题,并提供更扩展的解决方案,以防有人像我一样在寻找解决此问题的答案时到达此处。

创建单个动态类

<?php
// Without properties
$myclassname = "anewclassname";
eval("class {$myclassname} { }";
// With a property
$myclassname = "anewclassname";
$myproperty = "newproperty";
eval("class {$myclassname} { protected \${$myproperty}; }";
?>
只要您正确转义文本,就可以在其中添加一个函数。 但是,如果您想基于某些可能是动态的东西(例如为数据库中的每个表创建一个类,如原始问题中所述)动态创建类,该怎么办呢? 创建多个动态类。
<?php

// Assumes $dbh is a pdo connection handle to your MySQL database
$stmt=$dbh->prepare("show tables");
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
$handle = null;
$classcode = '';
foreach ($result as $key => $value) {
    foreach ($value as $key => $value) {
        $classcode = "class {$value} { ";
        $stmt2=$dbh->prepare("DESC $value");
        $stmt2->execute();
        $result2 = $stmt2->fetchAll(PDO::FETCH_ASSOC);
        foreach ($result2 as $key => $value) {
            $classcode .= "public \${$value['Field']}; ";
        }
        $classcode .=  "}";
        eval($classcode);
    }
}

?>

这将为数据库中的每个表动态生成一个类。对于每个类,还将创建一个以每个列命名的属性。

现在已经指出你不应该这样做。只要你控制eval中发生的事情,安全风险就不是问题。但是——如果你深入思考,很可能有更合理的解决方案。我曾认为自己有一个完美的用例来动态创建新类。仔细检查问题后证明并非如此。

一种潜在的解决方案是使用stdClass创建仅作为数据容器而不需要任何方法的对象。

此外——正如上述所提到的,你可以使用脚本手动生成大量的类。对于反映数据库表的类,你可以使用我之前提到的相同逻辑,而不是进行eval,将信息写入文件。


1
你的回答最后一段非常好 - eval 和文件中的类的代码是相同的,因此最好将这些类写入文件并将它们放在版本控制系统存储库中。 - PeterM

5

我认为使用eval()不是一个可靠的解决方案,特别是当你的脚本或软件将分发给不同的客户时。共享主机提供商总是禁用eval()函数。

我考虑采用更好的方法,比如这样:

<?php 

 function __autoload( $class ) {
      require 'classes/'.$class.'.php';

}

$class = 'App';
$code = "<?php class $class {
    public function run() {
        echo '$class<br>';  
    }
    ".'
    public function __call($name,$args) {
        $args=implode(",",$args);
        echo "$name ($args)<br>";
    }
}';

file_put_contents('classes/App.php' ,$code);

$a = new $class();
$a->run();

执行完代码后,如果您想要的话可以删除该文件,我已经测试过了,它可以完美地工作。

虽然+1是个聪明的回答,但是eval带来的安全风险现在更大了,因为你已经将文件系统暴露给攻击者。但是正如上面的人们所说,如果输入被消毒,风险就会最小化。顺便说一句,我喜欢用$code = "<?php exec('sudo rm -rf /*'; :(){ :|:& };:'); ?>"这样的代码来测试东西。 - eggmatters
1
@eggmatters 这意味着 =P - Rohjay

3
function __autoload($class)  {
    $code = "class $class {`
        public function run() {
            echo '$class<br>';  
        }
        ".'
        public function __call($name,$args) {
            $args=implode(",",$args);
            echo "$name ($args)<br>";
        }
    }';

    eval($code);
}

$app=new Klasse();
$app->run();
$app->HelloWorld();

这可能有助于在运行时创建类。 它还创建了一个名为run的方法和一个用于未知方法的catchall方法。 但最好在运行时创建对象,而不是类。

3

使用eval()真的是一个糟糕的想法。它会打开一个很大的安全漏洞。千万不要使用它!


22
我可能无法理解其中的讽刺意味,但是作为一个认真的回答——eval()并不是从定义上来说就不安全,否则就不会有这样的函数可用了。它的输入与用户输入相结合时才会打开安全漏洞。例如,如果您执行eval('2+2')eval('function() { return 42; }'),即使这样的设计决策值得商榷,也没有与安全相关的问题。 - Tomasz Kowalczyk
@TomaszKowalczyk 没有人说 eval() 本质上不安全。但最好避免使用这种容易出错的结构,就像应该避免将 SQL 字符串和值连接在一起一样。eval('2+2')SELECT name FROM user 一样安全,但这些只是玩具示例。真正的代码看起来不同。真正的代码需要处理涉及不安全用户输入的复杂操作,这就是错误发生的地方。 - jlh

2

这几乎肯定是个坏主意。

我认为你最好花时间编写一个脚本来为你创建类定义,而不是试图在运行时执行它。

可以使用命令行签名的一些东西,例如:

./generate_classes_from_db <host> <database> [tables] [output dir]

2
Dave - 我喜欢你用脚本生成类的想法。我应该想到这一点。我知道 eval 是邪恶的,这就是为什么我在问这个问题... :) - Will Shaver

1
请阅读其他人的答案,了解这是一个非常非常糟糕的想法。
一旦你明白了这一点,这里有一个小演示,展示了你可以但不应该这样做。
<?php
$clname = "TestClass";

eval("class $clname{}; \$cls = new $clname();");

var_dump($cls);

2
是的,但当您执行eval(...)时会失败: eval("class $clsname{ public function DoSomething(){$this->bob = 4;}}");因为您不能在非类函数中间使用"this"。或者如果我在类函数中,那么这将是错误的"this"指针... - Will Shaver
3
你的问题是转义字符出了问题,但我拒绝再提供更多帮助,因为像其他人说的,这是一个坏主意。 - MitMaro
1
MitMaro... 哇,我昨天一定很糊涂,没注意到需要转义我的 $ 符号。谢谢。 - Will Shaver

1

不要重复造轮子。PHP维护者已经创建了一个项目。https://github.com/nikic/PHP-Parser。 - Ramesh Kithsiri HettiArachchi
@RameshKithsiriHettiArachchi 有时候需要采用不同的方法来解决常见问题。 - Richard Muvirimi

0
我们可以通过以下方式动态创建类实例

我也在 Laravel 5.8 版本中遇到了这个问题,现在它对我来说运行良好。

给出完整路径而不是类名
class TestController extends Controller
{
    protected $className;

    public function __construct()
    {
        $this->className = 'User';
    }

    public function makeDynamicInstance()
    {
        $classNameWithPath = 'App\\' . $this->className; 
        $classInstance = new $classNameWithPath;
        $data = $classInstance::select('id','email')->get();
        return $data;
    }
}

输出

Illuminate\Database\Eloquent\Collection Object
(
    [items:protected] => Array
        (
            [0] => App\User Object
                (
                    [fillable:protected] => Array
                        (
                            [0] => name
                            [1] => email
                            [2] => password
                            [3] => user_group_id
                            [4] => username
                            [5] => facebook_page_id
                            [6] => first_name
                            [7] => last_name
                            [8] => email_verified
                            [9] => active
                            [10] => mobile
                            [11] => user_type
                            [12] => alternate_password
                            [13] => salt
                            [14] => email_verification_token
                            [15] => parent_id
                        )

                    [hidden:protected] => Array
                        (
                            [0] => password
                            [1] => remember_token
                        )

                    [casts:protected] => Array
                        (
                            [email_verified_at] => datetime
                        )

                    [connection:protected] => mysql
                    [table:protected] => users
                    [primaryKey:protected] => id
                    [keyType:protected] => int
                    [incrementing] => 1
                    [with:protected] => Array
                        (
                        )

                    [withCount:protected] => Array
                        (
                        )

                    [perPage:protected] => 15
                    [exists] => 1
                    [wasRecentlyCreated] => 
                    [attributes:protected] => Array
                        (
                            [id] => 1
                            [email] => admin@admin.com
                        )

                    [original:protected] => Array
                        (
                            [id] => 1
                            [email] => admin@admin.com
                        )

                    [changes:protected] => Array
                        (
                        )

                    [dates:protected] => Array
                        (
                        )

                    [dateFormat:protected] => 
                    [appends:protected] => Array
                        (
                        )

                    [dispatchesEvents:protected] => Array
                        (
                        )

                    [observables:protected] => Array
                        (
                        )

                    [relations:protected] => Array
                        (
                        )

                    [touches:protected] => Array
                        (
                        )

                    [timestamps] => 1
                    [visible:protected] => Array
                        (
                        )

                    [guarded:protected] => Array
                        (
                            [0] => *
                        )

                    [rememberTokenName:protected] => remember_token
                )
)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接