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

3

33

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?

Anemograph answered 13/12, 2009 at 21:22 Comment(2)
+1 I'd love to see if this is possible as well, though for the life of me I can't figure out how. Maybe PHP6 will have improved support for method overloading and detect multiple method signatures, so abstract public function update(Attribute $attr, $data) and abstract public function update(SimpleAttribute $attr, $data) could co-exist in the same class scope.Cann
This may be relevant #69620450Cornelius
L
22

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.

Ladd answered 13/12, 2009 at 22:2 Comment(6)
But, your first case would never happen because foo will not accept an instance of SimpleFactory. My question was leaning more towards why I cannot make the contract of the update method more strict in the concrete factory.Anemograph
The use case being that there are multiple concrete factories, say SimpleFactory, EnumberableFactory and so on, each of which is meant to just operate on its own type of attribute, SimpleAttribute and EnumerableAttribute, respectively. But, as it stands, since I'm not able to tighten the hint restriction in the factory subclass, all the concrete factories will have to accept all attribute types... barring a simple check in the top of the method body, which is fine, but I was just wondering if it was possible at the signature level.Anemograph
Still, I really appreciate your answer.Anemograph
foo will take an instance of SimpleFactory; just change the type hint for SimpleFactory::update and try it. Even though your code may not break the type hint contract, the language feature you need allows for contract breaking. Thus the feature will not be a part of PHP.Ladd
"My question was leaning more towards why I cannot make the contract of the update method more strict in the concrete factory." Take a closer look at my answer, which addresses this exactly. In short: it allows for the possibility of contracts to be broken.Ladd
You're welcome. It just goes to show the limitations of PHP's OOP support. In other languages, you could make use of overloading (real overloading, not what PHP calls overloading).Ladd
O
5

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.

Odeliaodelinda answered 9/2, 2014 at 6:58 Comment(2)
What would be passed to the constructor for SimpleFactory in your implementation? An instance of yet another factory class that does inherit from AbstractFactory?Spaceman
Yes if my example is to be followed strictly, you would pass in an instance of yet another factory class that does inherit from AbstractFactory . In my example up there, SimpleFactory is implemented with an AbstractFactory, however, this isn't necessarily so, ISimpleFactory could be implemented without an AbstractFactory if desired. My example just shows that you could hide that implementation detail, revealed only by the calling code via constructor injection.Odeliaodelinda
C
1

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.

Caiman answered 15/4, 2020 at 12:19 Comment(1)
No, it's the opposite. Contravariance allows you to use less specific parameter types in children classes/interfaces.Personable

© 2022 - 2024 — McMap. All rights reserved.