Different number of parameters in function overriding - php

I want to ask how to enable full error reporting, E_ALL and startup errors in php.ini don't have effect in my case.
My code:
class A
{
function funcA(arg1=null, arg2=null, arg3=false, arg4=null) {}
}
class B extends A
{
function funcB() {}
}
class C extends B
{
function funcA(arg1=null, arg2=null, arg3=false) {}
}
With php 7.0 it was allowed and it was working, after upgrading to php 7.2.15 there is some kind of crash of php, script execution stops, no errors in error logs. With php 7.2 there must be the same number of method parameters like in parent class, that's not a problem for me, but problem is that I don't have any feedback from php about this error.
Do you have any ideas why there is no error or exception? I'm using development php.ini with all errors display enabled.

This code always produces an incompatible signature warning from version 7.0.33 to 7.3.
It can be confirmed here: https://3v4l.org/Ifmbk
Actually, you are unintentionally breaking the L rule of the SOLID, which stands for Liskov's Substitution Principle:
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
Instances of C and A in your example are not literally interchangeable, even having optional arguments in the signature.
Still, you have at least two options and both of them requires a design change and the warning's itself confirming the existence of a smell.
Remove the inheritance if there is a really few common functionality and go with the composition:
class B
{
/**
* A
*/
private $a;
public function __construct(A $a) {
$this->a = $a;
}
}
Or split the functionality to different methods and have a more relaxed interface to accept AbcInterface in other places and validate the instance type you got in actual implementation:
interface AbcInterface
{
public function B();
}
class A implements AbcInterface
{
public function funcA($arg1=null, $arg2=null, $arg3=false, $arg4=null)
{
}
public function funcAWithLessArgs($arg1=null, $arg2=null, $arg3=false)
{
}
}
In reality, what you need here is function overloading which is does not exists in PHP ecosystem since the beginning.

Related

Should concrete class follow the type hint of its interface?

