80

PHP Interfaces Explained

 6 years ago
source link: https://daylerees.com/php-interfaces-explained/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

PHP Interfaces Explained

13 January 2018 on PHP, interfaces

A few years ago I wrote an article about PHP namespaces; a feature that seemed to mystify the beginner to mid-level PHP developer. Next up, we have interfaces. These are something that I see are misused all the time.

It's such a shame because interfaces are my absolute favourite feature of OO languages! Unfortunately, I often see developers use interfaces poorly; littering their code with them without purpose to try and demonstrate a knowledge of OO design.

Well, this article will show you some practical use for interfaces, so that you can call them out!

How to use interfaces.

First, let's take a look at an example interface.

<?php

interface Human  
{

}

Here we have an entirely empty interface. Interestingly, an empty interface is not without purpose. We can make something great happen from this.

Let's make a few classes that implement this interface.

<?php

class Dayle implements Human  
{

}

class Taylor implements Human  
{

}

class Elon implements Human  
{

}

The classes that implement our interface now share a common identity. This is useful because we can use instanceof to confirm that identity, for example:

<?php

$elon = new Elon;

if ($elon instanceof Human) {  
    echo 'Well that was a surprise!';
}

Here, we use instanceof to confirm that the Elon class implements the Human interface. This way we can separate our classes from those that don't implement the interface and deal with them in a different manner.

Of course, why check the class ourselves when we can let type-hinting do the hard work for us? If we're passing a class instance into a function, then we can type-hint the interface directly. Here's an example.

<?php

function feedHuman(Human $human)  
{
    // Feed the $human.
}

By type-hinting the interface in the function parameter list, we can be sure that any instance passed to this function is an implementation of the Human interface. We don't need to check it ourselves.

If we try to pass any other value into the function, such as an integer, a string or a class instance that doesn't implement the interface, then we'll receive an angry error message.

Fatal error: Uncaught TypeError: Argument 1 passed to feedHuman() must implement interface Human, integer given...  

This is really useful, now we know that we can type hint interfaces to ensure that we know exactly what type of instance we're working with. We know exactly what we're getting.

But wait, why does it matter that our parameter is a Human? Well right now, there's no reason. We have confirmed the identity, but that identity isn't linked to any functional signatures.

If you've been taught interfaces before, then you've heard that interfaces can define signatures for methods. Here's an example.

<?php

interface Human  
{
    public function eat($food);
}

Here we've defined a method signature. You'll notice there's no body to the method, we've only defined the method name, visibility, and the parameters it takes.

We don't need to write the body because what we're doing is creating a contract. Here's what the above code is saying:

If you make a class implement Human, then it MUST have a public eat() method that accepts a $food parameter.

If you were to make a class that implements the interface but doesn't satisfy the method, for example:

<?php

class Dayle implements Human  
{

}

Then at runtime, you'll receive the following error. Or if you use a fancy IDE, it might let you know before you run your code!

PHP Fatal error:  Class Dayle contains 1 abstract method and must, therefore, be declared abstract or implement the remaining methods  

An abstract method is what we call a method without a body. It's what you normally find in interfaces. What the error is saying is:

Look, you either need to implement the eat() method in your Dayle class, or you need to make Dayle an abstract class.

Adding the eat() method to the Dayle class as it appears in the interface (but with a functional body), would satisfy the interface's contract, and remove this error.

Or, we could make the Dayle class abstract, like this:

<?php

abstract class Dayle implements Human  
{

}

This means that the Dayle class can't be instantiated with the new keyword. Instead, it's a class that is designed to be extended by another class. Like this:

<?php

class BetterDayle extends Dayle implements Human  
{

}

What we've done, is shifted the responsibility of satisfying that eat() method down a level. Now, the BetterDayle class needs to implement the eat() method or be declared abstract. Either of those techniques will ensure that we don't get that annoying fatal error!

Here's an example of a class that satisfies the contract of our interface.

<?php

class Dayle implements Human  
{
    public function eat($food)
    {
        echo "Mmm, I love {$food}!";
    }
}

So, now we know that any instance of a class that has our Human interface implemented, or is implemented somewhere in the class inheritance chain (extending), will have a public eat() function.

This means that we can write functions and methods that look like this:

<?php

function feedHuman(Human $human)  
{
    $human->eat('pizza');
}

feedHuman($dayle); // Mmm, I love pizza!  

