在PHP 7中消除“声明...应该兼容”的警告

73
升级到PHP 7之后,日志几乎被这种错误淹没了:

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

我该如何在PHP 7中消除这些错误?

  • 在 PHP 7 之前,它们是 E_STRICT 类型的警告,可以轻松处理。现在它们只是普通的警告。由于我确实想知道其他警告,我不能完全关闭所有警告。

  • 我没有精力重写这些遗留 API,更不用说使用它们的所有软件了。猜猜怎么着,没人会为此付费。我也不是最初开发它们的人,所以我不应该受到责备。(单元测试吗?十年前的时尚不是这样的。)

  • 我希望尽可能地避免使用func_get_args等技巧

  • 我并不真正想将 PHP 降级到 5。

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

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


4
这些是“警告”,而不是“错误”。你不应该试图“消除”它们,而是要解决这个问题。警告的目的是告诉你,你的代码将来可能会遇到问题。 - arkascha
24
我不确定这些评论在这里是否真的必要。确实,在理想的世界中,你将拥有足够的时间和资金来愉快地修复旧代码。但在现实世界中,往往是不可能甚至不允许这样做的。 - Yoshi
6
@arkascha的回复中,原帖已经明确表明这是目前无法做到最佳情况的一种局面。我完全同意,在除了一些额外日志信息(从管理层的角度来看)之外,重新实现并可能完全重新设计整个API可能是绝对不切实际的。这是一个合理的问题,没有必要那么严厉。 - deceze
6
请注意,我写的是评论,而不是答案 :-) - arkascha
2
要得到一个答案,一个自定义错误处理程序应该允许您自由选择抑制任何您想要的内容;尽管这可能不像设置特定的error_reporting标志那样好。 - deceze
显示剩余10条评论
8个回答

134

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 不会在其他地方报告此警告的原因。

此外,此代码仅在 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违规。

  1. 有些情况非常简单。如果子类中缺少默认参数,则只需添加它并继续进行。例如,在这种情况下:

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

    你会做:

    - public function foo()
    + public function foo($bar = null)
    
  2. 如果在子类中添加了额外的限制条件,请将它们从定义中删除,并将其移到函数体内部。

  3. 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 */
    
  4. 如果您的子类比超类拥有更少的参数,并且您可以使它们在超类中成为可选的,请在子类中添加占位符。给定错误字符串:

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

    您需要做的是:

    - public function foo($param = '')
    + public function foo($param = '', $_ = null)
    
  5. 如果你在子类中看到有些参数被要求必须提供,请自行解决这个问题。

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

    如果你需要移除多个参数,这可能会变得非常繁琐。

  7. 如果您存在严重的替换原则违规情况,则情况变得更加有趣。如果没有输入类型的参数,则很容易。只需将所有额外的参数设置为可选,并检查它们是否存在。给定错误:

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

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

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

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


