Inhertitance, Traits and Interfaces

Published on 06/06/2023

There are various ways of sharing code between files in PHP. Probably the most obvious is Inheritance, where one class extends another. However, PHP only allows us to extend one class at a time - this article looks at where this might cause us an issue, and how to work around this using Traits. We'll also look at Interfaces, which help us to define some rules to keep our code under control.

A simple way to understand the difference between Inheritance, Traits and Interfaces is:

Inheritance
The class is a type of a thing
Trait
The class can do a thing
Interface
The class follows some rules

Inheritance

For example, an Elephant is a type of animal. The Elephant class can extend the Animal class

class Elephant extends Animal 
{

}
abstract class Animal 
{
    public function walk() {}
    
    public function makeSound() {}
}

Because the Elephant class extends Animal, it can use any of the methods in the Animal class - an Elephant can walk, and it can make a sound. an inheriting class can use the public and protected methods from its parent, but not private ones

So far, so good. But what about if we want to add another animal.... maybe... a snake?

A snake is a type of animal, so it makes sense that we extend the Animal class. But there's just one problem. Anything extending Animal automatically inherits the walk() method. And snakes can't walk....


The wrong solution

One option could be to have another class, that extends Animal, called WalkingAnimal, and add the walk method there. Elephant can extend WalkingAnimal, and snake can just extend Animal.

But then what if we add a Fish class? A fish can't walk either, but it can swim. We could add a swim method to Animal?
But what if we then add a Pigeon? A pigeon can't swim - but it can fly.
Do we need a WalkingAnimal and a SwimmingAnimal and a FlyingAnimal class?
But pigeons can both walk and fly, and seagulls... they can walk, fly, AND swim (and steal chips).

You can see how this can quickly become complicated - we need to split out the methods into different classes, so that animals like snakes don't end up with methods they shouldn't have. But some animals (like Seagulls), need multiple methods, but PHP doesn't allow us to inherit multiple classes at once.


Traits

This is where Traits can save the day. All of the methods we were trying to add to our Animal class are things we want (some of) our animals to be able to do. A Trait is a way of giving a class (animal) the ability to do a thing.

We can create a bunch of traits:

  • Walks
  • Slithers
  • Swims
  • Flies
  • MakesSound

Each of these traits will contain a relevant doTheThing method, for example

trait Walks
{
    public function walk(){
        print ("I'm Walking!");
    }
}
trait Swims
{
    public function swim(){
        print ("I'm Swimming!");
    }
}
trait Flies
{
    public function fly(){
        print ("I'm Flying!");
    }
}

The handy thing with Traits, is you can use as many of them as you need. So our Seagull class, can use the Walks, Swims and Flies traits

class Seagull
{
    use Walks;
    use Swims;
    use Flies;
}

When a class uses a Trait, it's not so different from extending another class - all the methods in the Trait are available for use. Let's make us a Seagull:

$bob = new Seagull(); // he's called Bob.  Because seagulls bob in the sea...
$bob->fly();

// result: "I'm Flying!"

Bob the seagull can use any methods defined on any of the traits he uses - he can walk, swim and fly.

We can also use these same traits for any other animals who can do the same things. Our Fish can use the Swims trait, as can our Elephant, who can also use Walks

class Elephant 
{
    use Walks;
    use Swims;
}
class Fish
{
    use Swims;
}

Because we've created a separate trait for each of the different things an animal might be able to do, we can use these traits in any combination as needed.


Inheriting Traits

Traits can also be used in combination with inheritance, if needed. All Animals can grow, but so can Plants. So we create a Grows trait that both Animals and Plants can use.

We could add the Grows trait to all of our animals one-by-one, but that's tedious. Instead, we can still have a common, Abstract class that all the Animals share - but it only contains things common to all animals.

trait Grows
{
    public function grow(int $amount)
    {
        print("I grew by $amount cm!");
    }
}
abstract class AbstractAnimal
{
    use Grows;
}
class Seagull extends AbstractAnimal
{
    use Walks;
    use Swims;
    use Flies;
    // also uses Grows, via AbstractAnimal
}

Because our Seagull (and all our other animals) extend AbstractAnimal, they can now all use the grow method - as well as any methods in any traits they extend directly.

$bob = new Seagull();
$bob->grow(1);

// result:  "I grew by 1cm!"

If any of our animals need any methods that no other animals have (eg a StealChips method for our Seagull), we can just define these on the animal's class like normal. You can also override some of the methods from a trait if you need to, by adding a method with the same name in your class.