In this code sample, the interface doesn't seem to care whether the implementing method foo() checks for an array type parameter even if it explicitly type-hinted array only.
<?php
declare(strict_types = 1);
interface MyInterface
{
public function foo(array $foo);
}
class Bar implements MyInterface
{
public function foo($foo)
{
return $foo;
}
}
echo (new Bar)->foo('test'); // runs just fine as string
I would expect at least a fatal, incompatible interface error; but there's none.
My questions are:
Is this an expected behavior?
Should the interface not have type hints at all because it's not respected anyway?
Short answer:
This is expected behaviour since 7.2, and interfaces type hints are enforced to an extent; but implementing classes may omit the interface's type declaration (but can't declare a parameter type different from the one declared in the interface).
Long answer:
This was a change introduced on PHP 7.2.
If you try this in PHP where PHP_VERSION_ID >= 7 && PHP_VERSION_ID < 7.2 you get:
Fatal error: Declaration of Bar::foo($foo) must be compatible with MyInterface::foo(array $foo)
But on PHP_VERSION_ID >= 7.2 it "works". The explanation for the changes is documented here, and says:
Parameter type widening
Parameter types from overridden methods and from interface
implementations may now be omitted. This is still in compliance with
LSP, since parameters types are contravariant.
interface A {
public function Test(array $input); }
class B implements A {
public function Test($input){} // type omitted for $input }
You can omit the parameter type, but you can't declare an incompatible type.
E.g. if in your example you tried:
public function foo(string $foo)
{
return $foo;
}
It would fail all around.
A couple of links for further reading regarding this change:
The PR
A post where the PR is explained and defended
An example of developers taking advantage of the new functionality.

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.
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)
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 */
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)
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");
+ }
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.
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().
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() {}
}
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($_);
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.
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?
I had this issue as well. I have a class that overrides a function of the parent class, but the override has different num of parameters. I can think of a few easy work arounds - but do require minor code change.
change the name of the function in the subclass (so it no longer overrides parent function)
-or-
change the parameters of the parent function, but make the extra parameters optional (e.g., function func($var1, $var2=null) - this may be easiest and require less code changes. But it may not be worth to change this in the parent if its used so many other places. So I went with #1 in my case.
If possible, instead of passing the extra params in the subclass function, use global to pull in the extra params. This is not ideal coding; but a possible band-aid anyway.
You can remove the parent class method definition altogether and intercept it with a magic method.
public function __call($name, $args)
{
if($name == 'do') {
// do things with the unknown # of args
} else {
throw new \Exception("Unknown method $name", 500);
}
}
I just ran into this problem and went this route
If the base class has fewer arguments than the derived class, it is possible to add additional argument(s) to the derived class like this:
$namespace = 'default';
if (func_num_args() > 2) {
$namespace = func_get_arg(2);
}
In this way, you add a 3rd "defaulted" argument, but do not change the signature.
I would only suggest this if you have a substantial amount of code that calls this and are unable to change that code, and want to maintain backward compatibility.
I found this situation in some old Joomla code (v1.5) where JSession::set added a $namespace param, but has JObject as a base class, where JObject::set has no such parameter.

PHP > 5.4: overriding constructor with different signature

We know that PHP doesn't accept child methods with a different signature than the parent. I thought that was the same with constructors: The PHP documentation states that
This also applies to constructors as of PHP 5.4. Before 5.4 constructor signatures could differ.
However, it appears that inherited constructors still can differ in PHP versions > 5.4. For example the following code does not trigger any warnings or notices:
class Something { }
class SomeOtherThing { }
class Foo
{
public function __construct(Something $foo)
{
}
public function yay()
{
echo 'yay';
}
}
class Bar extends Foo
{
public function __construct($foo, SomeOtherThing $bar = null)
{
}
}
$x = new Bar(new Something());
$x->yay();
According to the documentation, the code should trigger an error, as the contructor signatures are different.
Tried this on PHP 5.6.4. Same effect with other versions.
So, what's up with that? Are differing constructor signatures still legal, despite of what the documentation says? Or is this a bug which will be fixed in later versions?
According to the documentation
Unlike with other methods, PHP will not generate an E_STRICT level error message when __construct() is overridden with different parameters than the parent __construct() method has.
So, that is why you are not getting an error of level E_STRICT. Perhaps it will trigger something at a different level.
I think you somewhat misread the documentation, because it states:
Furthermore the signatures of the methods must match, i.e. the type
hints and the number of required arguments must be the same. For
example, if the child class defines an optional argument, where the
abstract method's signature does not, there is no conflict in the
signature.
You've defined an optional parameter, so it's ok.

Strict standards error when using __autoload

I am faced with an error when using __autoload().
Here is an example of two simple classes in the same file:
class A
{
public function check($a, $b)
{
$subClass = new B();
return $subClass->check($a);
}
}
class B extends A
{
public function check($a)
{
return $a;
}
}
$a = new A();
$a->check(77, 44);
Everything is OK; and it works as expected. But when I create a file for each class and use __autoload(), there is an error:
Strict standards: Declaration of B::check() should be compatible with that of A::check()
To reproduce:
file A.php
class A
{
public function check($a, $b)
{
$subClass = new B();
return $subClass->check($a);
}
}
file B.php:
class B extends A
{
public function check($a)
{
return $a;
}
}
file test.php
function __autoload($className)
{
require_once $className . '.php';
}
$a = new A();
$a->check(77, 44);
So, why does this error appear only when using __autoload?
Actually it's the non-autoload code that is being weird. The strict standards error is supposed to happen.
The reason why the error doesn't appear in the single-file case is due to a quirk of how PHP loads the classes, and the timing of when the error reporting mode is set. There is some explanation here: https://bugs.php.net/bug.php?id=46851.
In short:
If you define the classes in a separate file or files (such as with __autoload or include), they are loaded after you have turned on E_STRICT reporting in your main file, so you see the error.
If you define the classes directly in your main file, then the classes get loaded before the script starts running, before you turn on E_STRICT error reporting, so you don't see the error.
Oddly, if you define the classes in your main file but reverse the order (class B, then class A) you do see the error. According to the page linked above, this is because PHP doesn't load them until a bit later.
If you turn on E_STRICT error reporting in php.ini, so it is already on before the script starts, you see the error, regardless of how the classes are loaded.
To understand the purpose of the error, it is telling you that it is wrong for a subclass to override a method with a different number of arguments, which it is.
See Liskov substitution principle on Wikipedia:
The Liskov substitution principle states that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may substitute objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).
Currently we have this code:
$a = new A();
$a->check(77, 44);
Now since B extends A, this indicates that B is a subtype of A, and according to the above principle it should be logically possible for objects of type A to be replaced with objects of type B. But look what happens:
$a = new B();
$a->check(77, 44);
We call the check method as usual, but it is not expecting two arguments, so the call doesn't make sense.
The improper override works in PHP, because PHP is generally not very strict, but turning on strict standards errors lets it warn about this.
This message means that there are certain possible method calls which may fail at run-time. return $subClass->check($a); calls to class B method which has signature in parent method in class class A.
Different signature (2 parameters in A and 1 parameter in B) produces strict error, abstract or not.
It works if signatures are same:
class A
{
public function check($a, $b)
{
$subClass = new B();
return $subClass->check($a, NULL);
}
}
class B extends A
{
public function check($a, $b)
{
return $a;
}
}
function __autoload($className)
{
require_once $className . '.php';
}
$a = new A();
$a->check(77, 44);
Can I ask you what are you trying to achieve by instantiating new class in class which is extended by it?

