There are many ways to stop code duplication in classes. The simplest way is inheritance. But what if a class needs code from many different classes? PHP does not allow multiple inheritance. So the only way is to use traits. A good trait works well in any class. This post shows you how to write a good trait.
Note: this post does not explain what a trait is. This post shows good rules for writing and using traits.
Here is the main rule for writing traits: try to pass all variables as function parameters. A trait like this will work well in any class.
Sometimes you cannot pass all the parameters. For example, there may be too many of them. In this case, one solution is to use the class’s own properties directly. Let’s look at a simple example of a trait in PHP:
<?php declare(strict_types=1);
trait increment
{
public function increment(): void
{
$this->count++;
}
}
final class simpleClass
{
use \increment;
/** @var int */
protected $count = 1;
public function getCount(): int
{
return $this->count;
}
}
$obj = new simpleClass();
$obj->increment();
echo 'Count: ' . $obj->getCount() . PHP_EOL;
This code prints this result:
php scratch_1.php Count: 2
But this script has a problem. The code assumes every class with the increment trait has a $count property. What happens if we forget to add this property? Nothing bad happens right away. But the app will show a wrong result (Count: 1). This happens if error logging is on:
error_reporting(E_WARNING);
Then we see this message:
PHP Notice: Undefined property: simpleClass::$count in ... on line 7
But what if this problem happens only on the production server? There, we do not log these errors. Can we protect against this? Yes, we can. First, we must decide: is it better for the app to fail with a 500 error? Or is it better to silently return a wrong result? If you choose the second answer, you can stop reading now. If you choose the first answer, keep reading.
The first step is a small change to our trait. We use abstract functions. This forces every class with the increment trait to add two functions: getCounter and setCounter. We also must change the increment function to use these two functions:
<?php declare(strict_types=1);
trait increment
{
abstract protected function getCounter(): int;
abstract protected function setCounter(int $value): void;
public function increment(): void
{
$this->setCounter($this->getCounter() + 1);
}
}
...
Now, if we run our code, the app fails with an error:
PHP Fatal error: Class anotherSimpleClass contains 2 abstract methods and must therefore be declared abstract or implement the remaining methods (anotherSimpleClass::getCounter, anotherSimpleClass::setCounter) in .... on line 15
This is what we wanted. It is better for the program to stop than to give a wrong result without warning. Now we just need to add the missing functions to the simpleClass class. Here is the final code:
<?php declare(strict_types=1);
trait increment
{
abstract protected function getCounter(): int;
abstract protected function setCounter(int $value): void;
public function increment(): void
{
$this->setCounter($this->getCounter() + 1);
}
}
final class simpleClass
{
use \increment;
/** @var int */
protected $count = 1;
protected function getCounter(): int
{
return $this->count;
}
protected function setCounter(int $value): void
{
$this->count = $value;
}
public function getCount(): int
{
return $this->count;
}
}
$obj = new simpleClass();
$obj->increment();
echo 'Count: ' . $obj->getCount() . PHP_EOL;