PHP 7中的“声明……应兼容”静音警告


69

升级到PHP 7后,日志几乎因此类错误而阻塞:

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

如何在PHP 7中使这些错误以及仅这些错误静音?

  • 在PHP 7之前,它们是E_STRICT警告的类型,很容易处理。现在,它们只是普通的旧警告。由于我确实想了解其他警告,因此我不能完全关闭所有警告。

  • 我没有能力重写这些旧版API,甚至没有提及使用它们的所有软件。猜猜是什么,也没有人会为此付出代价。我都不是一开始就开发它们的,所以我不是要怪的人。(单元测试?十年前还没有流行。)

  • 我想尽量避免任何与之func_get_args相似的诡计

  • 我不是真的想降级到PHP 5。

  • 我仍然想知道其他错误和警告。

有没有一种干净而不错的方法来实现这一目标?


4
那些Warnings不是Errors。而且,您不应尝试使它们“静音”,而应解决该问题。警告的目的是告诉您您的代码将来会遇到问题。
arkascha

18
@arkascha我不确定这些评论在这里是否真的必要。没错,在理想的世界中,您将有所有的时间和金钱来快乐地修复遗留代码。但是在现实世界中,这通常是不可能甚至不允许的。
Yoshi

4
@arkascha OP明确指出,这是次优的情况,目前尚无法解决。我完全同意,当除了一些额外的日志消息(显然是管理的观点)之外目前“没有问题”时,重新实现并因此可能完全重新设计整个API绝对是不现实的。这是一个合理的问题,无需如此苛刻。
deceze

2
@arkascha我知道,我想说的是,尽管您是正确的,但这对您没有帮助。;)
Yoshi

5
请注意,我写的是评论,而不是答案:-)
arkascha

Answers:


118

1.解决方法

由于并非总能纠正所有您未编写的代码,尤其是遗留的代码...

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

该错误处理程序返回true警告,从警告开始Declaration of,基本上告诉PHP已经处理了警告。因此,PHP不会在其他地方报告此警告。

另外,此代码只能在PHP 7或更高版本中运行。


如果只希望针对特定的代码库执行此操作,则可以检查出现错误的文件是否属于该代码库或感兴趣的库:

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.正确的解决方案

