Skip to content
academia.sh

Lesson 04 / 12

Traits

Sharing methods between classes with a trait, resolving a conflict with insteadof and as, and the consumer stopping when a method of the same name is added to a trait it uses.

Published Updated

In this lesson

  • Defines a trait and mixes it into a class with use
  • Resolves the conflict with insteadof and as when two traits bring a method of the same name
  • Measures that adding a conflicting method to a used trait stops the consumer
  • Shows that the same conflict does not reach a class that has its own method
Contents

An abstract class with a body provided sharing, but left a class only one inheritance right. A trait makes this sharing horizontal: a set of methods is written once and mixed into many classes that are not related to each other. The question is what the using class experiences when a method is added to a trait it uses.

Traits and use

A trait is not a class and cannot be instantiated. Its methods are taken into a class with use and work as if they were written in the class. A class can use as many traits as it likes; this overcomes the single parent class limit of inheritance.

<?php
// Logged.php — a trait that carries the record-keeping behavior
trait Logged
{
    public function summary(): string
    {
        return 'record';
    }
}
<?php
// Timed.php — a second trait that carries time information
trait Timed
{
    public function time(): string
    {
        return '12:00';
    }
}
<?php
// Document.php — a class that uses two traits together
require_once 'Logged.php';
require_once 'Timed.php';

class Document
{
    use Logged, Timed;
}
<?php
// document_usage.php — trait methods are called like the class's own methods
require 'Document.php';

$document = new Document();
echo $document->summary(), ' ', $document->time(), "\n";
var_dump($document instanceof Logged);
record 12:00
bool(false)

OM13. Document wrote neither summary() nor time(), and we could call both. instanceof Logged gave false, though: a trait is not a type, it is code copied into a class. For this reason a trait cannot be relied on as a parameter type; if a type is needed, go back to the path in the Interfaces lesson.

Conflict Resolution

If two traits bring a method under the same name, the class has to declare which one it will use. insteadof decides the one selected, and as gives the method left out another name.

<?php
// conflict.php — two traits bring a method of the same name and the conflict is resolved
trait Logged
{
    public function summary(): string { return 'record'; }
}

trait Timed
{
    public function time(): string { return '12:00'; }
    public function summary(): string { return 'time'; }
}

class Document
{
    use Logged, Timed {
        Logged::summary insteadof Timed;
        Timed::summary as timedSummary;
    }
}

$document = new Document();
echo $document->summary(), ' ', $document->timedSummary(), ' ', $document->time(), "\n";
record time 12:00

OM14. The name summary() went to Logged, and Timed’s method stayed as timedSummary(). The resolution block is inside the class that uses the traits: resolving the conflict is the job of the consumer, not of the trait. The measurement below shows the cost of this sentence.

State and Abstract Methods in a Trait

A trait carries not only methods but properties too. In a method inside a trait, $this is the object of the class that uses the trait; that is, a trait does not work on its own, it gains meaning only when it enters a class. If a trait expects something from its user, it declares it with an abstract method (abstract): the using class must write that method. The property of each using object belongs to itself, because the property is copied into the class.

<?php
// Tallied.php — a trait that carries a property and an abstract method
trait Tallied
{
    private int $tally = 0;

    abstract public function name(): string;

    public function increment(): static
    {
        $this->tally++;
        return $this;
    }

    public function status(): string
    {
        return $this->name() . '=' . $this->tally;
    }
}
<?php
// Visit.php — a class that writes the abstract method the trait expects
require_once 'Tallied.php';

class Visit
{
    use Tallied;

    public function name(): string
    {
        return 'visit';
    }
}
<?php
// visit_usage.php — two objects carry separate states of the same trait
require 'Visit.php';

$first = new Visit();
$second = new Visit();
$first->increment()->increment();

echo $first->status(), ' ', $second->status(), "\n";
visit=2 visit=0

status() was written in the trait but called the method the class wrote through $this->name(); tally stayed separate for each object. A class that does not write the abstract method cannot use the trait.

# abstract_missing.sh — if the trait's abstract method is not written, the class cannot be declared
cat > Missing.php <<'PHP'
<?php
trait Tallied { abstract public function name(): string; }
class Missing { use Tallied; }
PHP
php -d display_errors=0 Missing.php; echo "exit=$?"
exit=255

Visibility and Alias

