PHP interfaces, traits, and abstract classes

Share a contract with an interface, share code with a trait, and share both with an abstract base.

PHP
<?php
interface Shape
{
    public function area(): float;
}

trait Describes
{
    public function describe(): string
    {
        return static::class . " with area " . number_format($this->area(), 2);
    }
}

abstract class Base implements Shape
{
    use Describes;
}

final class Circle extends Base
{
    public function __construct(private float $radius) {}

    public function area(): float
    {
        return M_PI * $this->radius ** 2;
    }
}

final class Rect extends Base
{
    public function __construct(private float $width, private float $height) {}

    public function area(): float
    {
        return $this->width * $this->height;
    }
}

foreach ([new Circle(1.5), new Rect(2, 3.5)] as $shape) {
    echo $shape->describe(), "\n";
}
echo (new Circle(1)) instanceof Shape ? "a Circle is a Shape" : "not a Shape", "\n";
Output
Circle with area 7.07
Rect with area 7.00
a Circle is a Shape

Explanation

PHP supports single inheritance, so a class has only one parent. An interface lists method signatures that implementing classes must provide, and a class can implement many interfaces. A trait is a reusable set of methods that is copied into a class with use. It is the way to share code between classes that are not related by inheritance.

An abstract class can combine both: it implements an interface, uses a trait, and leaves the remaining methods abstract. static::class refers to the actual class of the object, so one trait method can describe any subclass. final prevents further extension. Code that is typed against the interface works with every implementation.

Explore the dry run
Guided walkthroughReady
1<?php
2interface Shape
3{
4 public function area(): float;
5}
6
7trait Describes
8{
9 public function describe(): string
10 {
11 return static::class . " with area " . number_format($this->area(), 2);
12 }
13}
14
15abstract class Base implements Shape
16{
17 use Describes;
18}
19
20final class Circle extends Base
21{
22 public function __construct(private float $radius) {}
23
24 public function area(): float
25 {
26 return M_PI * $this->radius ** 2;
27 }
28}
29
30final class Rect extends Base
31{
32 public function __construct(private float $width, private float $height) {}
33
34 public function area(): float
35 {
36 return $this->width * $this->height;
37 }
38}
39
40foreach ([new Circle(1.5), new Rect(2, 3.5)] as $shape) {
41 echo $shape->describe(), "\n";
42}
43echo (new Circle(1)) instanceof Shape ? "a Circle is a Shape" : "not a Shape", "\n";

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
A contract: every shape has an area.—
—
Reusable code. It can call area() because the classes that use it provide one.—
—
Base promises the interface but leaves area() to the subclasses.—
—
Constructor promotion creates the private property.—
—
static::class gives the real class name, and area() is the subclass's version.—
Circle with area 7.07
Rect with area 7.00
instanceof also accepts interfaces.—
Circle with area 7.07
Rect with area 7.00
a Circle is a Shape

Cost and common mistakes

Each call is O(1).

  • Two traits with a method of the same name conflict. Resolve it with insteadof or as in the use block.
  • Traits are not types. You cannot write a parameter type of Describes.
  • number_format rounds for display only; keep the full float for calculations.

Try it yourself

Where would you declare a method that all shapes must implement?

Reveal answer and reasoning

In the Shape interface. Every class that implements it must then provide the method.

Further reading