Interfaces

Interfaces aren't quite the same as Inheritance and Traits. Rather than defining what something can do, they define what it must do. An Interface can be thought of as set of Rules.

Let's imagine we're building an enclosure at our Zoo. All enclosures need an Animal in them (else the visitors all complain).

Our first enclosure is an aquatic enclosure - it's nice big tank of water. Here's the class for our AquaticEnclosure

class AquaticEnclosure
{
    private AbstractAnimal $animal;
    
    public function __construct(AbstractAnimal $animal)
    {
        $this->animal = $animal;
    }
    
    public function getAnimal()
    {
        return $this->animal;
    }
}

We've specified that when we make an AquaticEnclosure, we need to put in an Animal - we've used the AbstractAnimal class as a typehint to make sure that whatever we pass here must be a type of Animal - it must be something extending the AbstractAnimal class.

    // Bob is a Seagull, a type of Animal, so he can go in the enclosure
    $bob = new Seagull();
    $pool = new AquaticEnclosure($bob); 

It's a good start. But it's not idiot-proof. We know that AquaticEnclosures are only suitable for animals who can swim. But what's to stop another zookeeper (developer) coming along in the future and trying to put a Pigeon in there? (Remember, pigeons can't swim).

We need to save that pigeon. Really, we don't want to allow any animal in an AquaticEnclosure - we only want animals who can swim.

Are we back to our good old SwimmingAnimal abstract class? Not quite. We don't actually care what type of animal goes in this enclosure, we just care that it isn't going to drown. We just want something that can swim

We can't use our Swims trait as a type-hint - because it's not a type, it's an ability. But what we can do, is put some rules on our AquaticEnclosure, via an Interface.

interface SwimmerInterface
{
    public function swim();
}

Notice the syntax is a bit different from our Classes - particularly, the swim function has no body. This is because it's not a method to enable an animal to do a thing - it's a rule. It's saying, anything that implements this Interface, must have a swim method.


An Interface is a Contract

If a class implements an Interface, this tells us that this class abides by the rules the Interface sets out. It means we can trust these classes to have certain methods.

In our example, we can implement the SwimmerInterface in any animal who is able to swim - this means, we know that we can put any Animal which implements SwimmerInterface in our AquaticExhibit, and we can tell it to swim. And it'll swim, rather than drown (or in code terms, throw an exception because the swim method isn't defined)

class Seagull extends AbstractAnimal implements SwimmerInterface
{
    use Walks;
    use Swims; // the Swims trait satisfies the swim rule in SwimmerInterface
    use Flies;
}

Our AquaticEnclosure class can now specify that it doesn't just want any animal, it wants an animal that meets the SwimmerInterface contract and obeys those rules.

class AquaticEnclosure
{
    private SwimmerInterface $animal;

    public function __construct(SwimmerInterface $animal)
    {
        $this->animal = $animal;
    }

    public function getAnimal()
    {
        return $this->animal;
    }
}

This change means that anything put into the AquaticEnclosure must follow the SwimmerInterface rules. Because the Seagull class implements SwimmerInterface, Bob the Seagull can live in our enclosure:

    $bob = new Seagull();
    $pool = new AquaticEnclosure($bob);
    $pool->getAnimal()->swim();

    // result: "I'm Swimming!"

But if we try and put a Pigeon in...

    $charlie = new Pigeon();
    $pool = new AquaticEnclosure($charlie); //this line causes an error
    $pool->getAnimal()->swim();

    // result: TypeError: AquaticEnclosure::__construct(): Argument #1 ($animal) must be of type SwimmerInterface, Pigeon given

Now even our daftest Zookeeper (or developer) isn't going to go accidentally drowning any Pigeons. But if they want to, they can put any other type of swimming animal - like an Elephant or a Fish, in the pool - provided those classes implement the SwimmerInterface

class Fish extends AbstractAnimal implements SwimmerInterface
{
    use Swims;
}
    $goldie = new Fish();
    $pool = new AquaticEnclosure($goldie);
    $pool->getAnimal()->swim();

    // result: "I'm Swimming!"

Conclusion

Whilst they provide similar functionality, Inheritance and Traits have subtly different jobs, and can be used together to help us write clean, readable code. Interfaces are a bit different, but when combined with Inheritance and Traits, give us really good control over how and where different classes can be used

If you'd like to have a closer look at all the classes used in this article, you can take a look at them in the GitHub Repo