Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP Interfaces and argument inheritance

I have Entities and Repositories in my project. To simplify, I have

  • EntityInterface
  • UserEntity
  • BusinessEntity

Interface:

interface Entity
{
    /**
     * @return EntityId
     */
    public function getId();
}

Implementations

class UserEntity implements Entity
{
    /**
     * @return EntityId
     */
    public function getId(){
      //...do something here for return
      return $userId;
    }
}

and

class BusinessEntity implements Entity
{
    /**
     * @return EntityId
     */
    public function getId(){
      //...do something here for return
      return $userId;
    }
}

I would like to define a Repository base-functionality, like save, so my interface looks like:

interface Repository
{
    /**
     * @param Entity $entity
     *
     * @throws \InvalidArgumentException If argument is not match for the repository.
     * @throws UnableToSaveException If repository can't save the Entity.
     *
     * @return Entity The saved entity
     */
    public function save(Entity $entity);
}

Later, I have different interfaces for different type of Repositories, like UserRepository and BusinessRepository

interface BusinessRepository extends Repository
{
    /**
     * @param BusinessEntity $entity
     *
     * @throws \InvalidArgumentException If argument is not match for the repository.
     * @throws UnableToSaveException If repository can't save the Entity.
     *
     * @return Entity The saved entity
     */
    public function save(BusinessEntity $entity);
}

The above code fails, because Declaration must be compatible with Repository...

however BusinessEntity implements Entity, so it's compatible.

I have many type of entities, so If I can't type-hint, I always need to check, that the passed instance is instanceof what I need. It's stupid.

The following code fails again:

class BusinessRepository implements Repository
{
    public function save(BusinessEntity $entity)
    {
      //this will fail, however BusinessEntity is an Entity
    }
}
like image 335
fureszpeter Avatar asked Jun 30 '26 18:06

fureszpeter


1 Answers

In general, method parameters have to be contravariant with respect to an inheritance hierarchy or invariant. This means that indeed BusinessEntity would not be "compatible" with Entity when used as a type for a method parameter.

Think of it from a "contract" point of view. Your interface Repository promises that its method save can handle arguments of type Entity. Subtypes inheriting from Repository should be bound to this introduced contract (because otherwise, what sense would it make to define types in the first place, if you cannot be sure what they promises to be able to do?).

Now, if a subtype all of a sudden only accepts more special types, like BusinessEntity, but no longer Entity, the contract's broken. You cannot use BusinessRepository as Repository any more, because you cannot call save with an Entity.

This is counterintuitive at first, but have a look at this: https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)#Contravariant_method_argument_type

Notice the inheritance arrow in the image.

What's to do? Get rid of the idea of inheritance being the holy grail in object oriented programming. Most of the time, it is not, and introduces all kinds of nasty coupling. Favor composition over inheritance, for example. Have a look at Parameter type covariance in specializations.

like image 68
stef77 Avatar answered Jul 03 '26 08:07

stef77



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!