错误的静态方法


Wrong Static Method

PHP在父类中调用私有方法,而不是在call_user_func调用的当前类中定义方法

class Car {
    public function run() {
        return call_user_func(array('Toyota','getName')); // should call toyota
    }
    private static function getName() {
        return 'Car';
    }
}
class Toyota extends Car {
    public static function getName() {
        return 'Toyota';
    }
}
$car = new Car();
echo $car->run(); //Car instead of Toyota
$toyota = new Toyota();
echo $toyota->run(); //Car instead of Toyota

我已经找到了一个不同的解决方案。

<?php
 class Car {
    public static function run() {
     return static::getName();
   }
   private static function getName() {
    return 'Car';
    }
  }
   class Toyota extends Car {
     public static function getName() {
        return 'Toyota';
      }
   }
echo Car::run();
echo Toyota::run();
  ?>

使用Late Static Binding

您可以这样使用:

<?php
class Car {
    public function run() {
        return static::getName();
    }
    private static function getName(){
        return 'Car';
    }
}
class Toyota extends Car {
    public static function getName(){
        return 'Toyota';
    }
}
$car = new Car();
echo $car->run();
echo PHP_EOL;
$toyota = new Toyota();
echo $toyota->run();
?>
输出:

Car
Toyota
PHP 5.4.5

这是一个似乎在很长一段时间内出现和消失的bug(参见@ decize在该问题的评论中的测试)。可以"修复"这个问题——也就是说,在不同的PHP版本中提供一致的行为——使用反射:

工作在PHP 5.3.2和更高的版本,由于依赖于ReflectionMethod::setAccessible()调用私有/保护的方法。我将进一步解释这段代码,它能做什么,不能做什么,以及它是如何工作的。

不幸的是,它不可能直接在3v4l.org上测试,因为代码太大了,然而这是第一个真正的用例,用于最小化PHP代码-如果你这样做,它确实在3v4l上工作,所以你可以随意玩,看看你是否可以打破它。我所知道的唯一问题是,它目前不理解parent。这也受到5.4之前闭包中缺乏$this支持的限制,但实际上没有什么可以做的。

