This question already has answers here:
Override method parameter with child interface as a new parameter
(2 answers)
Abstract function parameter type hint overriding in PHP 7
(2 answers)
Closed 3 years ago.
This code of dependency inversion should work fine but instead it gives error.
What am I doing wrong in here?
interface A { }
abstract class B implements A { }
class C extends B { }
abstract class D {
public function foo(A $a) { }
}
class E extends D {
public function foo(C $c) { }
}
The error is:
Warning: Declaration of E::foo(C $c) should be compatible with D::foo(A $a) in [...][...] on line 24
Surprisingly, doing the same for the constructor method works just fine:
interface A { }
abstract class B implements A { }
class C extends B { }
abstract class D {
public function __construct(A $a) { }
public function foo(A $a) { }
}
class E extends D {
public function __construct(C $c) { }
public function foo(A $c) { }
}
There are two questions here.
Why the first part raises a WARNING: the error is pretty explicit. Any time you override a method you need to respect the parent's signature, so a client application's expectations are met.
class Foo {}
class Bar extends Foo {}
class A {
public function test(Foo $foo) {}
}
class B extends A {
public function test(Bar $bar) {}
}
Now, since B extends A, and A::test() expects a Foo, a user might try to do this:
$a = new A();
$b = new B();
$foo = new Foo();
$bar = new Bar();
$b->test($bar);
$b->test($foo);
The first call to test() would work, but the second would fail (and fatally): a Foo is not a Bar.
The parameter type is more restrictive than the original, and thus breaks expectations from client applications. Note that if we had a legal declaration (B::test(Foo)), we could still call $b->test($bar) and would work as expected, so changing the signature is not really necessary.
In November 2019, When PHP 7.4 is released, contravariance will be supported for parameter types, and covariance will be supported for return types; so your example would still be invalid.
Finally, notice that the error raised is a WARNING, it won't crash the program immediately. But it is warning you that you are opening your application to behaviour that might crash it, and that's it's generally poor design (for the reasons explained above).
The second question: Why you do not get the same warning for the constructor? They do not raise this warning by design. If you check the docs, you'll find this:
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.
Why constructors are treated differently? Because it is generally understood that constructors are not part of the "public API" of an object, and do not need to be subjected to the same constraints. The Liskov Substitution Principle deals with objects, not with classes; and before an class is instantiated there is yet no object. Thus, the constructor is out of scope.
You can read more about this here, for example.
TL;DR C is implementing A, but it can also inherit other methods from B which implementing A doesn't guarantee. This creates situation where two classes of same inherited type can have two methods with different signatures. A sub-class must accept all inputs that the parent class would accept, but it can accept additional inputs if it wants,
Let's extend your example.
Nothing crazy, just some example methods.
interface A
{
public function gotA();
}
abstract class B implements A
{
abstract public function gotB();
}
class C extends B
{
public function gotA()
{
// ...
}
public function gotB()
{
// ...
}
}
I can agree that C is indirectly implementing A, but it also contains methods from B, so the call in E::foo() would be perfectly fine.
abstract class D
{
public function foo(A $a)
{
$a->gotA();
}
}
class E extends D
{
public function foo(C $c)
{
$c->gotB();
}
}
Another example class implementing A.
class ExampleA implements A
{
public function gotA()
{
// ...
}
}
Now the fun part. Assume this function anywhere else in your code. It assumes D (expecting A in foo) as parameter.
But E is also instanceof D and can be passed to x, but it expects C in foo which ExampleA has nothing to do with.
function x(D $d)
{
$exampleA = new ExampleA();
$d->foo($exampleA);
}
$e = new E();
x($e);
To sum up, the contract for function x has been met and yet the function broke.
You ended up with two 'different' D types. The one expecting A and the one expecting C.
As for why it doesn't throw warning with __constructor I don't know. Need further investigation.
Related
Strict Standards: Declaration of childClass::customMethod() should be compatible with that of parentClass::customMethod()
What are possible causes of this error in PHP? Where can I find information about what it means to be compatible?
childClass::customMethod() has different arguments, or a different access level (public/private/protected) than parentClass::customMethod().
This message means that there are certain possible method calls which may fail at run-time. Suppose you have
class A { public function foo($a = 1) {;}}
class B extends A { public function foo($a) {;}}
function bar(A $a) {$a->foo();}
The compiler only checks the call $a->foo() against the requirements of A::foo() which requires no parameters. $a may however be an object of class B which requires a parameter and so the call would fail at runtime.
This however can never fail and does not trigger the error
class A { public function foo($a) {;}}
class B extends A { public function foo($a = 1) {;}}
function bar(A $a) {$a->foo();}
So no method may have more required parameters than its parent method.
The same message is also generated when type hints do not match, but in this case PHP is even more restrictive. This gives an error:
class A { public function foo(StdClass $a) {;}}
class B extends A { public function foo($a) {;}}
as does this:
class A { public function foo($a) {;}}
class B extends A { public function foo(StdClass $a) {;}}
That seems more restrictive than it needs to be and I assume is due to internals.
Visibility differences cause a different error, but for the same basic reason. No method can be less visible than its parent method.
if you wanna keep OOP form without turning any error off, you can also:
class A
{
public function foo() {
;
}
}
class B extends A
{
/*instead of :
public function foo($a, $b, $c) {*/
public function foo() {
list($a, $b, $c) = func_get_args();
// ...
}
}
Just to expand on this error in the context of an interface, if you are type hinting your function parameters like so:
interface A
use Bar;
interface A
{
public function foo(Bar $b);
}
Class B
class B implements A
{
public function foo(Bar $b);
}
If you have forgotten to include the use statement on your implementing class (Class B), then you will also get this error even though the method parameters are identical.
I faced this problem while trying to extend an existing class from GitHub. I'm gonna try to explain myself, first writing the class as I though it should be, and then the class as it is now.
What I though
namespace mycompany\CutreApi;
use mycompany\CutreApi\ClassOfVendor;
class CutreApi extends \vendor\AwesomeApi\AwesomeApi
{
public function whatever(): ClassOfVendor
{
return new ClassOfVendor();
}
}
What I've finally done
namespace mycompany\CutreApi;
use \vendor\AwesomeApi\ClassOfVendor;
class CutreApi extends \vendor\AwesomeApi\AwesomeApi
{
public function whatever(): ClassOfVendor
{
return new \mycompany\CutreApi\ClassOfVendor();
}
}
So seems that this errror raises also when you're using a method that return a namespaced class, and you try to return the same class but with other namespace. Fortunately I have found this solution, but I do not fully understand the benefit of this feature in php 7.2, for me it is normal to rewrite existing class methods as you need them, including the redefinition of input parameters and / or even behavior of the method.
One downside of the previous aproach, is that IDE's could not recognise the new methods implemented in \mycompany\CutreApi\ClassOfVendor(). So, for now, I will go with this implementation.
Currently done
namespace mycompany\CutreApi;
use mycompany\CutreApi\ClassOfVendor;
class CutreApi extends \vendor\AwesomeApi\AwesomeApi
{
public function getWhatever(): ClassOfVendor
{
return new ClassOfVendor();
}
}
So, instead of trying to use "whatever" method, I wrote a new one called "getWhatever". In fact both of them are doing the same, just returning a class, but with diferents namespaces as I've described before.
Hope this can help someone.
I would like create an abstract class with abstract method which allow abstract type in return type. In my final class, I would like override type returned with type which implement abstract type initially declared.
<?php
abstract class A {
abstract public function test(A $foo): self;
}
class B extends A {
public function test(B $foo): self
{
return $this;
}
}
This compilation error is thrown:
Fatal error: Declaration of B::test(B $foo): B must be compatible with A::test(A $foo): A in ... on line 8
In documentation, covariance is explained with interface. But not with abstract class. More about PHP implementation, the documentation say:
In PHP 7.2.0, partial contravariance was introduced by removing type restrictions on parameters in a child method. As of PHP 7.4.0, full covariance and contravariance support was added.
I'm using PHP 7.4.
A quite core principle of object oriented programming is the Liskov substitution principle which essentially boils down to:
if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program
The way to achieve this is by having covariant method return types, contravariant method type arguments. Exceptions thrown kind of count as return types here so they also need to be covariant.
What you need is covariance of type arguments which breaks this principle.
The reason why can be seen by considering the example below:
abstract class A {
abstract public function test(A $foo): self;
}
class C extends A {
public function test(C $foo): self {
return $this;
}
}
class B extends A {
public function test(B $foo): self {
return $this;
}
}
$b = new B();
$c = new C();
$b->test($c); // Does not work
((A)$b)->test((A)$c); // Works
In the example above, you don't allow B::test to accept any type other than B as a type argument. However since B itself is a child of A and C is also a child of A by simple down-casting (which is allowed) the restriction is bypassed. You could always disable down-casting but that's almost saying you're disabling inheritance which is a core principle of OOP.
Now of course there are compelling reasons to allow covariance of type arguments, which is why some languages (such as e.g. Eiffel) allow it, however this is recognised to be a problem and even has been given the name CATcalling (CAT stands for Changed Availability or Type).
In PHP you can attempt to do runtime checks to remedy this situation:
abstract class A {
abstract public function test(A $foo) {
// static keyword resolve to the current object type at runtime
if (!$foo instanceof static) { throw new Exception(); }
}
}
class C extends A {
public function test(A $foo): self {
parent::test($foo);
return $this;
}
}
class B extends A {
public function test(A $foo): self {
parent::test($foo);
return $this;
}
}
However this is a bit messy and possibly unnecessary.
I have a class that looks like this. I will also paste it below for reference:
<?php
trait T1
{
abstract protected function _doStuff();
}
trait T2
{
protected function _doStuff()
{
echo "Doing stuff in trait\n";
}
}
class C
{
use T1 {
_doStuff as _traitDoStuff;
}
use T2 {
_doStuff as _traitDoStuff;
}
protected function _doStuff()
{
echo "Doing stuff in class\n";
$this->_traitDoStuff();
}
}
Here's what's happening here:
T1::_doStuff() is used, and aliased as _traitDoStuff(). As per PHP docs, this does not rename the method, but only adds an alias. So at this point, both _doStuff() and _traitDoStuff() exist as abstract protected methods.
T2::_doStuff() is used, and aliased as _traitDoStuff(). As per PHP docs, due to precedence, both are overridden by methods of T2. So at this point, T1::_doStuff() no longer exists. Even if it would, it would be implemented by T2::_doStuff().
C implements _doStuff(), which calls _traitDoStuff(). At this point, regardless of which implementation of _doStuff() is used, it is obvious that this method is implemented, so the contract defined by T1::_doStuff() is satisfied, or doesn't exist.
And yet, when I run this, it gives the following error:
Fatal error: Class C contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (C::_doStuff)
As can be seen from 3v4l, this manifests everywhere from PHP 5.4 to 7.2, which kinda hints that this is not an early trait bug. Can somebody please explain this to me?
Update
Apparently, I just forgot to specify the method that I am aliasing, i.e. T1::_doStuff as _traitDoStuff.
The lack of class-scope resolution operators (T1:: and T2::) were masking a deeper issue. Consider these simpler cases:
trait A {
abstract public function foo();
}
class B {
use A; // works
public function foo() {}
}
class C {
use A { A::foo as traitFoo; } // works, provided this:
public function traitFoo() {} // <-- is present
public function foo() {}
}
class D {
use A { A::foo as traitFoo; } // does not work by itself
public function foo() {}
}
What actually happens: aliasing an abstract method introduces another abstract method in your class. The PHP.net engine error message is hugely misleading:
Fatal error: Class D contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (D::foo)
But the HHVM engine is far more informative:
Fatal error: Uncaught Error: Class D contains abstract method (traitFoo) and must therefore be declared abstract or implement the remaining methods
The Horizontal Reuse (aka Trait) RFC does not explicitly discuss this case, so this is arguably a bug. Feel free to report it at bugs.php.net.
So why did adding the class resolution operator fix it?
When you added the class-scope resolution operators, which contained:
use T2 { T2::_doStuff as _traitDoStuff; }
you were satisfying the "phantom" abstract protected function _traitDoStuff introduced by:
use T1 { T1::_doStuff as _traitDoStuff; }
If you had removed the aliasing, like use T2; or use T2 { T2::_doStuff as _anotherMethod; } you would see the same crash.
Something like that maybe ?
<?php
trait T1
{
abstract protected function _doStuff();
}
trait T2
{
protected function _doStuff()
{
echo "Doing stuff in trait\n";
}
}
class C
{
use T1 {
T1::_doStuff as _traitDoStuff;
}
use T2 {
T2::_doStuff as _traitDoStuff;
}
protected function _doStuff()
{
echo "Doing stuff in class\n";
$this->_traitDoStuff();
}
}
Strict Standards: Declaration of childClass::customMethod() should be compatible with that of parentClass::customMethod()
What are possible causes of this error in PHP? Where can I find information about what it means to be compatible?
childClass::customMethod() has different arguments, or a different access level (public/private/protected) than parentClass::customMethod().
This message means that there are certain possible method calls which may fail at run-time. Suppose you have
class A { public function foo($a = 1) {;}}
class B extends A { public function foo($a) {;}}
function bar(A $a) {$a->foo();}
The compiler only checks the call $a->foo() against the requirements of A::foo() which requires no parameters. $a may however be an object of class B which requires a parameter and so the call would fail at runtime.
This however can never fail and does not trigger the error
class A { public function foo($a) {;}}
class B extends A { public function foo($a = 1) {;}}
function bar(A $a) {$a->foo();}
So no method may have more required parameters than its parent method.
The same message is also generated when type hints do not match, but in this case PHP is even more restrictive. This gives an error:
class A { public function foo(StdClass $a) {;}}
class B extends A { public function foo($a) {;}}
as does this:
class A { public function foo($a) {;}}
class B extends A { public function foo(StdClass $a) {;}}
That seems more restrictive than it needs to be and I assume is due to internals.
Visibility differences cause a different error, but for the same basic reason. No method can be less visible than its parent method.
if you wanna keep OOP form without turning any error off, you can also:
class A
{
public function foo() {
;
}
}
class B extends A
{
/*instead of :
public function foo($a, $b, $c) {*/
public function foo() {
list($a, $b, $c) = func_get_args();
// ...
}
}
Just to expand on this error in the context of an interface, if you are type hinting your function parameters like so:
interface A
use Bar;
interface A
{
public function foo(Bar $b);
}
Class B
class B implements A
{
public function foo(Bar $b);
}
If you have forgotten to include the use statement on your implementing class (Class B), then you will also get this error even though the method parameters are identical.
I faced this problem while trying to extend an existing class from GitHub. I'm gonna try to explain myself, first writing the class as I though it should be, and then the class as it is now.
What I though
namespace mycompany\CutreApi;
use mycompany\CutreApi\ClassOfVendor;
class CutreApi extends \vendor\AwesomeApi\AwesomeApi
{
public function whatever(): ClassOfVendor
{
return new ClassOfVendor();
}
}
What I've finally done
namespace mycompany\CutreApi;
use \vendor\AwesomeApi\ClassOfVendor;
class CutreApi extends \vendor\AwesomeApi\AwesomeApi
{
public function whatever(): ClassOfVendor
{
return new \mycompany\CutreApi\ClassOfVendor();
}
}
So seems that this errror raises also when you're using a method that return a namespaced class, and you try to return the same class but with other namespace. Fortunately I have found this solution, but I do not fully understand the benefit of this feature in php 7.2, for me it is normal to rewrite existing class methods as you need them, including the redefinition of input parameters and / or even behavior of the method.
One downside of the previous aproach, is that IDE's could not recognise the new methods implemented in \mycompany\CutreApi\ClassOfVendor(). So, for now, I will go with this implementation.
Currently done
namespace mycompany\CutreApi;
use mycompany\CutreApi\ClassOfVendor;
class CutreApi extends \vendor\AwesomeApi\AwesomeApi
{
public function getWhatever(): ClassOfVendor
{
return new ClassOfVendor();
}
}
So, instead of trying to use "whatever" method, I wrote a new one called "getWhatever". In fact both of them are doing the same, just returning a class, but with diferents namespaces as I've described before.
Hope this can help someone.
I've found something that appears to be a strange inheritance issue in PHP.
From the PHP manual:
Members declared protected can be accessed only within the class
itself and by inherited and parent classes.
To me this means:
A can access the protected members of B if A instanceof B or B instanceof A.
However, if both A and B extend Foo, and Foo has a protected constructor which is not overwritten in B, then I can create an instance of B from within A. This does not make sense to me, because A is not an instance of B and B is not an instance of A. I can also call the protected method $b->test() from within A, which executes the method implemented in B. (If B does not redeclare test() then the implementation in Foo is executed.) To me this is even more strange because I cannot create an instance of B from within A if B directly implements a protected constructor. It seems strange that I cannot access a protected constructor (also declared in the parent class) but accessing a protected method (also declared in the parent class) is no problem.
Note that I do get the expected behavior when I use a class C which does not extend Foo. If I try to instantiate B from within C, I get a fatal error because I'm trying to access a protected constructor. If I add a public constructor to B it is possible to instantiate it (which is expected) and I still cannot access the protected method test() (this is also expected behavior). I expect the same behavior when using A instead of C.
Sample code which explains again:
class Foo {
protected function __construct() {
echo('Constructing ' . get_called_class());
}
protected function test() {
echo('Hello world ' . __METHOD__);
}
}
class A extends Foo {
public function __construct() {
parent::__construct();
}
public function testB() {
// Both of these lines work
$b = new B();
$b->test();
}
}
class B extends Foo {
protected function test() {
echo('Hello world Again ' . __METHOD__);
}
}
class C {
public function __construct() {
}
public function testB() {
// Both of these lines cause fatal errors
$b = new B();
$b->test();
}
}
$a = new A();
$a->testB();
$c = new C();
$c->testB();
I'm probably not seeing something, but I can't find what. Could anyone explain the behavior to me?
You can access those methods because there is a declaration of them as protected in Foo, which is your parent and that gives you permission to access it. If you remove the declaration from the parent and declare the protected method in B you will get a Fatal Error.
This is reported as a bug in PHP https://bugs.php.net/bug.php?id=50892
There is no rationale about this, it has been reported 2 years ago: https://bugs.php.net/bug.php?id=52120