至于实际修复别人的遗留代码,在许多情况下可以在容易和可管理之间完成。在下面的示例中,classB是的子类A。请注意,您不一定会通过遵循以下示例来删除任何违反LSP的行为。

  1. 有些情况很容易。如果在子类中缺少默认参数,则只需添加它并继续。例如,在这种情况下:

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

    您可以这样做:

    - public function foo()
    + public function foo($bar = null)
    
  2. 如果您在子类中添加了其他约束,则在函数内部移动时将其从定义中删除。

    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. 如果子类的参数少于父类,并且可以使它们在父类中成为可选参数,则只需在子类中添加占位符即可。给定错误字符串:

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

    您可以这样做:

    - public function foo($param = '')
    + public function foo($param = '', $_ = null)
    
  4. 如果您在子类中看到某些必需的参数,请直接处理。

    - protected function foo($bar)
    + protected function foo($bar = null)
      {
    +     if (empty($bar['key'])) {
    +         throw new Exception("Invalid argument");
    +     }
    
  5. 有时,更改超类方法以完全排除可选参数可能更容易,这又回到了func_get_args魔术上。不要忘记记录缺少的参数。

      /**
    +  * @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的这种方式,但是您必须保留子类中的所有类型检查。


6
迄今为止最好的答案;其他大多数人都认为这是您的代码,因此“您”应该更正它。并非所有人都幸运地拥有对包括所有库在内的整个代码库100%的控制权。LSP警告在较早的库中非常常见,并且会分叉内部逻辑中违反LSP的其他人的旧库,但是公开的API可以很好地工作,这对于将错误引入稳定的代码库中的事情来说是大量的额外工作。 。如果这个答案可以使用任何东西,那将是检查您的顶部if是否错误也来自特定的库。
来自Qaribou,2013年

@JeffreyMCastro我将它作为在所有地方使用的包含文件中打开<?php标记后的第一个代码。
Will Bonde'7

1
这行不通。当PHP编译模板时,将触发警告。由于某种原因,即使在包含文件或类时注册了错误处理程序,也不会触发错误处理程序。但是,仅在首次编译文件时才触发警告。一旦编译并存储在操作缓存中,就不会发出进一步的警告。
Willem Stuursma '17

@WillemStuursma显然您正在干扰其他错误处理程序
sanmai

这适用于Apache,但不适用于通过命令行使用php的情况。有什么想法为什么cli无法获得此错误处理程序?
Frank Adrian

25

对于那些想实际更正您的代码,使其不再触发警告的人:我发现有必要了解一下,只要给它们提供默认值,就可以向子类中的重写方法添加其他参数。因此,例如,这将触发警告:

//"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() {}
}

我刚刚发现,您是否有php.net链接对此进行了解释?
MatTheCat

1
@MatTheCat我不记得我是如何发现它的,但这不是来自任何官方文档。但是,如果您想了解更多有关它的信息,它们全都基于Liskov替换原理(LSP)-缺省值为null不会违反LSP,因为子类不需要与父类不同的API。有关更多详细信息和链接,请参见下面有关此答案的评论。
马特·布朗

谢谢!我还观察到不管默认值是多少,它都能工作。
MatTheCat

21

如果必须使错误静默,则可以在静默的立即调用的函数表达式中声明该类:

<?php

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

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

不过,我强烈建议您反对。修复您的代码比使有关如何破坏代码的警告沉默更好。


如果需要保持PHP 5兼容性,请注意上述代码仅在PHP 7中有效,因为PHP 5没有统一的表达式语法。为了使其与PHP 5一起使用,您需要在调用它之前将函数分配给变量(或使其命名为函数):

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

1
这是否真的比实际解决根本问题更好的解决方案?甚至是OP急切希望避免的func_get_args骗局?就是说,为实际找到可行的解决方案而鸣谢。不过,这绝对不算是他要求的干净,不错的解决方案。我勉强+1。
辛巴

8
当然,这不是更好的解决方案,但实际上可以回答问题。
安德里亚(Andrea)

1
这是无法接受的,因为它使所有错误都消失了,不仅是我不想的那些错误。例如,@(function () {constant('nothing');})();
sanmai

@sanmai可以使定义类时发生的任何错误(但不能在此后停止)保持沉默,是的。如果您担心类会产生其他警告,则可以使用自定义错误处理程序编写更复杂的解决方案。
Andrea


18

PHP 7消除了E_STRICT错误级别。有关此信息,请参见PHP7兼容性说明。您可能还想阅读开发PHP 7时所讨论的提案文档

一个简单的事实是:这些E_STRICT通知是在多个版本中引入的,目的是通知开发人员他们使用了不良做法,但最初并未尝试强制进行任何更改。但是,最近的版本(尤其是PHP 7)在这些方面变得更加严格。

您遇到的错误是一个典型的案例:

您已经在类中定义了一个方法,该方法将覆盖父类中相同名称的方法,但是您的覆盖方法具有不同的参数签名。

大多数现代编程语言实际上根本不允许这样做。PHP过去一直允许开发人员摆脱类似的东西,但是每种版本的语言都变得更加严格,尤其是现在使用PHP 7时-他们专门采用了新的主要版本号,以便他们有理由做出重大改变来向后兼容。

您遇到的问题是因为您已经忽略了警告消息。您的问题暗示这是您要继续使用的解决方案,但是应将诸如“ strict”和“ deprecated”之类的消息视为明确警告,警告您的代码可能在将来的版本中中断。通过在过去的几年中忽略它们,您已经有效地将自己置于现在的状况中。(我知道这不是您想听到的,并且现在对情况并没有真正的帮助,但是必须弄清楚这一点很重要)

您确实找不到所需的解决方法。PHP语言正在发展,如果您想使用PHP 7,您的代码也需要发展。如果您确实无法修复代码,那么您要么必须禁止所有警告,要么必须忍受这些警告使日志混乱。

如果您打算坚持使用PHP 7,还需要知道的另一件事是,此版本还有许多其他兼容性中断,其中包括一些相当细微的问题。如果您的代码处于像您要报告的那样的错误状态,则意味着它可能存在了很长一段时间,并且可能还有其他问题会导致您在PHP 7中出现问题。对于这样的代码,我建议在提交PHP 7之前对代码进行更彻底的审核。如果您不准备这样做,或者不准备修复发现的错误(问题的含义是您没有这样做)。 ,那么我建议PHP 7可能对您来说太过升级了。

您确实可以选择恢复到PHP 5.6。我知道您说过您不想这样做,但是作为中短期的解决方案,它会让您更轻松。坦率地说,我认为这可能是您的最佳选择。


2
好吧,我知道这很糟糕。但是为什么为什么它们不会禁止使用,func_get_args因为它们使您可以通过“流畅”的界面获得完全相同的行为呢?为什么它们会迫使用户放弃明确的接口定义并诉诸各种作弊手段?谁将从中受益?
sanmai

PHP正朝着更加严格的方向发展。它还没有发展到现在,甚至可能永远也不会(它永远不会发展成为Java或C#语言)。func_get_args尽管在PHP 5.6中引入了可变参数函数参数,但仍然允许使用,但用例却少得多。我不认为它会很快被弃用,但是我也不认为许多为当前PHP版本编写代码的开发人员会非常使用它。
辛巴

值得补充的是,某些确实具有严格重写规则的语言(例如C#)也具有允许使用相同名称但使用不同参数列表定义多个方法的规则。换句话说,在C#中,您的覆盖方法根本不会被视为覆盖。这将被视为完全不同的方法。PHP当前不允许这样做(因此会出现错误),但是将来的版本中可能会出现类似的情况,这是完全可行的。现在对您没有帮助,但是很有趣。
辛巴

想象一下你是对的。那如何回答我的问题?
sanmai

我对您的问题的直接回答是在中间,我说“确实没有您想要的那种解决方法”。实际上,我发现您为自己找到了解决方法的答案;做得很好。这很丑陋,但是您做到了。做得好。我仍然坚持我所说的,特别是关于需要检查您的代码是否存在其他可能未显示警告的PHP7小故障。
辛巴

9

我同意:第一篇文章中的示例是不良做法。现在,如果您有该示例:

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在它们后面...

我是否认为在这个特定示例中此警告是错误的?


1
这是我面临的确切问题。您找到了解决方案吗?
Lucian D.

不,我没有。我开始隐藏这些警告,但是随后在运行了5年的脚本上出现了PHP7的分段错误,而在PHP5上没有出现任何问题,我发现PHP7可能尚未准备好……我将尝试一下稍后...
Zaziffic '17

1
实际上,此代码违反了Liskov替换原理,几乎不受任何OOP语言的支持,因为这样的实例BirdManagerDogManager不再可以在任何AnimalManager可以使用的地方安全地使用,并且也不能在编译时进行类型检查(因为当您有AnimalManager类型提示时,您实际上可能正在处理其子类之一。有关更多详细信息,请参见en.wikipedia.org/wiki/…en.wikipedia.org/wiki/Liskov_substitution_principle
马特·布朗

1
但是,有一个合法的用例可以满足LSP要求,但PHP目前尚不支持这种情况:反向参数类型-示例相反(反向表示子类中的参数类型比父类中的参数类型更通用) )。这是由于实施方面的挑战,但在本RFP中有提及。另请参见bugs.php.net/bug.php?id=72208
马特·布朗

1
所选答案似乎可以解决这种情况。更改您的方法以接受子类中的任何AnimalData可以防止出现此警告。class BirdManager extends AnimalManager { public static function displayProperties(AnimalData $bird) { assert($bird instanceof BirdData);...
Shaun Cockerill

6

我也有这个问题。我有一个重写父类功能的类,但是重写具有不同数量的参数。我可以想到一些简单的解决方法-但确实需要进行少量代码更改。

  1. 更改子类中的函数名称(因此它不再覆盖父函数)-或者-
  2. 更改父函数的参数,但使其他参数为可选(例如,函数func($ var1,$ var2 = null)-这可能是最简单的并且需要较少的代码更改。但是可能不值得在父母,如果它使用了很多其他地方,那么我就选择了#1。

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


谢谢@AeonTrek。我面临的问题是,我上面编写的简单代码段实际上是应用程序中更大的图片的一部分,其中具有相同的函数名称,相同的参数(...)使我可以广泛分解代码。我很感谢您的建议,但是-并以所有适当的尊重-它们是解决方案,不能真正解决问题。我感到只有PHP社区可以解决我的问题。这就是为什么我现在回到PHP5的原因。
Zaziffic

0

您可以完全删除父类方法定义,并使用magic方法拦截它。

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

我刚遇到这个问题,走了这条路

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.