71

升级到 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

8 回答 8

128

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 违规。

  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,尽管没有警告,但您可以保留子类中的所有类型检查。

于 2016-03-24T09:22:30.223 回答
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() {}
}
于 2016-05-06T21:36:52.387 回答
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($_);
于 2016-03-18T12:21:46.507 回答
18

PHP 7 删除了E_STRICT错误级别。有关这方面的信息可以在PHP7 兼容性说明中找到。您可能还想阅读在开发 PHP 7 时讨论过的提案文档。

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

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

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

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

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

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

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

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

于 2016-03-18T09:53:58.210 回答
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后面添加一个来使它们成为可选的......

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

于 2017-03-16T17:19:04.277 回答
6

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

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

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

于 2016-08-18T15:05:13.823 回答
0

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

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

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

于 2020-01-30T14:10:51.083 回答
0

如果基类的参数比派生类少,可以向派生类添加额外的参数,如下所示:

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

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

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

于 2021-05-27T16:31:56.557 回答