静默 PHP7中的“声明... 应该是兼容的”警告

升级到 PHP7之后,日志几乎因为这类错误而中断:

PHP Warning: Declaration of Example::do($a, $b, $c) should be compatible with ParentOfExample::do($c = null) in Example.php on line 22548

如何在 PHP7中消除这些错误,并且只消除这些错误?

  • 在 PHP7之前,它们是 E_STRICT类型的警告 这很容易处理。现在他们只是普通的警告。因为我 想了解其他警告,所以我不能完全关闭所有警告。

  • 我没有能力重写这些遗留的 API,甚至没有提到所有使用它们的软件。你猜怎么着,没人会为此付钱的。都不是我开发的,所以我不是那个该受责备的人。(单元测试?十年前可不是这样的。)

  • 我想避免 任何诡计func_get_args和类似的尽可能多。

  • 我不是真的想降级到 PHP5。

  • 我仍然希望了解其他错误和警告。

有没有一个干净漂亮的方法来达到这个目的?

49206 次浏览

PHP7消除了 E_STRICT错误级别。关于这方面的信息可以在 PHP7兼容性说明中找到。您可能还想阅读在开发 PHP7时讨论过的 建议文件

简单的事实是: E_STRICT通知是在很多版本之前引入的,目的是通知开发人员他们正在使用不良实践,但是最初并没有试图强制进行任何更改。然而,最近的版本,特别是 PHP7,已经对这些事情变得更加严格。

你正在经历的错误是一个典型的例子:

您已经在类中定义了一个重写父类中同名方法的方法,但重写方法具有不同的参数签名。

大多数现代编程语言实际上根本不允许这样做。PHP 过去允许开发人员使用类似的东西,但是语言对于每个版本都变得更加严格,特别是现在的 PHP 7——他们特别增加了一个新的主版本号,这样他们就可以有理由做出重大的改变来打破向下兼容。

你的问题是因为你已经忽略了警告信息。您的问题暗示这是您想要继续使用的解决方案,但是“严格”和“不推荐”之类的消息应该被视为一个明确的警告,说明您的代码在将来的版本中可能会中断。在过去的几年里,忽视它们,你实际上把自己放在了现在的处境中。(我知道这不是你想听到的,对现在的情况也没什么帮助,但重要的是要说清楚)

真的没有你想要的那种工作。PHP 语言正在进化,如果你想坚持使用 PHP 7,你的代码也需要进化。如果您真的无法修复代码,那么您要么不得不禁止所有警告,要么在日志中看到这些警告。

如果您计划继续使用 PHP7,还需要知道的另一件事是,这个版本还有许多其他兼容性问题,其中包括一些非常微妙的问题。如果您的代码处于类似您报告的错误的状态,这意味着它可能已经存在了相当长的时间,并且可能存在其他问题,这些问题将导致 PHP 7中出现问题。对于这样的代码,我建议在使用 PHP7之前对代码进行更彻底的审计。如果您还没有准备好这样做,或者还没有准备好修复已经发现的 bug (您的问题暗示您还没有准备好) ,那么我认为 PHP 7对您来说可能是一个太远的升级。

您可以选择恢复到 PHP 5.6。我知道你说过你不想这么做,但是作为一个中短期的解决方案,这会让事情对你来说更容易。说实话,我觉得这是你最好的选择。

如果你封闭了这个错误,你可以在一个封闭的立即调用函数表达式中声明这个类:

<?php


// unsilenced
class Fooable {
public function foo($a, $b, $c) {}
}


// silenced
@(function () {
class ExtendedFooable extends Fooable {
public function foo($d) {}
}
})();

但我强烈建议你不要这么做。更好的做法是修复代码,而不是让有关代码如何破坏的警告保持沉默。


如果需要保持 PHP5的兼容性,请注意上面的代码只能在 PHP7中工作,因为 PHP5没有统一的表达式语法。要使其与 PHP 5一起工作,您需要在调用它之前将函数分配给一个变量(或者使它成为一个命名的函数) :

$_ = function () {
class ExtendedFooable extends Fooable {
public function foo($d) {}
}
};
@$_();
unset($_);

1. 变通方法

因为并不总是可以纠正所有的代码 你没有写信,特别是遗留的..。

if (PHP_MAJOR_VERSION >= 7) {
set_error_handler(function ($errno, $errstr) {
return strpos($errstr, 'Declaration of') === 0;
}, E_WARNING);
}

这个错误处理程序返回以 Declaration of开头的警告 true,它基本上告诉 PHP 警告已经处理完毕。这就是 PHP 不会在其他地方报告此警告的原因。

另外,这段代码只能在 PHP7或更高版本中运行。


如果您希望这种情况只发生在特定的代码库中,那么您可以检查一个带有错误的文件是否属于该代码库或感兴趣的库:

if (PHP_MAJOR_VERSION >= 7) {
set_error_handler(function ($errno, $errstr, $file) {
return strpos($file, 'path/to/legacy/library') !== false &&
strpos($errstr, 'Declaration of') === 0;
}, E_WARNING);
}

2. 正确的解决办法