If we hadn't type-hinted the interface, then another developer might have passed a class instance that didn't have an eat() method or even another type like integer or string into the function, and it would have errored when the eat() function couldn't be found on the property.

What we've done is protected our code. We have ensured that any class instance passed to our function has an eat() method. We don't have to check it ourselves, it just has to have that method. Pretty neat, right?

This kind of code is really neat when creating applications that offer custom functionality. Let's take a look at a more practical example, shall we?

Let's imagine that we're writing a PHP application that makes use of a cache. We could write a cache class that looks like this.

<?php

class RedisCache  
{
    public function write(string $key, $value)
    {
        // Code to write the data to Redis...
    }

    public function read(string $key)
    {
        // Code to read the cached data from Redis...
    }
}

This is quite a common format for caching. We've created a class with two methods. One for writing key-value cache data to Redis (an in-memory key-value storage software) and another for reading data back from Redis by key.

I've omitted the code for actually writing and reading from Redis because it isn't really important for this article. Just imagine that it's there!

This code should function fine, but it's not ideal. Can you tell why? What if the Redis project dies? (I really hope not!) What if we decide to switch to database caching? What if our application is an open source project that's designed to be used by others, do we want to restrict them to using Redis? After all, there are a thousand and one other platforms that can store key-value data, isn't there?

Interfaces to the rescue! Let's use interfaces to future-proof this code, or to allow others to use their own cache mechanisms. First, we're going to need to write the interface. Often, I'll begin by writing the interface first, if I know what the implementations are going to need to do.

We know that caches need to read and write data, and we know that they use keys to identify the cached content. Right then, let's get writing.

<?php

interface Cache  
{
    public function write(string $key, $value): void;

    public function read(string $key);
}

So we've got our two methods for reading and writing, with the correct type-hinted parameters but no body. That bit is important. We never add a body to an interface. We've type-hinted void as the return type of the write method. It's a PHP 7.1 feature that means the method shouldn't return anything.

Now that we've created an interface, we should type-hint that in the functions or constructors of classes that make use of the cache. Here's an example:

<?php

class Articles  
{
    private $cache;

    public function __construct(Cache $cache)
    {
        $this->cache = $cache;
    }

    public function fetch()
    {
        if ($articles = $this->cache->read('articles')) {
            return $articles;
        }

        // Fetch from original non-cached data store and return.
    }
}

Here we've got a class for fetching articles. By type-hinting the Cache in the constructor, we ensure that you can only instantiate the Articles class by providing a class instance that implements the Cache interface, and the two methods that are required. This means that we can make use of read() and write() reliably.

What we've actually done here is 'injected' a cache implementation into the class, and set it as a private property of that class instance. This is known as 'dependency injection'. You might have heard of that term thrown around before. It's a great way of making clean, extensible software.

You see, if we'd done something like $this->cache = new RedisCache; in the constructor, then the only way to replace the cache with another type would be to extend the class, and override the constructor and instantiate our new Cache. That would mean creating another class and would be much messier than simply passing in the cache instance that we want to use.

Anyway, before we get distracted, you'll notice that the fetch() method makes use of our 'injected' cache and attempts to read a list of articles from the cache. If they can't be found, it will fall back to the original data-store. I've hidden that code since it's not important here.

Right, let's create two implementations of our Cache interface.

<?php

class RedisCache implements Cache  
{
    public function write(string $key, $value): void
    {
        // Write cache data to Redis.
    }

    public function read(string $key)
    {
        // Read cache data from Redis.
    }
}

class MongoCache implements Cache  
{
    public function write(string $key, $value): void
    {
        // Write cache data to MongoDB.
    }

    public function read(string $key)
    {
        // Read cache data from MongoDB.
    }
}

Here we've created classes for caching to Redis and MongoDB. They both implement the Cache interface and the required methods. I've put them in a single example, but really, they'd be in different files and potentially different namespaces. You can read more about namespaces in another article on my blog.

Now that we have our two cache implementations, it's time to use our imagination a bit. Let's imagine that we have a config loader that reads configuration values from a PHP array. This will let users re-configure our application. Here's an example.

<?php

return [  
    'cache' => RedisCache::class
];

We've set the cache configuration option to the class name of our Redis cache. Let's further use our imagine and understand that there's a config($key) helper function that reads entries from the configuration. There are loads of different libraries that do configuration in many ways, or you could even use an environmental variable, but we'll use our imaginary loader for now.

