I have the following hierarchy of classes:
class O_Base {...}
class O extends O_Base {...}
abstract class A_Abstract {
public function save(O_Base $obj) {
Your code is a clear violation of the Liskov Substitution principle. The abstract class requires an instance of O_Base
to be passed to the save
method, therefore all of the children of the A_Abstract
should be defined in such a way that they can accept all instances of O_Base
. Your child class implements a version of save
that further restricts the API. see another example here
In your code, A
breaches the contract which Abstract_A
enforces/describes. Just like in real life, if you sign a contract, you have to agree on certain terms, and all be in agreement over the terminology you're going to use. That's why most contracts start by naming the parties, and then saying something along the lines of "Henceforth Mr X will be referred to as the employee".
These terms, like your abstract type hints are non-negociable, so that, further down the line you can't say: "Oh, well... what you call expenses, I call standard wages"
Ok, I'll stop using these half-arsed analogies, and just use a simple example to illustrate why what you're doing is, rightfully, not allowed.
Consider this:
abstract class Foo
{
abstract public function save(Guaranteed $obj);
//or worse still:
final public function doStuff(Guaranteed $obj)
{
$obj->setSomething('to string');
return $this->save($obj);//<====!!!!
}
}
class FBar extends Foo
{
public function save(Guaranteed $obj)
{
return $obj->setFine(true);
}
}
class FBar2 extends Foo
{
public function save(ChildOfGuaranteed $obj)
{//FAIL: This method is required by Foo::doStuff to accept ALL instances of Guaranteed
}
}
See here, in this case the perfectly valid abstract class is calling the save
method with an instance of Guaranteed
. If you were allowed to enforce a stricter type-hint in the children of this class, you could easily break this doStuff
method. To help you protect yourself against these kinds of self-inflicted wounds, child classes should not be allowed to enforce stricter types on methods they inherit from their parent classes.
Also consider the scenario where we're looping over some instances and checking if they have this save
method, based on these instances being an instanceof Foo
:
$arg = new OtherChildOfGuaranteed;
$array = array(
'fb' => new FBar,
'fb2' => new FBar2
);
foreach($array as $k => $class)
{
if ($class instanceof Foo) $class->save($arg);
}
Now this will work fine if you just hint at Guaranteed
in the method signature. But in the second case, we've made the type-hint a little too strict, and this code will result in a fatal error. Have fun debugging this in a more complex project...
PHP is very forgiving, if not too forgiving in most cases, but not here. Instead of having you scratching your head 'till your hear falls out, PHP very sensibly gives you a heads up, saying the implementation of your method breaches the contract, so you must fix this issue.
Now the quick workaround (and it is one in common use, too) would be this:
class FBar2 extends Foo
{
/**
* FBar2 save implementation requires instance of ChildOfGuaranteed
* Signature enforced by Foo
* @return Fbar2
* @throw InvalidArgumentException
**/
public function save(Guaranteed $obj)
{
if (!$obj instanceof ChildOfGuaranteed)
throw new InvalidArgumentException(__METHOD__.' Expects instance of ChildOfGuaranteed, you passed '.get_class($obj));
//do save here...
}
}
So, you just leave the abstract type-hint as-is, but use the docblocks to document your code
I had similar problem.
For me the explanation is much simpler, without cool theories: in PHP the above code is making method overriding. But the idea of such code in my case was method overloading which is not possible in PHP in this way.
So, to create required logic (required by your application) you need to use workarounds in order to make compatible method signatures (by inheriting parameters types or making some parameters optional).
Do you see the difference of the function signatures?
// A_Abstract
public function save(O_Base $obj) {...}
// A
public function save(O $obj) {
They are not compatible.
Change A_Abstract to
abstract class A_Abstract {
public function save(O $obj) {...}
}
No matter that O
is extended from O_Base
, they are two different objects, and as A
extends A_Abstract
the save functions need to be compatible with each other, meaning you need to pass the same object in, O
.