I will be using the following example to illustrate my question:
class Attribute {} class SimpleAttribute extends Attribute {} abstract class AbstractFactory { abstract public function update(Attribute $attr, $data); } class SimpleFactory extends AbstractFactory { public function update(SimpleAttribute $attr, $data); }
If you try to run this, PHP will throw a fatal error, saying that the Declaration of SimpleFactory::update() must be compatible with that of AbstractFactory::update()
I understand exactly what this means: That SimpleFactory::update()
s method signature must exactly match that of its parent abstract class.
However, my question: Is there any way to allow the concrete method (in this case, SimpleFactory::update()
) to redefine the type hint to a valid descendant of the original hint?
An example would be the instanceof
operator, which would return true in the following case:
SimpleAttribute instanceof Attribute // => true
I do realize that as a work around, I could make the type hint the same in the concrete method, and do an instanceof check in the method body itself, but is there a way to simply enforce this at the signature level?
Advertisement
Answer
I wouldn’t expect so, as it can break type hinting contracts. Suppose a function foo
took an AbstractFactory and was passed a SimpleFactory.
function foo(AbstractFactory $maker) { $attr = new Attribute(); $maker->update($attr, 42); } ... $packager=new SimpleFactory(); foo($packager);
foo
calls update
and passes an Attribute to the factory, which it should take because the AbstractFactory::update
method signature promises it can take an Attribute. Bam! The SimpleFactory has an object of type it can’t handle properly.
class Attribute {} class SimpleAttribute extends Attribute { public function spin() {...} } class SimpleFactory extends AbstractFactory { public function update(SimpleAttribute $attr, $data) { $attr->spin(); // This will fail when called from foo() } }
In contract terminology, descendent classes must honor the contracts of their ancestors, which means function parameters can get more basal/less specified/offer a weaker contract and return values can be more derived/more specified/offer a stronger contract. The principle is described for Eiffel (arguably the most popular design-by-contract language) in “An Eiffel Tutorial: Inheritance and Contracts“. Weakening and strengthening of types are examples of contravariance and covariance, respectively.
In more theoretical terms, this is an example of LSP violation. No, not that LSP; the Liskov Substitution Principle, which states that objects of a subtype can be substituted for objects of a supertype. SimpleFactory
is a subtype of AbstractFactory
, and foo
takes an AbstractFactory
. Thus, according to LSP, foo
should take a SimpleFactory
. Doing so causes a “Call to undefined method” fatal error, which means LSP has been violated.