沉默 PHP 7 中的“声明......应该兼容”警告

Posted

技术标签:

【中文标题】沉默 PHP 7 中的“声明......应该兼容”警告【英文标题】:Silence "Declaration ... should be compatible" warnings in PHP 7 【发布时间】:2016-07-04 22:17:08 【问题描述】:

升级到 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 类型的警告 which could be easily dealt with。现在它们只是简单的旧警告。由于我确实想了解其他警告,因此我不能完全关闭所有警告。

我没有能力重写这些遗留 API,更不用说使用它们的所有软件了。猜猜看,没有人会为此付出代价。我一开始都没有开发它们,所以我不是罪魁祸首。 (单元测试?十年前不流行。)

我想尽可能避免 any trickery 和 func_get_args 和类似的东西。

我真的不想降级到 PHP 5。

我仍然想了解其他错误和警告。

有没有干净又好的方法来完成这个?

【问题讨论】:

这些是Warnings,而不是Errors。而且你不应该试图让他们“沉默”,而是要解决这个问题。警告的目的是告诉您您的代码将来会遇到问题。 @arkascha 我不确定这些 cmets 在这里是否真的必要。诚然,在理想的世界中,您将拥有所有的时间和金钱来愉快地修复遗留代码。但在现实世界中,这通常是不可能的,甚至是不允许的。 @arkascha OP 明确指出这是一个次优情况,目前无能为力。我完全同意重新实现并因此可能完全重新设计整个 API 是绝对不现实的,因为目前除了一些额外的日志消息之外“没有问题”(显然是管理层的观点)。这是一个合理的问题,无需如此苛刻。 @arkascha 我知道,我想说的是,虽然你是对的,但这在这里没有帮助。 ;) 请注意,我写的是 cmets,而不是答案 :-) 【参考方案1】:

PHP 7 删除了E_STRICT 错误级别。可以在PHP7 compatibility notes 中找到有关此的信息。您可能还想阅读 the proposal document 在开发 PHP 7 时讨论它的地方。

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

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

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

大多数现代编程语言实际上根本不允许这样做。 PHP 曾经允许开发人员摆脱这样的事情,但是每个版本的语言都变得更加严格,尤其是现在 PHP 7 ——他们专门使用了一个新的主要版本号,以便他们可以证明做出重大改变是合理的向后兼容。

您遇到的问题是因为您已经忽略了警告消息。您的问题暗示这是您要继续使用的解决方案,但应将“严格”和“不推荐”等消息视为您的代码可能在未来版本中中断的明确警告。通过在过去几年中忽略它们,您实际上已将自己置于现在的情况中。 (我知道这不是你想听到的,并且对现在的情况没有真正的帮助,但重要的是要说清楚)

确实没有您正在寻找的那种解决方法。 PHP 语言正在发展,如果您想坚持使用 PHP 7,您的代码也需要发展。如果您确实无法修复代码,那么您将不得不禁止所有警告,或者忍受这些警告使您的日志变得混乱。

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

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

【讨论】:

