在 PHP 7 中静音 "Declaration ... should be compatible" 个警告

Silence "Declaration ... should be compatible" warnings in PHP 7

升级到 PHP7 后,日志几乎被这种错误阻塞:

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 错误级别。关于此的信息可以在 PHP7 compatibility notes. You might also want to read the proposal document 中找到,在 PHP 7 正在开发时讨论过。

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

您遇到的错误是 classic 案例:

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

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

您遇到的问题是因为您已经忽略了警告消息。您的问题暗示这是您想要继续的解决方案,但是像 "strict" 和 "deprecated" 这样的消息应该被视为一个明确的警告,表明您的代码可能会在未来的版本中中断。通过在过去的几年中忽略它们,您实际上已经将自己置于现在的境地。 (我知道这不是你想听到的,对现在的情况也无济于事,但重要的是要说清楚)

确实没有您正在寻找的变通方法。 PHP 语言在不断发展,如果您想坚持使用 PHP 7 您的代码也需要改进。如果您真的无法修复代码,那么您要么必须禁止显示所有警告,要么忍受这些使您的日志混乱的警告。

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

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

如果您必须消除错误,您可以在一个消除的、立即调用的函数表达式中声明class:

<?php

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

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

不过,我强烈建议不要这样做。修复您的代码比消除有关代码如何损坏的警告更好。


如果你需要保持PHP5的兼容性,请注意上面的代码只适用于PHP7,因为PHP 5 did not have uniform syntax for expressions。要使其与 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);
}

此错误处理程序 returns 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。正确的解决方案

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

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

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

    你会做:

    - public function foo()
    + public function foo($bar = null)
    
  2. 如果您在子函数中添加了额外的约束class,请将它们从定义中移除,同时在函数体内移动。

    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. 如果你的 subclass 比 superclass 的参数少,你可以在 superclass 中将它们设为可选,只需在子class。给出的错误字符串:

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

    你会做:

    - public function foo($param = '')
    + public function foo($param = '', $_ = null)
    
  4. 如果您看到子class中需要提出一些论点,请把事情交给您。

    - protected function foo($bar)
    + protected function foo($bar = null)
      {
    +     if (empty($bar['key'])) {
    +         throw new Exception("Invalid argument");
    +     }
    
  5. 有时更改 superclass 方法以完全排除可选参数可能更容易,回到 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. 如果你有一个完整的 classes 层次结构和一个发散的接口,它可能更容易进一步发散。在每个 class 中重命名具有冲突定义的函数。然后为这些 classes:

    在单个中间父级中添加代理功能
    function save($arg = null) // conforms to the parent
    {
        $args = func_get_args();
        return $this->saveExtra(...$args); // diverged interface
    }
    

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

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

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

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

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

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

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

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

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

您可以完全删除父 class 方法定义并使用魔术方法拦截它。

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

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

如果基础 class 的参数少于派生 class,则可以向派生 class 添加额外的参数,如下所示:

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

通过这种方式,您添加了第三个“默认”参数,但不更改签名。 如果您有大量代码调用它并且无法更改该代码并希望保持向后兼容性,我只会建议这样做。

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