The as keyword is not only for resolving a conflict. Used alone, it changes the visibility of a trait method in the using class; when a name is added, it gives the same method a second name and visibility. That way the trait method’s original name and original visibility can be closed to the outside of the class while the new name is left open.

<?php
// visibility.php — as changes the visibility and adds an alias
trait Logged
{
    public function summary(): string
    {
        return 'record';
    }
}

class Hidden
{
    use Logged {
        summary as protected;
        summary as public recordSummary;
    }
}

$hidden = new Hidden();
echo $hidden->recordSummary(), "\n";

try {
    $hidden->summary();
} catch (Error $e) {
    echo get_class($e), "\n";
}
record
Error

The same method stood under two names: recordSummary() was open, while summary() raised Error when called from outside because it had been made protected.

Precedence and the Difference from Inheritance

If a method of the same name comes from more than one place, the order is this: the class’s own method, the trait method, the parent class’s method. For this reason a trait can override the parent class’s method, but it cannot override what the class itself wrote.

<?php
// precedence.php — the winner when the class, a trait and the parent class carry the same name
class Base
{
    public function greet(): string { return 'base'; }
}

trait Polite
{
    public function greet(): string { return 'trait'; }
}

class Child extends Base
{
    use Polite;
}

class Custom extends Base
{
    use Polite;

    public function greet(): string { return 'class'; }
}

echo (new Child())->greet(), ' ', (new Custom())->greet(), "\n";
trait class

Child did not write its own greet() method and the trait won; Custom did and the class won. The parent class stayed last in both cases.

A trait does not replace inheritance, because there are three differences between them. First, a trait is not a type: it cannot be asked about with instanceof and cannot be a parameter type. Second, the methods are copied into the using class, so the static property of two classes that use the same trait is a separate copy. Third, a trait has no parent class; it builds no related hierarchy, it only carries a body.

<?php
// static_trait.php — a trait's static property is a separate copy per using class
trait Counter
{
    public static int $count = 0;

    public static function increment(): int
    {
        return ++self::$count;
    }
}

class First { use Counter; }
class Second { use Counter; }

First::increment();
First::increment();
echo First::$count, ' ', Second::$count, "\n";
2 0

First incremented twice and Second stayed at zero: if shared state is wanted, what is needed is not a trait but a class’s static property or a parent class. Inheritance builds the “this is a thing” relationship, a trait builds the “this also carries that behavior” relationship; using inheritance to remove code repetition builds the relationship wrongly and grows the fragile base class problem from the Inheritance and Abstract Classes lesson.

Using a Trait and an Interface Together

Because a trait is not a type, if the shared body is also to appear as a type, an interface and a trait are used together: the interface gives the contract and the type, the trait gives the body. The class declares the interface with implements and takes the body of the method with use.

<?php
// together.php — the interface gives the type, the trait provides the body
interface Labeled
{
    public function label(): string;
}

trait LabelBody
{
    public function label(): string
    {
        return 'label';
    }
}

class Post implements Labeled
{
    use LabelBody;
}

$post = new Post();
var_dump($post instanceof Labeled);
echo $post->label(), "\n";
bool(true)
label

Post is of the type Labeled and has not written the body again. This combination brings the contract in the Interfaces lesson together with the sharing in this lesson; but the risk that a method added to a trait later stops its user stays exactly the same.

The Measurement Core

This lesson’s measurement also uses the file reach.php; the core was explained line by line in Classes and Objects.

<?php
// reach.php — helpers that measure where a change reaches

function write_files(string $dir, array $files): void
{
    foreach ($files as $name => $content) {
        if ($content === null) {
            continue; // null: the file was deleted
        }
        $path = "$dir/$name";
        if (!is_dir(dirname($path))) {
            mkdir(dirname($path), 0777, true);
        }
        file_put_contents($path, $content);
    }
}

function read_files(string $dir): array
{
    $files = [];
    $walker = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
    );
    foreach ($walker as $file) {
        $name = substr($file->getPathname(), strlen($dir) + 1);
        $files[$name] = file_get_contents($file->getPathname());
    }
    return $files;
}

function run(string $dir): array
{
    $command = 'cd ' . escapeshellarg($dir) . ' && php -d display_errors=0 main.php 2>/dev/null';
    exec($command, $lines, $code);
    return [$code, implode("\n", $lines)];
}

