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

After upgrade to PHP 7 the logs almost choked on this kind of errors:

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

How do I silence these and only these errors in PHP 7?

  • Before PHP 7 they were E_STRICT type of warnings which could be easily dealt with. Now they're just plain old warnings. Since I do want to know about other warnings, I can't just turn off all warnings altogether.

  • I don't have a mental capacity to rewrite these legacy APIs not even mentioning all the software that uses them. Guess what, nobody's going to pay for that too. Neither I develop them in the first place so I'm not the one for blame. (Unit tests? Not in the fashion ten years ago.)

  • I would like to avoid any trickery with func_get_args and similar as much as possible.

  • Not really I want to downgrade to PHP 5.

  • I still want to know about other errors and warnings.

Is there a clean and nice way to accomplish this?


1. Workaround

Since it is not always possible to correct all the code you did not write, especially the legacy one...

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

This error handler returns true for warnings beginning with Declaration of which basically tells PHP that a warning was taken care of. That's why PHP won't report this warning elsewhere.

Plus, this code will only run in PHP 7 or higher.


If you want this to happen only in regard to a specific codebase, then you could check if a file with an error belongs to that codebase or a library of interest:

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. Proper solution

As for actually fixing someone else's legacy code, there is a number of cases where this could be done between easy and manageable. In examples below class B is a subclass of A. Note that you do not necessarily will remove any LSP violations by following these examples.

  1. Some cases are pretty easy. If in a subclass there's a missing default argument, just add it and move on. E.g. in this case:

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

    You would do:

    - public function foo()
    + public function foo($bar = null)
    
  2. If you have additional constrains added in a subclass, remove them from the definition, while moving inside the function's body.

    Declaration of B::add(Baz $baz) should be compatible with A::add($n)
    

    You may want to use assertions or throw an exception depending on a severity.

    - public function add(Baz $baz)
    + public function add($baz)
      {
    +     assert($baz instanceof Baz);
    

    If you see that the constraints are being used purely for documentation purposes, move them where they belong.

    - protected function setValue(Baz $baz)
    + /**
    +  * @param Baz $baz
    +  */
    + protected function setValue($baz)
      {
    +     /** @var $baz Baz */
    
  3. If you subclass has less arguments than a superclass, and you could make them optional in the superclass, just add placeholders in the subclass. Given error string:

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

    You would do:

    - public function foo($param = '')
    + public function foo($param = '', $_ = null)
    
  4. If you see some arguments made required in a subclass, take the matter in your hands.

    - protected function foo($bar)
    + protected function foo($bar = null)
      {
    +     if (empty($bar['key'])) {
    +         throw new Exception("Invalid argument");
    +     }
    
  5. Sometimes it may be easier to alter the superclass method to exclude an optional argument altogether, falling back to func_get_args magic. Do not forget to document the missing argument.

      /**
    +  * @param callable $bar
       */
    - public function getFoo($bar = false)
    + public function getFoo()
      {
    +     if (func_num_args() && $bar = func_get_arg(0)) {
    +         // go on with $bar
    

    Sure this can become very tedious if you have to remove more than one argument.

  6. Things get much more interesting if you have serious violations of substitution principle. If you do not have typed arguments, then it is easy. Just make all extra arguments optional, then check for their presence. Given error:

    Declaration of B::save($key, $value) should be compatible with A::save($foo = NULL)
    

    You would do:

    - public function save($key, $value)
    + public function save($key = null, $value = null)
      {
    +     if (func_num_args() < 2) {
    +         throw new Exception("Required argument missing");
    +     }
    

    Note that we couldn't use func_get_args() here because it does not account for default (non-passed) arguments. We are left with only func_num_args().

  7. If you have a whole hierarchies of classes with a diverging interface, it may be easier diverge it even further. Rename a function with conflicting definition in every class. Then add a proxy function in a single intermediary parent for these classes:

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

    This way LSP would still be violated, although without a warning, but you get to keep all type checks you have in subclasses.


For those who want to actually correct your code so it no longer triggers the warning: I found it useful to learn that you can add additional parameters to overridden methods in subclasses as long as you give them default values. So for example, while this will trigger the warning:

//"Warning: Declaration of B::foo($arg1) should be compatible with A::foo()"
class B extends A {
    function foo($arg1) {}
}

class A {
    function foo() {}
}

This will not:

class B extends A {
    function foo($arg1 = null) {}
}

class A {
    function foo() {}
}