A Simple Trait Validation Technique

PHP Traits are a language feature that I generally recommend you don't use unless you really know what you're doing. Traits are intended to emulate multiple inheritance and on the surface seem like a great idea. Unfortunately, sometimes great ideas, even well intentioned ones, dont work out in the real world. Here's a greatly simplified example of what usually ends up happening every time i've encountered traits in the wild.

Lets say we have a base class called ComicBook. ComicBook implements a "BookInterface" that defines all of the methods required to be considered a valid book.

In this simplified example, a book just has to have an illustration.

interface BookInterface 
{
    // Returns a url to the illustration to be displayed on the provided page 
    // or null if an illustration does not exist
    public function getIllustration(Page $page): string|null;
}

For reasons probably related to the lack of multiple inheritance in PHP, the decision is made to implement this method using a trait. This trait is written as a dependency to the ComicBook class, meaning it depends on functionality that exists only in a "ComicBook" and not a general purpose "Book". This is fine at the time because the trait is currently only used in a single place.

class ComicBook extends SomeOtherClass implements BookInterface
{
    use IllustrationTrait;

    protected function doComicSpecificThing()
    {
        // Retrieves the comic book pictures in a class specific way that is not 
        // in any way intended to be reusable
    }
} 
trait IllustrationTrait
{
    public function getIllustration(Page $page)
    {
        // IllustrationTrait depends on a method that only exists in ComicBook 
        // and may or may not exist in other classes.
        return $this->doComicSpecificThing();
    }
}

Later, the business requirements of the app you are working on change and the company you are working for has decided to also start selling online magazines. Magazines are also books and thus adding this new functionality should be easy. We should be able to reuse the IllustrationTrait we used in the ComicBook class

class Magazine extends SomeOtherClass implements BookInterface
{
    use IllustrationTrait;
}

However, when we try and instantiate and run this Magazine class we will get an error message because IllustrationTrait depended on methods internal to the "ComicBook" class. Now we have to go back to the drawing board and what should be a simple addition ends up taking a lot more time than expected.

## Some Ways To Avoid This Problem

1. Use an abstract class

An abstract class is similar to a trait in that they allow you to abstract specific implementations and you cannot instantiate them directly. Unlike Traits, however, abstract classes are valid, structured classes that do not blindly copy methods into potentially incompatible objects at runtime. Instead, you can mark methods which will have specific implementations as abstract and any classes that extend this abstract class are required to provide an implementation for this abstract method. In practice an abstract class is Kind of like a hybrid between an interface and a trait.

We could use an abstract class to clean up the example above.

abstract class Book extends SomeOtherClass
{
    public fuction bookMethodOne()
    {
        // Something that everything that is considered a book will need
    }

    public function bookMethodTwo()
    {
        // Something else that everything that is considered a book will need
    }

    // Instead of using a trait to add this method to your books, we now depend on the implementing
    // class to provide us a method to get the illustration in whatever way it sees fit.  Note
    // that we have not added any code here
    abstract public function getIllustration();
}

Our ComicBook class now extends this abstract book class and implements a comic specific getIllustration method

class ComicBook extends Book 
{
    protected function doComicSpecificThing($page)
    {
        // something specific to a comic book
    }

    // We provide actual code for the getIllustration method in the ComicBook class
    public function getIllustration($page)
    {
        return $this->doComicSpecificThing($page);
    }
}

And similarly, our new Magazine class also extends the abstract book class and implements it's own (different) method of retrieving an illustration

class Magazine extends Book 
{
    protected function doMagazineThing($page)
    {
        // something specific to a magazine
    }

    // Magazines retrieve illustrations completely differently than comic books. 
    // Abstract classes help make this very clear to other developers
    public function getIllustration($page)
    {
        return $this->doMagazineThing($page);
    }
}

Now your code has a bit more structure and will be easier to understand when you come back 6 months later to change somehting.


2. Runtime validation in your trait

Abstract classes require extension by a child class so there are times when you may have no choice but to use traits. A common example is when adding reusable functionality to Eloquent models in the Laravel framework. Because Eloquent models extend the laravel Model class, you cannot directly use an abstract class to define reusable methods. Instead, developers often attempt to add reusable functionality using traits.

These traits end up depending on some piece of data internal to a specific Eloquent model (methods, variables, database columns, etc) making it silently incompatible with other Model classes. When you attempt to reuse this trait on another model, you receive an error similar to the example above.

Eloquent models are essentially database queries, so it makes a lot of sense to try and reuse queries between models. However, since there's nothing within the PHP runtime checking your work, it's very easy to make a mistake and end up with something tied to a specific implementation like the example I provided.

Bugs that pop up from these situations can be a nightmare to debug, so instead, I find it helpful to add runtime validation to ensure that they never happen to begin with. This runtime validation ensures that the environment in which this trait is running is exactly the environment I intended and nothing else. If another developer attempts to reuse this trait in an environment in which it wasn't intended to be used, we immediately halt execution by throwing an exception. This exception is accompanied by a very detailed explanation as to what failed and why to ensure that the new developer doesn't just comment out the validation and continue with their day.

Here's an example of adding some runtime validation to the Book example from above

trait IllustrationTrait
{
    public function getIllustration(Page $page)
    {
        // Before running this function we do a runtime check to ensure this trait has not been used in an
        // unintended way. If so, we throw an exception with a very detailed explanation as to why we 
        // are halting execution. All methods of this trait should call this validation function 
        // before running their intended code.
        $this->ValidateIllustrationTraitRuntimeEnv();


        // IllustrationTrait depends on a method that only exists in ComicBook and may or may not exist in
        // other classes.
        return $this->doComicSpecificThing();
    }

    /**
    *  Checks to ensure that this trait is being used in the environment intended by the original developer. 
    *  If this trait is not being used in the intended way, we throw an exception with a detailed 
    *  explaination of what went wrong. Not a perfect solution, but at least we are doing our 
    *  best to not allow our application things to silently explode in the future.
    */
    protected function ValidateIllustrationTraitRuntimeEnv()
    {
        // Returns a list of all interfaces used by this class
        $listOfClassInterfaces = class_implements(self::class);

        // Checks to ensure that the current class imeplements a specific interface
        if(! array_key_exists("XYZInterface", $listOfClassInterfaces) {
            // Throw an exception explaining that this trait must be used by classes 
            // that implement XYZInterface 
        }

        // Checks that the current class is a child of some other class
        if(! is_subclass_of($this, "ABCDlass")) {
            // Throw an exception explaining this trait can only be used by classes 
            // that extend ABCDClass
        }

        // Add any other things required to run this trait to this validation method. You have full 
        // access to the underlying class using this trait so be creative
        if(! $this->someOtherValidationMethod)
        {
            // Throw an excep..... you get the idea
        }
    }
}

This solution isn't perfect. In fact, it sort of just takes a code smell and sprays it with a can of Axe Body Spray. Yeah it doesn't smell as bad as before but now it just smells like Axe Body Spray.

The biggest problem is that it's possible to instantiate a class using this trait and not see any errors as long as you don't call any methods belonging to this trait. That said perfect is the enemy of good and as long as the error messages provided by your exception provide sufficient explanation as to why this happened it's better than nothing.

Similar Blog Posts

Batch Converting PNG files to WEBP

A script I made to convert png files to webp

How I nearly accidentally automated my entire department out of a job

A story of an internship I did with a governmenmental IT Dept

Some Helpful Vuex Tips

A couple of helpful tips for working with Vuex