Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to redefine a type hint to a descendant class when extending an abstract class?

Tags:

php

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?

like image 512
jason Avatar asked Dec 13 '09 21:12

jason


3 Answers

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.

like image 169
outis Avatar answered Oct 24 '22 15:10

outis


The accepted answer is correct in answering the OP that what he attempted to do violates the Liskov Substitution Principle. The OP's should utilize a new interface and use composition instead of inheritance to solve his function signature issue. The change in the OP's example problem is fairly minor.

class Attribute {}

class SimpleAttribute extends Attribute {}

abstract class AbstractFactory {
    abstract public function update(Attribute $attr, $data);
}

interface ISimpleFactory {
    function update(SimpleAttribute $attr, $data);
}

class SimpleFactory implements ISimpleFactory {
   private $factory;
   public function __construct(AbstractFactory $factory)
   {
       $this->factory = $factory;
   }
   public function update(SimpleAttribute $attr, $data)
   {
       $this->factory->update($attr, $data);
   }

}

The code example above does two things: 1) Creates the ISimpleFactory interface that all code dependent upon a factory for SimpleAttributes would code against 2) Implementing SimpleFactory using a generic factory would require SimpleFactory to take via the constructor an instance of the AbstractFactory derived class that it would then use in the update function from the ISimpleFactory interface method override in SimpleFactory.

This allows any dependency upon a generic factory to be encapsulated from any code that depended on ISimpleFactory, but allowed SimpleFactory to substitute any factory derived from AbstractFactory (Satisfying LSP) without having to change any of its code (the calling code would provide the dependency). A new derived class of the ISimpleFactory may decide to not use any AbstractFactory derived instances to implement itself, and all calling code would be shielded from that detail.

Inheritance can be of great value, however, sometimes Composition is overlooked and its my preferred way of reducing tight coupling.

like image 36
The Prophet Avatar answered Oct 24 '22 16:10

The Prophet


Since PHP 7.4 it is allowed to use more specific type-hint in children classes by using interfaces

interface OtherAttributeCompatibleInterface{};
interface SimpleAttributeCompatibleInterface{};

interface AllAttributesInterface extends OtherAttributeCompatibleInterface, SimpleAttributeCompatibleInterface{};


class Attribute implements AllAttributesInterface{};

class SimpleAttribute extends Attribute  implements SimpleAttributeCompatibleInterface{};


abstract class AbstractFactory  {
    abstract public function update(AllAttributesInterface $n);
}

class SimpleFactory  extends AbstractFactory  {
  public function update (SimpleAttributeCompatibleInterface $n){
  }
}

Cause AllAttributesInterface inherits SimpleAttributeCompatibleInterface it is allowed to overwrite the update method from AbstractFactory by SimpleFactory.

like image 21
l0 Mag Avatar answered Oct 24 '22 17:10

l0 Mag