Let's write some code that will use the Articles class we wrote earlier to fetch a list of articles. Here we go:

<?php

// Create our cache driver.
$cache = new config('cache');

// Inject it into a new Articles instance.
$articles = new Articles($cache);

// Load our article list.
$articlesList = $articles->fetch();

The first line creates a new cache instance using the class that we set in our configuration under the 'cache' key. Because we set it to RedisCache::class it will set $cache to a new instance of that class.

In the second line, we instantiate a new Articles class, injecting our cache instance into it.

Finally, on the third line, we use the fetch() method to populate an article list. Internally, our Articles instance has used our RedisCache to pull this information from Redis. (We'll assume the articles have already been written to the cache!)

Now here's the magic. We can change this code to use MongoDB instead of Redis to store our cache data simply by changing a single line! Here's an example:

<?php

return [  
    'cache' => MongoCache::class
];

By changing our cache configuration option to reference our MongoCache we'll receive an instance of that cache injected into the Articles class, and it will pull data from MongoDB. That's super handy! If we wanted to add a new cache type in advance, we'd just make another class that implements the Cache interface, and swap to it in the configuration.

Since we've used an interface for the cache, it won't work if you change the 'cache' configuration option to something that doesn't implement the Cache interface. It means that those read and write methods definitely exist, and we've protected any code that's using them from trying to call an instance that doesn't have them.

Hopefully, you now have an understanding of how you can use interfaces to make your code more robust, to enforce the 'shape' (signature) of class instances, and to make your applications more flexible. Now go out there and show the world how to use interfaces for good!

Extra Information

Oh, you're still here? Fine, here's some interesting topics that relate to interfaces.

Laravel Dependency Injection

If you use the Laravel framework, you'll find that you can inject services into various classes, by type-hinting a 'contract' in the constructor. Interestingly, what's happening here is incredibly similar to the cache driver example above.

Instead of instantiating the injected instance based on configuration, Laravel checks its IoC container to find a class instance bound to that interface and then injects it directly into the class for you. That's cool, right?

Furthermore, Laravel uses cache adapter pattern above for its own services. You can swap out caches, databases, and more in Laravel's configuration just by changing a few options. The only difference is that Laravel hides the class names behind some friendly strings.

You've learned more than you think!

By reading this article, you've actually learned more than you think.

Being able to inject an instance that can change type into a function is called 'Polymorphism'. I know, it's a big nasty word, but it's super useful. Essentially, if a class "looks right" and implements the required methods, then it can be passed into the function, no matter which class it is.

You've also learned a design pattern! Go you! You've learned about the 'adapter pattern'. That's exactly what we've created in the last example. Our classes that implement the Cache interface are our adapters, and they are interchangeable. Sometimes this pattern is also called the Gateway pattern when it's used to create interchangeable providers that talk to external services.

Maybe you want an abstract class?

If you want some method signatures with bodies and some without. For example, you want some methods to be overridden, and others must be implemented, then you probably want to use an abstract class instead.

Abstract classes can be used like interfaces to enforce the shape of an implementation, but you can also add fully-functional methods to them. So why not use abstract classes all the time, you ask? Well for one, they aren't meant to replace interfaces, and it's much more simple to implement multiple interfaces within a class than to create a long awkward inheritance chain.

For example:

class Something implements Danceable, Jumpable, Singable  
{

}

Here we've got a class that implements multiple interfaces. Unfortunately, PHP can only extend a single class at a time, so imagine what it would look like trying to do the above with abstract classes. Yeah, pretty messy.

It's worth noting that instanceof works just fine on abstract classes too.

A constant source of inspiration.

Fun fact! Interfaces can contain constants.

For example:

<?php

interface Cat  
{
    const FUR = 'super furry';

    const SOUND = 'meow!';
}

echo Cat::SOUND; // meow!  

So if you need to store some constants somewhere, it's pointless to add them to a class that can be instantiated. Just put them on an interface instead!

That's all there is to it!


Well, that's all there is to this article. You're now a master of PHP interfaces. Are they your favourite feature now too?

If you found this article useful, then please leave some feedback. Also, I'm currently working really hard on a side project, it's called Rainglow and it's a collection of hundreds of syntax themes for loads of software! If you can spare a moment, please share it with your friends! That would be awesome. :)

If you'd like to learn more about PHP or Laravel, then there are a bunch of books that I've written on those topics.

Thanks for reading!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK