Reading — step 1 of 7
Learn
Interfaces and Traits
A class extends exactly one parent, which leaves two everyday problems unsolved:
how does an unrelated set of classes promise the same capability, and how do two
classes share an implementation without inventing a common ancestor?
Interfaces answer the first — a contract with no code in it. Traits answer the second — code with no contract. They are not competing features; most real classes use both.
Interfaces — a contract with no code in it
<?php
interface Priceable {
public function priceCents(): int;
}
interface Labelled {
public function label(): string;
}
class Book implements Priceable, Labelled {
private string $title;
private int $cents;
public function __construct(string $title, int $cents) {
$this->title = $title;
$this->cents = $cents;
}
public function priceCents(): int {
return $this->cents;
}
public function label(): string {
return $this->title;
}
}
function receipt(Priceable $item): string {
return sprintf('%.2f', $item->priceCents() / 100);
}
$b = new Book('Atlas', 1850);
echo $b->label(), "\n";
echo receipt($b), "\n";
//> Atlas
//> 18.50
implementstakes a comma-separated list, so one class can satisfy as many contracts as it likes.- Interface methods have no body and are always
public. - A class uses
implementsto reach an interface; one interface usesextendsto build on another interface.
The payoff is receipt(). It is typed Priceable, so it accepts anything that implements the contract and never needs to know what a Book is. That is what stands in for the multiple inheritance PHP does not have.
Traits — sharing an implementation sideways
<?php
trait Audited {
private int $changes = 0;
public function touch(): void {
$this->changes++;
}
public function changeCount(): int {
return $this->changes;
}
}
class Invoice {
use Audited;
}
class Customer {
use Audited;
}
$i = new Invoice();
$i->touch();
$i->touch();
echo $i->changeCount(), "\n";
$c = new Customer();
echo $c->changeCount(), "\n";
//> 2
//> 0
use Audited; inside a class body copies the trait's methods and properties in at compile time. It is not inheritance: Invoice and Customer share the code without sharing a parent, each object still gets its own counter, and a class may use several traits. PHP Intermediate covers traits properly — this is the shape you need to recognise.
Late static binding — self:: versus static::
<?php
class Job {
public static function make(): self {
return new static();
}
public static function makeSelf(): self {
return new self();
}
}
class EmailJob extends Job {}
echo get_class(EmailJob::make()), "\n";
echo get_class(EmailJob::makeSelf()), "\n";
//> EmailJob
//> Job
self means "the class this line is written in" and is settled when the file is compiled. static means "the class the call actually went through" and is settled at run time. A factory method almost always wants static.
Grader note: PHP 8.0 lets you spell the return type
: static, which describesnew static()far better than: selfdoes. On this course's PHP 7.4 grader that is a parse error —syntax error, unexpected 'static' (T_STATIC). Write: self, or leave the return type off.new static()itself has worked since PHP 5.3 and is fine here.
The two together
An interface says what a type promises; a trait supplies an implementation that several unrelated types can reuse. Putting them side by side is the everyday shape:
<?php
interface Shape {
public function area(): float;
}
trait Describable {
public function describe(): string {
$parts = explode("\\", static::class);
return sprintf('%s area %.2f', end($parts), $this->area());
}
}
class Square implements Shape {
use Describable;
private float $side;
public function __construct(float $side) { $this->side = $side; }
public function area(): float { return $this->side * $this->side; }
}
echo (new Square(3.0))->describe(), "\n";
//> Square area 9.00
Square promises area() through the interface and inherits describe() from
the trait without either one knowing about the other. Note static::class inside
the trait: it resolves to the class that actually used the trait, which is why one
describe() serves every shape.
Common mistakes
- Mixing up
extendsandimplements— a classextendsone class andimplementsinterfaces; only an interfaceextendsanother interface. - Putting a body on an interface method — interface methods declare a signature and nothing else, and they are always
public. - Two traits defining the same method name — a fatal conflict (
Trait method hi has not been applied, because there are collisions) until you pick a winner withinsteadof. - Assuming
self::classin a trait names the trait — it names the class that used the trait. The two only diverge once a subclass appears: givenclass Sub extends Rect {},static::classreportsSubwhileself::classstill reportsRect. Preferstatic::so the method keeps working when someone extends your class.
Your exercise
Shape Contract asks for both halves of this lesson at once. Declare
interface Shape with a single area(): float. Declare trait Describable
whose describe(): string returns the class name, a space, the word area, a
space, and the area to two decimals — Rectangle area 12.00. Then write
Rectangle and Circle, each implementing Shape and using Describable.
The input is a count, then that many lines: r <width> <height> for a rectangle,
c <radius> for a circle. Print one describe() line each.
Three traps the grader catches. The file declares strict_types=1, so the pieces
you split off the input line are strings: new Rectangle($parts[1], $parts[2])
dies with Argument 1 passed to Rectangle::__construct() must be of the type float, string given. Cast with (float). Second, build the number with
sprintf('%.2f', ...) — a circle of radius 2 must read 12.57, not
12.566370614359; use M_PI for pi. Third, each class needs its own
use Describable; line: leave it out and the class has no describe() at all,
which ends the run with Call to undefined method Circle::describe().
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…