2012-12-21 71 views
15

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 
+4

为什么getName()函数在汽车和公共汽车中是私有的? – 1615903

+0

您使用的是哪个版本的php?因为在PHP 5.4上,它反复“丰田”而不是“汽车”。如果我在你的情况下正确理解,它会发生相反。 – Leri

+6

似乎在不同的PHP版本中有很大的不同:http://3v4l.org/ekaEs - PHP中的错误!世界必须真正结束。 – deceze

回答

1

这是出现在进出存在震荡不稳很长一段时间的错误(见@ deceze在这个问题上的评论)。这是可能的“解决”这个问题 - 那就是,给整个PHP版本一致的行为 - 使用reflection

作品在PHP 5.3.2,后来由于对ReflectionMethod::setAccessible()依赖调用private/protected方法。我将为此代码添加进一步的解释,它可以做什么以及不可以做什么,以及它如何在很短的时间内工作。

可惜这不是可以直接在3v4l.org测试这一点,因为代码太大,然而,这是有史以来第一次真正的使用情况minifying PHP代码 - 它在3v4l工作,如果你这样做,可以随意玩耍,看看你是否可以打破它。我知道的唯一问题是它目前不了解parent。它也受到了5.4之前缺乏对闭合支持的支持,但对此没有什么可做的。

<?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; 
    } 
} 
+0

不要忘了提及它的PHP错误... – Baba

+0

@Baba在第一段中添加了注释。 – DaveRandom

0

的问题是,我认为,有两个的getName功能不同的访问级别。如果您将getname()的基类版本设为public(与派生类版本相同),那么在PHP 5.3.15(在我的Mac上)中,您将获得丰田。我认为,由于不同的访问级别,最终会在Toyota类中得到两个不同版本的getname()函数,而不是派生类版本重写基类版本。换句话说,你有重载而不是重写。因此,当run()函数在丰田类中寻找getname()函数来执行时,它会找到两个并获取第一个,这将是第一个要声明的(来自基类)。

当然,这只是我的假设,但这听起来似乎是合理的。

6

我已经找到了不同的方法解决..

<?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 ..

3

你可能会使用这样的:

<?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

0

使用get_called_called功能待办事项此

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

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

1

如果您只想从父代和子代访问,请使用“受保护”修饰符。国际海事组织,这很明显。例如:

<?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'。