7
迄今为止最佳答案;其他大多数答案都假设这是你的代码,所以“你”应该进行更正。并不是所有人都有100%的控制权管理他们的整个代码库,包括所有库。LSP警告在旧版库中非常常见,而复制某人遗留的库,即使其内部逻辑存在LSP违规,但公开的API仍能完美地运作,这将是一项巨大的额外工作,可能会在稳定的代码库中引入bug。如果这个答案需要完善,那就应该在你的顶部的if语句中检查错误是否来自特定的库。 - Josh from Qaribou
@JeffreyMCastro 我把它放在一个包含文件的开头 <?php 标签后面,这个文件被到处使用。 - Will Bonde
1
这个不起作用。当PHP编译模板时,会触发警告。由于某种原因,即使在文件或类被包含的同时注册了错误处理程序,它也不会触发错误处理程序。然而,只有在第一次编译文件时才会触发警告。一旦编译并存储在opcache中,就不会再发出任何警告。 - user6632554
@WillemStuursma,显然你有其他的错误处理程序干扰了。 - sanmai
这适用于Apache,但在我通过命令行使用PHP时不起作用。有什么想法为什么CLI没有获取此错误处理程序? - Frank Adrian
显示剩余2条评论

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 我不记得我是怎么发现这个的,但它并不是来自任何官方文档。然而,如果你想了解更多,它都基于里氏替换原则(LSP)- 默认值为null并不违反LSP,因为子类不需要比父类有不同的API。请参见我在下面此答案中的评论,以获取更多详细信息和链接。 - Matt Browne
谢谢!我也观察到它可以工作,无论默认值是什么。 - 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。 - Simba
8
这确实不是更好的解决方案,但它确实回答了这个问题。 - Andrea
1
这是不可接受的,因为它会使所有错误无声,而不仅仅是我不想要的那些。例如:@(function () {constant('nothing');})(); - sanmai
@sanmai 它会在定义类时(但之后不会)消除任何错误。如果您担心您的类会产生其他警告,可以使用自定义错误处理程序编写更复杂的解决方案。 - Andrea
2
这就是我最终所做的。 - sanmai

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版本编写代码的开发人员会经常使用它。 - Simba
值得一提的是,一些具有严格覆盖规则的语言(如C#)也有规则允许使用相同名称但参数列表不同的多个方法进行定义。换句话说,在C#中,您的覆盖方法将不会被视为覆盖;它将被视为完全不同的方法。PHP目前不允许这样做(因此您正在遇到错误),但未来版本可能会引入类似的功能。虽然现在对您没有帮助,但这是一个有趣的想法。 - Simba
想象你是正确的。那怎么回答我的问题? - sanmai
我的直接回答在中间,我说:“实际上,没有你要找的那种解决方法”。事实上,我看到你已经为自己提供了一个解决方法;做得好。虽然它很丑陋和不专业,但你做到了。干得好。尽管如此,我仍然坚持我的观点,特别是需要检查您的代码是否存在其他可能不会显示警告的PHP7故障。 - Simba
就像我说的那样,这并不是我的代码,我并没有编写它。幸运的是,到目前为止还没有出现其他严重的故障,但还是谢谢你。 - sanmai

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.
没有,我开始隐藏那些警告,但是在一个已经在PHP5上运行了5年而没有任何问题的脚本中,我遇到了PHP7的分段错误,我想可能是因为PHP7还没有准备好...稍后我会再试一下... - Zaziffic
1
实际上,这段代码违反了Liskov替换原则,在几乎所有面向对象编程语言中都不受支持。因为BirdManagerDogManager的实例不能再安全地用于任何可以使用AnimalManager的地方,并且在编译时也无法进行类型检查(因为任何时候你有一个AnimalManager类型提示,你实际上可能正在处理它的子类之一)。更多详情请参见https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)#Covariant_method_return_type和https://en.wikipedia.org/wiki/Liskov_substitution_principle。 - Matt Browne
1
然而,有一个合法的用例满足LSP但目前在PHP中不受支持:逆变参数类型-与您的示例相反(逆变意味着子类中的参数类型比父类中的参数类型更通用)。这是由于实现挑战,但它在此[RFP](https://wiki.php.net/rfc/return_types#future_work)中提到。另请参见https://bugs.php.net/bug.php?id=72208。 - Matt Browne
1
所选答案似乎涵盖了这种情况。将您的方法更改为在子类中接受任何AnimalData应该可以防止警告。class BirdManager extends AnimalManager { public static function displayProperties(AnimalData $bird) { assert($bird instanceof BirdData);... - Shaun Cockerill
确实是这样@ShaunCockerill,谢谢。还要感谢sanmai提供的详细答案! 目前,我只是隐藏了警告-懒惰的兔子-但最终我会使用assert方法。 至于分段错误,它们已经消失了。我相信我的安装应该受到责备,否则问题会更加普遍...所以PHP 7太棒了! - Zaziffic

6

我也遇到了这个问题。我有一个类,它覆盖了父类的一个函数,但是覆盖使用了不同数量的参数。我可以想到一些简单的解决方法 - 但需要进行轻微的代码更改。

  1. 在子类中更改函数的名称(因此不再覆盖父函数) 或者
  2. 更改父函数的参数,但使额外的参数可选(例如,function func($var1, $var2=null) - 这可能是最容易的,并且需要较少的代码更改。但如果在其他许多地方都使用了该函数,则可能不值得在父对象中更改。所以在我的情况下,我选择了#1。

  3. 如果可能的话,不要在子类函数中传递额外的参数,而是使用全局变量来获取额外的参数。这不是理想的编码方式,但仍然是一种可能的临时措施。


感谢@AeonTrek。我面临的问题是,我上面写的简单片段实际上是我的应用程序中更大的一部分,在那里拥有相同的函数名称、相同的参数(...)让我可以广泛地因式分解我的代码。我感谢您的建议,但是 - 并且恕我直言 - 它们只是解决方法,而不是真正的问题答案。不幸的是,我只觉得PHP社区能够解决我的问题。这就是为什么我现在回到PHP5的原因。 - Zaziffic

0
如果基类的参数比派生类少,可以像这样向派生类添加额外的参数:
    $namespace = 'default';
    if (func_num_args() > 2) {
        $namespace = func_get_arg(2);
    }

这种方式可以添加第三个“默认”的参数,但不会改变签名。 我只建议在您有大量调用此代码且无法更改该代码的情况下使用,并希望保持向后兼容性。

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


0
您可以完全删除父类方法定义,并使用魔术方法拦截它。
public function __call($name, $args)
{
    if($name == 'do') {
        // do things with the unknown # of args
    } else {
        throw new \Exception("Unknown method $name", 500);
    }
}

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接