<?php
function call_user_func_fixed()
{
    $args = func_get_args();
    $callable = array_shift($args);
    return call_user_func_array_fixed($callable, $args);
}
function call_user_func_array_fixed($callable, $args)
{
    $isStaticMethod = false;
    $expr = '/^([a-z_'x7f-'xff]['w'x7f-'xff]*)::([a-z_'x7f-'xff]['w'x7f-'xff]*)$/i';
    // Extract the callable normalized to an array if it looks like a method call
    if (is_string($callable) && preg_match($expr, $callable, $matches)) {
        $func = array($matches[1], $matches[2]);
    } else if (is_array($callable)
                   && count($callable) === 2
                   && isset($callable[0], $callable[1])
                   && (is_string($callable[0]) || is_object($callable[0]))
                   && is_string($callable[1])) {
        $func = $callable;
    }
    // If we're not interested in it use the regular mechanism
    if (!isset($func)) {
        return call_user_func_array($func, $args);
    }
    $backtrace = debug_backtrace(); // passing args here is fraught with complications for backwards compat :-(
    if ($backtrace[1]['function'] === 'call_user_func_fixed') {
        $called = 'call_user_func_fixed';
        $contextKey = 2;
    } else {
        $called = 'call_user_func_array_fixed';
        $contextKey = 1;
    }
    try {
        // Get a reference to the target static method if possible
        switch (true) {
            case $func[0] === 'self':
            case $func[0] === 'static':
                if (!isset($backtrace[$contextKey]['object'])) {
                    throw new Exception('Use of self:: in an invalid context');
                }
                $contextClass = new ReflectionClass($backtrace[$contextKey][$func[0] === 'self' ? 'class' : 'object']);
                $contextClassName = $contextClass->getName();
                $method = $contextClass->getMethod($func[1]);
                $ownerClassName = $method->getDeclaringClass()->getName();
                if (!$method->isStatic()) {
                    throw new Exception('Attempting to call instance method in a static context');
                }
                $invokeContext = null;
                if ($method->isPrivate()) {
                    if ($ownerClassName !== $contextClassName
                            || !method_exists($method, 'setAccessible')) {
                        throw new Exception('Attempting to call private method in an invalid context');
                    }
                    $method->setAccessible(true);
                } else if ($method->isProtected()) {
                    if (!method_exists($method, 'setAccessible')) {
                        throw new Exception('Attempting to call protected method in an invalid context');
                    }
                    while ($contextClass->getName() !== $ownerClassName) {
                        $contextClass = $contextClass->getParentClass();
                    }
                    if ($contextClass->getName() !== $ownerClassName) {
                        throw new Exception('Attempting to call protected method in an invalid context');
                    }
                    $method->setAccessible(true);
                }
                break;
            case is_object($func[0]):
                $contextClass = new ReflectionClass($func[0]);
                $contextClassName = $contextClass->getName();
                $method = $contextClass->getMethod($func[1]);
                $ownerClassName = $method->getDeclaringClass()->getName();
                if ($method->isStatic()) {
                    $invokeContext = null;
                    if ($method->isPrivate()) {
                        if ($ownerClassName !== $contextClassName || !method_exists($method, 'setAccessible')) {
                            throw new Exception('Attempting to call private method in an invalid context');
                        }
                        $method->setAccessible(true);
                    } else if ($method->isProtected()) {
                        if (!method_exists($method, 'setAccessible')) {
                            throw new Exception('Attempting to call protected method in an invalid context');
                        }
                        while ($contextClass->getName() !== $ownerClassName) {
                            $contextClass = $contextClass->getParentClass();
                        }
                        if ($contextClass->getName() !== $ownerClassName) {
                            throw new Exception('Attempting to call protected method in an invalid context');
                        }
                        $method->setAccessible(true);
                    }
                } else {
                    $invokeContext = $func[0];
                }
                break;
            default:
                $contextClass = new ReflectionClass($backtrace[$contextKey]['object']);
                $method = new ReflectionMethod($func[0], $func[1]);
                $ownerClassName = $method->getDeclaringClass()->getName();
                if (!$method->isStatic()) {
                    throw new Exception('Attempting to call instance method in a static context');
                }
                $invokeContext = null;
                if ($method->isPrivate()) {
                    if (empty($backtrace[$contextKey]['object'])
                            || $func[0] !== $contextClass->getName()
                            || !method_exists($method, 'setAccessible')) {
                        throw new Exception('Attempting to call private method in an invalid context');
                    }
                    $method->setAccessible(true);
                } else if ($method->isProtected()) {
                    $contextClass = new ReflectionClass($backtrace[$contextKey]['object']);
                    if (empty($backtrace[$contextKey]['object']) || !method_exists($method, 'setAccessible')) {
                        throw new Exception('Attempting to call protected method outside a class context');
                    }
                    while ($contextClass->getName() !== $ownerClassName) {
                        $contextClass = $contextClass->getParentClass();
                    }
                    if ($contextClass->getName() !== $ownerClassName) {
                        throw new Exception('Attempting to call protected method in an invalid context');
                    }
                    $method->setAccessible(true);
                }
                break;
        }
        // Invoke the method with the passed arguments and return the result
        return $method->invokeArgs($invokeContext, $args);
    } catch (Exception $e) {
        trigger_error($called . '() expects parameter 1 to be a valid callback: ' . $e->getMessage(), E_USER_ERROR);
        return null;
    }
}

如果你只想从父级和子级获得访问权限,请使用"protected"修饰符。在我看来,这很明显。例如:

<?php
class Car {
    public function run() {
        return call_user_func(array('static','getName'));
    }
    protected static function getName() {
        return 'Car';
    }
}
class Toyota extends Car {
    protected static function getName() {
        return 'Toyota';
    }
}
$car = new Car();
echo $car->run(); // "Car"
$toyota = new Toyota();
echo $toyota->run(); // "Toyota"

你可以使用get_called_class()来代替'static'。

我认为,问题在于两个getname函数的访问级别不同。如果你让getname()的基类版本公开(与派生类版本相同),那么在php 5.3.15(在我的Mac上),你得到丰田。我认为,由于不同的访问级别,您最终会在Toyota类中得到两个不同版本的getname()函数,而不是派生类版本覆盖基类版本。换句话说,是重载而不是重写。因此,当run()函数在Toyota类中查找要执行的getname()函数时,它找到了两个,并接受第一个,它将是第一个被声明的(从基类)。

虽然这只是我的假设,但听起来很有道理。

使用get_called_called函数来执行此操作

public function run() {
    $self = get_called_class();
    return $self::getName();
}

我相信你的函数是相互覆盖的,默认情况下是第一个。除非你改变一个函数的参数,或者重命名函数,否则它将始终默认为父类函数。