至于实际修复其他人的遗留代码,有许多情况下可以在简单和可管理之间完成。在下面的例子中,类 BA的一个子类。请注意,您不一定要通过以下示例来删除任何 LSP 违规。

  1. 有些案子很简单。如果子类中缺少一个默认参数,只需添加它,然后继续。例如:

    Declaration of B::foo() should be compatible with A::foo($bar = null)
    

    你可以这么做:

    - public function foo()
    + public function foo($bar = null)
    
  2. If you have additional constrains added in a subclass, remove them from the definition, while moving inside the function's body.

    Declaration of B::add(Baz $baz) should be compatible with A::add($n)
    

    您可能希望根据严重程度使用断言或抛出异常。

    - public function add(Baz $baz)
    + public function add($baz)
    {
    +     assert($baz instanceof Baz);
    

    如果您看到约束纯粹用于文档目的,那么将它们移动到它们所属的位置。

    - protected function setValue(Baz $baz)
    + /**
    +  * @param Baz $baz
    +  */
    + protected function setValue($baz)
    {
    +     /** @var $baz Baz */
    
  3. If you subclass has less arguments than a superclass, and you could make them optional in the superclass, just add placeholders in the subclass. Given error string:

    Declaration of B::foo($param = '') should be compatible with A::foo($x = 40, $y = '')
    

    你可以这么做:

    - public function foo($param = '')
    + public function foo($param = '', $_ = null)
    
  4. If you see some arguments made required in a subclass, take the matter in your hands.

    - protected function foo($bar)
    + protected function foo($bar = null)
    {
    +     if (empty($bar['key'])) {
    +         throw new Exception("Invalid argument");
    +     }
    
  5. Sometimes it may be easier to alter the superclass method to exclude an optional argument altogether, falling back to func_get_args magic. Do not forget to document the missing argument.

      /**
    +  * @param callable $bar
    */
    - public function getFoo($bar = false)
    + public function getFoo()
    {
    +     if (func_num_args() && $bar = func_get_arg(0)) {
    +         // go on with $bar
    

    当然,如果必须删除多个参数,这会变得非常单调乏味。

  6. 如果你严重违反了替代原则,事情会变得更有趣。如果没有类型化参数,那么就很容易。只要让所有额外的参数可选,然后检查他们的存在。给定错误:

    Declaration of B::save($key, $value) should be compatible with A::save($foo = NULL)
    

    你可以这么做:

    - public function save($key, $value)
    + public function save($key = null, $value = null)
    {
    +     if (func_num_args() < 2) {
    +         throw new Exception("Required argument missing");
    +     }
    

    注意,我们在这里不能使用 func_get_args(),因为它没有考虑默认(未传递的)参数。我们只剩下 func_num_args()了。

  7. 如果您有一个具有发散接口的完整的类层次结构,那么进一步发散它可能会更容易。在每个类中用冲突的定义重命名一个函数。然后为这些类在单个中间父类中添加一个代理函数:

    function save($arg = null) // conforms to the parent
    {
    $args = func_get_args();
    return $this->saveExtra(...$args); // diverged interface
    }
    

    这样虽然没有警告,但仍然违反了 LSP,但是您可以保留子类中的所有类型检查。

对于那些希望真正纠正代码以使其不再触发警告的人来说: 我发现学习如何向子类中重写的方法添加额外的参数是很有用的,只要你给它们默认值。例如,虽然这会触发警告:

//"Warning: Declaration of B::foo($arg1) should be compatible with A::foo()"
class B extends A {
function foo($arg1) {}
}


class A {
function foo() {}
}

这不会:

class B extends A {
function foo($arg1 = null) {}
}


class A {
function foo() {}
}

我也有这个问题。我有一个重写父类的函数的类,但重写的参数数目不同。我可以想出一些简单的解决办法——但是确实需要小的代码更改。

  1. 更改子类中函数的名称(使其不再重写父函数) 或者
  2. 更改父函数的参数,但是使额外的参数成为可选的(例如,函数 func ($var1,$var2 = null))——这可能是最简单的,并且需要更少的代码更改。但是,如果它在其他地方使用了这么多,那么在父节点中改变这一点可能是不值得的。所以我选了第一条。

  3. 如果可能的话,不要在子类函数中传递额外的参数,而是使用 global 来引入额外的参数。这不是理想的编码方式,但却可能是一种创可贴。

我同意: 第一篇文章中的例子是不好的做法。 现在,如果你有这样的例子:

class AnimalData {
public $shout;
}


class BirdData extends AnimalData {
public $wingNumber;
}


class DogData extends AnimalData {
public $legNumber;
}


class AnimalManager {
public static function displayProperties(AnimalData $animal) {
var_dump($animal->shout);
}
}


class BirdManager extends AnimalManager {
public static function displayProperties(BirdData $bird) {
self::displayProperties($bird);
var_dump($bird->wingNumber);
}
}


class DogManager extends AnimalManager {
public static function displayProperties(DogData $dog) {
self::displayProperties($dog);
var_dump($dog->legNumber);
}
}

我相信这是一个合法的代码结构,但是这会在我的日志中引起警告,因为 displayProperties()没有相同的参数。此外,我不能让他们可选的增加一个 = null后,他们..。

在这个具体的例子中,我认为这个警告是错误的,对吗?

您可以完全删除父类方法定义并使用魔法方法截获它。

public function __call($name, $args)
{
if($name == 'do') {
// do things with the unknown # of args
} else {
throw new \Exception("Unknown method $name", 500);
}
}

我只是遇到了这个问题,然后走了这条路

如果基类的参数少于派生类,则可以像下面这样向派生类添加其他参数:

    $namespace = 'default';
if (func_num_args() > 2) {
$namespace = func_get_arg(2);
}

这样,您可以添加第3个“默认”参数,但不更改签名。 如果你有大量的代码调用这个函数,但是不能改变这个代码,并且想要维护这个向下兼容,那么我建议你这么做。

我在一些旧的 Joomla 代码(v1.5)中发现了这种情况,其中 jSession: : set 添加了一个 $nampace 参数,但是将 jObject 作为基类,其中 jObject: : set 没有这样的参数。