PHP - Using Traits

Introduction

Traits, which were introduced in PHP 5.4, are defined as:

Traits are a mechanism for code reuse in single inheritance languages such as PHP. A Trait is intended to reduce some limitations of single inheritance by enabling a developer to reuse sets of methods freely in several independent classes living in different class hierarchies.

Up until now I have never really felt the need for multiple inheritance and have always managed to get by with using factories/composition/interfaces, but whilst building an API, I decided to look into using traits because:

  • AWS uses them in their PHP SDK codebase.
  • Lots of my API resources have the same groups of methods, but different resources have a combination of different method groups, so I can't just use inheritance.

It is worth noting that Netbeans fully supports auto-completion of classes that use traits, as well as "go to source" (ctrl-B).

Examples

Below is a very simple example of using a trait to have a class print "hello world" when its hello method is called.

trait MyTrait
{
    public function hello() 
    {
        print "hello world" . PHP_EOL;
    }
}


class MyClass
{
    use MyTrait;
}

$myObj = new MyClass();
$myObj->hello();

Multiple Traits

Your classes can use multiple traits as shown below:

trait Trait1
{
    public function hello() 
    {
        print "hello ";
    }
}

trait Trait2
{
    public function world() 
    {
        print "world";
    }
}

class MyClass
{
    use Trait1;
    use Trait2;
}

$myObj = new MyClass();
$myObj->hello();
$myObj->world();

Traits Within Traits

Traits can be comprised of other traits as well. With this example, there is never a need to use Trait1 if you have Trait2.

trait Trait1
{
    public function hello() 
    {
        print "hello ";
    }
}

trait Trait2
{
    use Trait1;

    public function world() 
    {
        print "world";
    }
}

class MyClass
{
    use Trait2;
}

$myObj = new MyClass();
$myObj->hello();
$myObj->world();

If you are immediately thinking about method name conflicts between two traits, this is covered later under "Remapping".

Inheritance With Traits

Traits will work with inheritance as shown in this script which will print "hello world":


trait MyTrait
{
    public function hello() 
    {
        print "hello ";
        parent::hello();
    }
}

class ParentClass
{
    public function hello()
    {
        print "world";
    }
}


class MyClass extends ParentClass
{
    use MyTrait;
}

$myObj = new MyClass();
$myObj->hello();

A trait's method will automatically try to use the child's methods before resorting to the parents. To demonstrate this, run the code below once and then see what it does when you remove world from MyClass:

trait MyTrait
{
    public function hello() 
    {
        print "hello ";
        $this->world();
    }
}

class ParentClass
{
    public function world()
    {
        print "there";
    }
}


class MyClass extends ParentClass
{
    use MyTrait {
        hello as private hey;
    }

    public function world()
    {
        print "world";
    }
}

$myObj = new MyClass();
$myObj->hello();

Overriding And Order of Precedence

Traits are overridden if the method with the same name is defined in the class that uses the traits. The precedence order is that members from the current class override Trait methods, which in turn override inherited methods. The script below will just print "hi there":

trait MyTrait
{
    public function hello() 
    {
        print "hello ";
        parent::hello();
    }
}

class ParentClass
{
    public function hello()
    {
        print "world";
    }
}


class MyClass extends ParentClass
{
    use MyTrait;

    public function hello()
    {
        print "hi there";
    }
}

$myObj = new MyClass();
$myObj->hello();

Remapping

If you want to work around a trait's method being overridden, or you need to resolve a conflict of the same method name being used in two traits of a class, you can "remap" a trait's methods by changing their names and even whether the methods are private, public, protected. The example below extends the previous one an prints "Using hey method: hello world":

trait MyTrait
{
    public function hello() 
    {
        print "hello ";
        parent::hello();
    }
}

class ParentClass
{
    public function hello()
    {
        print "world";
    }
}


class MyClass extends ParentClass
{
    use MyTrait {
        hello as private hey;
    }

    public function hello()
    {
        print "Using hey method: ";
        $this->hey();
    }
}

$myObj = new MyClass();
$myObj->hello();

Abstract Methods

Traits can specify abstract methods, forcing the class that uses the trait to implement methods that the trait's other methods need as shown below:

trait MyTrait
{
    public function runSomething() 
    {
        return $this->something();
    }

    public abstract function something();
}

class MyClass
{
    use MyTrait;

    public function something() 
    {
        // do something here.
    }
}

$myObj = new MyClass();
$myObj->runSomething();

Disadvantages

Interfaces Not Supported

For some reason, traits do not support interfaces (yet).

Hidden Issues and Complicated Codebases

If you are writing traits that use methods that aren't defined in the trait itself, it can be easy to get into a situation whereby updates to your classes that implement such traits, or act as the parent to a class that does, will break your code without warning (and will even run perfectly fine until you try to execute that non-existant method route). I would recommend that any methods that are called from the trait itself on $this implement define that method as abstract to try and prevent such issues.

Combining traits with inheritance could make for an incredibly complicated codebase that is hard to understand and maintain. You can tell you are getting into a rat's nest as soon as you need to perform remapping on the traits.

References

Author

Programster

Stuart is a software developer with a passion for Linux and open source projects.

comments powered by Disqus