好吧,我知道这很糟糕。但是为什么他们不会禁止func_get_args,因为他们让您通过“流畅”界面获得完全相同的行为?他们为什么要强迫用户放弃对接口的明确定义并诉诸各种欺骗手段?谁将从中受益? PHP 正朝着更加严格的方向发展;它还没有走到那一步,而且可能永远不会(它永远不会演变成一种 Java 或 C# 语言)。 func_get_args 仍然是允许的,尽管在 PHP 5.6 中引入了可变参数函数参数,它的用例要少得多。我认为它不会很快被弃用,但我也不认为许多为当前 PHP 版本编写代码的开发人员会大量使用它。 值得补充的是,一些确实具有严格覆盖规则的语言(如 C#)也具有允许使用相同名称但不同参数列表定义多个方法的规则。换句话说,在 C# 中,您的覆盖方法根本不会被视为覆盖。这将被视为一种完全不同的方法。 PHP目前不允许这样做(因此您会遇到错误),但是在未来的版本中可能会出现类似的东西是完全可行的。现在对你没有帮助,但一个有趣的想法。 想象你是对的。这如何回答我的问题? 我对您问题的直接回答是在中间,我说 “确实没有您正在寻找的那种解决方法”。事实上,我看到你已经为自己提供了一个答案,你确实设法找到了解决方法;做得很好。这是丑陋和hacky,但你做到了。做得好。尽管如此,我仍然坚持我所说的,特别是需要检查您的代码是否存在其他可能不会出现警告的 PHP7 故障。【参考方案2】:

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

<?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 did not have uniform syntax for expressions。要使其与 PHP 5 一起使用,您需要在调用它之前将函数分配给变量(或使其成为命名函数):

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

【讨论】:

这真的是比实际解决根本问题更好的解决方案吗?甚至是OP如此热衷于避免的 func_get_args 诡计?也就是说,为实际找到可行的解决方案而感到自豪。不过,这绝对算不上他要求的干净而好的解决方案。我勉强 +1。 这当然不是一个更好的解决方案,但它确实回答了这个问题。 这是不可接受的,因为它会消除所有错误,而不仅仅是那些我不想要的错误。例如,@(function () constant('nothing');)(); @sanmai 它消除了定义类时发生的任何错误(但不是此后),是的。如果您担心您的类会产生其他警告,您可以使用自定义错误处理程序编写更复杂的解决方案。 That's what I did in the end.【参考方案3】:

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。适当的解决方案

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

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

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

    你会这样做:

    - public function foo()
    + public function foo($bar = null)
    

    如果您在子类中添加了其他约束,请将它们从定义中删除,同时在函数体内移动。

    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 */
    

    如果您的子类的参数少于超类,并且您可以在超类中将它们设为可选,只需在子类中添加占位符即可。给定错误字符串:

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

    你会这样做:

    - public function foo($param = '')
    + public function foo($param = '', $_ = null)
    

    如果您看到子类中需要一些参数,请自行处理。

    - protected function foo($bar)
    + protected function foo($bar = null)
      
    +     if (empty($bar['key'])) 
    +         throw new Exception("Invalid argument");
    +     
    

    有时更改超类方法以完全排除可选参数可能更容易,回退到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
    

    如果您必须删除多个参数,这肯定会变得非常乏味。

    如果您严重违反替代原则,事情会变得更加有趣。如果您没有键入的参数,那么这很容易。只需将所有额外的参数设为可选,然后检查它们是否存在。给定错误:

    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()

    如果您有一个具有不同接口的整个类层次结构,则可能更容易将其进一步分开。重命名每个类中定义冲突的函数。然后在这些类的单个中间父级中添加一个代理函数:

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

    这种方式仍然会违反 LSP,尽管没有警告,但您可以保留子类中的所有类型检查。

【讨论】:

迄今为止最好的答案;大多数其他人都认为这是您的代码,因此“您”应该更正它。并非我们所有人都幸运地能够 100% 控制他们的整个代码库,包括所有库。 LSP 警告在较旧的库中很常见,并且分叉其他人的遗留库,该库在其内部逻辑中存在 LSP 违规,但暴露的 API 运行良好,对于只能将错误引入稳定代码库的东西来说是大量的额外工作.如果这个答案可以使用任何东西,如果错误也来自特定的库,那就是检查你的顶部if @JeffreyMCastro 我把它作为开头 这不起作用。 PHP 编译模板时会触发警告。出于某种原因,即使在包含文件或类时已注册,这也不会触发错误处理程序。但是,只有在首次编译文件时才会触发警告。一旦编译并存储在 opcache 中,就不会发出进一步的警告。 @WillemStuursma 显然你有其他错误处理程序干扰 这适用于 apache,但不适用于我通过命令行使用 php 时。有什么想法为什么 cli 没有得到这个错误处理程序?【参考方案4】:

对于那些想要真正更正您的代码以使其不再触发警告的人:我发现学习可以向子类中的重写方法添加其他参数很有用,只要您给它们提供默认值。例如,虽然这会触发警告:

//"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 我不记得我是如何发现这个的,但它不是来自任何官方文档。但是,如果您想了解更多关于它的信息,这一切都基于 Liskov 替换原则 (LSP) - 默认值为 null 不会违反 LSP,因为子类不需要 要求 a与父类不同的 API。有关更多详细信息和链接,请参阅下面this answer 我的 cmets。 谢谢!我还观察到无论默认值是什么,它都能正常工作。【参考方案5】:

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

    更改子类中函数的名称(使其不再覆盖父函数) -或-

    更改父函数的参数,但使额外的参数可选(例如,函数 func($var1, $var2=null) - 这可能是最简单的并且需要较少的代码更改。但它可能不是如果它使用了很多其他地方,值得在父级中更改它。所以我选择了#1。

    如果可能,不要在子类函数中传递额外的参数,而是使用 global 来拉入额外的参数。这不是理想的编码;但无论如何都是一种可能的创可贴。

【讨论】:

感谢@AeonTrek。我面临的问题是,我上面写的简单 sn-p 实际上是我应用程序中更大图景的一部分,其中具有相同的函数名称、相同的参数 (...) 允许我广泛地分解我的代码。我很欣赏你的建议,但是 - 恕我直言 - 它们是变通方法,没有真正的问题答案。不幸的是,我觉得只有 PHP 社区可以解决我的问题。这就是我现在回到 PHP5 的原因。【参考方案6】:

我同意:第一篇文章中的示例是不好的做法。 现在如果你有那个例子怎么办:

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 来使它们成为可选...

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

【讨论】:

这正是我面临的问题。你找到解决办法了吗? 不,我没有。我开始隐藏这些警告,但后来我在一个脚本上遇到了 PHP7 的分段错误,该脚本已经运行了 5 年,在 PHP5 上没有一个问题,我发现 PHP7 可能还没有准备好......我会试一试稍后... 实际上这段代码违反了 Liskov 替换原则,几乎所有 OOP 语言都不支持,因为这样BirdManagerDogManager 的实例就不能再安全地在AnimalManager 可以存在的任何地方使用使用过,并且也不能在编译时进行类型检查(因为任何时候你有一个AnimalManager 类型提示,你实际上可能正在处理它的一个子类)。请参阅en.wikipedia.org/wiki/… 和en.wikipedia.org/wiki/Liskov_substitution_principle 了解更多详情。 但是,有一个合法的用例满足 LSP 但 PHP 目前不支持:逆变参数类型 - 与您的示例相反(逆变意味着子类中的参数类型更通用比父类中的参数类型)。这是由于实施方面的挑战,但在此RFP 中提到)。另见bugs.php.net/bug.php?id=72208 所选答案似乎涵盖了这种情况。更改您的方法以接受子类中的任何 AnimalData 应该可以防止警告。 class BirdManager extends AnimalManager public static function displayProperties(AnimalData $bird) assert($bird instanceof BirdData);...【参考方案7】:

你可以把父类的方法定义干脆去掉,用魔术方法拦截。

public function __call($name, $args)

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

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

【讨论】:

【参考方案8】:

如果基类的参数比派生类少,可以像这样向派生类添加额外的参数:

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

这样,您添加了第三个“默认”参数,但不更改签名。 仅当您有大量代码调用此代码且无法更改该代码并希望保持向后兼容性时,我才会建议您这样做。

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

【讨论】:

以上是关于沉默 PHP 7 中的“声明......应该兼容”警告的主要内容,如果未能解决你的问题,请参考以下文章

魔兽世界单机版把自己沉默了怎么办

《沉默的大多数》 - 王小波

沉默的答案!直播源码禁言技术的实现

沉默聊数据测试重要性

沉默聊数据测试重要性

《沉默的大多数》的读后感作文3500字