PHP Override way

What is the PHP way to do a correct override ?
php -a
Interactive mode enabled
php > error_reporting(E_ALL);
php > class A { public function z($a, $b){} }
php > class B extends A { public function z($a){parent::z($a, '1');} }
PHP Strict standards: Declaration of B::z() should be compatible with A::z($a, $b) in php shell code on line 1
Note: Override is the correct word in english ?
In your example, in class A function z have 2 parameters, in class B you are trying to declare it with 1 parameter. In php it wont work.
Btw. I guess you probably want to OVERLOAD funciotn z not override it, and it is not really supported in php. If I'm right here is little explanation fro manual:
Note:
PHP's interpretation of "overloading" is different than most object oriented languages. Overloading traditionally provides the ability to have multiple methods with the same name but different quantities and types of arguments.
The reason for the E_STRICT notice (which is just a programming hint, not an error as such) is that a child class should theoretically be usable interchangeably with its parent (the posh term is the "Liskov Substitution Principle").
In your case, you need to know that you have an instance of B in order to leave off the second parameter. PHP is being relaxed and letting you do it, but it's not really how inheritance should work - B extends A should mean that everything that "is a B" also "is an A".
If you used an interface to define the contract for function z($a, $b), class B would violate that contract.
A better approach for this problem would be to use delegation rather than inheritance: class B could act as an "adaptor" which uses A's behaviour, but simplifies it, e.g.
class B
{
private $delegated_a;
public function __construct() {
$this->delegated_a = new A;
}
public function z($a) {
$this->delegated_a->z($a, '1');
}
}
Here, we're making no promise to calling code about the relationship between A and B, so can re-define the methods however we like.
You mean method overloading :)
Well actually this works a bit different then you would expect if you programmed in Java before. Please also checkout the comment in the PHP manual.
The difference is that in eg. Java you can have multiple methods with the same name but another footprint. Eg:
public void max(int a, int b) {
// calculate max value
}
public void max(double a, double b, double c) {
// do some other calculation
}
Now if you would call the method max, Java will automatically decide which method to use.
Now in PHP this does not work. You can overwrite a method in a child class, but only if the footprint is the same, and you can only define the method in the child class. Example:
class A {
function z($a, $b) {
echo 'called in A';
}
}
class B extends A {
function z($a, $b) {
echo 'called in B';
}
}
$a = new A;
$b = new B;
$a->z(1, 2); // prints out "called in A"
$b->z(1, 2); // prints out "called in B"

Categories