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

47,185

Solution 1

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.

Solution 2

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

Solution 3

If you must silence the error, you can declare the class inside a silenced, immediately-invoked function expression:

<?php

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

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

I would strongly recommend against this, though. It is better to fix your code than to silence warnings about how it is broken.


If you need to maintain PHP 5 compatibility, be aware that the above code only works in PHP 7, because PHP 5 did not have uniform syntax for expressions. To make it work with PHP 5, you would need to assign the function to a variable before invoking it (or make it a named function):

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

Solution 4

PHP 7 removes the E_STRICT error level. Info about this can be found in the PHP7 compatibility notes. You might also want to read the proposal document where it was discussed while PHP 7 was being developed.

The simple fact is this: The E_STRICT notices were introduced a number of versions ago, in an attempt to notify developers that they were using bad practice, but initially without trying to force any changes. However recent versions, and PHP 7 in particular, have become more strict about these things.

The error you're experiencing is a classic case:

You have defined a method in your class that overrides a method of the same name in the parent class, but your override method has a different argument signature.

Most modern programming languages would not actually allow this at all. PHP used to allow developers to get away with stuff like this, but the language is becoming more strict with every version, especially now with PHP 7 -- they went with a new major version number specifically so that they could justify making significant changes that break backward compatibility.

The problem you have is because you've already been ignoring the warning messages. Your question implies that this is the solution you want to continue with, but messages like "strict" and "deprecated" should be treated as an explicit warning that your code is likely to break in future versions. By ignoring them for the past number of years, you have effectively placed yourself in the situation you have now. (I know that's not what you want to hear, and doesn't really help the situation now, but it's important to make it clear)

There really isn't a work around of the kind you're looking for. The PHP language is evolving, and if you want to stick with PHP 7 your code will need to evolve too. If you really can't fix the code, then you will either have to suppress all warnings or else live with these warnings cluttering up your logs.

The other thing you need to know if you plan to stick with PHP 7 is that there are a number of other compatibility breaks with this version, including some that are quite subtle. If your code is in a state where it has errors like the one you're reporting, it means that it's probably been around for quite a while, and likely has other issues that will cause you problems in PHP 7. For code like this, I would suggest doing a more thorough audit of the code before committing to PHP 7. If you're not prepared to do that, or not prepared to fix the bugs that are found (and the implication from your question is that you are not), then I'd suggest that PHP 7 is probably an upgrade too far for you.

You do have the option of reverting to PHP 5.6. I know you said you don't want to do that, but as a short-to-medium term solution it will make things easier for you. Frankly, I think it might be your best option.

Solution 5

I agree: the example in the first post is bad practice. Now what if you have that example :

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);
        }
}

I believe this is a legitimate code structure, nevertheless this will raise a warning in my logs because the displayProperties() do not have the same parameters. Moreover I can't make them optional by adding a = null after them...

Am I right thinking this warning is wrong in this specific example please?

Share:
47,185
sanmai
Author by

sanmai

Updated on July 05, 2022

