Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
286 lines (208 sloc) 8.66 KB
layout category title tags image image_copyrights
post
dev
How to use PHP traits?
php
traits
coding
/assets/images/posts/IMG_20181014_110212-01.jpg
Image by Pol Dellaiera

{% include JB/setup %}

Recently, I've been busy rewriting small PHP libraries like ValueWrapper, HTMLTag, PHPNgrams, DynamicObjects, PHPartition, PHPermutations and Memoize.

I mostly rewrote them because of multiple things I wanted to do:

This article will explain what are traits and will try to propose, without pretension, a better way to write them.

Let's start with the definition of a trait.

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. From php.net

When designing a software or a library, we are often busy thinking on how to make it work properly and how it could do the job as expected. This is already a good part of the job. When it comes to designing the library and it's classes organisation, it's another world that sometimes can take as much time as the real implementation.

  • How to reduce and minimize code duplication,
  • How to make sure that the inheritance hierarchy is optimal,
  • Which design pattern to use,
  • etc etc

Sometimes, it's possible that you'd wish your class to inherit from multiple classes, but unlike in Python (and probably some other languages), it's not possible in PHP (Yet?).

Of course, inheritance with multiple classes can be hard to work with, it adds complexity and has issues like the Diamond problem.

The "diamond problem" (sometimes referred to as the "deadly diamond of death") is an ambiguity that arises when two classes B and C inherit from A, and class D inherits from both B and C. If there is a method in A that B and C have overridden, and D does not override it, then which version of the method does D inherit: that of B, or that of C? From wikipedia

But obviously, PHP is not concerned by this.

Introduced in PHP 5.4, using traits is a way to solve this. Basically, it allows developers to reuse horizontally the same code across independent and different classes hierarchies.

It seems to be an amazing discovery, but it's nothing new, this concept is used in languages like Scala and Perl.

A trait is a good way to provide new features to a class, it's a good way, but it has drawbacks.

Let's see with a basic example.

In the following example, we want to create a trait that compute the greatest common divisor of 2 integers.

<?php

declare(strict_types = 1);

trait GreatestCommonDivisor
{
  /**
   * Get the divisors of a given number.
   *
   * @param int $num
   *   The number.
   *
   * @param int $start
   *   The number to start from.
   *
   * @return int[]
   *   The divisors of the number.
   */
  public function factors(int $num, int $start = 2): array
  {
    $return = [1, $num];

    $end = ceil(sqrt($num)) + 1;

    for ($i = $start; $i < $end; $i++)
    {
      if (0 !== $num % $i) {
        continue;
      }

      $return[$i] = $i;
      $return[$num/$i] = $num/$i;
    }

    asort($return);

    return array_values($return);
  }

  /**
   * Get the greatest common divisor.
   * 
   * @param int ...$x
   *   The numbers.
   * 
   * @return int
   *   The greatest common divisor.
   */
  public function gcd(...$x): int
  {
    $x = array_map([$this, 'factors'], $x);
      
    $intersect = array_intersect(...$x);
      
    return end($intersect);
  }
}

This trait can be used in any classes just by adding:

<?php

declare(strict_types = 1);

class Foo
{
  use GreatestCommonDivisor;
}

This will add the 2 methods to your class and you'll be able to call them.

<?php 

declare(strict_types = 1);

$foo = new Foo();

$factors = $foo->factors(10);

$gcd = $foo->gcd(10, 15);

At this point, one may raise 2 questions.

The first one is obvious, what about name collisions ? What if the class that uses this trait has already methods having the same names ?

There is a way to avoid name collisions:

use \your\namespace\GreatestCommonDivisor {
  GreatestCommonDivisor::gcd as traitGcd;
}

Then, in that case, if your class has already got a gcd() method, you won't have naming collisions and you'll be able to use the method traitGcd().

The second question is, what if I only want to expose the gcd() method and not the other one(s) ?

In this particular case it's not a big deal, but if you want to create a library using a trait, there are chances that you would like to expose only the relevant methods.

Unfortunately, there is no way to restrict the visibility of methods defined in a trait. Of course, you could wrap your trait in an new object, then wrap that object in another new trait... but I think you agree with me, it's messy.

This issue raise another set of issues. Using traits usually gives you more work. Why ?

As the visibility of methods cannot be changed, you'll have to test all the methods, individually.

Testing private or protected methods could be cumbersome and in some cases, it's better to test public methods which internally uses those methods.

A way to avoid those situations is to use traits in a different way, based on an object.

See the following example, it's the same as the first example, but rewritten.

First we are going to create a single class.

<?php

declare(strict_types = 1);

class GreatestCommonDivisor
{
  /**
   * Get the divisors of a given number.
   *
   * @param int $num
   *   The number.
   *
   * @param int $start
   *   The number to start from.
   *
   * @return int[]
   *   The divisors of the number.
   */
  public function factors(int $num, int $start = 2): array
  {
    $return = [1, $num];

    $end = ceil(sqrt($num)) + 1;

    for ($i = $start; $i < $end; $i++)
    {
      if (0 !== $num % $i) {
        continue;
      }

      $return[$i] = $i;
      $return[$num/$i] = $num/$i;
    }

    asort($return);

    return array_values($return);
  }


  /**
   * Get the greatest common divisor.
   * 
   * @param int ...$x
   *   The numbers.
   * 
   * @return int
   *   The greatest common divisor.
   */
  public function gcd(...$x): int
  {
    $x = array_map([$this, 'factors'], $x);
      
    $intersect = array_intersect(...$x);
      
    return end($intersect);
  }
}

Then a trait

<?php

declare(strict_types = 1);

trait GreatestCommonDivisor
{
  /**
   * Get the greatest common divisor.
   * 
   * @param int ...$x
   *   The first number.
 * 
   * @return int
   *   The greatest common divisor.
   */
  function gcd(...$x): int
  {
    return (new GreatestCommonDivisor())->gcd(...$x);
  }
}

You can find the online code at: https://3v4l.org/LCmNp

What are we doing here ?

Basically, the GreatestCommonDivisor object is wrapped in a trait, and the trait exposes only the method needed.

When this trait will be used, in any object you want, it will only provide 1 method and nothing else.

To summarize this post:

  • Even if traits seems to be wonderful, use them with parsimony, at the last resort,
  • All the traits that you create should wrap an inner class that does the job properly,
  • Reduce or don't do logic in your traits,
  • Do not overload traits with a lot of methods, try to keep in mind that "1 trait = 1 method",
  • Carefully select which methods you want to expose in your traits,
  • Unit test your objects and not your traits.