In object-oriented programming, polymorphism is one of the most important concepts. The general description of the word polymorphism in the programming community is the provision of a single interface to entities of different types. There are several types of polymorphism, some of them cannot be achieved in PHP, due to language constraints.
Ad-hoc polymorphism allows a polymorphic value to exhibit different behaviors when viewed at different types. The most common example of this is method overloading when we create different implementations that share the same method name within the same class. The only difference between these methods is in its arguments, they should be of different types. Then the compiler at a runtime chooses the appropriate implementation based on the type of the passed arguments. But sadly in PHP, we don’t have such a feature. Our interpreter will simply die as soon as it finds a class with to or more identically named methods.
This is the most commonly known type of polymorphism. In OOP terms it means that when we have methods in different classes that do similar things, we should give these methods the same name. These classes shouldn’t even belong to a one base type. We ensure that all our classes have the same interface: they all have the same methods that take the same arguments. Why is it important? What does it give to us? When our interface is implemented, we don’t need to care about how these classes work. According to their common interface, we know their methods, their behavior, so we exactly know how to work with all of them. There is no more reason to consider every single class separately, instead, we consider all of them as a whole. Let me show you some examples, to demonstrate it. At first, we will cover an example without polymorphism.
For example, we have base
Cache abstract class and some concrete implementations:
RedisCache. And we have some logic to flush cache:
Now you see what happens when are forced different classes that do similar things but don’t provide a common interface. We should use these ugly
if-else statements to provide a logic branch for every specific class. And next time when we add a new cache implementation we need to change this
flushCache method and add a new branch for this class. To fix this issue we need a common interface. Then we can extract every branch to its own class and hide it behind the common interface. It looks like we need an abstract
flush method in our base
Cache class. Then in every child class, we write a concrete implementation for this method.
Now, we can modify
flushCache method and remove all of these type checks and logic branches, because we don’t care about concrete classes anymore. We know for sure that all of them implement
flush method, so we can safely call it.
Besides the fact that
flushCache method has become very simple and clear, we can now safely add new
NullCache implementation without touching
Application class at all. It already knows how to collaborate with newly created
NullCache class. Very nice, right? This is what we call polymorphism. We have hidden different logic behind one name, so we can safely add new implementations and every class that knows this name automatically knows how to collaborate with new implementations.
We can achieve the same different logic one name with interfaces when our classes are not related. For example, we have
Cache class that uses cacheable objects to store them in cache. Every cacheable object implements an interface that has a method for representing a cache key.
Category classes work very differently internally. But because they implement the same interface, we can use them both exactly the same way (of course, only in the context of this current interface):
Cache doesn’t care what exactly class it works with. It only knows that it implements
getCacheKey() method, and it is enough for it. It will work with any class that implements
Polymorphism here is the combination of implementing an interface (abstract class or real interface) in your classes and then depending only on that interface.
Unlike ad-hoc polymorphism, this type of polymorphism PHP supports out of the box, because of the loosely typed nature of the language. When we have no type hints, we can pass into function/method anything we want:
The difference with ad-hoc polymorphism is that parametric polymorphism means that we don’t care about the type, we implement the function the same for any. Ad-hoc polymorphism instead means that we have a different implementation depending on the type of the argument. Now let’s refresh the example with
Cache from subtype polymorphism:
store here uses parametric polymorphism. It is implemented for any concrete implementation of
Cacheable type. As long as it receives an argument that is an instance of the
Cacheable data type, it should execute successfully, just as our
sum function did. And again we achieve the same name, different logic. Same name here means that we don’t care about the concrete implementation of the
Cacheable data type, we accept any of them. And each concrete type will have its own implementation, its own different logic.
As you can see subtype polymorphism and parametric polymorphism are closely related. We create different subtypes (with inheritance or interfaces), and then use them depending on the base type (class or interface).