Comments

  • sanmai
    sanmai almost 2 years

    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?

  • Simba
    Simba about 8 years
    Is this really a better solution than, you know, actually fixing the root problem? Or even the func_get_args trickery that the OP is so keen to avoid? That said, kudos for actually finding a working solution. It definitely doesn't count as the clean and nice solution he asked for though. A grudging +1 from me.
  • Andrea
    Andrea about 8 years
    It's certainly not a better solution, but it does actually answer the question.
  • sanmai
    sanmai about 8 years
    Okay I get this is bad. But why they won't ban func_get_args since they let you get exactly same behavior with "fluent" interfaces? Why are they forcing users to forfeit clear definitions of interfaces and resort to various cheats? Who's going to benefit from this?
  • Simba
    Simba about 8 years
    PHP is moving toward being more strict; it hasn't gone all the way there yet, and probably never will (it's never going to evolve to become a Java or C# kind of language). func_get_args is still allowed, although with the introduction of variadic function arguments in PHP 5.6, there are a lot fewer use-cases for it. I don't think it'll be deprecated any time soon, but I also don't think many devs writing code for current PHP versions will be using it very much.
  • Simba
    Simba about 8 years
    It's worth adding that some languages (like C#) that do have strict override rules also have rules that allow multiple methods to be defined with the same name but different argument lists. In other words, in C#, your override method wouldn't be seen as an override at all; it would be seen as an entirely different method. PHP doesn't allow this currently (hence the error you're getting), but it's perfectly feasible that something like this might come in with a future version. Not helpful for you now, but an interesting thought.
  • sanmai
    sanmai about 8 years
    This is unacceptable because it silences all and every error, not only those I do not want. E.g., @(function () {constant('nothing');})();
  • sanmai
    sanmai about 8 years
    Imagine that you're right. How does that answer my question?
  • Simba
    Simba about 8 years
    My direct answer to your question was in the middle, where I said "There really isn't a work around of the kind you're looking for". In fact, I see you've provided an answer for yourself where you did manage to find a work-around; well done for that. It's ugly and hacky but you did it. Well done. I still stand by what I said though, particularly about needing to check your code for other possible PHP7 glitches that may not show up with warnings.
  • sanmai
    sanmai about 8 years
    Like I said, this isn't mine code in a sense that I didn't write it. Luckily there aren't any other serious glitches so far, but thanks anyway.
  • Andrea
    Andrea about 8 years
    @sanmai It silences any errors that occur when the class is defined (but not thereafter), yes. You can write a more complicated solution using a custom error handler if you're worried that your class will produce other warnings.
  • Josh from Qaribou
    Josh from Qaribou over 7 years
    Best answer by far; most of the others assume this is your code, so "you" should correct it. Not all of us are lucky enough to have 100% control over their entire code base, including all libraries. LSP warnings are pretty common in older libs, and forking someone else's legacy lib that has LSP violations in its internal logic, but the exposed API works perfectly well, is a huge amount of extra work for something that could only introduce bugs into a stable codebase. If this answer could use anything, it would be to check in your top if if the error is coming from a specific lib too.
  • sanmai
    sanmai about 7 years
  • Lucian D.
    Lucian D. about 7 years
    This is the exact problem I'm facing. Have you found any solution to this?
  • Zaziffic
    Zaziffic about 7 years
    No I haven't. I started hiding those warnings but then I had a segmentation fault with PHP7 on a script that has been running for 5 years without one issue on PHP5 and I figured out that maybe PHP7 was not ready yet... I'll give it a try later...
  • Zaziffic
    Zaziffic about 7 years
    Thanks @AeonTrek. The problem I'm facing is that the simple snippet I wrote above is actually part of far bigger picture in my application where having the same function names, the same parameters (...) allows me to factorise my code extensively. I appreciate your suggestions but - and with all due respect - they are work arounds, no real answers to the problem. I'm feeling only the PHP community can address my problem unfortunately. Which is why I'm back to PHP5 for now.
  • Matt Browne
    Matt Browne almost 7 years
    Actually this code violates the Liskov Substitution Principle and is not supported in almost any OOP language, because then instances of BirdManager and DogManager could no longer be safely used anywhere that AnimalManager can be used, and also can't be type-checked at compile time (since any time you have an AnimalManager type hint, you could actually be dealing with one of its subclasses). See en.wikipedia.org/wiki/… and en.wikipedia.org/wiki/Liskov_substitution_principle for more details.
  • Matt Browne
    Matt Browne almost 7 years
    However, there is a legitimate use case that satisfies the LSP but isn't currently supported in PHP: contravariant parameter types - the reverse of your example (contravariant means the parameter type in the subclass is more generic than the parameter type in the parent class). This is due to implementation challenges, but it is mentioned in this RFP). See also bugs.php.net/bug.php?id=72208
  • Will Bonde
    Will Bonde almost 7 years
    @JeffreyMCastro I put it as the very first code after the opening <?php tag in an include file used everywhere.
  • Admin
    Admin almost 7 years
    This doesn't work. The warning is triggered when PHP compiles the template. For some reason, this does not trigger the error handler even though it is registered at the time the file or class is included. However, the warning is only triggered when the file is first compiled. Once compiled and stored in the opcache, there are no further warnings emitted.
  • sanmai
    sanmai over 6 years
    @WillemStuursma apparently you have some other error handler interfering
  • MatTheCat
    MatTheCat over 6 years
    I just discovered that, do you have a link to php.net which explain it?
  • Matt Browne
    Matt Browne over 6 years
    @MatTheCat I don't remember how I discovered this but it wasn't from any official documentation. However, if you want to read more about it, it's all based on the Liskov Substitution Principle (LSP) - having a default value of null doesn't violate the LSP because then subclasses don't require a different API than the parent class. See my comments on this answer below for some more details and links.
  • MatTheCat
    MatTheCat over 6 years
    Thanks! I also observed it works whatever the default value is.
  • Frank Adrian
    Frank Adrian over 6 years
    This works for apache but not when i use php via the command line. Any thoughts why cli does not get this error handler?
  • Shaun Cockerill
    Shaun Cockerill about 6 years
    The selected answer appears to cover this situation. Changing your method to accept any AnimalData in your child classes should prevent the warning. class BirdManager extends AnimalManager { public static function displayProperties(AnimalData $bird) { assert($bird instanceof BirdData);...
  • Zaziffic
    Zaziffic about 6 years
    It does indeed @ShaunCockerill, thanks. And thank you sanmai for the thorough answer ! For the moment being, I just hide the warning - lazy bunny - but I will use the assert method instead eventually. As for the segmentation faults, they are gone. I believe my installation was to be blamed or else the issue would have been slightly more popular... So yay PHP 7 !
  • sanmai
    sanmai about 6 years
    @thinkwinwin you have to ask a separate question with your code
  • Jdahern
    Jdahern about 5 years
    Thank you for giving both the direct answer, and the right answer