function measure(string $name, array $base, array $change, array $seams = []): string
{
    if (array_key_exists('main.php', $change)) {
        throw new LogicException("$name: the consumer is not edited");
    }
    write_files("measure/$name/before", $base);
    write_files("measure/$name/after", array_merge($base, $change));
    [$codeBefore, $outputBefore] = run("measure/$name/before");
    [$codeAfter, $outputAfter] = run("measure/$name/after");
    if ($codeBefore !== 0) {
        throw new LogicException("$name: the baseline did not run");
    }
    $reach = match (true) {
        $codeAfter !== 0 => 'breaking',
        $outputBefore !== $outputAfter => 'implicit',
        array_intersect(array_keys($change), $seams) !== [] => 'at-seam',
        default => 'in-place',
    };
    printf(
        "%-18s files=%d  exit=%-3d output %-7s -> %s\n",
        $name,
        count($change),
        $codeAfter,
        $outputBefore === $outputAfter ? 'same' : 'changed',
        $reach
    );
    return $reach;
}

A Method Is Added to the Trait

The change measured is adding a summary() method with the same name as Logged’s to the Timed trait. The using class Document applies both traits with use and prints the values of summary() and time(); Document and main.php are not edited, and the only file edited is the trait file.

<?php
// trait.php — a method of the same name is added to a used trait
require_once 'reach.php';

measure('trait-conflict', [
    'traits.php' => <<<'PHP'
        <?php
        trait Logged
        {
            public function summary(): string { return 'record'; }
        }
        trait Timed
        {
            public function time(): string { return '12:00'; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'traits.php';
        class Document
        {
            use Logged, Timed;
        }
        $document = new Document();
        echo $document->summary(), ' ', $document->time(), "\n";
        PHP,
], [
    'traits.php' => <<<'PHP'
        <?php
        trait Logged
        {
            public function summary(): string { return 'record'; }
        }
        trait Timed
        {
            public function time(): string { return '12:00'; }
            public function summary(): string { return 'time'; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/trait-conflict/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
trait-conflict     files=1  exit=255 output changed -> breaking
before exit=0 output=record 12:00
after exit=255 output=

OM15. Not a single line changed in the consumer file and the exit code became 255. The conflict is born on the use line: when two traits bring a method under the same name, the using class cannot be declared. This is the cost of the conflict resolution belonging to the consumer; the trait’s author cannot see who uses it while adding a new method. The reach of this lesson is breaking.

The Bounding Measurement

Does every addition break? If the using class already carries its own summary() method, the conflict between the traits’ methods of the same name does not reach the class: the class’s own method overrides all of the trait methods.

<?php
// the_class_itself.php — the class writes summary() itself, the same addition is made to the trait
require_once 'reach.php';

measure('the-class-itself', [
    'traits.php' => <<<'PHP'
        <?php
        trait Logged
        {
            public function summary(): string { return 'record'; }
        }
        trait Timed
        {
            public function time(): string { return '12:00'; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'traits.php';
        class Document
        {
            use Logged, Timed;
            public function summary(): string { return 'document'; }
        }
        $document = new Document();
        echo $document->summary(), ' ', $document->time(), "\n";
        PHP,
], [
    'traits.php' => <<<'PHP'
        <?php
        trait Logged
        {
            public function summary(): string { return 'record'; }
        }
        trait Timed
        {
            public function time(): string { return '12:00'; }
            public function summary(): string { return 'time'; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/the-class-itself/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
the-class-itself   files=1  exit=0   output same    -> in-place
before exit=0 output=document 12:00
after exit=0 output=document 12:00

OM16. The same addition, the same trait file; this time the class stayed in place and the output continued as document 12:00. The break was born from the name summary() reaching the class from two traits at once; the class’s own definition cut off that reaching. So whether an addition to a trait will break lies not in the trait but in whether the user has already claimed the name summary().

Summary

  • A trait mixes methods into a class with use, a class can use several traits, and a trait is not a type.
  • If two traits bring the same name, the using class chooses with insteadof and gives the second method a new name with as.
  • When a conflicting summary() was added to the used trait, the consumer file stopped without changing at all, exit 255; the reach of this lesson is breaking.
  • If the using class writes the summary() method itself, the same addition stayed in place.
  • The conflict resolution belongs to the consumer, so the trait’s author does not see the classes while adding a new method.

Next Step

Traits shared methods, but state and constants are another topic. The Static Members and Constants lesson asks about members bound to the class: why does moving the way of creating an object to a static factory method and adding a parameter to the constructor not stop the consumer?

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close