Initiating a class from string with extra step?

2020-04-23 11:05发布

I've been looking into substantiating a new class instance from a string in PHP. This is seems to be an acceptable procedure, however I am curious why it can't be done with the returns of a function call. I posted a short test below, and my results indicate it works if there is a variable intermediary (i.e. $bar = new $foo->callBar(); does not work, while $x = $foo->callBar(); $bar = new $x; does).

class Foo {
    function callBar() {
        return 'Bar';
    }
}
class Bar {
    function sayHi() {
        echo 'Hi';
    }
}

$foo = new Foo();
$bar = new $foo->callBar();

Warning: Uncaught Error: Class name must be a valid object or a string
in php shell code:1 Stack trace:
#0 {main} thrown in php shell code on line 1

$x = $foo->callBar();
$bar = new $x;
$bar->sayHi();
//Output: Hi

I'd love to know why that is, or if I'm wrong, what is the correct why of going about this?

2条回答
该账号已被封号
2楼-- · 2020-04-23 11:35

There are exactly four options of creating a class instance using new that I know:

  • Name class directly: $bar = new Bar;
  • Use variable directly: $barName = 'Bar'; $bar = new $barName;
  • Use object property: $obj = (object) ['barName' => 'Bar']; $bar = new $obj->barName;
  • Use existing instance: $bar1 = new Bar; $bar2 = new $bar1;

First half of an answer to your question is PHP parser: it prohibits many things like new (Foo) and new "Foo" that could used to build a hack.

Second half may hide in PHP sources: here's the C function that throws that exception.

查看更多
爱情/是我丢掉的垃圾
3楼-- · 2020-04-23 11:39

As stated in the PHP Manual the new keyword accepts a string or an object as a class name. Optionally you can provide arguments to the constructor in parentheses at the end.

What this means is that the syntax roughly expects:

    new         [string|object]     ()
//  ^^ keyword  ^^ name             ^^ optional parentheses

Another important fact is that the keyword new has the highest precedence of all operators.

Combining the two facts together means that we cannot use parentheses to increase the precedence of the operation with new keyword. If we use parentheses PHP will treat them as the optional part of new syntax. Something like this won't work.

$bar = new   ($foo->callBar());
//         ^ missing class name

There is just no unambiguous way of telling PHP parser to treat this otherwise.

There is also another caveat worth remembering, which Edward Surov has partially mentioned in his answer. The class name can come from any variable which is a string or an instance.
The following code will create an object of class Bar, which seems to violate the operator precedence. As of now I still don't understand this behaviour.

$obj = ['a'=>'Bar'];
$bar = new $obj['a'];
// or
$obj = (object) ['a'=>'Bar'];
$bar = new $obj->a;

So, let's explain what your code does.

    new         $foo->callBar     ()
//  ^^ keyword  ^^ name           ^^ optional parentheses

Because your Foo class doesn't have a property callBar it will trigger Notice message, but PHP will check if it can be used as a class name anyway. And because it doesn't exist it can't be a string or an instance, which is why you see the error.

This will be fixed in PHP 8.
See this RFC: https://wiki.php.net/rfc/variable_syntax_tweaks#arbitrary_expression_support_for_new_and_instanceof

查看更多
登录 后发表回答